Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mongo commands to otel span attributes #40191

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public class MongoClientBuildTimeConfig {
@ConfigItem(name = "force-default-clients")
public boolean forceDefaultClients;

/**
* Whether or not tracing spans of driver commands are sent in case the quarkus-opentelemetry extension is present.
*/
@ConfigItem(name = "tracing.enabled")
public boolean tracingEnabled;

/**
* Configuration for DevServices. DevServices allows Quarkus to automatically start MongoDB in dev and test mode.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Default;
Expand Down Expand Up @@ -85,6 +86,8 @@ public class MongoClientProcessor {

private static final DotName MONGO_CLIENT_CUSTOMIZER = DotName.createSimple(MongoClientCustomizer.class.getName());

private static final String MONGODB_TRACING_COMMANDLISTENER_CLASSNAME = "io.quarkus.mongodb.tracing.MongoTracingCommandListener";

private static final String SERVICE_BINDING_INTERFACE_NAME = "io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter";

@BuildStep
Expand Down Expand Up @@ -147,10 +150,14 @@ CommandListenerBuildItem collectCommandListeners(CombinedIndexBuildItem indexBui
MongoClientBuildTimeConfig buildTimeConfig, Capabilities capabilities) {
Collection<ClassInfo> commandListenerClasses = indexBuildItem.getIndex()
.getAllKnownImplementors(DotName.createSimple(CommandListener.class.getName()));
List<String> names = commandListenerClasses.stream()
.map(ci -> ci.name().toString())
.collect(Collectors.toList());
return new CommandListenerBuildItem(names);
Stream<String> names = commandListenerClasses.stream()
.map(ci -> ci.name().toString());
Stream<String> tracing = Stream.empty();
if (buildTimeConfig.tracingEnabled && capabilities.isPresent(Capability.OPENTELEMETRY_TRACER)) {
tracing = Stream.of(MONGODB_TRACING_COMMANDLISTENER_CLASSNAME);
}
var items = Stream.concat(names, tracing).toList();
return new CommandListenerBuildItem(items);
loicmathieu marked this conversation as resolved.
Show resolved Hide resolved
}

@BuildStep
Expand Down
5 changes: 5 additions & 0 deletions extensions/mongodb-client/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mutiny-reactive-streams-operators</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.quarkus.mongodb.tracing;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;

import jakarta.inject.Inject;

import org.jboss.logging.Logger;

import com.mongodb.event.*;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;

public class MongoTracingCommandListener implements CommandListener {
private static final org.jboss.logging.Logger LOGGER = Logger.getLogger(MongoTracingCommandListener.class);
private static final String KEY = "mongodb.command";
private final Map<Integer, ContextEvent> requestMap;
private final Instrumenter<CommandStartedEvent, Void> instrumenter;

private record ContextEvent(Context context, CommandStartedEvent commandEvent) {
}

@Inject
public MongoTracingCommandListener(OpenTelemetry openTelemetry) {
requestMap = new ConcurrentHashMap<>();
SpanNameExtractor<CommandStartedEvent> spanNameExtractor = CommandEvent::getCommandName;
instrumenter = Instrumenter.<CommandStartedEvent, Void> builder(
openTelemetry, "quarkus-mongodb-client", spanNameExtractor)
.addAttributesExtractor(new CommandEventAttrExtractor())
.buildInstrumenter(SpanKindExtractor.alwaysClient());
LOGGER.debugf("MongoTracingCommandListener created");
}

@Override
public void commandStarted(CommandStartedEvent event) {
LOGGER.tracef("commandStarted event %s", event.getCommandName());

Context parentContext = Context.current();
if (instrumenter.shouldStart(parentContext, event)) {
Context context = instrumenter.start(parentContext, event);
requestMap.put(event.getRequestId(), new ContextEvent(context, event));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned with the amount of data you are storing here... can you give an example of one of this events?
Can you just store what you need later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brunobat it is not big, just description of mongo connection, db name and command. You can inspect it by using BookResourceTest. But I could create another record to just save command name and the command itself, if you prefer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brunobat I've moved the implementation to opentelemetry extension. Is this enough for a new PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is but there are also other improvements needed:

Also we lack documentation entries and additional IT tests for Uni, Multi, parent-child relationships and failure cases.

Also, the assertions need to verify the attributes of the spans.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brunobat Opened #40472

I'm not sure about additional IT tests. There is already a test for for reactive endpoint with assertion for attributes:

    @Test
    void reactiveClient() {
        testInsertBooks("/reactive-books");
        assertTraceAvailable("my-reactive-collection");
    }

}
}

@Override
public void commandSucceeded(CommandSucceededEvent event) {
LOGGER.tracef("commandSucceeded event %s", event.getCommandName());
ContextEvent contextEvent = requestMap.remove(event.getRequestId());
if (contextEvent != null) {
instrumenter.end(contextEvent.context(), contextEvent.commandEvent(), null, null);
}
}

@Override
public void commandFailed(CommandFailedEvent event) {
LOGGER.tracef("commandFailed event %s", event.getCommandName());
ContextEvent contextEvent = requestMap.remove(event.getRequestId());
if (contextEvent != null) {
instrumenter.end(
contextEvent.context(),
contextEvent.commandEvent(),
null,
event.getThrowable());
}
}

private static class CommandEventAttrExtractor implements AttributesExtractor<CommandStartedEvent, Void> {
@Override
public void onStart(AttributesBuilder attributesBuilder,
Context context,
CommandStartedEvent commandStartedEvent) {
attributesBuilder.put(KEY, commandStartedEvent.getCommand().toJson());
}

@Override
public void onEnd(AttributesBuilder attributesBuilder,
Context context,
CommandStartedEvent commandStartedEvent,
@Nullable Void unused,
@Nullable Throwable throwable) {

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package io.quarkus.mongodb.tracing;

import static org.assertj.core.api.Assertions.assertThatNoException;

import org.bson.BsonDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.mongodb.ServerAddress;
import com.mongodb.connection.ClusterId;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerId;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandStartedEvent;
import com.mongodb.event.CommandSucceededEvent;

import io.opentelemetry.api.OpenTelemetry;

class MongoTracingCommandListenerTest {
private ConnectionDescription connDescr;
private MongoTracingCommandListener listener;
private BsonDocument command;

@BeforeEach
void setUp() {
connDescr = new ConnectionDescription(new ServerId(new ClusterId(), new ServerAddress()));
listener = new MongoTracingCommandListener(OpenTelemetry.noop());
command = new BsonDocument();
}

@Test
void commandStarted() {
var startEvent = new CommandStartedEvent(
null,
1L,
10,
connDescr,
"db",
"find",
command);
assertThatNoException().isThrownBy(() -> listener.commandStarted(startEvent));

CommandSucceededEvent successEvent = new CommandSucceededEvent(null,
startEvent.getOperationId(),
startEvent.getRequestId(),
connDescr,
startEvent.getDatabaseName(),
startEvent.getCommandName(),
startEvent.getCommand(),
10L);
assertThatNoException().isThrownBy(() -> listener.commandSucceeded(successEvent));
}

@Test
void commandSucceeded() {
CommandSucceededEvent cmd = new CommandSucceededEvent(null,
1L,
10,
connDescr,
"db",
"find",
command,
10L);
assertThatNoException().isThrownBy(() -> listener.commandSucceeded(cmd));
}

@Test
void commandFailed() {
var startedEvent = new CommandStartedEvent(
null,
1L,
10,
connDescr,
"db",
"find",
command);
assertThatNoException().isThrownBy(() -> listener.commandStarted(startedEvent));

CommandFailedEvent failedEvent = new CommandFailedEvent(null,
1L,
10,
connDescr,
"db",
"find",
10L,
new IllegalStateException("command failed"));
assertThatNoException().isThrownBy(() -> listener.commandFailed(failedEvent));
}

@Test
void commandFailedNoEvent() {
CommandFailedEvent cmd = new CommandFailedEvent(null,
1L,
10,
connDescr,
"db",
"find",
10L,
new IllegalStateException("command failed"));
assertThatNoException().isThrownBy(() -> listener.commandFailed(cmd));
}

}
Loading
Loading