Skip to content

Implementing Custom traceId with extract + TextMapAdapter


Author: 刘锐

Introduction

In certain scenarios, you need to implement a custom traceId programmatically.

The approach: Use tracer.extract to construct a SpanContext, then use that SpanContext as the parent context by passing it to asChildOf(SpanContext) to create the current span.

How traceId Parameters Are Defined

When constructing a SpanContext via tracer.extract, the internal ContextInterpreter parses the headers and extracts the corresponding traceId and spanId. Part of the ContextInterpreter implementation is shown below.

Propagators

ddtrace supports several propagation protocols, and the parameter names for traceId differ across protocols.
For Java, ddtrace supports two propagation protocols:

  • Datadog: Default propagation protocol
  • B3: B3 propagation is a specification for headers b3 and headers prefixed with x-b3-. These headers are used to propagate tracing context across service boundaries. B3 has two modes:
    • B3SINGLE (B3_SINGLE_HEADER): the header key is b3
    • B3 (B3MULTI): the header key is x-b3-

Implementing Custom traceId with the Datadog Propagator

Enabling the Datadog Propagator

-Ddd.propagation.style.extract=Datadog
-Ddd.propagation.style.inject=Datadog

Mechanism Source Code Overview

ddtrace uses Datadog as the default propagation protocol, with the interceptor DatadogContextInterpreter. Part of its code is as follows:

    public boolean accept(String key, String value) {
        case 'x':
            if ("x-datadog-trace-id".equalsIgnoreCase(key)) {
                classification = 0;
            } else if ("x-datadog-parent-id".equalsIgnoreCase(key)) {
                classification = 1;
            } else if ("x-datadog-sampling-priority".equalsIgnoreCase(key)) {
                classification = 3;
            } else if ("x-datadog-origin".equalsIgnoreCase(key)) {
                classification = 2;


        ....

        switch(classification) {
            case 0:
                this.traceId = DDId.from(firstValue);
                break;
            case 1:
                this.spanId = DDId.from(firstValue);
                break;
            case 2:
                this.origin = firstValue;
                break;
            case 3:
                this.samplingPriority = Integer.parseInt(firstValue);
                break;
        ....
    }

Code Implementation

    /***
     * Custom traceId related information to implement custom trace
     * @param traceId
     * @param parentId
     * @param treeLength
     * @return
     */
    @GetMapping("/customTrace")
    @ResponseBody
    public String customTrace(String traceId, String parentId, Integer treeLength) {
        Tracer tracer = GlobalTracer.get();
        traceId = StringUtils.isEmpty(traceId) ? IdGenerationStrategy.RANDOM.generate().toString() : traceId;
        parentId = StringUtils.isEmpty(parentId) ? DDId.ZERO.toString() : parentId;
        treeLength = treeLength == null ? 3 : treeLength;
        for (int i = 0; i < treeLength; i++) {
            Map<String, String> data = new HashMap<>();
            data.put("x-datadog-trace-id", traceId);
            data.put("x-datadog-parent-id", parentId);

            SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(data));
            Span serverSpan = tracer.buildSpan("opt" + i)
                    .withTag("service_name", "someService" + i)
                    .asChildOf(extractedContext)
                    .start();
            tracer.activateSpan(serverSpan).close();
            serverSpan.finish();
            parentId = serverSpan.context().toSpanId();
        }
        return "build success!";
    }

Implementing Custom traceId with the B3 Propagator

<About B3 Propagation>

B3 has two encoding formats: Single Header and Multiple Header.

  • Multiple Header encoding uses individual headers prefixed with X-B3- for each item in the trace context.
  • Single Header encoding combines the context into a single header named b3. When extracting fields, the single header variant takes precedence over the multiple header variant.

Here is an example flow using Multiple Header encoding, assuming an HTTP request carries the propagated trace:

image

Enabling the B3 Propagator

-Ddd.propagation.style.extract=B3SINGLE
-Ddd.propagation.style.inject=B3SINGLE

Mechanism Source Code Overview

    public boolean accept(String key, String value) {
        ...
        char first = Character.toLowerCase(key.charAt(0));
        switch (first) {
            case 'f':
                if (this.handledForwarding(key, value)) {
                    return true;
                }
                break;
            case 'u':
                if (this.handledUserAgent(key, value)) {
                    return true;
                }
                break;
            case 'x':
                if ((this.traceId == null || this.traceId == DDId.ZERO) && "X-B3-TraceId".equalsIgnoreCase(key)) {
                    classification = 0;
                } else if ((this.spanId == null || this.spanId == DDId.ZERO) && "X-B3-SpanId".equalsIgnoreCase(key)) {
                    classification = 1;
                } else if (this.samplingPriority == this.defaultSamplingPriority() && "X-B3-Sampled".equalsIgnoreCase(key)) {
                    classification = 3;
                } else if (this.handledXForwarding(key, value)) {
                    return true;
                }
        }

        ...

        String firstValue = HttpCodec.firstHeaderValue(value);
        if (null != firstValue) {
            switch (classification) {
                case 0:
                    if (this.setTraceId(firstValue)) {
                        return true;
                    }
                    break;
                case 1:
                    this.setSpanId(firstValue);
                    break;
                case 2:
                    String mappedKey = (String)this.taggedHeaders.get(lowerCaseKey);
                    if (null != mappedKey) {
                        if (this.tags.isEmpty()) {
                            this.tags = new TreeMap();
                        }

                        this.tags.put(mappedKey, HttpCodec.decode(firstValue));
                    }
                    break;
                case 3:
                    this.samplingPriority = this.convertSamplingPriority(firstValue);
                    break;
                case 4:
                    if (this.extractB3(firstValue)) {
                        return true;
                    }
            }
        }

        ...

The following method handles the Single Header format:

    private boolean extractB3(String firstValue) {
        if (firstValue.length() == 1) {
            this.samplingPriority = this.convertSamplingPriority(firstValue);
        } else {
            int firstIndex = firstValue.indexOf("-");
            int secondIndex = firstValue.indexOf("-", firstIndex + 1);
            String b3SpanId;
            if (firstIndex != -1) {
                b3SpanId = firstValue.substring(0, firstIndex);
                if (this.setTraceId(b3SpanId)) {
                    return true;
                }
            }

            if (secondIndex == -1) {
                b3SpanId = firstValue.substring(firstIndex + 1);
                this.setSpanId(b3SpanId);
            } else {
                b3SpanId = firstValue.substring(firstIndex + 1, secondIndex);
                this.setSpanId(b3SpanId);
                String b3SamplingId = firstValue.substring(secondIndex + 1);
                this.samplingPriority = this.convertSamplingPriority(b3SamplingId);
            }
        }

        return false;
    }

Multiple Header Code Implementation

    private static void b3TraceByMultiple(){
        String traceId = DDId.from("6917954032704516265").toHexStringOrOriginal();
        Tracer tracer = GlobalTracer.get();
        String parentId = DDId.from("4025816492133344807").toHexStringOrOriginal();
        for (int i = 0; i < 3; i++) {
            Map<String, String> data = new HashMap<>();
            data.put("X-B3-TraceId", traceId);
            data.put("X-B3-SpanId", parentId);

            SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(data));
            Span serverSpan = tracer.buildSpan("opt"+i)
                    .withTag("service","someService"+i)
                    .asChildOf(extractedContext)
                    .start();
            serverSpan.setTag("code","200");
            tracer.activateSpan(serverSpan).close();
            serverSpan.finish();
            parentId = DDId.from(serverSpan.context().toSpanId()).toHexStringOrOriginal();
            System.out.println( traceId+"\t"+serverSpan.context().toTraceId()+"\t"+parentId);
        }

    }

Note: Multiple Header requires passing two headers: X-B3-TraceId and X-B3-SpanId. According to the interceptor analysis, the headers are case-insensitive.

6001828a33d570a9    6917954032704516265 58c4b35f113ee353
6001828a33d570a9    6917954032704516265 330359b7aaea9d6b
6001828a33d570a9    6917954032704516265 1ac0dcd332f9262f

Single Header Code Implementation

    private static void b3TraceBySingle(){
        String traceId = DDId.from("6917954032704516265").toHexStringOrOriginal();
        Tracer tracer = GlobalTracer.get();
        String parentId = DDId.from("4025816492133344807").toHexStringOrOriginal();
        for (int i = 0; i < 3; i++) {
            String b3 = traceId+ "-"+parentId+"-1";
            Map<String, String> data = new HashMap<>();
            data.put("b3",b3);
            SpanContext extractedContext = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapAdapter(data));
            Span serverSpan = tracer.buildSpan("opt"+i)
                    .withTag("service","someService"+i)
                    .asChildOf(extractedContext)
                    .start();
            serverSpan.setTag("code","200");
            tracer.activateSpan(serverSpan).close();
            serverSpan.finish();
            parentId = DDId.from(serverSpan.context().toSpanId()).toHexStringOrOriginal();
            System.out.println( traceId+"\t"+serverSpan.context().toTraceId()+"\t"+parentId);
            System.out.println("b3="+b3);
        }

    }
6001828a33d570a9    6917954032704516265 308287d022272ed9
b3=6001828a33d570a9-37de92c518846627-1
6001828a33d570a9    6917954032704516265 5e6fbaad91daef5c
b3=6001828a33d570a9-308287d022272ed9-1
6001828a33d570a9    6917954032704516265 2cfbc225bddf5e6d
b3=6001828a33d570a9-5e6fbaad91daef5c-1

Note: Single Header only requires passing the header b3, with the format traceId-parentId-Sampled.

Enabling Multiple Propagators

Choose either of the following two methods:

  • System Property:
-Ddd.propagation.style.inject=Datadog,B3SINGLE
-Ddd.propagation.style.extract=Datadog,B3SINGLE
  • Environment Variable:
DD_PROPAGATION_STYLE_INJECT=Datadog,B3SINGLE
DD_PROPAGATION_STYLE_EXTRACT=Datadog,B3SINGLE

Feedback

Is this page helpful?