Skip to content
Last9
Book demo

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'

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

  1. Switch your launcher to OtelLauncher

    In your Maven or Gradle build, change the main class from io.vertx.core.Launcher to the OTel launcher:

    <!-- Maven: pom.xml -->
    <properties>
    <mainClass>io.last9.tracing.otel.v4.OtelLauncher</mainClass>
    </properties>
    // Gradle: build.gradle
    mainClassName = 'io.last9.tracing.otel.v4.OtelLauncher'

    OtelLauncher extends the standard Vert.x Launcher. It auto-configures the OpenTelemetry SDK, installs the RxJava context propagation hooks, and enables Vert.x tracing — all before your verticles deploy.

  2. Use TracedRouter instead of Router

    Replace the standard Vert.x Router factory with TracedRouter to 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);

    Without TracedRouter, HTTP spans are named with just the HTTP method (e.g., GET). TracedRouter updates 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=true

Sampling 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_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of new traces

parentbased_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

SignalCoverage
HTTP ServerAll routes via TracedRouter — spans named METHOD /path/:param
HTTP ClientOutgoing requests via WebClient (Vert.x 4)
EventBusPublish/request/reply messages
SQL (PostgreSQL, MySQL)Via Vert.x SQL client (Vert.x 4)
RedisVia vertx-redis-client (Vert.x 4)
KafkaVia vertx-kafka-client (Vert.x 4)
RxJava operatorsContext preserved across subscribeOn, observeOn, flatMap
JVM metricsMemory, GC, threads, CPU, class loading
LogsCorrelated with traces via MDC + OTLP export

Verify the integration

Start your application and make a few HTTP requests. You should see:

  1. Traces in Last9 with spans for HTTP requests named by route pattern
  2. Logs with trace_id and span_id fields linking to the corresponding trace
  3. JVM metrics (memory, GC, CPU) in Last9 under the service name

For troubleshooting, set OTEL_LOG_LEVEL=debug to see detailed SDK initialization logs.