diff --git a/brave/src/main/java/brave/propagation/Propagation.java b/brave/src/main/java/brave/propagation/Propagation.java index c1bfcdc892..d8bbd1b03e 100644 --- a/brave/src/main/java/brave/propagation/Propagation.java +++ b/brave/src/main/java/brave/propagation/Propagation.java @@ -77,6 +77,7 @@ interface KeyFactory { /** Replaces a propagated key with the given value */ interface Setter { + // BRAVE6: make this a charsequence as there's no need to allocate a string void put(C carrier, K key, String value); } diff --git a/pom.xml b/pom.xml index 60f1b8987c..8aa5cd531e 100755 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,7 @@ brave-bom brave-tests context + propagation instrumentation spring-beans diff --git a/propagation/trace-context/README.md b/propagation/trace-context/README.md new file mode 100644 index 0000000000..2a627c2390 --- /dev/null +++ b/propagation/trace-context/README.md @@ -0,0 +1,5 @@ +# brave-propagation-trace-context + +TODO: this is very early impl and doesn't do things like compare if it +should really resume a trace or not. It is a several hour spike and will +continue \ No newline at end of file diff --git a/propagation/trace-context/pom.xml b/propagation/trace-context/pom.xml new file mode 100644 index 0000000000..416e5a277f --- /dev/null +++ b/propagation/trace-context/pom.xml @@ -0,0 +1,33 @@ + + + + io.zipkin.brave + brave-propagation-parent + 5.1.6-SNAPSHOT + + 4.0.0 + + brave-propagation-trace-context + Brave Propagation: Trace-Context (traceparent, tracestate) + + + ${project.basedir}/../.. + 1.6 + java16 + + + + + + maven-jar-plugin + + + + brave.propagation.trace-context + + + + + + + diff --git a/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextExtractor.java b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextExtractor.java new file mode 100644 index 0000000000..e8735199fd --- /dev/null +++ b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextExtractor.java @@ -0,0 +1,74 @@ +package brave.propagation.tracecontext; + +import brave.propagation.Propagation.Getter; +import brave.propagation.SamplingFlags; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContextOrSamplingFlags; +import brave.propagation.tracecontext.TraceContextPropagation.Extra; +import java.util.Collections; +import java.util.List; + +import static brave.propagation.tracecontext.TraceparentFormat.FORMAT_LENGTH; +import static brave.propagation.tracecontext.TraceparentFormat.maybeExtractParent; +import static brave.propagation.tracecontext.TraceparentFormat.validateFormat; + +final class TraceContextExtractor implements Extractor { + final Getter getter; + final K tracestateKey; + final TracestateFormat tracestateFormat; + + TraceContextExtractor(TraceContextPropagation propagation, Getter getter) { + this.getter = getter; + this.tracestateKey = propagation.tracestateKey; + this.tracestateFormat = new TracestateFormat(propagation.stateName); + } + + @Override + public TraceContextOrSamplingFlags extract(C carrier) { + if (carrier == null) throw new NullPointerException("carrier == null"); + String tracestateString = getter.get(carrier, tracestateKey); + if (tracestateString == null) return EMPTY; + + TraceparentFormatHandler handler = new TraceparentFormatHandler(); + CharSequence otherState = tracestateFormat.parseAndReturnOtherState(tracestateString, handler); + + List extra; + if (otherState == null) { + extra = DEFAULT_EXTRA; + } else { + Extra e = new Extra(); + e.otherState = otherState; + extra = Collections.singletonList(e); + } + + if (handler.context == null) { + if (extra == DEFAULT_EXTRA) return EMPTY; + return TraceContextOrSamplingFlags.newBuilder() + .extra(extra) + .samplingFlags(SamplingFlags.EMPTY) + .build(); + } + return TraceContextOrSamplingFlags.newBuilder().context(handler.context).extra(extra).build(); + } + + static final class TraceparentFormatHandler implements TracestateFormat.Handler { + TraceContext context; + + @Override + public boolean onThisState(CharSequence tracestateString, int pos) { + if (validateFormat(tracestateString, pos) < FORMAT_LENGTH) { + return false; + } + context = maybeExtractParent(tracestateString, pos); + return true; + } + } + + /** When present, this context was created with TracestatePropagation */ + static final Extra MARKER = new Extra(); + + static final List DEFAULT_EXTRA = Collections.singletonList(MARKER); + static final TraceContextOrSamplingFlags EMPTY = + TraceContextOrSamplingFlags.EMPTY.toBuilder().extra(DEFAULT_EXTRA).build(); +} diff --git a/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextInjector.java b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextInjector.java new file mode 100644 index 0000000000..05ec37627a --- /dev/null +++ b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextInjector.java @@ -0,0 +1,39 @@ +package brave.propagation.tracecontext; + +import brave.propagation.Propagation.Setter; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Injector; +import brave.propagation.tracecontext.TraceContextPropagation.Extra; + +import static brave.propagation.tracecontext.TraceparentFormat.writeTraceparentFormat; + +final class TraceContextInjector implements Injector { + final TracestateFormat tracestateFormat; + final Setter setter; + final K traceparentKey, tracestateKey; + + TraceContextInjector(TraceContextPropagation propagation, Setter setter) { + this.tracestateFormat = new TracestateFormat(propagation.stateName); + this.traceparentKey = propagation.traceparentKey; + this.tracestateKey = propagation.tracestateKey; + this.setter = setter; + } + + @Override + public void inject(TraceContext traceContext, C carrier) { + String thisState = writeTraceparentFormat(traceContext); + setter.put(carrier, traceparentKey, thisState); + + CharSequence otherState = null; + for (int i = 0, length = traceContext.extra().size(); i < length; i++) { + Object next = traceContext.extra().get(i); + if (next instanceof Extra) { + otherState = ((Extra) next).otherState; + break; + } + } + + String tracestate = tracestateFormat.write(thisState, otherState); + setter.put(carrier, tracestateKey, tracestate); + } +} diff --git a/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextPropagation.java b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextPropagation.java new file mode 100644 index 0000000000..1c2df6acdb --- /dev/null +++ b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceContextPropagation.java @@ -0,0 +1,67 @@ +package brave.propagation.tracecontext; + +import brave.propagation.Propagation; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import java.util.Arrays; +import java.util.List; + +public final class TraceContextPropagation implements Propagation { + + public static final Propagation.Factory FACTORY = + new Propagation.Factory() { + @Override + public Propagation create(KeyFactory keyFactory) { + return new TraceContextPropagation<>(keyFactory); + } + + @Override + public boolean requires128BitTraceId() { + return true; + } + + @Override + public String toString() { + return "TracestatePropagationFactory"; + } + }; + + final String stateName; + final K traceparentKey, tracestateKey; + final List fields; + + TraceContextPropagation(KeyFactory keyFactory) { + this.stateName = "tc"; + this.traceparentKey = keyFactory.create("traceparent"); + this.tracestateKey = keyFactory.create("tracestate"); + this.fields = Arrays.asList(traceparentKey, tracestateKey); + } + + @Override + public List keys() { + return fields; + } + + @Override + public Injector injector(Setter setter) { + if (setter == null) throw new NullPointerException("setter == null"); + return new TraceContextInjector<>(this, setter); + } + + @Override + public Extractor extractor(Getter getter) { + if (getter == null) throw new NullPointerException("getter == null"); + return new TraceContextExtractor<>(this, getter); + } + + static final class Extra { // hidden intentionally + CharSequence otherState; + + @Override + public String toString() { + return "TracestatePropagation{" + + (otherState != null ? ("fields=" + otherState.toString()) : "") + + "}"; + } + } +} diff --git a/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceparentFormat.java b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceparentFormat.java new file mode 100644 index 0000000000..90a4b1e8f6 --- /dev/null +++ b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TraceparentFormat.java @@ -0,0 +1,114 @@ +package brave.propagation.tracecontext; + +import brave.internal.Nullable; +import brave.propagation.TraceContext; +import java.util.logging.Logger; + +import static brave.internal.HexCodec.lenientLowerHexToUnsignedLong; +import static brave.internal.HexCodec.writeHexLong; + +final class TraceparentFormat { + static final Logger logger = Logger.getLogger(TraceparentFormat.class.getName()); + static final int FORMAT_LENGTH = 55; + + static String writeTraceparentFormat(TraceContext context) { + // 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + char[] result = new char[FORMAT_LENGTH]; + result[0] = '0'; // version + result[1] = '0'; // version + result[2] = '-'; // delimiter + writeHexLong(result, 3, context.traceIdHigh()); + writeHexLong(result, 19, context.traceId()); + result[35] = '-'; // delimiter + writeHexLong(result, 36, context.spanId()); + result[52] = '-'; // delimiter + result[53] = '0'; // options + result[54] = context.sampled() != null && context.sampled() ? '1' : '0'; // options + return new String(result); + } + + /** returns the count of valid characters read from the input position */ + static int validateFormat(CharSequence parent, int pos) { + int length = Math.max(parent.length() - pos, FORMAT_LENGTH); + if (length < FORMAT_LENGTH) { + logger.fine("Bad length."); + return 0; + } + + for (int i = 0; i < length; i++) { + char c = parent.charAt(i + pos); + if (c == '-') { + // There are delimiters separating the version, trace ID, span ID and options fields. + if (i != 2 && i != 35 && i != 52) { + logger.fine("Expected hyphen at " + (i + pos)); + return i; + } + // Everything else is hex + } else if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { + logger.fine("Expected lower hex at " + (i + pos)); + return i; + } + } + return length; + } + + static @Nullable TraceContext maybeExtractParent(CharSequence parent, int pos) { + int version = parseUnsigned16BitLowerHex(parent, pos); + if (version == -1) { + logger.fine("Malformed version."); + return null; + } + if (version != 0) { + logger.fine("Unsupported version."); + return null; + } + + long traceIdHigh = lenientLowerHexToUnsignedLong(parent, pos + 3, pos + 19); + long traceId = lenientLowerHexToUnsignedLong(parent, pos + 19, pos + 35); + if (traceIdHigh == 0L && traceId == 0L) { + logger.fine("Invalid input: expected non-zero trace ID"); + return null; + } + + long spanId = lenientLowerHexToUnsignedLong(parent, pos + 36, pos + 52); + if (spanId == 0L) { + logger.fine("Invalid input: expected non-zero span ID"); + return null; + } + + int traceOptions = parseUnsigned16BitLowerHex(parent, pos + 53); + if (traceOptions == -1) { + logger.fine("Malformed trace options."); + return null; + } + + // TODO: treat it as a bitset? + // https://github.com/w3c/distributed-tracing/issues/8#issuecomment-382958021 + // TODO handle deferred decision https://github.com/w3c/distributed-tracing/issues/8 + boolean sampled = (traceOptions & 1) == 1; + + return TraceContext.newBuilder() + .traceIdHigh(traceIdHigh) + .traceId(traceId) + .spanId(spanId) + .sampled(sampled) + .build(); + } + + /** Returns -1 if it wasn't hex */ + static int parseUnsigned16BitLowerHex(CharSequence lowerHex, int pos) { + int result = 0; + for (int i = 0; i < 2; i++) { + char c = lowerHex.charAt(pos + i); + result <<= 4; + if (c >= '0' && c <= '9') { + result |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + result |= c - 'a' + 10; + } else { + return -1; + } + } + return result; + } +} diff --git a/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TracestateFormat.java b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TracestateFormat.java new file mode 100644 index 0000000000..2ffce55f3d --- /dev/null +++ b/propagation/trace-context/src/main/java/brave/propagation/tracecontext/TracestateFormat.java @@ -0,0 +1,96 @@ +package brave.propagation.tracecontext; + +import brave.internal.Nullable; + +import static brave.propagation.tracecontext.TraceparentFormat.FORMAT_LENGTH; + +final class TracestateFormat { + final String stateName; + final int stateNameLength; + final int stateEntryLength; + + TracestateFormat(String stateName) { + this.stateName = stateName; + this.stateNameLength = stateName.length(); + this.stateEntryLength = stateNameLength + 1 /* = */ + FORMAT_LENGTH; + } + + enum Op { + THIS_STATE, + OTHER_STATE + } + + interface Handler { + boolean onThisState(CharSequence tracestateString, int pos); + } + + String write(String thisState, CharSequence otherState) { + int extraLength = otherState == null ? 0 : otherState.length(); + + char[] result; + if (extraLength == 0) { + result = new char[stateEntryLength]; + } else { + result = new char[stateEntryLength + 1 /* , */ + extraLength]; + } + + int pos = 0; + for (int i = 0; i < stateNameLength; i++) { + result[pos++] = stateName.charAt(i); + } + result[pos++] = '='; + + for (int i = 0, len = thisState.length(); i < len; i++) { + result[pos++] = thisState.charAt(i); + } + + if (extraLength > 0) { // Append others after ours + result[pos++] = ','; + for (int i = 0; i < extraLength; i++) { + result[pos++] = otherState.charAt(i); + } + } + return new String(result); + } + + @Nullable + CharSequence parseAndReturnOtherState(String tracestateString, Handler handler) { + StringBuilder currentString = new StringBuilder(), otherState = null; + Op op; + OUTER: + for (int i = 0, length = tracestateString.length(); i < length; i++) { + char c = tracestateString.charAt(i); + if (c == ' ') continue; // trim whitespace + if (c == '=') { // we reached a field name + if (++i == length) break; // skip '=' character + if (currentString.indexOf(stateName) == 0) { + op = Op.THIS_STATE; + } else { + op = Op.OTHER_STATE; + if (otherState == null) otherState = new StringBuilder(); + otherState.append(',').append(currentString); + } + currentString.setLength(0); + } else { + currentString.append(c); + continue; + } + // no longer whitespace + switch (op) { + case OTHER_STATE: + otherState.append(c); + while (i < length && (c = tracestateString.charAt(i)) != ',') { + otherState.append(c); + i++; + } + break; + case THIS_STATE: + if (!handler.onThisState(tracestateString, i)) { + break OUTER; + } + break; + } + } + return otherState; + } +} diff --git a/propagation/trace-context/src/test/java/brave/propagation/tracecontext/TraceContextPropagationTest.java b/propagation/trace-context/src/test/java/brave/propagation/tracecontext/TraceContextPropagationTest.java new file mode 100644 index 0000000000..3432a5a46b --- /dev/null +++ b/propagation/trace-context/src/test/java/brave/propagation/tracecontext/TraceContextPropagationTest.java @@ -0,0 +1,101 @@ +package brave.propagation.tracecontext; + +import brave.propagation.Propagation.KeyFactory; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import brave.propagation.TraceContextOrSamplingFlags; +import brave.propagation.tracecontext.TraceContextPropagation.Extra; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +import static brave.internal.HexCodec.lowerHexToUnsignedLong; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class TraceContextPropagationTest { + Map carrier = new LinkedHashMap<>(); + Injector> injector = + TraceContextPropagation.FACTORY.create(KeyFactory.STRING).injector(Map::put); + Extractor> extractor = + TraceContextPropagation.FACTORY.create(KeyFactory.STRING).extractor(Map::get); + + TraceContext sampledContext = + TraceContext.newBuilder() + .traceIdHigh(lowerHexToUnsignedLong("67891233abcdef01")) + .traceId(lowerHexToUnsignedLong("2345678912345678")) + .spanId(lowerHexToUnsignedLong("463ac35c9f6413ad")) + .sampled(true) + .build(); + String validTraceparent = "00-67891233abcdef012345678912345678-463ac35c9f6413ad-01"; + String otherState = "congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4="; + + @Test + public void injects_tc_when_no_other_tracestate() { + Extra extra = new Extra(); + + sampledContext = sampledContext.toBuilder().extra(asList(extra)).build(); + + injector.inject(sampledContext, carrier); + + assertThat(carrier).containsEntry("tracestate", "tc=" + validTraceparent); + } + + @Test + public void injects_tc_before_other_tracestate() { + Extra extra = new Extra(); + extra.otherState = otherState; + + sampledContext = sampledContext.toBuilder().extra(asList(extra)).build(); + + injector.inject(sampledContext, carrier); + + assertThat(carrier).containsEntry("tracestate", "tc=" + validTraceparent + "," + otherState); + } + + @Test + public void extracts_tc_when_no_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", "tc=" + validTraceparent); + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(new Extra()) + .context(sampledContext) + .build()); + } + + @Test + public void extracts_tc_before_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", "tc=" + validTraceparent + "," + otherState); + + Extra extra = new Extra(); + extra.otherState = otherState; + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(extra) + .context(sampledContext) + .build()); + } + + @Test + public void extracts_tc_after_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", otherState + ",tc=" + validTraceparent); + + Extra extra = new Extra(); + extra.otherState = otherState; + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(extra) + .context(sampledContext) + .build()); + } +} diff --git a/propagation/trace-context/src/test/resources/log4j2.properties b/propagation/trace-context/src/test/resources/log4j2.properties new file mode 100755 index 0000000000..5d75397687 --- /dev/null +++ b/propagation/trace-context/src/test/resources/log4j2.properties @@ -0,0 +1,8 @@ +appenders=console +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=%d{ABSOLUTE} %-5p [%t] %C{2} (%F:%L) [%X{traceId}/%X{spanId}] - %m%n +rootLogger.level=debug +rootLogger.appenderRefs=stdout +rootLogger.appenderRef.stdout.ref=STDOUT