Skip to content
This repository has been archived by the owner on Jul 1, 2022. It is now read-only.

Commit

Permalink
Add W3C TraceContext codec/propagation (#694)
Browse files Browse the repository at this point in the history
* Add support for tracecontext

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Address formatting issues

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Add additional tests, remove unused constructor

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Add test for version builder

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Fix incorrect header name

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Remove bit where we generate left hand side of trace id when we only had 64 bits of data

Signed-off-by: Jonah Back <jonah@jonahback.com>

* Add W3C TraceContext codec

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Some cleanup

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* smaller nits

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Fix padding with zeros in test

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Add tracestate header

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Just propagate tracestate

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Cleanup

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Add tracestate factory method to span builder

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

* Remove header

Signed-off-by: Pavol Loffay <ploffay@redhat.com>

Co-authored-by: Jonah Back <jonah@jonahback.com>
  • Loading branch information
pavolloffay and backjo authored Mar 11, 2020
1 parent f726853 commit 568ab68
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 4 deletions.
12 changes: 11 additions & 1 deletion jaeger-core/src/main/java/io/jaegertracing/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.jaegertracing.internal.propagation.BinaryCodec;
import io.jaegertracing.internal.propagation.CompositeCodec;
import io.jaegertracing.internal.propagation.TextMapCodec;
import io.jaegertracing.internal.propagation.TraceContextCodec;
import io.jaegertracing.internal.reporters.CompositeReporter;
import io.jaegertracing.internal.reporters.LoggingReporter;
import io.jaegertracing.internal.reporters.RemoteReporter;
Expand Down Expand Up @@ -167,7 +168,12 @@ public enum Propagation {
/**
* The Zipkin B3 trace context propagation format.
*/
B3
B3,

/**
* The W3C TraceContext propagation format.
*/
W3C
}

/**
Expand Down Expand Up @@ -467,6 +473,10 @@ public CodecConfiguration withPropagation(Propagation propagation) {
addCodec(codecs, Format.Builtin.HTTP_HEADERS, new B3TextMapCodec.Builder().build());
addCodec(codecs, Format.Builtin.TEXT_MAP, new B3TextMapCodec.Builder().build());
break;
case W3C:
addCodec(codecs, Format.Builtin.HTTP_HEADERS, new TraceContextCodec.Builder().build());
addCodec(codecs, Format.Builtin.TEXT_MAP, new TraceContextCodec.Builder().build());
break;
default:
log.error("Unhandled propagation format '" + propagation + "'");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class JaegerSpanContext implements SpanContext {
private final JaegerObjectFactory objectFactory;
private final String traceIdAsString;
private final String spanIdAsString;
private String traceState;

public JaegerSpanContext(long traceIdHigh, long traceIdLow, long spanId, long parentId, byte flags) {
this(
Expand Down Expand Up @@ -125,6 +126,10 @@ public byte getFlags() {
return flags;
}

public String getTraceState() {
return traceState;
}

public boolean isSampled() {
return (flags & flagSampled) == flagSampled;
}
Expand Down Expand Up @@ -163,6 +168,13 @@ public JaegerSpanContext withFlags(byte flags) {
return objectFactory.createSpanContext(traceIdHigh, traceIdLow, spanId, parentId, flags, baggage, debugId);
}

public JaegerSpanContext withTraceState(String traceState) {
JaegerSpanContext spanContext = objectFactory
.createSpanContext(traceIdHigh, traceIdLow, spanId, parentId, flags, baggage, debugId);
spanContext.traceState = traceState;
return spanContext;
}

/**
* @return true when the instance of the context contains a non-zero trace and span ID,
* indicating a valid trace. It may return false if the context was created with only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2020, OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.jaegertracing.internal.propagation;

import io.jaegertracing.internal.JaegerObjectFactory;
import io.jaegertracing.internal.JaegerSpanContext;
import io.jaegertracing.spi.Codec;
import io.opentracing.propagation.TextMap;
import java.util.Collections;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

/**
* Implementation of the TraceContext propagation protocol. See <a
* href=https://github.com/w3c/distributed-tracing>w3c/distributed-tracing</a>.
*
* This implementation is mostly copied over from OpenTelemetry Java SDK
* https://github.com/open-telemetry/opentelemetry-java/blob/ed98c35c0569a48f66339769913670334d6c8a95/api/src/main/java/io/opentelemetry/trace/propagation/HttpTraceContext.java#L40
*/
@Slf4j
public class TraceContextCodec implements Codec<TextMap> {

static final String TRACE_PARENT = "traceparent";
static final String TRACE_STATE = "tracestate";

private static final String VERSION = "00";
private static final int VERSION_SIZE = 2;
private static final char TRACEPARENT_DELIMITER = '-';
private static final int TRACEPARENT_DELIMITER_SIZE = 1;
private static final int TRACE_ID_HEX_SIZE = 2 * 16;
private static final int SPAN_ID_HEX_SIZE = 2 * 8;
private static final int TRACE_FLAGS_HEX_SIZE = 2;
private static final int TRACE_ID_OFFSET = VERSION_SIZE + TRACEPARENT_DELIMITER_SIZE;
private static final int SPAN_ID_OFFSET =
TRACE_ID_OFFSET + TRACE_ID_HEX_SIZE + TRACEPARENT_DELIMITER_SIZE;
private static final int TRACE_OPTION_OFFSET =
SPAN_ID_OFFSET + SPAN_ID_HEX_SIZE + TRACEPARENT_DELIMITER_SIZE;
private static final int TRACEPARENT_HEADER_SIZE = TRACE_OPTION_OFFSET + TRACE_FLAGS_HEX_SIZE;
private static final byte SAMPLED_FLAG = 1;

private final JaegerObjectFactory objectFactory;

private TraceContextCodec(Builder builder) {
this.objectFactory = builder.objectFactory;
}

private JaegerSpanContext extractContextFromTraceParent(String traceparent, String tracestate) {
// TODO(bdrutu): Do we need to verify that version is hex and that
// for the version the length is the expected one?
boolean isValid =
traceparent != null
&& traceparent.charAt(TRACE_OPTION_OFFSET - 1) == TRACEPARENT_DELIMITER
&& (traceparent.length() == TRACEPARENT_HEADER_SIZE
|| (traceparent.length() > TRACEPARENT_HEADER_SIZE
&& traceparent.charAt(TRACEPARENT_HEADER_SIZE) == TRACEPARENT_DELIMITER))
&& traceparent.charAt(SPAN_ID_OFFSET - 1) == TRACEPARENT_DELIMITER
&& traceparent.charAt(TRACE_OPTION_OFFSET - 1) == TRACEPARENT_DELIMITER;
if (!isValid) {
log.warn("Unparseable traceparent header. Returning null span context.");
return null;
}

Long traceIdHigh = HexCodec.hexToUnsignedLong(traceparent, TRACE_ID_OFFSET, TRACE_ID_OFFSET + 16);
Long traceIdLow = HexCodec.hexToUnsignedLong(traceparent, TRACE_ID_OFFSET + 16, TRACE_ID_OFFSET + 32);
Long spanId = HexCodec.hexToUnsignedLong(traceparent, SPAN_ID_OFFSET, SPAN_ID_OFFSET + 16);

boolean sampled = false;
long traceContextFlags = HexCodec.hexToUnsignedLong(traceparent, TRACE_OPTION_OFFSET, TRACE_OPTION_OFFSET + 2);
if ((traceContextFlags & SAMPLED_FLAG) == SAMPLED_FLAG) {
sampled = true;
}

if (traceIdLow == null || traceIdLow == 0 || spanId == null || spanId == 0) {
log.warn("Unparseable traceparent header. Returning null span context.");
return null;
}

JaegerSpanContext spanContext = this.objectFactory.createSpanContext(
traceIdHigh,
traceIdLow,
spanId,
0,
sampled ? (byte) 1 : (byte) 0,
Collections.<String, String>emptyMap(), null);
return spanContext.withTraceState(tracestate);
}

@Override
public JaegerSpanContext extract(TextMap carrier) {
String traceParent = null;
String traceState = null;
for (Map.Entry<String, String> entry: carrier) {
if (TRACE_PARENT.equals(entry.getKey())) {
traceParent = entry.getValue();
}
if (TRACE_STATE.equals(entry.getKey())) {
traceState = entry.getValue();
}
}
return extractContextFromTraceParent(traceParent, traceState);
}

@Override
public void inject(JaegerSpanContext spanContext, TextMap carrier) {
char[] chars = new char[TRACEPARENT_HEADER_SIZE];
chars[0] = VERSION.charAt(0);
chars[1] = VERSION.charAt(1);
chars[2] = TRACEPARENT_DELIMITER;
HexCodec.writeHexLong(chars, TRACE_ID_OFFSET, spanContext.getTraceIdHigh());
HexCodec.writeHexLong(chars, TRACE_ID_OFFSET + 16, spanContext.getTraceIdLow());
chars[SPAN_ID_OFFSET - 1] = TRACEPARENT_DELIMITER;
HexCodec.writeHexLong(chars, SPAN_ID_OFFSET, spanContext.getSpanId());
chars[TRACE_OPTION_OFFSET - 1] = TRACEPARENT_DELIMITER;
chars[TRACE_OPTION_OFFSET] = '0';
chars[TRACE_OPTION_OFFSET + 1] = spanContext.isSampled() ? '1' : '0';
carrier.put(TRACE_PARENT, new String(chars));

if (spanContext.getTraceState() != null && !spanContext.getTraceState().isEmpty()) {
carrier.put(TRACE_STATE, spanContext.getTraceState());
}
}

public static class Builder {
private JaegerObjectFactory objectFactory = new JaegerObjectFactory();

/**
* Specify JaegerSpanContext factory. Used for creating new span contexts. The default factory
* is an instance of {@link JaegerObjectFactory}.
*/
public Builder withObjectFactory(JaegerObjectFactory objectFactory) {
this.objectFactory = objectFactory;
return this;
}

public TraceContextCodec build() {
return new TraceContextCodec(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.jaegertracing.internal.propagation.BinaryCodec;
import io.jaegertracing.internal.propagation.TestBinaryCarrier;
import io.jaegertracing.internal.propagation.TextMapCodec;
import io.jaegertracing.internal.propagation.TraceContextCodec;
import io.jaegertracing.internal.samplers.ConstSampler;
import io.jaegertracing.internal.samplers.ProbabilisticSampler;
import io.jaegertracing.internal.samplers.RateLimitingSampler;
Expand Down Expand Up @@ -480,15 +481,20 @@ public void testB3CodecsWith128BitTraceId() {
@Test
public void testCodecFromString() {
CodecConfiguration codecConfiguration = CodecConfiguration
.fromString(String.format("%s,%s", Propagation.B3.name(), Propagation.JAEGER.name()));
.fromString(String.format("%s,%s,%s",
Propagation.B3.name(),
Propagation.JAEGER.name(),
Propagation.W3C.name()));
assertEquals(2, codecConfiguration.getCodecs().size());
assertEquals(2, codecConfiguration.getCodecs().get(Builtin.HTTP_HEADERS).size());
assertEquals(2, codecConfiguration.getCodecs().get(Builtin.TEXT_MAP).size());
assertEquals(3, codecConfiguration.getCodecs().get(Builtin.HTTP_HEADERS).size());
assertEquals(3, codecConfiguration.getCodecs().get(Builtin.TEXT_MAP).size());
assertEquals(1, codecConfiguration.getBinaryCodecs().get(Builtin.BINARY).size());
assertTrue(codecConfiguration.getCodecs().get(Builtin.HTTP_HEADERS).get(0) instanceof B3TextMapCodec);
assertTrue(codecConfiguration.getCodecs().get(Builtin.HTTP_HEADERS).get(1) instanceof TextMapCodec);
assertTrue(codecConfiguration.getCodecs().get(Builtin.HTTP_HEADERS).get(2) instanceof TraceContextCodec);
assertTrue(codecConfiguration.getCodecs().get(Builtin.TEXT_MAP).get(0) instanceof B3TextMapCodec);
assertTrue(codecConfiguration.getCodecs().get(Builtin.TEXT_MAP).get(1) instanceof TextMapCodec);
assertTrue(codecConfiguration.getCodecs().get(Builtin.TEXT_MAP).get(2) instanceof TraceContextCodec);
assertTrue(codecConfiguration.getBinaryCodecs().get(Builtin.BINARY).get(0) instanceof BinaryCodec);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2020, Uber Technologies, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package io.jaegertracing.internal.propagation;

import static io.jaegertracing.internal.propagation.TraceContextCodec.TRACE_PARENT;
import static io.jaegertracing.internal.propagation.TraceContextCodec.TRACE_STATE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import io.jaegertracing.internal.JaegerSpanContext;
import io.opentracing.propagation.TextMapAdapter;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;

public class TraceContextCodecTest {

private static final JaegerSpanContext SPAN_CONTEXT =
new JaegerSpanContext(0, 1, 2, 3, (byte)0);
private static final String EXAMPLE_TRACE_PARENT = "00-00000000000000000000000000000001-0000000000000002-00";

private TraceContextCodec traceContextCodec = new TraceContextCodec.Builder().build();

@Test
public void support128BitTraceIdExtraction() {
String hex128Bits = "463ac35c9f6413ad48485a3953bb6124";
String parentSpan = "d1595c6ec91668af";
String tracecontext = String.format("00-%s-%s-01", hex128Bits, parentSpan);

TextMapAdapter textMap = new TextMapAdapter(new HashMap<>());
textMap.put(TRACE_PARENT, tracecontext);
JaegerSpanContext context = traceContextCodec.extract(textMap);

assertNotNull(HexCodec.lowerHexToUnsignedLong(parentSpan));
assertEquals(HexCodec.lowerHexToUnsignedLong(hex128Bits).longValue(), context.getTraceIdLow());
assertEquals(HexCodec.higherHexToUnsignedLong(hex128Bits).longValue(), context.getTraceIdHigh());
assertEquals(HexCodec.lowerHexToUnsignedLong(parentSpan).longValue(), context.getSpanId());
assertTrue(context.isSampled());
}

@Test
public void testInject() {
Map<String, String> carrier = new HashMap<>();
TextMapAdapter textMap = new TextMapAdapter(carrier);
long traceIdLow = 1;
long spanId = 2;
long parentId = 3;
long traceIdHigh = HexCodec.hexToUnsignedLong("c281c27976c85681", 0, 16);
JaegerSpanContext spanContext = new JaegerSpanContext(traceIdHigh, traceIdLow, spanId, parentId, (byte) 0);

traceContextCodec.inject(spanContext, textMap);

String expectedTraceContextHeader = "00-c281c27976c856810000000000000001-0000000000000002-00";
assertEquals(1, carrier.size());
assertNotNull(carrier.get(TRACE_PARENT));
assertEquals(expectedTraceContextHeader, carrier.get(TRACE_PARENT));
}

@Test
public void testInjectWith64bit() {
Map<String, String> carrier = new HashMap<>();
TextMapAdapter textMap = new TextMapAdapter(carrier);

traceContextCodec.inject(SPAN_CONTEXT, textMap);
assertEquals(1, carrier.size());

String traceParent = carrier.get(TRACE_PARENT);
assertEquals(EXAMPLE_TRACE_PARENT, traceParent);
JaegerSpanContext extractedContext = traceContextCodec.extract(textMap);
assertEquals("1:2:0:0", extractedContext.toString());
}

@Test
public void testInvalidTraceId() {
TextMapAdapter textMap = new TextMapAdapter(new HashMap<>());
textMap.put(TRACE_PARENT, "00-00000000000000000000000000000000-0000000000000002-00");
JaegerSpanContext spanContext = traceContextCodec.extract(textMap);
assertNull(spanContext);
}

@Test
public void testNoTraceHeader() {
TextMapAdapter textMap = new TextMapAdapter(new HashMap<>());
JaegerSpanContext spanContext = traceContextCodec.extract(textMap);
assertNull(spanContext);
}

@Test
public void testInvalidParentId() {
TextMapAdapter textMap = new TextMapAdapter(new HashMap<>());
textMap.put(TRACE_PARENT, "00-00000000000000000000000000000001-0000000000000000-00");
JaegerSpanContext spanContext = traceContextCodec.extract(textMap);
assertNull(spanContext);
}

@Test
public void testTraceStatePropagation() {
Map<String, String> extractCarrier = new HashMap<>();
TextMapAdapter textMap = new TextMapAdapter(extractCarrier);
textMap.put(TRACE_PARENT, EXAMPLE_TRACE_PARENT);
textMap.put(TRACE_STATE, "whatever");
JaegerSpanContext spanContext = traceContextCodec.extract(textMap);

Map<String, String> injectCarrier = new HashMap<>();
traceContextCodec.inject(spanContext, new TextMapAdapter(injectCarrier));
assertEquals(extractCarrier, injectCarrier);
}

@Test
public void testEmptyTraceStateNotPropagated() {
Map<String, String> extractCarrier = new HashMap<>();
TextMapAdapter textMap = new TextMapAdapter(extractCarrier);
textMap.put(TRACE_PARENT, EXAMPLE_TRACE_PARENT);
textMap.put(TRACE_STATE, "");
JaegerSpanContext spanContext = traceContextCodec.extract(textMap);

Map<String, String> injectCarrier = new HashMap<>();
traceContextCodec.inject(spanContext, new TextMapAdapter(injectCarrier));
assertEquals(1, injectCarrier.size());
assertEquals(EXAMPLE_TRACE_PARENT, injectCarrier.get(TRACE_PARENT));
}
}

0 comments on commit 568ab68

Please sign in to comment.