Drop a JAR on the JVM. Get traces, metrics, and logs. No code changes.
| Stack | Agent |
|---|---|
| Vert.x 3.9+ / RxJava 2 | vertx3-otel-agent.jar |
| Vert.x 4.5+ / RxJava 3 | vertx4-otel-agent.jar |
export OTEL_SERVICE_NAME=my-service
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.last9.io
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <token>"
java -javaagent:vertx3-otel-agent.jar -jar my-app.jarThat's it. Every HTTP endpoint, database query, Kafka message, cache operation, and outbound HTTP call is traced automatically.
Download the latest agent from Releases:
# Vert.x 3
curl -L -o vertx3-otel-agent.jar \
https://github.com/last9/vertx-opentelemetry/releases/download/v2.3.4/vertx3-otel-agent-2.3.4.jar
# Vert.x 4
curl -L -o vertx4-otel-agent.jar \
https://github.com/last9/vertx-opentelemetry/releases/download/v2.3.4/vertx4-otel-agent-2.3.4.jarByteBuddy instruments these at class-load time — no wrappers, no annotations, no XML:
| Component | Span Kind | Key Attributes |
|---|---|---|
| Netty HTTP server | SERVER | http.request.method, url.path, http.response.status_code |
| Router (RxJava2 + core) | SERVER | http.route (pattern, not literal path) |
| WebClient | CLIENT | url.full, server.address, http.response.status_code |
| JDBCClient | CLIENT | db.system, db.name, db.statement |
| KafkaProducer | PRODUCER | messaging.destination.name |
| KafkaConsumer | CONSUMER | messaging.batch.message_count |
| AerospikeClient | CLIENT | db.system=aerospike, net.peer.name |
| MySQLPool / PgPool | CLIENT | db.system, db.statement |
| Jedis (Pool, Cluster, Pipeline) | CLIENT | db.system=redis, db.statement |
| Lettuce (sync/async/reactive) | CLIENT | db.system=redis, db.statement |
Raw JDBC (Statement.execute*) |
CLIENT | db.system (auto-detected), db.statement |
| Netty HTTP client | CLIENT | http.method, net.peer.name |
| RESTEasy (JAX-RS) | — | @Path templates → http.route |
| AWS SQS (SDK v1 + v2) | CONSUMER | messaging.system=AmazonSQS |
The agent injects TracingOptions at startup, activating the native VertxTracer SPI. No ByteBuddy needed for the core stack:
| Component | How | What you get |
|---|---|---|
| HTTP server | VertxTracer SPI | SERVER spans for every request |
| HTTP client | VertxTracer SPI | CLIENT spans + traceparent injection |
| EventBus | VertxTracer SPI | INTERNAL spans for send/publish |
| SQL client (PgPool, MySQLPool) | VertxTracer SPI | CLIENT spans with SQL |
| Redis client | VertxTracer SPI | CLIENT spans |
| Kafka | VertxTracer SPI | PRODUCER/CONSUMER spans |
| Router | ByteBuddy | Route patterns: GET /v1/users/:id |
| Vert.x metrics | Micrometer → OTel bridge | HTTP pools, event bus, event loop lag |
| Jedis, Lettuce, JDBC, Aerospike, RESTEasy, SQS | ByteBuddy | Same as v3 |
- RxJava context propagation — trace context flows across
subscribeOn,observeOn,flatMap - W3C
traceparent— injected on every outgoing HTTP and Kafka call - Log-trace correlation —
trace_idandspan_idauto-injected into every Logback line. Nologback.xmlchanges. - JVM metrics — memory, GC, threads, CPU
- Cloud resource detection — AWS (EC2, ECS, EKS) and GCP (GCE, GKE, Cloud Run)
The OTel Java Agent assumes ThreadLocal context propagation. Vert.x doesn't use ThreadLocal. The result: broken spans on async HTTP calls (#11860), lost context across RxJava operators, and broken virtual thread support on Java 21 (#10526).
This library works with Vert.x's event-loop model — handler-based instrumentation for v3, native VertxTracer SPI for v4, RxJava assembly hooks for context propagation across all operators.
Java 8+. The agent uses the OkHttp-based OTLP sender (shaded as io.last9.internal.okhttp3) instead of java.net.http.HttpClient. Same JAR on Java 8, 11, 17, 21.
curl -L -o /opt/otel/vertx3-otel-agent.jar \
https://github.com/last9/vertx-opentelemetry/releases/download/v2.3.4/vertx3-otel-agent-2.3.4.jar
java -javaagent:/opt/otel/vertx3-otel-agent.jar -jar /opt/app/my-app.jarFROM eclipse-temurin:11-jre-alpine
COPY target/my-app.jar /app/my-app.jar
COPY vertx3-otel-agent.jar /app/vertx3-otel-agent.jar
CMD ["java", "-javaagent:/app/vertx3-otel-agent.jar", "-jar", "/app/my-app.jar"]- A 2-class shim (
AgentBootstrap) loads on the system classloader — compiled to Java 8 bytecode - The embedded library JAR (
agent-impl.jar) is injected onto the system classloader - All OTel SDK, ByteBuddy, and OkHttp classes are shaded under
io.last9.internal.*— no classpath conflicts even if your app bundles its own OTel SDK - ByteBuddy transformers intercept Vert.x APIs at load time
- RxJava context propagation hooks are installed automatically
- Log-trace correlation is auto-installed into Logback
No Maven dependency required. Drop the JAR and go.
Do not add
io.last9:vertx3-rxjava2-otel-autoconfigure(or the v4 equivalent) as a Maven dependency when using-javaagent. The app's unshaded classes will shadow the agent's shaded ones — you'll get a no-op tracer and zero spans.
All standard OpenTelemetry environment variables work:
| Variable | Default |
|---|---|
OTEL_SERVICE_NAME |
unknown-service |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4318 |
OTEL_EXPORTER_OTLP_HEADERS |
— |
OTEL_EXPORTER_OTLP_TIMEOUT |
10000 |
OTEL_RESOURCE_ATTRIBUTES |
— |
OTEL_TRACES_SAMPLER |
parentbased_always_on |
OTEL_LOGS_EXPORTER |
otlp |
OTEL_METRICS_EXPORTER |
otlp |
OTEL_METRIC_EXPORT_INTERVAL |
60000 |
OTEL_BSP_SCHEDULE_DELAY |
5000 |
Set OTEL_EXPORTER_OTLP_TIMEOUT=30000 when exporting to remote backends.
Disconnected traces (outgoing calls show as separate root spans)
- Look for
bytecode instrumentation installedin logs — confirms the agent loaded - Look for
RxJava context propagation installed— confirms async context flows - Verify the downstream service is also OTel-instrumented and reads
traceparent
No spans exported
- Check
OTEL_EXPORTER_OTLP_ENDPOINTis reachable - Check
OTEL_EXPORTER_OTLP_HEADERSauth is correct - Look for
Connection refusedin stderr - Set
OTEL_LOG_LEVEL=debugfor verbose export logs
CLIENT spans present but no SERVER spans
- Look for
transformed io.vertx.reactivex.ext.web.Router (loaded=false)andHttpServerAdviceHelper: wrapping requestHandlerin logs - Look for
Tracer OK (io.last9.internal.otel.sdk.trace.SdkTracer)on stderr — if you seeTracer is NO-OP, SDK init failed - Look for
WARNING: HttpServerAdviceHelper is missing version marker— this means the app bundles a conflictingio.last9library dependency. Remove it.
| Agent | Java | Vert.x | RxJava |
|---|---|---|---|
vertx3-otel-agent |
8+ | 3.9+ | 2.x |
vertx4-otel-agent |
11+ | 4.5+ | 3.x |
Use the agent. Library mode exists for environments where
-javaagentis not feasible.
<dependency>
<groupId>io.last9</groupId>
<artifactId>vertx4-rxjava3-otel-autoconfigure</artifactId>
<version>2.3.4</version>
</dependency>// Use OtelLauncher as main class
// <mainClass>io.last9.tracing.otel.v4.OtelLauncher</mainClass>
Router router = TracedRouter.create(vertx);
TracedDBPool traced = TracedDBPool.wrap(PgPool.pool(vertx, opts, poolOpts), "postgresql", "mydb");
consumer.batchHandler(KafkaTracing.tracedBatchHandler(topicName, this::handleBatch));<dependency>
<groupId>io.last9</groupId>
<artifactId>vertx3-rxjava2-otel-autoconfigure</artifactId>
<version>2.3.4</version>
</dependency>// <mainClass>io.last9.tracing.otel.v3.OtelLauncher</mainClass>
Router router = TracedRouter.create(vertx);
WebClient client = TracedWebClient.create(vertx);
SQLClient sql = TracedSQLClient.wrap(JDBCClient.createShared(vertx, config), "mysql", "mydb");
TracedAerospikeClient aero = TracedAerospikeClient.wrap(new AerospikeClient("localhost", 3000), "ns");
TracedKafkaProducer<String, String> producer = TracedKafkaProducer.wrap(KafkaProducer.create(vertx, config));
TracedKafkaConsumer.create(vertx, config, "topic", "group", records -> { ... });import io.last9.tracing.otel.OtelSdkSetup;
import io.last9.tracing.otel.v3.RxJava2ContextPropagation; // or v4.RxJava3ContextPropagation
OtelSdkSetup.initialize();
RxJava2ContextPropagation.install();MIT