Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
66be356
Bootstrap NATS instrumentation with Connection.publish(Message) tracing
AlixBa May 1, 2025
9757a46
don't propagate into headers when not possible
AlixBa May 2, 2025
830bf68
cover all publish methods with tests
AlixBa May 2, 2025
c4f11a6
Instrument NATS Subscription.nextMessage
AlixBa May 4, 2025
56e3656
Instrument NATS library 'request' method
AlixBa May 5, 2025
2bb53a2
Instrument NATS agent 'request' method
AlixBa May 29, 2025
c2ca73a
Instrument NATS library 'dispatcher/subscribe' methods
AlixBa Jun 1, 2025
fa341b2
Refactor tests/names & minor fixes
AlixBa Jun 8, 2025
b02272e
Instrument NATS agent 'dispatcher' methods
AlixBa Jun 8, 2025
2f78f45
./gradlew spotlessApply
otelbot[bot] Jun 8, 2025
487dffb
review & fixes
AlixBa Jun 21, 2025
5c4d6da
fix muzzle
AlixBa Jun 21, 2025
7d1b355
add captured-headers
AlixBa Jun 22, 2025
360a21f
fix dispatcher in library instrumentation
AlixBa Jun 22, 2025
4e0741d
Merge branch 'main' into nats-instrumentation
AlixBa Jun 27, 2025
ef671c7
Merge branch 'main' into nats-instrumentation
AlixBa Aug 25, 2025
617961d
Merge branch 'main' into nats-instrumentation
AlixBa Aug 27, 2025
dd00a7f
generate metadata
AlixBa Aug 27, 2025
e43d741
Merge branch 'main' into nats-instrumentation
AlixBa Aug 27, 2025
1c88b1c
remove muzzle check for version without dependency
AlixBa Aug 27, 2025
f289e05
rename nats-2.21 to nats-2.17
AlixBa Aug 28, 2025
3aae560
review fixes
AlixBa Aug 28, 2025
6c092ad
disabled_by_default: false
AlixBa Aug 28, 2025
fefeebd
use @BeforeAll over static init
AlixBa Aug 28, 2025
01a48a8
<publish/request>(args) to <publish/request>(Message)
AlixBa Aug 29, 2025
bdad7d3
remove receive spans
AlixBa Aug 29, 2025
041a7a6
fix to minimal version
AlixBa Aug 29, 2025
bad1806
Merge branch 'main' into nats-instrumentation
AlixBa Aug 29, 2025
85a0d63
./gradlew spotlessApply
otelbot[bot] Aug 29, 2025
0a89c79
remove unwanted code
AlixBa Aug 29, 2025
df431c4
add tests on default inbox monitoring
AlixBa Aug 29, 2025
6a3baf1
remove readOnly=false on headers
AlixBa Aug 29, 2025
84ee013
forward all calls to (subject, replyTo, headers, data) instead of (me…
AlixBa Sep 3, 2025
a2d15b4
review fixes
AlixBa Sep 6, 2025
9b0d45f
Merge branch 'main' into nats-instrumentation
AlixBa Sep 6, 2025
fec879b
reviews
AlixBa Sep 12, 2025
d6ec30e
Merge branch 'main' into nats-instrumentation
AlixBa Sep 12, 2025
34e2f1d
add test on Request methods
AlixBa Sep 16, 2025
e7fb172
Merge branch 'main' into nats-instrumentation
laurit Sep 17, 2025
043c857
review
laurit Sep 17, 2025
e599182
switch back to messaging.client_id, it is renamed to messaging.client…
laurit Sep 17, 2025
bb41af8
Merge branch 'main' into nats-instrumentation
jaydeluca Oct 2, 2025
2d652da
Update instrumentation/nats/nats-2.17/library/README.md
AlixBa Oct 2, 2025
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
6 changes: 6 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ out/
######################
.vscode
**/bin/
.metals

# Others #
##########
Expand Down
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ These are the supported libraries and frameworks:
| [Micrometer](https://micrometer.io/) | 1.5+ (disabled by default) | [opentelemetry-micrometer-1.5](../instrumentation/micrometer/micrometer-1.5/library) | none |
| [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | [opentelemetry-mongo-3.1](../instrumentation/mongo/mongo-3.1/library) | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
| [MyBatis](https://mybatis.org/mybatis-3/) | 3.2+ | N/A | none |
| [NATS](https://github.com/nats-io/nats.java) | 3.8+ | [nats-2.21](../instrumentation/nats/nats-2.21/library) | [Messaging Spans] |
| [Netty HTTP codec [5]](https://github.com/netty/netty) | 3.8+ | [opentelemetry-netty-4.1](../instrumentation/netty/netty-4.1/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
| [OpenSearch Rest Client](https://github.com/opensearch-project/opensearch-java) | 1.0+ | | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
| [OkHttp](https://github.com/square/okhttp/) | 2.2+ | [opentelemetry-okhttp-3.0](../instrumentation/okhttp/okhttp-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics] |
Expand Down
20 changes: 20 additions & 0 deletions instrumentation/nats/nats-2.21/javaagent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Auth-instrumentation for NATS version 2.21

Provides OpenTelemetry auto-instrumentation for [NATS 2.21](https://github.com/nats-io/nats.java).

### Trace propagation

It's recommended to provide `Message` with a writable `Header` structure
to allow propagation between publishers and subscribers. Without headers,
the tracing context will not be propagated in the headers.

```java
import io.nats.client.impl.Headers;
import io.nats.client.impl.NatsMessage;

// don't
Message msg = NatsMessage.builder().subject("sub").build();

// do
Message msg = NatsMessage.builder().subject("sub").headers(new Headers()).build();
```
39 changes: 39 additions & 0 deletions instrumentation/nats/nats-2.21/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("io.nats")
module.set("jnats")
versions.set("[2.17.2,)")
assertInverse.set(true)
}
}

dependencies {
library("io.nats:jnats:2.21.0")

implementation(project(":instrumentation:nats:nats-2.21:library"))
testImplementation(project(":instrumentation:nats:nats-2.21:testing"))
}

tasks {
val testMessagingReceive by registering(Test::class) {
filter {
includeTestsMatching("NatsInstrumentationMessagingReceiveTest")
}
jvmArgs("-Dotel.instrumentation.messaging.experimental.receive-telemetry.enabled=true")
}

test {
usesService(gradle.sharedServices.registrations["testcontainersBuildService"].service)
filter {
excludeTestsMatching("NatsInstrumentationMessagingReceiveTest")
}
}

check {
dependsOn(testMessagingReceive)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.nats.v2_21;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import java.util.concurrent.CompletableFuture;

public final class CompletableFutureWrapper {

private CompletableFutureWrapper() {}

public static <T> CompletableFuture<T> wrap(CompletableFuture<T> future, Context context) {
CompletableFuture<T> result = new CompletableFuture<>();
future.whenComplete(
(T value, Throwable throwable) -> {
try (Scope ignored = context.makeCurrent()) {
if (throwable != null) {
result.completeExceptionally(throwable);
} else {
result.complete(value);
}
}
});

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* 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.nats.client.impl.Headers;
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 ConnectionPublishInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("io.nats.client.Connection"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isPublic()
.and(named("publish"))
.and(takesArguments(2))
.and(takesArgument(0, String.class))
.and(takesArgument(1, byte[].class)),
ConnectionPublishInstrumentation.class.getName() + "$PublishBodyAdvice");
transformer.applyAdviceToMethod(
isPublic()
.and(named("publish"))
.and(takesArguments(3))
.and(takesArgument(0, String.class))
.and(takesArgument(1, named("io.nats.client.impl.Headers")))
.and(takesArgument(2, byte[].class)),
ConnectionPublishInstrumentation.class.getName() + "$PublishHeadersBodyAdvice");
transformer.applyAdviceToMethod(
isPublic()
.and(named("publish"))
.and(takesArguments(3))
.and(takesArgument(0, String.class))
.and(takesArgument(1, String.class))
.and(takesArgument(2, byte[].class)),
ConnectionPublishInstrumentation.class.getName() + "$PublishReplyToBodyAdvice");
transformer.applyAdviceToMethod(
isPublic()
.and(named("publish"))
.and(takesArguments(4))
.and(takesArgument(0, String.class))
.and(takesArgument(1, String.class))
.and(takesArgument(2, named("io.nats.client.impl.Headers")))
.and(takesArgument(3, byte[].class)),
ConnectionPublishInstrumentation.class.getName() + "$PublishReplyToHeadersBodyAdvice");
transformer.applyAdviceToMethod(
isPublic()
.and(named("publish"))
.and(takesArguments(1))
.and(takesArgument(0, named("io.nats.client.Message"))),
ConnectionPublishInstrumentation.class.getName() + "$PublishMessageAdvice");
}

@SuppressWarnings("unused")
public static class PublishBodyAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This Connection connection,
@Advice.Argument(0) String subject,
@Advice.Argument(1) byte[] body,
@Advice.Local("otelContext") Context otelContext,
@Advice.Local("otelScope") Scope otelScope,
@Advice.Local("natsRequest") NatsRequest natsRequest) {
Context parentContext = Context.current();
natsRequest = NatsRequest.create(connection, null, subject, null, body);

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.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);
}
}

@SuppressWarnings("unused")
public static class PublishHeadersBodyAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This Connection connection,
@Advice.Argument(0) String subject,
@Advice.Argument(1) Headers headers,
@Advice.Argument(2) byte[] body,
@Advice.Local("otelContext") Context otelContext,
@Advice.Local("otelScope") Scope otelScope,
@Advice.Local("natsRequest") NatsRequest natsRequest) {
Context parentContext = Context.current();
natsRequest = NatsRequest.create(connection, null, subject, headers, body);

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.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);
}
}

@SuppressWarnings("unused")
public static class PublishReplyToBodyAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This Connection connection,
@Advice.Argument(0) String subject,
@Advice.Argument(1) String replyTo,
@Advice.Argument(2) byte[] body,
@Advice.Local("otelContext") Context otelContext,
@Advice.Local("otelScope") Scope otelScope,
@Advice.Local("natsRequest") NatsRequest natsRequest) {
Context parentContext = Context.current();
natsRequest = NatsRequest.create(connection, replyTo, subject, null, body);

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.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);
}
}

@SuppressWarnings("unused")
public static class PublishReplyToHeadersBodyAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.This Connection connection,
@Advice.Argument(0) String subject,
@Advice.Argument(1) String replyTo,
@Advice.Argument(2) Headers headers,
@Advice.Argument(3) byte[] body,
@Advice.Local("otelContext") Context otelContext,
@Advice.Local("otelScope") Scope otelScope,
@Advice.Local("natsRequest") NatsRequest natsRequest) {
Context parentContext = Context.current();
natsRequest = NatsRequest.create(connection, replyTo, subject, headers, body);

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.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);
}
}

@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.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);
}
}
}
Loading
Loading