1

I'd like to create and activate a Span with a given Trace ID value instead of an autogenerated one.

Context: I am using OpenTelemetry bridge. Trace ID is sent using custom header (not traceparent). I still want the traceparent header to work like it does but in case it is empty I am trying to start a span using Trace ID provided from another (legacy) header.

Example that I tried so far (not sure if it is even remotely a correct approach) - new Span is indeed started but the Trace ID value is still the autogenerated one.

var traceId = legacyTraceIdHeaderValue.replaceAll("-", "");
LOG.info("legacy traceId: {}", traceId);
var contextWithCustomTraceId = tracer.traceContextBuilder()
        .traceId(traceId)
        .sampled(true)
        .build();
var span = this.tracer.spanBuilder().setParent(contextWithCustomTraceId);

try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
    // wrapped code here, in my case it was this as I am implementing Spring Servlet Filter: chain.doFilter(req, res);
    LOG.info("Span started");
} finally {
    span.end();
}

EDIT: After looking at micrometer-tracing code I think what I should do is:

  1. Implement a custom TextMapPropagator that will delegate to W3CTraceContextPropagator in case traceparent header is present or do customised context extraction in case another header is used.
  2. Register this implementation as a conditional Bean with custom property value (see OpenTelemetryAutoConfiguration):
        @Bean
        @ConditionalOnProperty(prefix = "management.tracing.propagation", name = "type", havingValue = "my-custom-w3c-extension")
  1. Enable this in Spring Application Properties
management.tracing.propagation.type="my-custom-w3c-extension"

Alternatively the custom propagator might be a totally separate implementation (not using W3CTraceContextPropagator as delegate) and they can be coupled together in Bean definition by using TextMapPropagator.composite - that way both should run but there might be no guarantee on the order.

mckulpa
  • 374
  • 4
  • 9
  • 1
    In my case, your initial code is working using the micrometer-tracing-bridge-brave, but not when I switch to micrometer-tracing-bridge-otel. What bridge implementation are you using? I would expect both bridges to behave the same way, probably a bug in the OTEL implementation? – boskop Jun 08 '23 at 05:00
  • That's interesting! Might be, I am using OTEL indeed. – mckulpa Jun 09 '23 at 09:37

1 Answers1

0

My solution seems to work but I think it as a little bit too tightly coupled with the underlying implementation (OpenTelemetry TextMapPropagator and wrapping of parameters for the W3CTraceContextPropagator delegate). I also do not like that I was forced to use @Primary to override the original W3CTraceContextPropagator Bean (custom type set via management.tracing.propagation.type="my-custom-w3c-extension" did not work as this value is mapped to enum and accepts only W3C or B3).

If anybody has a better approach to do this please feel free to share.

import io.micrometer.tracing.internal.EncodingUtils;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import org.apache.commons.collections4.IterableUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Stream;

@Component
@Primary
public final class LegacyExtendedW3CTraceContextPropagator implements TextMapPropagator {
    private static final Logger LOG = LoggerFactory.getLogger(LegacyExtendedW3CTraceContextPropagator.class);
    private static final String TRACE_ID = "TraceId";
    private static final String TRACE_PARENT = "traceparent";
    private static final W3CTraceContextPropagator delegate = W3CTraceContextPropagator.getInstance();
    private static final List<String> FIELDS = Stream.concat(
            Stream.of(TRACE_ID),
            delegate.fields().stream()
    ).toList();

    private static final Random random = new Random();

    @Override
    public Collection<String> fields() {
        return FIELDS;
    }

    @Override
    public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) {
        delegate.inject(context, carrier, setter);
    }

    @Override
    public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) {
        var traceParent = getter.get(carrier, TRACE_PARENT);
        var legacyTraceId = getter.get(carrier, TRACE_ID);
        if (legacyTraceId != null && traceParent == null) {
            var legacyTraceIdWithoutHyphens = legacyTraceId.replaceAll("-", "");
            var randomSpanId = EncodingUtils.fromLong(random.nextLong());
            traceParent = "00-" + legacyTraceIdWithoutHyphens + "-" + randomSpanId+ "-00";
            LOG.debug("Using {} set up from {}: {}", TRACE_PARENT, TRACE_ID, traceParent);
            return delegate.extract(context, carrier, new LegacyGetterWrapper<>(getter, traceParent));
        } else {
            return delegate.extract(context, carrier, getter);
        }
    }

    @Override
    public String toString() {
        return LegacyExtendedW3CTraceContextPropagator.class.getSimpleName();
    }

    private record LegacyGetterWrapper<C>(TextMapGetter<C> getter, String traceParent) implements TextMapGetter<C> {

        @Override
        public Iterable<String> keys(C carrier) {
            List<String> keys = IterableUtils.toList(getter.keys(carrier));
            if (!keys.contains(TRACE_PARENT)) {
                keys.add(TRACE_PARENT);
            }
            return keys;
        }

        @Override
        public String get(C carrier, String key) {
            if (key.equals(TRACE_PARENT)) {
                return traceParent;
            } else {
                return getter.get(carrier, key);
            }
        }
    }
}
mckulpa
  • 374
  • 4
  • 9
  • 1
    If you're using the micrometer brave bridge, you can avoid the `@Primary` annotation on your propagator bean, by providing an `Propagator.Factory` bean instead. Specifying one yourself stops the other propagator from being created. – Jochem Kuijpers Jul 14 '23 at 11:49