Vertx OpenTracing

Vert.x integrates with OpenTracing thanks to the Jaeger client.

You can configure Vert.x to use the Jaeger client configured via Environment

Vertx vertx = Vertx.vertx(new VertxOptions()
  .setTracingOptions(
    new OpenTracingOptions()
  )
);

You can also pass a custom Tracer allowing for greater control over the configuration.

Vertx vertx = Vertx.vertx(new VertxOptions()
  .setTracingOptions(
    new OpenTracingOptions(tracer)
  )
);

Tracing policy

The tracing policy defines the behavior of a component when tracing is enabled:

  • PROPAGATE: the component reports a span in the active trace

  • ALWAYS: the component reports a span in the active trace or creates a new active trace

  • IGNORE: the component will not be involved in any trace.

The tracing policy is usually configured in the component options.

HTTP tracing

The Vert.x HTTP server and client reports span around HTTP requests:

  • operationName: the HTTP method

  • tags

  • http.method: the HTTP method

  • http.url: the request URL

  • http.status_code: the HTTP status code

The default HTTP server tracing policy is ALWAYS, you can configure the policy with setTracingPolicy

HttpServer server = vertx.createHttpServer(new HttpServerOptions()
  .setTracingPolicy(TracingPolicy.IGNORE)
);

The default HTTP client tracing policy is PROPAGATE, you can configure the policy with setTracingPolicy

HttpClient client = vertx.createHttpClient(new HttpClientOptions()
  .setTracingPolicy(TracingPolicy.IGNORE)
);

To initiate a trace for a client call, you need to create it first and make Vert.x aware of it by using OpenTracingUtil.setSpan:

Span span = tracer.buildSpan("my-operation")
  .withTag("some-key", "some-value")
  .start();
OpenTracingUtil.setSpan(span);
// Do something, e.g. client request
span.finish();

In an HTTP scenario between two Vert.x services, a span will be created client-side, then the trace context will be propagated server-side and another span will be added to the trace.

EventBus tracing

The Vert.x EventBus reports spans around message exchanges.

The default sending policy is PROPAGATE, you can configure the policy with setTracingPolicy.

DeliveryOptions options = new DeliveryOptions().setTracingPolicy(TracingPolicy.ALWAYS);
vertx.eventBus().send("the-address", "foo", options);

Obtain current Span

Vert.x stores current Span object in local context. To obtain it, use method OpenTracingUtil.getSpan().

This method will work only on Vert.x threads (instances of VertxThread). Obtaining from non-Vert.x thread doesn’t work by design, method will return null.

Coroutines support

There is no direct support for coroutines, but it can be achieved with minimal instrumentation.

There are several steps to achieve this.

  1. Use CoroutineVerticle.

  2. Convert every route handler you have to a coroutine.

  3. Use CoroutineContext to store Tracer and current Span object

Example code:

class TracedVerticle(private val tracer: Tracer): CoroutineVerticle() {
   override suspend fun start() {
       val router = Router.router(vertx)

       router.route("/hello1")
           .method(HttpMethod.GET)
           .coroutineHandler { ctx ->                          // (1)
               launch { println("Hello to Console") }
               ctx.end("Hello from coroutine handler")
           }

       router.route("/hello2")
           .method(HttpMethod.GET)
           .coroutineHandler(::nonSuspendHandler)              // (2)

       vertx.createHttpServer()
           .requestHandler(router)
           .listen(8080)
           .await()
   }

   private fun nonSuspendHandler(ctx: RoutingContext) {
       ctx.end("Hello from usual handler")
   }

   private fun Route.coroutineHandler(handler: Handler<RoutingContext>): Route = // (3)
       this.coroutineHandler(handler::handle)

   private fun Route.coroutineHandler(                                           // (4)
       handler: suspend (RoutingContext) -> (Unit)
   ): Route = handler { ctx ->
       val span: Span = OpenTracingUtil.getSpan()                                // (5)
       launch(ctx.vertx().dispatcher() + SpanElement(tracer, span)) {            // (6)
           val spanElem = coroutineContext[SpanElement]                          // (7)
           if (spanElem == null) {
               handler(ctx)
           } else {
               val span = spanElem.span
               val tracer = spanElem.tracer
               val childSpan = span                                                // (8)
               try {
                   withContext(SpanElement(tracer, childSpan)) { handler(ctx) }    // (9)
               } finally {
                   // childSpan.finish()                                           // (10)
               }
           }
           // OR create a helper method for further reuse
           withContextTraced(coroutineContext) {
               try {
                   handler(ctx)
               } catch (t: Throwable) {
                   ctx.fail(t)
               }
           }
       }
   }
}
  1. Creates a coroutine handler with coroutineHandler extension method.

  2. Creates usual async handler, which is then wrapped to a coroutine.

  3. Extension method to convert Handler<RoutingContext> to suspendable function.

  4. Extension method which creates and launches a coroutine on Vert.x EventLoop.

  5. Get current Span from Vert.x local context (populated automatically).

  6. Create a wrapper coroutine, add current Span to CoroutineContext.

  7. Retrieve a Span from coroutine context.

  8. Either reuse span or create a new span with tracer.buildSpan("").asChildOf(span).start().

  9. Put a new Span to a context.

  10. Finish childSpan, if you created a new one.

Helper code, your implementation may vary:

/**
* Keeps references to a tracer and current Span inside CoroutineContext
*/
class SpanElement(val tracer: Tracer, val span: Span) :
   ThreadContextElement<Scope>,
   AbstractCoroutineContextElement(SpanElement) {

   companion object Key : CoroutineContext.Key<SpanElement>

   /**
   *  Will close current [Scope] after continuation's pause.
   */
   override fun restoreThreadContext(context: CoroutineContext, oldState: Scope) {
       oldState.close()
   }

   /**
   * Will create a new [Scope] after each continuation's resume, scope is activated with provided [span] instance.
   */
   override fun updateThreadContext(context: CoroutineContext): Scope {
       return tracer.activateSpan(span)
   }
}

/**
* Advanced helper method with a few options, also shows how to use MDCContext to pass a Span to a logger.
*/
suspend fun <T> withContextTraced(
   context: CoroutineContext,
   reuseParentSpan: Boolean = true,
   block: suspend CoroutineScope.() -> T
): T {
   return coroutineScope {
       val spanElem = this.coroutineContext[SpanElement]

       if (spanElem == null) {
           logger.warn { "Calling 'withTracer', but no span found in context" }
           withContext(context, block)
       } else {
           val childSpan = if (reuseParentSpan) spanElem.span
           else spanElem.tracer.buildSpan("").asChildOf(spanElem.span).start()

           try {
               val mdcSpan = mapOf(MDC_SPAN_KEY to childSpan.toString())
               withContext(context + SpanElement(spanElem.tracer, childSpan) + MDCContext(mdcSpan), block)
           } finally {
               if (!reuseParentSpan) childSpan.finish()
           }
       }
   }
}
private const val MDC_SPAN_KEY = "request.span.id"