Vert.x
Zero-code OpenTelemetry instrumentation for Vert.x 3/4 + RxJava 2/3 applications — traces, logs, and JVM metrics to Last9
This guide shows how to instrument a Vert.x application with OpenTelemetry using the
vertx-opentelemetry library and send traces, logs, and JVM metrics to Last9.
The library supports both Vert.x 4 + RxJava 3 and Vert.x 3 + RxJava 2. It works entirely through environment variables — no code changes required beyond a one-line main class switch and adding a router call.
Prerequisites
- Java 11 or later
- Vert.x 4.x (or Vert.x 3.9.x for the legacy module)
- Logback as the SLF4J implementation (for MDC trace injection)
- Last9 account with OTLP endpoint configured
Step 1: Add the dependency
Maven
<dependency> <groupId>io.last9</groupId> <artifactId>vertx4-rxjava3-otel-autoconfigure</artifactId> <version>1.5.0</version></dependency>Gradle
implementation 'io.last9:vertx4-rxjava3-otel-autoconfigure:1.5.0'Maven
<dependency> <groupId>io.last9</groupId> <artifactId>vertx3-rxjava2-otel-autoconfigure</artifactId> <version>1.5.0</version></dependency>Gradle
implementation 'io.last9:vertx3-rxjava2-otel-autoconfigure:1.5.0'The library ships as a fat JAR with all OpenTelemetry SDK dependencies shaded internally — you don’t need to add OTel SDK dependencies separately.
Step 2: Change the main class
-
Switch your launcher to
OtelLauncherIn your Maven or Gradle build, change the main class from
io.vertx.core.Launcherto the OTel launcher:<!-- Maven: pom.xml --><properties><mainClass>io.last9.tracing.otel.v4.OtelLauncher</mainClass></properties>// Gradle: build.gradlemainClassName = 'io.last9.tracing.otel.v4.OtelLauncher'<!-- Maven: pom.xml --><properties><mainClass>io.last9.tracing.otel.v3.OtelLauncher</mainClass></properties>// Gradle: build.gradlemainClassName = 'io.last9.tracing.otel.v3.OtelLauncher'OtelLauncherextends the standard Vert.xLauncher. It auto-configures the OpenTelemetry SDK, installs the RxJava context propagation hooks, and enables Vert.x tracing — all before your verticles deploy. -
Use
TracedRouterinstead ofRouterReplace the standard Vert.x
Routerfactory withTracedRouterto get HTTP SERVER spans with proper route pattern names:import io.last9.tracing.otel.v4.TracedRouter;// Before:// Router router = Router.router(vertx);// After:Router router = TracedRouter.create(vertx);import io.last9.tracing.otel.v3.TracedRouter;// Before:// Router router = Router.router(vertx);// After:Router router = TracedRouter.create(vertx);Without
TracedRouter, HTTP spans are named with just the HTTP method (e.g.,GET).TracedRouterupdates span names to include the matched route pattern (e.g.,GET /v1/users/:id) after route matching completes.
Step 3: Configure Logback for trace injection (optional)
To include trace_id and span_id in log lines, add the MdcTraceTurboFilter to your logback.xml:
<configuration> <turboFilter class="io.last9.tracing.otel.MdcTraceTurboFilter"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{trace_id} span_id=%X{span_id} - %msg%n</pattern> </encoder> </appender>
<root level="INFO"> <appender-ref ref="CONSOLE"/> </root></configuration>This injects trace context directly from Span.current() before each log event, ensuring logs are correlated with
traces even across RxJava thread boundaries.
Step 4: Set environment variables
export OTEL_SERVICE_NAME="your-service-name"export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"export OTEL_TRACES_EXPORTER="otlp"export OTEL_METRICS_EXPORTER="otlp"export OTEL_LOGS_EXPORTER="otlp"export OTEL_TRACES_SAMPLER="parentbased_always_on"Find your OTLP endpoint and auth header in the Last9 console under Integrations → OpenTelemetry.
Cloud resource detection
Enable resource providers for your cloud environment so resource attributes (cloud.provider, host.name, etc.)
are automatically attached to all telemetry:
# AWS (EC2, ECS, EKS, Lambda)export OTEL_RESOURCE_PROVIDERS_AWS_ENABLED=true
# GCP (GCE, GKE, Cloud Run, App Engine)export OTEL_RESOURCE_PROVIDERS_GCP_ENABLED=trueSampling for high-traffic services
For services handling more than 10k requests per second, use ratio-based sampling to control costs:
export OTEL_TRACES_SAMPLER=parentbased_traceidratioexport OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of new tracesparentbased_traceidratio preserves the sampling decision from upstream services — if an upstream
service sampled the trace, all downstream spans continue to be recorded.
What gets traced automatically
| Signal | Coverage |
|---|---|
| HTTP Server | All routes via TracedRouter — spans named METHOD /path/:param |
| HTTP Client | Outgoing requests via WebClient (Vert.x 4) |
| EventBus | Publish/request/reply messages |
| SQL (PostgreSQL, MySQL) | Via Vert.x SQL client (Vert.x 4) |
| Redis | Via vertx-redis-client (Vert.x 4) |
| Kafka | Via vertx-kafka-client (Vert.x 4) |
| RxJava operators | Context preserved across subscribeOn, observeOn, flatMap |
| JVM metrics | Memory, GC, threads, CPU, class loading |
| Logs | Correlated with traces via MDC + OTLP export |
Verify the integration
Start your application and make a few HTTP requests. You should see:
- Traces in Last9 with spans for HTTP requests named by route pattern
- Logs with
trace_idandspan_idfields linking to the corresponding trace - JVM metrics (memory, GC, CPU) in Last9 under the service name
For troubleshooting, set OTEL_LOG_LEVEL=debug to see detailed SDK initialization logs.