diff --git a/.fossa.yml b/.fossa.yml index ac92e1573593..8f7ad65d4ac2 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -691,6 +691,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:mongo:mongo-async-3.3:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:nats:nats-2.21:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:nats:nats-2.21:library' - type: gradle path: ./ target: ':instrumentation:netty:netty-3.8:javaagent' diff --git a/.gitignore b/.gitignore index d5e39e5b25bd..be22b2970b84 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ out/ ###################### .vscode **/bin/ +.metals # Others # ########## diff --git a/instrumentation/nats/nats-2.21/javaagent/build.gradle.kts b/instrumentation/nats/nats-2.21/javaagent/build.gradle.kts new file mode 100644 index 000000000000..c1d88aa5675b --- /dev/null +++ b/instrumentation/nats/nats-2.21/javaagent/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("io.nats") + module.set("jnats") + versions.set("[2.21.0,)") + } +} + +dependencies { + library("io.nats:jnats:2.21.0") + + implementation(project(":instrumentation:nats:nats-2.21:library")) +} + +tasks { + test { + usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service) + } +} diff --git a/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/ConnectionInstrumentation.java b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/ConnectionInstrumentation.java new file mode 100644 index 000000000000..f39570d40037 --- /dev/null +++ b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/ConnectionInstrumentation.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nats.v2_21; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface; +import static io.opentelemetry.javaagent.instrumentation.nats.v2_21.NatsSingletons.PRODUCER_INSTRUMENTER; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import io.nats.client.Connection; +import io.nats.client.Message; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.nats.v2_21.internal.NatsRequest; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class ConnectionInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return implementsInterface(named("io.nats.client.Connection")); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isPublic() + .and(named("publish")) + .and(takesArguments(1)) + .and(takesArgument(0, named("io.nats.client.Message"))), + ConnectionInstrumentation.class.getName() + "$PublishMessageAdvice"); + } + + @SuppressWarnings("unused") + public static class PublishMessageAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.This Connection connection, + @Advice.Argument(0) Message message, + @Advice.Local("otelContext") Context otelContext, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("natsRequest") NatsRequest natsRequest) { + Context parentContext = Context.current(); + natsRequest = NatsRequest.create(connection, message); + + if (!PRODUCER_INSTRUMENTER.shouldStart(parentContext, natsRequest)) { + return; + } + + otelContext = PRODUCER_INSTRUMENTER.start(parentContext, natsRequest); + otelScope = otelContext.makeCurrent(); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit( + @Advice.Thrown Throwable throwable, + @Advice.This Connection connection, + @Advice.Argument(0) Message message, + @Advice.Local("otelContext") Context otelContext, + @Advice.Local("otelScope") Scope otelScope, + @Advice.Local("natsRequest") NatsRequest natsRequest) { + if (otelScope == null) { + return; + } + + otelScope.close(); + PRODUCER_INSTRUMENTER.end(otelContext, natsRequest, null, throwable); + } + } +} diff --git a/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationModule.java b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationModule.java new file mode 100644 index 000000000000..1cf3ba651e13 --- /dev/null +++ b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationModule.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nats.v2_21; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class NatsInstrumentationModule extends InstrumentationModule { + + public NatsInstrumentationModule() { + super("nats", "nats-2.21"); + } + + // TODO classLoaderMatcher + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new ConnectionInstrumentation()); + } +} diff --git a/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsSingletons.java b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsSingletons.java new file mode 100644 index 000000000000..9b5053dbec6d --- /dev/null +++ b/instrumentation/nats/nats-2.21/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsSingletons.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nats.v2_21; + +import static io.opentelemetry.instrumentation.nats.v2_21.internal.NatsInstrumenterFactory.createProducerInstrumenter; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.nats.v2_21.internal.NatsRequest; + +public final class NatsSingletons { + + public static final Instrumenter PRODUCER_INSTRUMENTER = + createProducerInstrumenter(GlobalOpenTelemetry.get()); + + private NatsSingletons() {} +} diff --git a/instrumentation/nats/nats-2.21/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationTest.java b/instrumentation/nats/nats-2.21/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationTest.java new file mode 100644 index 000000000000..2dba5ff5c8d0 --- /dev/null +++ b/instrumentation/nats/nats-2.21/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/nats/v2_21/NatsInstrumentationTest.java @@ -0,0 +1,131 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.nats.v2_21; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; + +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.Nats; +import io.nats.client.Subscription; +import io.nats.client.impl.Headers; +import io.nats.client.impl.NatsMessage; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import java.io.IOException; +import java.time.Duration; +import java.util.LinkedList; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +@SuppressWarnings("deprecation") // using deprecated semconv +class NatsInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static final DockerImageName natsImage = DockerImageName.parse("nats:2.11.2-alpine3.21"); + + static final GenericContainer natsContainer = + new GenericContainer<>(natsImage).withExposedPorts(4222); + + static String natsUrl; + static Connection connection; + static Subscription subscription; + + static LinkedList publishedMessages = new LinkedList<>(); + + @BeforeAll + static void beforeAll() throws IOException, InterruptedException { + natsContainer.start(); + natsUrl = "nats://" + natsContainer.getHost() + ":" + natsContainer.getMappedPort(4222); + connection = Nats.connect(natsUrl); + subscription = connection.subscribe("*"); + } + + @AfterAll + static void afterAll() throws InterruptedException { + subscription.drain(Duration.ofSeconds(10)); + connection.close(); + natsContainer.close(); + } + + @Test + void testPublishMessageNoHeaders() throws InterruptedException { + // given + int clientId = connection.getServerInfo().getClientId(); + NatsMessage message = NatsMessage.builder().subject("sub").data("x").build(); + + // when + testing.runWithSpan("testPublishMessage", () -> connection.publish(message)); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("testPublishMessage").hasNoParent(), + span -> + span.hasName("sub publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_SYSTEM, "nats"), + equalTo(MESSAGING_DESTINATION_NAME, "sub"), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, 1), + equalTo( + AttributeKey.stringKey("messaging.client_id"), + String.valueOf(clientId))))); + + // and + Message published = subscription.nextMessage(Duration.ofSeconds(1)); + assertThat(published.getHeaders()).isNull(); + } + + @Test + void testPublishMessageWithHeaders() throws InterruptedException { + // given + int clientId = connection.getServerInfo().getClientId(); + NatsMessage message = + NatsMessage.builder().subject("sub").data("x").headers(new Headers()).build(); + + // when + testing.runWithSpan("testPublishMessage", () -> connection.publish(message)); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("testPublishMessage").hasNoParent(), + span -> + span.hasName("sub publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_SYSTEM, "nats"), + equalTo(MESSAGING_DESTINATION_NAME, "sub"), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, 1), + equalTo( + AttributeKey.stringKey("messaging.client_id"), + String.valueOf(clientId))))); + + // and + Message published = subscription.nextMessage(Duration.ofSeconds(1)); + assertThat(published.getHeaders().get("traceparent")).isNotEmpty(); + } +} diff --git a/instrumentation/nats/nats-2.21/library/build.gradle.kts b/instrumentation/nats/nats-2.21/library/build.gradle.kts new file mode 100644 index 000000000000..14496c845003 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("io.nats:jnats:2.21.0") + + compileOnly("com.google.auto.value:auto-value-annotations") + annotationProcessor("com.google.auto.value:auto-value") + + testImplementation(project(":instrumentation:nats:nats-2.21:testing")) +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetry.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetry.java new file mode 100644 index 000000000000..3ac82d451343 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetry.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21; + +import io.nats.client.Connection; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.nats.v2_21.internal.NatsRequest; + +public final class NatsTelemetry { + + public static NatsTelemetry create(OpenTelemetry openTelemetry) { + return new NatsTelemetryBuilder(openTelemetry).build(); + } + + public static NatsTelemetryBuilder builder(OpenTelemetry openTelemetry) { + return new NatsTelemetryBuilder(openTelemetry); + } + + private final Instrumenter producerInstrumenter; + + public NatsTelemetry(Instrumenter producerInstrumenter) { + this.producerInstrumenter = producerInstrumenter; + } + + public OpenTelemetryConnection wrap(Connection connection) { + return new OpenTelemetryConnection(connection, this.producerInstrumenter); + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetryBuilder.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetryBuilder.java new file mode 100644 index 000000000000..79de839074f6 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/NatsTelemetryBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.nats.v2_21.internal.NatsInstrumenterFactory; + +public final class NatsTelemetryBuilder { + + private final OpenTelemetry openTelemetry; + + NatsTelemetryBuilder(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + public NatsTelemetry build() { + return new NatsTelemetry(NatsInstrumenterFactory.createProducerInstrumenter(openTelemetry)); + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/OpenTelemetryConnection.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/OpenTelemetryConnection.java new file mode 100644 index 000000000000..22630d523659 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/OpenTelemetryConnection.java @@ -0,0 +1,360 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21; + +import io.nats.client.Connection; +import io.nats.client.ConnectionListener; +import io.nats.client.ConsumerContext; +import io.nats.client.Dispatcher; +import io.nats.client.ForceReconnectOptions; +import io.nats.client.JetStream; +import io.nats.client.JetStreamApiException; +import io.nats.client.JetStreamManagement; +import io.nats.client.JetStreamOptions; +import io.nats.client.KeyValue; +import io.nats.client.KeyValueManagement; +import io.nats.client.KeyValueOptions; +import io.nats.client.Message; +import io.nats.client.MessageHandler; +import io.nats.client.ObjectStore; +import io.nats.client.ObjectStoreManagement; +import io.nats.client.ObjectStoreOptions; +import io.nats.client.Options; +import io.nats.client.Statistics; +import io.nats.client.StreamContext; +import io.nats.client.Subscription; +import io.nats.client.api.ServerInfo; +import io.nats.client.impl.Headers; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.nats.v2_21.internal.NatsRequest; +import java.io.IOException; +import java.net.InetAddress; +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +public class OpenTelemetryConnection implements Connection { + + private final Connection delegate; + private final Instrumenter producerInstrumenter; + + public OpenTelemetryConnection( + Connection connection, Instrumenter producerInstrumenter) { + this.delegate = connection; + this.producerInstrumenter = producerInstrumenter; + } + + @Override + public void publish(String subject, byte[] body) { + wrapPublish(NatsRequest.create(this, subject, body), () -> delegate.publish(subject, body)); + } + + @Override + public void publish(String subject, Headers headers, byte[] body) { + wrapPublish( + NatsRequest.create(this, subject, headers, body), + () -> delegate.publish(subject, headers, body)); + } + + @Override + public void publish(String subject, String replyTo, byte[] body) { + wrapPublish( + NatsRequest.create(this, subject, body), () -> delegate.publish(subject, replyTo, body)); + } + + @Override + public void publish(String subject, String replyTo, Headers headers, byte[] body) { + wrapPublish( + NatsRequest.create(this, subject, headers, body), + () -> delegate.publish(subject, replyTo, headers, body)); + } + + @Override + public void publish(Message message) { + wrapPublish(NatsRequest.create(this, message), () -> delegate.publish(message)); + } + + @Override + public CompletableFuture request(String s, byte[] bytes) { + return delegate.request(s, bytes); + } + + @Override + public Message request(String s, byte[] bytes, Duration duration) throws InterruptedException { + return delegate.request(s, bytes, duration); + } + + @Override + public CompletableFuture request(String s, Headers headers, byte[] bytes) { + return delegate.request(s, headers, bytes); + } + + @Override + public Message request(String s, Headers headers, byte[] bytes, Duration duration) + throws InterruptedException { + return delegate.request(s, headers, bytes, duration); + } + + @Override + public CompletableFuture request(Message message) { + return delegate.request(message); + } + + @Override + public Message request(Message message, Duration duration) throws InterruptedException { + return delegate.request(message, duration); + } + + @Override + public CompletableFuture requestWithTimeout(String s, byte[] bytes, Duration duration) { + return delegate.requestWithTimeout(s, bytes, duration); + } + + @Override + public CompletableFuture requestWithTimeout( + String s, Headers headers, byte[] bytes, Duration duration) { + return delegate.requestWithTimeout(s, headers, bytes, duration); + } + + @Override + public CompletableFuture requestWithTimeout(Message message, Duration duration) { + return delegate.requestWithTimeout(message, duration); + } + + @Override + public Subscription subscribe(String s) { + return delegate.subscribe(s); + } + + @Override + public Subscription subscribe(String s, String s1) { + return delegate.subscribe(s, s1); + } + + @Override + public Dispatcher createDispatcher(MessageHandler messageHandler) { + return delegate.createDispatcher(messageHandler); + } + + @Override + public Dispatcher createDispatcher() { + return delegate.createDispatcher(); + } + + @Override + public void closeDispatcher(Dispatcher dispatcher) { + delegate.closeDispatcher(dispatcher); + } + + @Override + public void addConnectionListener(ConnectionListener connectionListener) { + delegate.addConnectionListener(connectionListener); + } + + @Override + public void removeConnectionListener(ConnectionListener connectionListener) { + delegate.removeConnectionListener(connectionListener); + } + + @Override + public void flush(Duration duration) throws TimeoutException, InterruptedException { + delegate.flush(duration); + } + + @Override + public CompletableFuture drain(Duration duration) + throws TimeoutException, InterruptedException { + return delegate.drain(duration); + } + + @Override + public void close() throws InterruptedException { + delegate.close(); + } + + @Override + public Status getStatus() { + return delegate.getStatus(); + } + + @Override + public long getMaxPayload() { + return delegate.getMaxPayload(); + } + + @Override + public Collection getServers() { + return delegate.getServers(); + } + + @Override + public Statistics getStatistics() { + return delegate.getStatistics(); + } + + @Override + public Options getOptions() { + return delegate.getOptions(); + } + + @Override + public ServerInfo getServerInfo() { + return delegate.getServerInfo(); + } + + @Override + public String getConnectedUrl() { + return delegate.getConnectedUrl(); + } + + @Override + public InetAddress getClientInetAddress() { + return delegate.getClientInetAddress(); + } + + @Override + public String getLastError() { + return delegate.getLastError(); + } + + @Override + public void clearLastError() { + delegate.clearLastError(); + } + + @Override + public String createInbox() { + return delegate.createInbox(); + } + + @Override + public void flushBuffer() throws IOException { + delegate.flushBuffer(); + } + + @Override + public void forceReconnect() throws IOException, InterruptedException { + delegate.forceReconnect(); + } + + @Override + public void forceReconnect(ForceReconnectOptions forceReconnectOptions) + throws IOException, InterruptedException { + delegate.forceReconnect(forceReconnectOptions); + } + + @Override + public Duration RTT() throws IOException { + return delegate.RTT(); + } + + @Override + public StreamContext getStreamContext(String s) throws IOException, JetStreamApiException { + return delegate.getStreamContext(s); + } + + @Override + public StreamContext getStreamContext(String s, JetStreamOptions jetStreamOptions) + throws IOException, JetStreamApiException { + return delegate.getStreamContext(s, jetStreamOptions); + } + + @Override + public ConsumerContext getConsumerContext(String s, String s1) + throws IOException, JetStreamApiException { + return delegate.getConsumerContext(s, s1); + } + + @Override + public ConsumerContext getConsumerContext(String s, String s1, JetStreamOptions jetStreamOptions) + throws IOException, JetStreamApiException { + return delegate.getConsumerContext(s, s1, jetStreamOptions); + } + + @Override + public JetStream jetStream() throws IOException { + return delegate.jetStream(); + } + + @Override + public JetStream jetStream(JetStreamOptions jetStreamOptions) throws IOException { + return delegate.jetStream(jetStreamOptions); + } + + @Override + public JetStreamManagement jetStreamManagement() throws IOException { + return delegate.jetStreamManagement(); + } + + @Override + public JetStreamManagement jetStreamManagement(JetStreamOptions jetStreamOptions) + throws IOException { + return delegate.jetStreamManagement(jetStreamOptions); + } + + @Override + public KeyValue keyValue(String s) throws IOException { + return delegate.keyValue(s); + } + + @Override + public KeyValue keyValue(String s, KeyValueOptions keyValueOptions) throws IOException { + return delegate.keyValue(s, keyValueOptions); + } + + @Override + public KeyValueManagement keyValueManagement() throws IOException { + return delegate.keyValueManagement(); + } + + @Override + public KeyValueManagement keyValueManagement(KeyValueOptions keyValueOptions) throws IOException { + return delegate.keyValueManagement(keyValueOptions); + } + + @Override + public ObjectStore objectStore(String s) throws IOException { + return delegate.objectStore(s); + } + + @Override + public ObjectStore objectStore(String s, ObjectStoreOptions objectStoreOptions) + throws IOException { + return delegate.objectStore(s, objectStoreOptions); + } + + @Override + public ObjectStoreManagement objectStoreManagement() throws IOException { + return delegate.objectStoreManagement(); + } + + @Override + public ObjectStoreManagement objectStoreManagement(ObjectStoreOptions objectStoreOptions) + throws IOException { + return delegate.objectStoreManagement(objectStoreOptions); + } + + private void wrapPublish(NatsRequest natsRequest, Runnable publish) { + Context parentContext = Context.current(); + + if (!Span.fromContext(parentContext).getSpanContext().isValid() + || !producerInstrumenter.shouldStart(parentContext, natsRequest)) { + publish.run(); + return; + } + + Context context = producerInstrumenter.start(parentContext, natsRequest); + try (Scope ignored = context.makeCurrent()) { + publish.run(); + } finally { + producerInstrumenter.end(context, natsRequest, null, null); + } + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageMessagingAttributesGetter.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageMessagingAttributesGetter.java new file mode 100644 index 000000000000..4cbea9008313 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageMessagingAttributesGetter.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21.internal; + +import io.nats.client.impl.Headers; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesGetter; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +enum MessageMessagingAttributesGetter implements MessagingAttributesGetter { + INSTANCE; + + @Nullable + @Override + public String getSystem(NatsRequest request) { + return "nats"; + } + + @Nullable + @Override + public String getDestination(NatsRequest request) { + return request.getSubject(); + } + + @Nullable + @Override + public String getDestinationTemplate(NatsRequest request) { + return null; + } + + @Override + public boolean isTemporaryDestination(NatsRequest request) { + return false; + } + + @Override + public boolean isAnonymousDestination(NatsRequest request) { + return false; + } + + @Nullable + @Override + public String getConversationId(NatsRequest request) { + return null; + } + + @Nullable + @Override + public Long getMessageBodySize(NatsRequest request) { + return request.getDataSize(); + } + + @Nullable + @Override + public Long getMessageEnvelopeSize(NatsRequest request) { + return null; + } + + @Nullable + @Override + public String getMessageId(NatsRequest request, @Nullable Void unused) { + return null; + } + + @Nullable + @Override + public String getClientId(NatsRequest request) { + return String.valueOf(request.getConnection().getServerInfo().getClientId()); + } + + @Nullable + @Override + public Long getBatchMessageCount(NatsRequest request, @Nullable Void unused) { + return null; + } + + @Override + public List getMessageHeader(NatsRequest request, String name) { + Headers headers = request.getHeaders(); + return headers == null ? Collections.emptyList() : headers.get(name); + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageTextMapSetter.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageTextMapSetter.java new file mode 100644 index 000000000000..247b190dabe1 --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/MessageTextMapSetter.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21.internal; + +import io.opentelemetry.context.propagation.TextMapSetter; +import javax.annotation.Nullable; + +enum MessageTextMapSetter implements TextMapSetter { + INSTANCE; + + @Override + /* Can not work if getHeaders doesn't return a writable structure. */ + public void set(@Nullable NatsRequest request, String key, String value) { + if (request == null || request.getHeaders() == null || request.getHeaders().isReadOnly()) { + return; + } + + request.getHeaders().put(key, value); + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsInstrumenterFactory.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsInstrumenterFactory.java new file mode 100644 index 000000000000..00b4ab1ec54b --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsInstrumenterFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21.internal; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor; +import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingSpanNameExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time.", or "This class is internal and experimental. Its APIs are unstable and can change at + * any time. Its APIs (or a version of them) may be promoted to the public stable API in the future, + * but no guarantees are made. + */ +public final class NatsInstrumenterFactory { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.nats-2.21"; + + public static final SpanNameExtractor PRODUCER_SPAN_NAME_EXTRACTOR = + MessagingSpanNameExtractor.create( + MessageMessagingAttributesGetter.INSTANCE, MessageOperation.PUBLISH); + + public static final AttributesExtractor PUBLISH_ATTRIBUTES_EXTRACTOR = + MessagingAttributesExtractor.create( + MessageMessagingAttributesGetter.INSTANCE, MessageOperation.PUBLISH); + + public static Instrumenter createProducerInstrumenter( + OpenTelemetry openTelemetry) { + return Instrumenter.builder( + openTelemetry, INSTRUMENTATION_NAME, PRODUCER_SPAN_NAME_EXTRACTOR) + .addAttributesExtractor(PUBLISH_ATTRIBUTES_EXTRACTOR) + .buildProducerInstrumenter(MessageTextMapSetter.INSTANCE); + } + + private NatsInstrumenterFactory() {} +} diff --git a/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsRequest.java b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsRequest.java new file mode 100644 index 000000000000..7cf2acd050ed --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/internal/NatsRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21.internal; + +import com.google.auto.value.AutoValue; +import io.nats.client.Connection; +import io.nats.client.Message; +import io.nats.client.impl.Headers; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time.", or "This class is internal and experimental. Its APIs are unstable and can change at + * any time. Its APIs (or a version of them) may be promoted to the public stable API in the future, + * but no guarantees are made. + */ +@AutoValue +public abstract class NatsRequest { + + public static NatsRequest create(Connection connection, String subject, byte[] data) { + return new AutoValue_NatsRequest(connection, subject, null, getDataSize(data)); + } + + public static NatsRequest create( + Connection connection, String subject, Headers headers, byte[] data) { + return new AutoValue_NatsRequest(connection, subject, headers, getDataSize(data)); + } + + public static NatsRequest create(Connection connection, Message message) { + return new AutoValue_NatsRequest( + message.getConnection() == null ? connection : message.getConnection(), + message.getSubject(), + message.getHeaders(), + getDataSize(message.getData())); + } + + public abstract Connection getConnection(); + + public abstract String getSubject(); + + @Nullable + public abstract Headers getHeaders(); + + public abstract long getDataSize(); + + private static long getDataSize(byte[] data) { + return data == null ? 0 : data.length; + } +} diff --git a/instrumentation/nats/nats-2.21/library/src/test/java/io/opentelemetry/instrumentation/nats/v2_21/NatsPublishTest.java b/instrumentation/nats/nats-2.21/library/src/test/java/io/opentelemetry/instrumentation/nats/v2_21/NatsPublishTest.java new file mode 100644 index 000000000000..53e5723553fd --- /dev/null +++ b/instrumentation/nats/nats-2.21/library/src/test/java/io/opentelemetry/instrumentation/nats/v2_21/NatsPublishTest.java @@ -0,0 +1,98 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_BODY_SIZE; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.nats.client.Message; +import io.nats.client.impl.Headers; +import io.nats.client.impl.NatsMessage; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +@SuppressWarnings("deprecation") // using deprecated semconv +class NatsPublishTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Test + void testPublishMessageNoHeaders() { + // given + NatsTelemetry telemetry = NatsTelemetry.create(testing.getOpenTelemetry()); + TestConnection testConnection = new TestConnection(); + OpenTelemetryConnection connection = telemetry.wrap(testConnection); + NatsMessage message = NatsMessage.builder().subject("sub").data("x").build(); + + // when + testing.runWithSpan("testPublishMessage", () -> connection.publish(message)); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("testPublishMessage").hasNoParent(), + span -> + span.hasName("sub publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_SYSTEM, "nats"), + equalTo(MESSAGING_DESTINATION_NAME, "sub"), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, 1), + equalTo(AttributeKey.stringKey("messaging.client_id"), "1")))); + + // and + Message published = testConnection.publishedMessages.peekLast(); + assertNotNull(published); + assertThat(published.getHeaders()).isNull(); + } + + @Test + void testPublishMessageWithHeaders() { + // given + NatsTelemetry telemetry = NatsTelemetry.create(testing.getOpenTelemetry()); + TestConnection testConnection = new TestConnection(); + OpenTelemetryConnection connection = telemetry.wrap(testConnection); + NatsMessage message = + NatsMessage.builder().subject("sub").headers(new Headers()).data("x").build(); + + // when + testing.runWithSpan("testPublishMessage", () -> connection.publish(message)); + + // then + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> span.hasName("testPublishMessage").hasNoParent(), + span -> + span.hasName("sub publish") + .hasKind(SpanKind.PRODUCER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(MESSAGING_OPERATION, "publish"), + equalTo(MESSAGING_SYSTEM, "nats"), + equalTo(MESSAGING_DESTINATION_NAME, "sub"), + equalTo(MESSAGING_MESSAGE_BODY_SIZE, 1), + equalTo(AttributeKey.stringKey("messaging.client_id"), "1")))); + + // and + Message published = testConnection.publishedMessages.peekLast(); + assertNotNull(published); + assertThat(published.getHeaders().get("traceparent")).isNotEmpty(); + } +} diff --git a/instrumentation/nats/nats-2.21/metadata.yaml b/instrumentation/nats/nats-2.21/metadata.yaml new file mode 100644 index 000000000000..c386bbe8692a --- /dev/null +++ b/instrumentation/nats/nats-2.21/metadata.yaml @@ -0,0 +1,7 @@ +disabled_by_default: true +description: > + TODO +configurations: + - name: TODO + description: TODO + default: false diff --git a/instrumentation/nats/nats-2.21/testing/build.gradle.kts b/instrumentation/nats/nats-2.21/testing/build.gradle.kts new file mode 100644 index 000000000000..10338c07fb9d --- /dev/null +++ b/instrumentation/nats/nats-2.21/testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + + compileOnly("io.nats:jnats:2.21.0") +} diff --git a/instrumentation/nats/nats-2.21/testing/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/TestConnection.java b/instrumentation/nats/nats-2.21/testing/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/TestConnection.java new file mode 100644 index 000000000000..858138518c9f --- /dev/null +++ b/instrumentation/nats/nats-2.21/testing/src/main/java/io/opentelemetry/instrumentation/nats/v2_21/TestConnection.java @@ -0,0 +1,340 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.nats.v2_21; + +import io.nats.client.Connection; +import io.nats.client.ConnectionListener; +import io.nats.client.ConsumerContext; +import io.nats.client.Dispatcher; +import io.nats.client.ForceReconnectOptions; +import io.nats.client.JetStream; +import io.nats.client.JetStreamApiException; +import io.nats.client.JetStreamManagement; +import io.nats.client.JetStreamOptions; +import io.nats.client.KeyValue; +import io.nats.client.KeyValueManagement; +import io.nats.client.KeyValueOptions; +import io.nats.client.Message; +import io.nats.client.MessageHandler; +import io.nats.client.ObjectStore; +import io.nats.client.ObjectStoreManagement; +import io.nats.client.ObjectStoreOptions; +import io.nats.client.Options; +import io.nats.client.Statistics; +import io.nats.client.StreamContext; +import io.nats.client.Subscription; +import io.nats.client.api.ServerInfo; +import io.nats.client.impl.Headers; +import io.nats.client.impl.NatsMessage; +import java.io.IOException; +import java.net.InetAddress; +import java.time.Duration; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; + +public class TestConnection implements Connection { + + public final LinkedList publishedMessages = new LinkedList<>(); + + @Override + public void publish(String subject, byte[] body) { + this.publish(NatsMessage.builder().subject(subject).data(body).build()); + } + + @Override + public void publish(String subject, Headers headers, byte[] body) { + this.publish(NatsMessage.builder().subject(subject).headers(headers).data(body).build()); + } + + @Override + public void publish(String subject, String replyTo, byte[] body) { + this.publish(NatsMessage.builder().subject(subject).replyTo(replyTo).data(body).build()); + } + + @Override + public void publish(String subject, String replyTo, Headers headers, byte[] body) { + this.publish( + NatsMessage.builder() + .subject(subject) + .replyTo(replyTo) + .headers(headers) + .data(body) + .build()); + } + + @Override + public void publish(Message message) { + publishedMessages.add(message); + } + + @Override + public CompletableFuture request(String subject, byte[] body) { + return null; + } + + @Override + public Message request(String subject, byte[] body, Duration timeout) + throws InterruptedException { + return null; + } + + @Override + public CompletableFuture request(String subject, Headers headers, byte[] body) { + return null; + } + + @Override + public Message request(String subject, Headers headers, byte[] body, Duration timeout) + throws InterruptedException { + return null; + } + + @Override + public CompletableFuture request(Message message) { + return null; + } + + @Override + public Message request(Message message, Duration timeout) throws InterruptedException { + return null; + } + + @Override + public CompletableFuture requestWithTimeout( + String subject, byte[] body, Duration timeout) { + return null; + } + + @Override + public CompletableFuture requestWithTimeout( + String subject, Headers headers, byte[] body, Duration timeout) { + return null; + } + + @Override + public CompletableFuture requestWithTimeout(Message message, Duration timeout) { + return null; + } + + @Override + public Subscription subscribe(String subject) { + return null; + } + + @Override + public Subscription subscribe(String subject, String queueName) { + return null; + } + + @Override + public Dispatcher createDispatcher(MessageHandler handler) { + return null; + } + + @Override + public Dispatcher createDispatcher() { + return null; + } + + @Override + public void closeDispatcher(Dispatcher dispatcher) {} + + @Override + public void addConnectionListener(ConnectionListener connectionListener) {} + + @Override + public void removeConnectionListener(ConnectionListener connectionListener) {} + + @Override + public void flush(Duration timeout) throws TimeoutException, InterruptedException {} + + @Override + public CompletableFuture drain(Duration timeout) + throws TimeoutException, InterruptedException { + return null; + } + + @Override + public void close() throws InterruptedException {} + + @Override + public Status getStatus() { + return null; + } + + @Override + public long getMaxPayload() { + return 0; + } + + @Override + public Collection getServers() { + return Collections.emptyList(); + } + + @Override + public Statistics getStatistics() { + return null; + } + + @Override + public Options getOptions() { + return null; + } + + @Override + public ServerInfo getServerInfo() { + return new ServerInfo( + "{" + + "\"server_id\": \"SID\", " + + "\"server_name\": \"opentelemetry-nats\", " + + "\"version\": \"2.10.24\", " + + "\"go\": \"go1.23.4\", " + + "\"host\": \"0.0.0.0\", " + + "\"headers_supported\": true, " + + "\"auth_required\": true, " + + "\"nonce\": null, " + + "\"tls_required\": false, " + + "\"tls_available\": false, " + + "\"ldm\": false, " + + "\"jetstream\": false, " + + "\"port\": 4222, " + + "\"proto\": 1, " + + "\"max_payload\": 1048576, " + + "\"client_id\": 1, " + + "\"client_ip\": \"192.168.1.1\", " + + "\"cluster\": \"opentelemetry-nats\", " + + "\"connect_urls\": []" + + "}"); + } + + @Override + public String getConnectedUrl() { + return ""; + } + + @Override + public InetAddress getClientInetAddress() { + return null; + } + + @Override + public String getLastError() { + return ""; + } + + @Override + public void clearLastError() {} + + @Override + public String createInbox() { + return ""; + } + + @Override + public void flushBuffer() throws IOException {} + + @Override + public void forceReconnect() throws IOException, InterruptedException {} + + @Override + public void forceReconnect(ForceReconnectOptions options) + throws IOException, InterruptedException {} + + @Override + public Duration RTT() throws IOException { + return null; + } + + @Override + public StreamContext getStreamContext(String streamName) + throws IOException, JetStreamApiException { + return null; + } + + @Override + public StreamContext getStreamContext(String streamName, JetStreamOptions options) + throws IOException, JetStreamApiException { + return null; + } + + @Override + public ConsumerContext getConsumerContext(String streamName, String consumerName) + throws IOException, JetStreamApiException { + return null; + } + + @Override + public ConsumerContext getConsumerContext( + String streamName, String consumerName, JetStreamOptions options) + throws IOException, JetStreamApiException { + return null; + } + + @Override + public JetStream jetStream() throws IOException { + return null; + } + + @Override + public JetStream jetStream(JetStreamOptions options) throws IOException { + return null; + } + + @Override + public JetStreamManagement jetStreamManagement() throws IOException { + return null; + } + + @Override + public JetStreamManagement jetStreamManagement(JetStreamOptions options) throws IOException { + return null; + } + + @Override + public KeyValue keyValue(String bucketName) throws IOException { + return null; + } + + @Override + public KeyValue keyValue(String bucketName, KeyValueOptions options) throws IOException { + return null; + } + + @Override + public KeyValueManagement keyValueManagement() throws IOException { + return null; + } + + @Override + public KeyValueManagement keyValueManagement(KeyValueOptions options) throws IOException { + return null; + } + + @Override + public ObjectStore objectStore(String bucketName) throws IOException { + return null; + } + + @Override + public ObjectStore objectStore(String bucketName, ObjectStoreOptions options) throws IOException { + return null; + } + + @Override + public ObjectStoreManagement objectStoreManagement() throws IOException { + return null; + } + + @Override + public ObjectStoreManagement objectStoreManagement(ObjectStoreOptions options) + throws IOException { + return null; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 20d3eb0cdc82..e84edf82be15 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -407,6 +407,9 @@ include(":instrumentation:mongo:mongo-4.0:javaagent") include(":instrumentation:mongo:mongo-async-3.3:javaagent") include(":instrumentation:mongo:mongo-common:testing") include(":instrumentation:mybatis-3.2:javaagent") +include(":instrumentation:nats:nats-2.21:javaagent") +include(":instrumentation:nats:nats-2.21:library") +include(":instrumentation:nats:nats-2.21:testing") include(":instrumentation:netty:netty-3.8:javaagent") include(":instrumentation:netty:netty-4.0:javaagent") include(":instrumentation:netty:netty-4.1:javaagent")