Inject and Extract


Introduction

In order to trace across process boundaries and RPC calls in distributed systems, spanContext needs to propagated over the wire. The OpenTracing Java API provides two methods in the Tracer interface to do just that, inject(SpanContext, format, carrier) and extract(format, carrier).

Format Options and Carriers

The format parameter refers to one of the three standard encodings the OpenTracing API defines:

  1. TEXT_MAP where spanContext is encoded as a collection of string key-value pairs,
  2. BINARY where spanContext is encoded as an opaque byte array,
  3. HTTP_HEADERS, which is similar to TEXT_MAP except that the keys must be safe to be used as HTTP headers.

The carrier is an abstraction over the underlying RPC framework. For example, a carrier for TEXT_MAP format is an interface that allows the tracer to write key-value pairs via put(key, value) method, while a carrier for BINARY format is simply a ByteBuffer.

Injecting and Extracting HTTP

When your service calls a downstream service, it’s useful to pass down the SpanContext, so that Spans generated by this service could join the Spans from our service in a single trace. To do that, our service needs to Inject the SpanContext into the payload. On the other side of the connection, the downstream service needs then to Extract the SpanContext before it creates any Spans.

Note that manually dealing with the Inject and Extract operations is not common in Java: there are OpenTracing instrumentation libraries that take care of this for you, such as the java-web-servlet-filter for the Extract operation, or one of the following for the Inject operation:

You are still encouraged to read on, to understand what happens behind the scenes.

Injecting the spanContext to HTTP

In order to pass a spanContext over the HTTP request, the developer needs to call the tracer.inject before building the HTTP request, like so:

Tags.SPAN_KIND.set(tracer.activeSpan(), Tags.SPAN_KIND_CLIENT);
Tags.HTTP_METHOD.set(tracer.activeSpan(), "GET");
Tags.HTTP_URL.set(tracer.activeSpan(), url.toString());
tracer.inject(tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new RequestBuilderCarrier(requestBuilder));

Notice that a couple additional tags have been added to the span with some metadata about the HTTP request, following the OpenTracing Semantic Conventions. In this case the carrier is HTTP request headers object, which can be adapted to the carrier API by wrapping it in a helper class as follows:

public class RequestBuilderCarrier implements io.opentracing.propagation.TextMap {
    private final Request.Builder builder;

    RequestBuilderCarrier(Request.Builder builder) {
        this.builder = builder;
    }

    @Override
    public Iterator<Map.Entry<String, String>> iterator() {
        throw new UnsupportedOperationException("carrier is write-only");
    }

    @Override
    public void put(String key, String value) {
        builder.addHeader(key, value);
    }
}

Extracting the Span’s Context from Incoming Request

The logic on the client side instrumentation is similar, the only difference is that tracer.extract is used and the span is tagged as span.kind=server.

public static Scope startServerSpan(Tracer tracer, javax.ws.rs.core.HttpHeaders httpHeaders,
        String operationName) {
    // format the headers for extraction
    MultivaluedMap<String, String> rawHeaders = httpHeaders.getRequestHeaders();
    final HashMap<String, String> headers = new HashMap<String, String>();
    for (String key : rawHeaders.keySet()) {
        headers.put(key, rawHeaders.get(key).get(0));
    }

    Tracer.SpanBuilder spanBuilder;
    try {
        SpanContext parentSpan = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapExtractAdapter(headers));
        if (parentSpan == null) {
            spanBuilder = tracer.buildSpan(operationName);
        } else {
            spanBuilder = tracer.buildSpan(operationName).asChildOf(parentSpan);
        }
    } catch (IllegalArgumentException e) {
        spanBuilder = tracer.buildSpan(operationName);
    }
    return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).startActive(true);
}

In the above example, instead of using a dedicated adapter class to convert JAX-RS HttpHeaders type into io.opentracing.propagation.TextMap, the headers are copied to a plain HashMap<String, String> and converted using a standard adapter TextMapExtractAdapter.

The complete tutorial is available here.

Injecting and Extracting TextMap

The process of injecting and extracting TextMap is similar to that of HTTP. Given below are some elaborated code examples of injecting and extracting tracing information using TextMap format.

protected void attachTraceInfo(Tracer tracer, Span span, final Invocation inv) {
    tracer.inject(span.context(), Format.Builtin.TEXT_MAP, new TextMap() {

        @Override
        public void put(String key, String value) {
            inv.getAttachments().put(key, value);
        }

        @Override
        public Iterator<Map.Entry<String, String>> iterator() {
            throw new UnsupportedOperationException("TextMapInjectAdapter should only be used with Tracer.inject()");
        }
    });
}

The above method injects the spanContext into the carrier on the consumer side.

protected Span extractTraceInfo(Tracer tracer, Invoker<?> invoker, Invocation inv) {

    Tracer.SpanBuilder span = tracer.buildSpan(Operation_Name_Here);
    try {
        SpanContext spanContext = tracer.extract(Format.Builtin.TEXT_MAP, new TextMapExtractAdapter(inv.getAttachments()));
        if (spanContext != null) {
            //if spanContext is extracted, the spanContext is propagated to the new span

            span.asChildOf(spanContext);
        }
    } catch (Exception e) {
        span.withTag("Error", "extract from request fail, error msg:" + e.getMessage());
    }
    return span.startManual();
}

On the provider side, tracer.extract() is used to extract the spanContext. The logic is the reverse operation of the tracer.inject().

Injecting and Extracting BINARY

BINARY format is used for injecting/extracting spanContext encoded in an opaque byte array. Opaque means that the inner representation of the array is not known.

Injecting and extracting BINARY can be implemented as follows:

//Inject binary
public void byteBufferInjection() throws Exception {
  byte[] key = "foo".getBytes(ByteBufferContext.CHARSET), value = "bar".getBytes(ByteBufferContext.CHARSET);
  ByteBuffer byteBuffer = ByteBuffer.allocate(2 + 2 * 4 + key.length + value.length);
  tracer.inject(spanContext, Format.Builtin.BINARY, byteBuffer);

}

//Extract binary
public void byteBufferExtraction(ByteBuffer byteBuffer) throws Exception {
  SpanContext spanContext = tracer.extract(Format.Builtin.BINARY, byteBuffer);
  //do something with spanContext...
}

The complete code is available here.