diff --git a/brave-bom/pom.xml b/brave-bom/pom.xml index c9ecda3600..226d06e002 100644 --- a/brave-bom/pom.xml +++ b/brave-bom/pom.xml @@ -272,6 +272,11 @@ brave-spring-beans ${project.version} + + ${project.groupId} + brave-propagation-w3c + ${project.version} + diff --git a/instrumentation/benchmarks/pom.xml b/instrumentation/benchmarks/pom.xml index f04a4e4e0b..5796ab79b7 100644 --- a/instrumentation/benchmarks/pom.xml +++ b/instrumentation/benchmarks/pom.xml @@ -239,6 +239,12 @@ grpc-core ${grpc.version} + + + ${project.groupId} + brave-propagation-w3c + ${project.version} + diff --git a/instrumentation/benchmarks/src/main/java/brave/propagation/B3SinglePropagationBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/propagation/B3SinglePropagationBenchmarks.java index 227b15c633..5d2692fc9f 100644 --- a/instrumentation/benchmarks/src/main/java/brave/propagation/B3SinglePropagationBenchmarks.java +++ b/instrumentation/benchmarks/src/main/java/brave/propagation/B3SinglePropagationBenchmarks.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 The OpenZipkin Authors + * Copyright 2013-2020 The OpenZipkin 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 diff --git a/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TraceContextPropagationBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TraceContextPropagationBenchmarks.java new file mode 100644 index 0000000000..b3b2e87a57 --- /dev/null +++ b/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TraceContextPropagationBenchmarks.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.internal.HexCodec; +import brave.propagation.Propagation; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import brave.propagation.TraceContextOrSamplingFlags; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 10, time = 1) +@Fork(3) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class TraceContextPropagationBenchmarks { + static final Propagation tc = + TraceContextPropagation.FACTORY.create(Propagation.KeyFactory.STRING); + static final Injector> tcInjector = tc.injector(Map::put); + static final Extractor> tcExtractor = tc.extractor(Map::get); + + static final TraceContext context = TraceContext.newBuilder() + .traceIdHigh(HexCodec.lowerHexToUnsignedLong("67891233abcdef01")) + .traceId(HexCodec.lowerHexToUnsignedLong("2345678912345678")) + .spanId(HexCodec.lowerHexToUnsignedLong("463ac35c9f6413ad")) + .sampled(true) + .build(); + + // TODO: add tracestate examples which prefer the b3 entry + static final Map incoming128 = new LinkedHashMap() { + { + put("traceparent", TraceparentFormat.writeTraceparentFormat(context)); + } + }; + + static final Map incomingPadded = new LinkedHashMap() { + { + put("traceparent", + TraceparentFormat.writeTraceparentFormat(context.toBuilder().traceIdHigh(0).build())); + } + }; + + static final Map incomingMalformed = new LinkedHashMap() { + { + put("traceparent", "b970dafd-0d95-40aa-95d8-1d8725aebe40"); // not ok + } + }; + + static final Map nothingIncoming = Collections.emptyMap(); + + @Benchmark public void inject() { + Map carrier = new LinkedHashMap<>(); + tcInjector.inject(context, carrier); + } + + @Benchmark public TraceContextOrSamplingFlags extract_128() { + return tcExtractor.extract(incoming128); + } + + @Benchmark public TraceContextOrSamplingFlags extract_padded() { + return tcExtractor.extract(incomingPadded); + } + + @Benchmark public TraceContextOrSamplingFlags extract_nothing() { + return tcExtractor.extract(nothingIncoming); + } + + @Benchmark public TraceContextOrSamplingFlags extract_malformed() { + return tcExtractor.extract(incomingMalformed); + } + + // Convenience main entry-point + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .addProfiler("gc") + .include(".*" + TraceContextPropagationBenchmarks.class.getSimpleName()) + .build(); + + new Runner(opt).run(); + } +} diff --git a/pom.xml b/pom.xml index bc48b49c28..b62f69547c 100755 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,7 @@ brave-bom brave-tests context + propagation instrumentation spring-beans diff --git a/propagation/pom.xml b/propagation/pom.xml new file mode 100644 index 0000000000..e5582c3979 --- /dev/null +++ b/propagation/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + + io.zipkin.brave + brave-parent + 5.10.2-SNAPSHOT + + + brave-propagation-parent + Brave: Trace Propagation Formats + pom + + + ${project.basedir}/.. + + + + w3c + + + + + ${project.groupId} + brave + + + ${project.groupId} + brave-tests + test + + + diff --git a/propagation/w3c/RATIONALE.md b/propagation/w3c/RATIONALE.md new file mode 100644 index 0000000000..358d4267a4 --- /dev/null +++ b/propagation/w3c/RATIONALE.md @@ -0,0 +1,62 @@ +# brave-propagation-w3c rationale + +## Trace Context Specification + +### Why do we write a tracestate entry? +We write both "traceparent" and a "tracestate" entry for two reasons. The first is due to +incompatibility between the "traceparent" format and our trace context. This is described in another +section. + +The other reason is durability across hops. When your direct upstream is not the same tracing +system, its span ID (which they call parentId in section 3.2.2.4) will not be valid: using it will +break the trace. Instead, we look at our "tracestate" entry, which represents the last known place +in the same system. + +### What is supported by Brave, but not by traceparent format? + +#### `SamplingFlags` can precede a trace +B3 has always had the ability to influence the start of a trace with a sampling decision. For +example, you can propagate `b3=0` to force the next hop to not trace the request. This is used for +things like not sampling health checks. Conversely, you can force sampling by propagating `b3=d` +(the debug flag). `traceparent` requires a trace ID and a span ID, so cannot propagate this. + +#### `TraceIdContext` (trace ID only) +Amazon trace format can propagate a trace ID prior to starting a trace, for correlation purposes. +`traceparent` requires a trace ID and a span ID, so cannot a trace ID standalone. + +#### Not yet sampled/ deferred decision +B3 and Amazon formats support assigning a span ID prior to a sampling decision. `traceparent` has no +way to tell the difference between an explicit no and lack of a decision, as it only has one bit +flag. + +#### Debug flag +B3 has always had a debug flag, which is a way to force a trace even if normal sampling says no. +`traceparent` cannot distinguish between this and a normal decision, as it only has one bit flag. + +#### Trace-scoped sampling decision +`traceparent` does not distinguish between a hop-level or a trace scoped decision in the format. +This means that traces can be broken as it is valid to change the decision at every step (which +breaks the hierarchy). This is the main reason why we need a separate `tracestate` entry. + +### Why serialize the trace context in two formats? + +The "traceparent" header is only portable to get the `TraceContext.traceId()` and +`TraceContext.spanId()`. Section 3.2.2.5.1, the sampled flag, is incompatible with B3 sampling. The +format also lacks fields for `TraceContext.parentId()` and `TraceContext.debug()`. This requires us +to re-serialize the same context in two formats: one for compliance ("traceparent") and one that +actually stores the context (B3 single format). + +The impact on users will be higher overhead and confusion when comparing the sampled value of +"traceparent" which may be different than "b3". + +### Why is traceparent incompatible with B3? + +It may seem like incompatibility between "traceparent" and B3 were accidental, but that was not the +case. The Zipkin community held the first meetings leading to this specification, and were directly +involved in the initial design. Use cases of B3 were well known by working group members. Choices to +become incompatible with B3 (and Amazon X-Ray format) sampling were conscious, as were decisions to +omit other fields we use. These decisions were made in spite of protest from Zipkin community +members and others. There is a rationale document for the specification, but the working group has +so far not explained these decisions, or even mention B3 at all. + +https://github.com/w3c/trace-context/blob/d2ed8084780efcedab7e60c48b06ca3c00bea55c/http_header_format_rationale.md diff --git a/propagation/w3c/README.md b/propagation/w3c/README.md new file mode 100644 index 0000000000..ca53626ded --- /dev/null +++ b/propagation/w3c/README.md @@ -0,0 +1,14 @@ +# brave-propagation-w3c + +This project includes propagation handlers for W3C defined headers. + +## Trace Context +The [Trace Context][https://w3c.github.io/trace-context/] specification defines two headers: + + * `traceparent` - almost the same as our [B3-single format](https://github.com/openzipkin/b3-propagation#single-header) + * `tracestate` - vendor-specific (or format-specific): may impact how to interpret `traceparent` + +This implementation can survive mixed systems who follow the specification and forward the +`tracestate` header. When writing the `traceparent` header, this also overwrites the `tracestate` +entry named 'b3' (in B3 single format). When reading headers, this entry is favored over the +`traceparent`, allowing the the next span to re-attach to the last known 'b3' header. diff --git a/propagation/w3c/pom.xml b/propagation/w3c/pom.xml new file mode 100644 index 0000000000..4f975e78dd --- /dev/null +++ b/propagation/w3c/pom.xml @@ -0,0 +1,64 @@ + + + + + io.zipkin.brave + brave-propagation-parent + 5.10.2-SNAPSHOT + + 4.0.0 + + brave-propagation-w3c + Brave Propagation: W3C Tracing headers (traceparent, tracestate, etc.) + + + ${project.basedir}/../.. + 1.6 + java16 + + + + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + + + + + maven-jar-plugin + + + + brave.propagation.w3c + + + + + + + diff --git a/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextExtractor.java b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextExtractor.java new file mode 100644 index 0000000000..6a4abc2191 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextExtractor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +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.w3c.TraceContextPropagation.Extra; +import java.util.Collections; +import java.util.List; + +import static brave.propagation.B3SingleFormat.parseB3SingleFormat; + +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 value = getter.get(carrier, tracestateKey); + if (value == null) return EMPTY; + + B3SingleFormatHandler handler = new B3SingleFormatHandler(); + CharSequence otherEntries = tracestateFormat.parseAndReturnOtherEntries(value, handler); + + List extra; + if (otherEntries == null) { + extra = DEFAULT_EXTRA; + } else { + Extra e = new Extra(); + e.otherEntries = otherEntries; + extra = Collections.singletonList(e); + } + + TraceContext context = handler.context; + if (context == null) { + if (extra == DEFAULT_EXTRA) return EMPTY; + return TraceContextOrSamplingFlags.newBuilder() + .extra(extra) + .samplingFlags(SamplingFlags.EMPTY) + .build(); + } + return TraceContextOrSamplingFlags.newBuilder().context(context).extra(extra).build(); + } + + static final class B3SingleFormatHandler implements TracestateFormat.Handler { + TraceContext context; + + @Override + public boolean onThisEntry(CharSequence tracestate, int beginIndex, int endIndex) { + TraceContextOrSamplingFlags extracted = parseB3SingleFormat(tracestate, beginIndex, endIndex); + if (extracted != null) context = extracted.context(); + return context != null; + } + } + + /** 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/w3c/src/main/java/brave/propagation/w3c/TraceContextInjector.java b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextInjector.java new file mode 100644 index 0000000000..b0b6dc4d17 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextInjector.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.propagation.Propagation.Setter; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Injector; +import brave.propagation.w3c.TraceContextPropagation.Extra; + +import static brave.propagation.B3SingleFormat.writeB3SingleFormat; +import static brave.propagation.w3c.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) { + + setter.put(carrier, traceparentKey, writeTraceparentFormat(traceContext)); + + 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).otherEntries; + break; + } + } + + String tracestate = tracestateFormat.write(writeB3SingleFormat(traceContext), otherState); + setter.put(carrier, tracestateKey, tracestate); + } +} diff --git a/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextPropagation.java b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextPropagation.java new file mode 100644 index 0000000000..f539af5c78 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextPropagation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.propagation.Propagation; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import java.util.Arrays; +import java.util.List; + +public final class TraceContextPropagation implements Propagation { + // TODO: not sure if we will want a constant here, or something like a builder. For example, we + // probably will need a primary state handler (ex b3) to catch data no longer in the traceparent + // format. At any rate, we will need to know what state is primarily ours, so that probably means + // not having a constant, unless that constant uses b3 single impl for the tracestate entry. + public static final Propagation.Factory FACTORY = + new Propagation.Factory() { + @Override public Propagation create(KeyFactory keyFactory) { + return new TraceContextPropagation<>(keyFactory); + } + + /** + * Traceparent doesn't support sharing the same span ID, though it may be possible to support + * this by extending it with a b3 state entry. + */ + @Override public boolean supportsJoin() { + return false; + } + + @Override public TraceContext decorate(TraceContext context) { + // TODO: almost certain we will need to decorate as not all contexts will start with an + // incoming request (ex schedule or client-originated traces) + return super.decorate(context); + } + + @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 = "b3"; + 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); + } + + /** + * This only contains other entries. The entry for the current trace is only written during + * injection. + */ + static final class Extra { // hidden intentionally + CharSequence otherEntries; + + @Override public String toString() { + return "TracestatePropagation{" + + (otherEntries != null ? ("entries=" + otherEntries.toString()) : "") + + "}"; + } + } +} diff --git a/propagation/w3c/src/main/java/brave/propagation/w3c/TraceparentFormat.java b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceparentFormat.java new file mode 100644 index 0000000000..9f17ade44e --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceparentFormat.java @@ -0,0 +1,297 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.internal.Nullable; +import brave.internal.Platform; +import brave.propagation.TraceContext; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; + +import static brave.internal.HexCodec.writeHexLong; + +/** Implements https://w3c.github.io/trace-context/#traceparent-header */ +final class TraceparentFormat { + /** Version '00' is fixed length, though future versions may be longer. */ + static final int FORMAT_LENGTH = 3 + 32 + 1 + 16 + 3; // 00-traceid128-spanid-01 + + static final int // instead of enum for smaller bytecode + FIELD_VERSION = 1, + FIELD_TRACE_ID = 2, + FIELD_PARENT_ID = 3, + FIELD_TRACE_FLAGS = 4; + + /** Writes all "traceparent" defined fields in the trace context to a hyphen delimited string. */ + public static String writeTraceparentFormat(TraceContext context) { + char[] buffer = getCharBuffer(); + int length = writeTraceparentFormat(context, buffer); + return new String(buffer, 0, length); + } + + /** + * Like {@link #writeTraceparentFormat(TraceContext)}, but for carriers with byte array or byte + * buffer values. For example, {@link ByteBuffer#wrap(byte[])} can wrap the result. + */ + public static byte[] writeTraceparentFormatAsBytes(TraceContext context) { + char[] buffer = getCharBuffer(); + int length = writeTraceparentFormat(context, buffer); + return asciiToNewByteArray(buffer, length); + } + + static int writeTraceparentFormat(TraceContext context, char[] result) { + int pos = 0; + result[pos++] = '0'; + result[pos++] = '0'; + result[pos++] = '-'; + long traceIdHigh = context.traceIdHigh(); + writeHexLong(result, pos, traceIdHigh); + pos += 16; + writeHexLong(result, pos, context.traceId()); + pos += 16; + result[pos++] = '-'; + writeHexLong(result, pos, context.spanId()); + pos += 16; + + result[pos++] = '-'; + result[pos++] = '0'; + result[pos++] = Boolean.TRUE.equals(context.sampled()) ? '1' : '0'; + + return pos; + } + + @Nullable + public static TraceContext parseTraceparentFormat(CharSequence parent) { + return parseTraceparentFormat(parent, 0, parent.length()); + } + + /** + * This reads a trace context a sequence potentially larger than the format. The use-case is + * reducing garbage, by re-using the input {@code value} across multiple parse operations. + * + * @param value the sequence that contains a {@code traceparent} formatted trace context + * @param beginIndex the inclusive begin index: {@linkplain CharSequence#charAt(int) index} of the + * first character in {@code traceparent} format. + * @param endIndex the exclusive end index: {@linkplain CharSequence#charAt(int) index} + * after the last character in {@code traceparent} format. + */ + @Nullable + public static TraceContext parseTraceparentFormat(CharSequence value, int beginIndex, + int endIndex) { + int length = endIndex - beginIndex; + + if (length == 0) { + Platform.get().log("Invalid input: empty", null); + return null; + } + + int version = 0; + TraceContext.Builder builder = getBuilder(); + boolean traceIdHighZero = false; + + int currentField = FIELD_VERSION, currentFieldLength = 0; + // Used for hex-decoding, performed by bitwise addition + long buffer = 0L; + + // Instead of pos < endIndex, this uses pos <= endIndex to keep field processing consolidated. + // Otherwise, we'd have to process again when outside the loop to handle dangling data on EOF. + LOOP: + for (int pos = beginIndex; pos <= endIndex; pos++) { + // treat EOF same as a hyphen for simplicity + boolean isEof = pos == endIndex; + char c = isEof ? '-' : value.charAt(pos); + + if (c == '-') { + if (!validateFieldLength(currentField, currentFieldLength)) { + return null; + } + + switch (currentField) { + case FIELD_VERSION: + // 8-bit unsigned 255 is disallowed https://w3c.github.io/trace-context/#version + version = (int) buffer; + if (version == 0xff) { + log(currentField, "Invalid input: ff {0}"); + return null; + } else if (version == 0 && length > FORMAT_LENGTH) { + Platform.get().log("Invalid input: too long", null); + return null; + } + + currentField = FIELD_TRACE_ID; + break; + case FIELD_TRACE_ID: + if (traceIdHighZero && buffer == 0L) { + logReadAllZeros(currentField); + return null; + } + + builder.traceId(buffer); + + currentField = FIELD_PARENT_ID; + break; + case FIELD_PARENT_ID: + if (buffer == 0L) { + logReadAllZeros(currentField); + return null; + } + + builder.spanId(buffer); + + currentField = FIELD_TRACE_FLAGS; + break; + case FIELD_TRACE_FLAGS: + int traceparentFlags = (int) (buffer & 0xff); + // Only one flag is defined at version 0: sampled + // https://w3c.github.io/trace-context/#sampled-flag + builder.sampled(((traceparentFlags & 1) == 1)); + + // If the version is greater than zero ignore other flags and fields + // https://w3c.github.io/trace-context/#other-flags + if (version == 0) { + if ((traceparentFlags & ~1) != 0) { + log(currentField, "Invalid input: only choices are 00 or 01 {0}"); + return null; + } + + if (!isEof) { + Platform.get().log("Invalid input: more than 3 fields exist", null); + return null; + } + } + break LOOP; + default: + throw new AssertionError(); + } + + buffer = 0L; + currentFieldLength = 0; + continue; + } + + // At this point, 'c' is not a hyphen + + // When we get to a non-hyphen at position 16, we have a 128-bit trace ID. + if (currentField == FIELD_TRACE_ID && currentFieldLength == 16) { + // traceIdHigh can be zeros when a 64-bit trace ID is encoded in 128-bits. + traceIdHighZero = buffer == 0L; + builder.traceIdHigh(buffer); + + // This character is the next hex. If it isn't, the next iteration will throw. Either way, + // reset so that we can capture the next 16 characters of the trace ID. + buffer = 0L; + } + + currentFieldLength++; + + // The rest of this is normal lower-hex decoding + buffer <<= 4; + if (c >= '0' && c <= '9') { + buffer |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + buffer |= c - 'a' + 10; + } else { + log(currentField, "Invalid input: only valid characters are lower-hex for {0}"); + return null; + } + } + + try { + return builder.build(); + } catch (IllegalArgumentException e) { // trace ID or span ID were all zeros + Platform.get().log(e.getMessage(), null); + return null; + } + } + + static boolean validateFieldLength(int field, int length) { + int expectedLength = + (field == FIELD_TRACE_FLAGS || field == FIELD_VERSION) ? 2 + : field == FIELD_TRACE_ID ? 32 : 16; + if (length == 0) { + log(field, "Invalid input: empty {0}"); + return false; + } else if (length < expectedLength) { + log(field, "Invalid input: {0} is too short"); + return false; + } else if (length > expectedLength) { + log(field, "Invalid input: {0} is too long"); + return false; + } + return true; + } + + static void logReadAllZeros(int currentField) { + log(currentField, "Invalid input: read all zeros {0}"); + } + + static void log(int fieldCode, String s) { + String field; + switch (fieldCode) { + case FIELD_VERSION: + field = "version"; + break; + case FIELD_TRACE_ID: + field = "trace ID"; + break; + // Confusingly, the spec calls the span ID field parentId + // https://w3c.github.io/trace-context/#parent-id + case FIELD_PARENT_ID: + field = "parent ID"; + break; + case FIELD_TRACE_FLAGS: + field = "trace flags"; + break; + default: + throw new AssertionError("field code unmatched: " + fieldCode); + } + Platform.get().log(s, field, null); + } + + static byte[] asciiToNewByteArray(char[] buffer, int length) { + byte[] result = new byte[length]; + for (int i = 0; i < length; i++) { + result[i] = (byte) buffer[i]; + } + return result; + } + + static final ThreadLocal> BUILDER_REF = new ThreadLocal<>(); + + static TraceContext.Builder getBuilder() { + WeakReference ref = BUILDER_REF.get(); + TraceContext.Builder builder; + if (ref == null || (builder = ref.get()) == null) { + builder = TraceContext.newBuilder(); + ref = new WeakReference<>(builder); + BUILDER_REF.set(ref); + } else { + builder.clear(); + } + return builder; + } + + static final ThreadLocal CHAR_BUFFER = new ThreadLocal<>(); + + static char[] getCharBuffer() { + char[] charBuffer = CHAR_BUFFER.get(); + if (charBuffer == null) { + charBuffer = new char[FORMAT_LENGTH]; + CHAR_BUFFER.set(charBuffer); + } + return charBuffer; + } + + TraceparentFormat() { + } +} diff --git a/propagation/w3c/src/main/java/brave/propagation/w3c/TracestateFormat.java b/propagation/w3c/src/main/java/brave/propagation/w3c/TracestateFormat.java new file mode 100644 index 0000000000..e8ab6f222e --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TracestateFormat.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.internal.Nullable; + +import static brave.propagation.w3c.TraceparentFormat.FORMAT_LENGTH; + +/** + * Implements https://w3c.github.io/trace-context/#tracestate-header + * + *

In the above specification, a tracestate entry is sometimes called member. The key of the + * entry is most often called vendor name, but it is more about a tracing system vs something vendor + * specific. We choose to not use the term vendor as this is open source code. Instead, we use term + * entry (key/value). + */ +final class TracestateFormat { + final String key; + final int keyLength; + final int entryLength; + + TracestateFormat(String key) { + this.key = key; + this.keyLength = key.length(); + this.entryLength = keyLength + 1 /* = */ + FORMAT_LENGTH; + } + + enum Op { + THIS_ENTRY, + OTHER_ENTRIES + } + + interface Handler { + boolean onThisEntry(CharSequence tracestate, int beginIndex, int endIndex); + } + + // TODO: SHOULD on 512 char limit https://w3c.github.io/trace-context/#tracestate-limits + String write(String thisValue, CharSequence otherEntries) { + int extraLength = otherEntries == null ? 0 : otherEntries.length(); + + char[] result; + if (extraLength == 0) { + result = new char[entryLength]; + } else { + result = new char[entryLength + 1 /* , */ + extraLength]; + } + + int pos = 0; + for (int i = 0; i < keyLength; i++) { + result[pos++] = key.charAt(i); + } + result[pos++] = '='; + + for (int i = 0, len = thisValue.length(); i < len; i++) { + result[pos++] = thisValue.charAt(i); + } + + if (extraLength > 0) { // Append others after ours + result[pos++] = ','; + for (int i = 0; i < extraLength; i++) { + result[pos++] = otherEntries.charAt(i); + } + } + return new String(result, 0, pos); + } + + // TODO: characters were added to the valid list, so it is possible this impl no longer works + // TODO: 32 max entries https://w3c.github.io/trace-context/#tracestate-header-field-values + // TODO: empty and whitespace-only allowed Ex. 'foo=' or 'foo= ' + @Nullable CharSequence parseAndReturnOtherEntries(String tracestate, Handler handler) { + StringBuilder currentString = new StringBuilder(), otherEntries = null; + Op op; + OUTER: + for (int i = 0, length = tracestate.length(); i < length; i++) { + char c = tracestate.charAt(i); + // OWS is zero or more spaces or tabs https://httpwg.org/specs/rfc7230.html#rfc.section.3.2 + if (c == ' ' || c == '\t') continue; // trim whitespace + if (c == '=') { // we reached a field name + if (++i == length) break; // skip '=' character + if (currentString.indexOf(key) == 0) { + op = Op.THIS_ENTRY; + } else { + op = Op.OTHER_ENTRIES; + if (otherEntries == null) otherEntries = new StringBuilder(); + otherEntries.append(',').append(currentString); + } + currentString.setLength(0); + } else { + currentString.append(c); + continue; + } + // no longer whitespace + switch (op) { + case OTHER_ENTRIES: + otherEntries.append(c); + while (i < length && (c = tracestate.charAt(i)) != ',') { + otherEntries.append(c); + i++; + } + break; + case THIS_ENTRY: + int nextComma = tracestate.indexOf(',', i); + int endIndex = nextComma != -1 ? nextComma : length; + if (!handler.onThisEntry(tracestate, i, endIndex)) { + break OUTER; + } + i = endIndex; + break; + } + } + return otherEntries; + } +} diff --git a/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationTest.java b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationTest.java new file mode 100644 index 0000000000..0276171b56 --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +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.w3c.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 validB3Single = "67891233abcdef012345678912345678-463ac35c9f6413ad-1"; + String otherState = "congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4="; + + @Test public void injects_b3_when_no_other_tracestate() { + Extra extra = new Extra(); + + sampledContext = sampledContext.toBuilder().extra(asList(extra)).build(); + + injector.inject(sampledContext, carrier); + + assertThat(carrier).containsEntry("tracestate", "b3=" + validB3Single); + } + + @Test public void injects_b3_before_other_tracestate() { + Extra extra = new Extra(); + extra.otherEntries = otherState; + + sampledContext = sampledContext.toBuilder().extra(asList(extra)).build(); + + injector.inject(sampledContext, carrier); + + assertThat(carrier).containsEntry("tracestate", "b3=" + validB3Single + "," + otherState); + } + + @Test public void extracts_b3_when_no_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", "b3=" + validB3Single); + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(new Extra()) + .context(sampledContext) + .build()); + } + + @Test public void extracts_b3_before_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", "b3=" + validB3Single + "," + otherState); + + Extra extra = new Extra(); + extra.otherEntries = otherState; + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(extra) + .context(sampledContext) + .build()); + } + + @Test public void extracts_b3_after_other_tracestate() { + carrier.put("traceparent", validTraceparent); + carrier.put("tracestate", otherState + ",b3=" + validB3Single); + + Extra extra = new Extra(); + extra.otherEntries = otherState; + + assertThat(extractor.extract(carrier)) + .isEqualTo( + TraceContextOrSamplingFlags.newBuilder() + .addExtra(extra) + .context(sampledContext) + .build()); + } +} diff --git a/propagation/w3c/src/test/java/brave/propagation/w3c/TraceparentFormatTest.java b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceparentFormatTest.java new file mode 100644 index 0000000000..8870911c77 --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceparentFormatTest.java @@ -0,0 +1,383 @@ +/* + * Copyright 2013-2020 The OpenZipkin 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 brave.propagation.w3c; + +import brave.internal.Platform; +import brave.propagation.TraceContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import static brave.propagation.w3c.TraceparentFormat.parseTraceparentFormat; +import static brave.propagation.w3c.TraceparentFormat.writeTraceparentFormat; +import static brave.propagation.w3c.TraceparentFormat.writeTraceparentFormatAsBytes; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +// Added to declutter console: tells power mock not to mess with implicit classes we aren't testing +@PowerMockIgnore({"org.apache.logging.*", "javax.script.*"}) +@PrepareForTest({Platform.class, TraceparentFormat.class}) +public class TraceparentFormatTest { + String traceIdHigh = "1234567890123459"; + String traceId = "1234567890123451"; + String parentId = "1234567890123452"; + String spanId = "1234567890123453"; + + Platform platform = mock(Platform.class); + + @Before public void setupLogger() { + mockStatic(Platform.class); + when(Platform.get()).thenReturn(platform); + } + + /** Either we asserted on the log messages or there weren't any */ + @After public void ensureNothingLogged() { + verifyNoMoreInteractions(platform); + } + + /** unsampled isn't the same as not-yet-sampled, but we have no better choice */ + @Test public void writeTraceparentFormat_notYetSampled_128() { + TraceContext context = TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-" + traceIdHigh + traceId + "-" + spanId + "-00") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + @Test public void writeTraceparentFormat_unsampled() { + TraceContext context = TraceContext.newBuilder() + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(false).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-0000000000000000" + traceId + "-" + spanId + "-00") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + @Test public void writeTraceparentFormat_sampled() { + TraceContext context = TraceContext.newBuilder() + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-0000000000000000" + traceId + "-" + spanId + "-01") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + /** debug isn't the same as sampled, but we have no better choice */ + @Test public void writeTraceparentFormat_debug() { + TraceContext context = TraceContext.newBuilder() + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .debug(true).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-0000000000000000" + traceId + "-" + spanId + "-01") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + /** + * There is no field for the parent ID in "traceparent" format. What it calls "parent ID" is + * actually the span ID. + */ + @Test public void writeTraceparentFormat_parent() { + TraceContext context = TraceContext.newBuilder() + .traceId(Long.parseUnsignedLong(traceId, 16)) + .parentId(Long.parseUnsignedLong(parentId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-0000000000000000" + traceId + "-" + spanId + "-01") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + @Test public void writeTraceparentFormat_largest() { + TraceContext context = TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .parentId(Long.parseUnsignedLong(parentId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .debug(true).build(); + + assertThat(writeTraceparentFormat(context)) + .isEqualTo("00-" + traceIdHigh + traceId + "-" + spanId + "-01") + .isEqualTo(new String(writeTraceparentFormatAsBytes(context), UTF_8)); + } + + @Test public void parseTraceparentFormat_sampled() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-01")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build() + ); + } + + @Test public void parseTraceparentFormat_unsampled() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-00")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(false).build() + ); + } + + @Test public void parseTraceparentFormat_padded() { + assertThat(parseTraceparentFormat("00-0000000000000000" + traceId + "-" + spanId + "-01")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build() + ); + } + + @Test public void parseTraceparentFormat_padded_right() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + "0000000000000000-" + spanId + "-01")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build() + ); + } + + @Test public void parseTraceparentFormat_newer_version() { + assertThat(parseTraceparentFormat("10-" + traceIdHigh + traceId + "-" + spanId + "-00")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(false).build() + ); + } + + @Test public void parseTraceparentFormat_newer_version_ignores_extra_fields() { + assertThat(parseTraceparentFormat("10-" + traceIdHigh + traceId + "-" + spanId + "-00-fobaly")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(false).build() + ); + } + + @Test public void parseTraceparentFormat_newer_version_ignores_extra_flags() { + assertThat(parseTraceparentFormat("10-" + traceIdHigh + traceId + "-" + spanId + "-ff")) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build() + ); + } + + /** for example, parsing inside tracestate */ + @Test public void parseTraceparentFormat_middleOfString() { + String input = "tc=00-" + traceIdHigh + traceId + "-" + spanId + "-01,"; + assertThat(parseTraceparentFormat(input, 3, input.length() - 1)) + .isEqualToComparingFieldByField(TraceContext.newBuilder() + .traceIdHigh(Long.parseUnsignedLong(traceIdHigh, 16)) + .traceId(Long.parseUnsignedLong(traceId, 16)) + .spanId(Long.parseUnsignedLong(spanId, 16)) + .sampled(true).build() + ); + } + + @Test public void parseTraceparentFormat_middleOfString_incorrectIndex() { + String input = "tc=00-" + traceIdHigh + traceId + "-" + spanId + "-00,"; + assertThat(parseTraceparentFormat(input, 0, 12)) + .isNull(); // instead of raising exception + + verify(platform) + .log("Invalid input: only valid characters are lower-hex for {0}", "version", null); + } + + /** This tests that the being index is inclusive and the end index is exclusive */ + @Test public void parseTraceparentFormat_ignoresBeforeAndAfter() { + String encoded = "00-" + traceIdHigh + traceId + "-" + spanId + "-01"; + String sequence = "??" + encoded + "??"; + assertThat(parseTraceparentFormat(sequence, 2, 2 + encoded.length())) + .isEqualToComparingFieldByField(parseTraceparentFormat(encoded)); + } + + @Test public void parseTraceparentFormat_malformed() { + assertThat(parseTraceparentFormat("not-a-tumor")) + .isNull(); // instead of raising exception + + verify(platform) + .log("Invalid input: only valid characters are lower-hex for {0}", "version", null); + } + + @Test public void parseTraceparentFormat_malformed_notAscii() { + assertThat(parseTraceparentFormat( + "00-" + traceIdHigh + traceId + "-" + spanId.substring(0, 15) + "💩-1")) + .isNull(); // instead of crashing + + verify(platform) + .log("Invalid input: only valid characters are lower-hex for {0}", "parent ID", null); + } + + @Test public void parseTraceparentFormat_malformed_uuid() { + assertThat(parseTraceparentFormat("b970dafd-0d95-40aa-95d8-1d8725aebe40")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too long", "version", null); + } + + @Test public void parseTraceparentFormat_short_traceId() { + assertThat( + parseTraceparentFormat("00-" + traceId + "-" + spanId + "-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "trace ID", null); + } + + @Test public void parseTraceparentFormat_zero_traceId() { + assertThat( + parseTraceparentFormat("00-00000000000000000000000000000000-" + spanId + "-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: read all zeros {0}", "trace ID", null); + } + + @Test public void parseTraceparentFormat_fails_on_extra_flags() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-ff")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: only choices are 00 or 01 {0}", "trace flags", null); + } + + @Test public void parseTraceparentFormat_fails_on_extra_fields() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-0-")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "trace flags", null); + } + + @Test public void parseTraceparentFormat_fails_on_version_ff() { + assertThat(parseTraceparentFormat("ff-" + traceIdHigh + traceId + "-" + spanId + "-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: ff {0}", "version", null); + } + + @Test public void parseTraceparentFormat_zero_spanId() { + assertThat( + parseTraceparentFormat("00-" + traceIdHigh + traceId + "-0000000000000000-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: read all zeros {0}", "parent ID", null); + } + + @Test public void parseTraceparentFormat_empty() { + assertThat(parseTraceparentFormat("")).isNull(); + + verify(platform).log("Invalid input: empty", null); + } + + @Test public void parseTraceparentFormat_empty_version() { + assertThat(parseTraceparentFormat("-" + traceIdHigh + traceId + "-" + spanId + "-00")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: empty {0}", "version", null); + } + + @Test public void parseTraceparentFormat_empty_traceId() { + assertThat(parseTraceparentFormat("00--" + spanId + "-00")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: empty {0}", "trace ID", null); + } + + @Test public void parseTraceparentFormat_empty_spanId() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "--01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: empty {0}", "parent ID", null); + } + + @Test public void parseTraceparentFormat_empty_flags() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: empty {0}", "trace flags", null); + } + + @Test public void parseTraceparentFormat_truncated_traceId() { + assertThat(parseTraceparentFormat("00-1-" + spanId + "-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "trace ID", null); + } + + @Test public void parseTraceparentFormat_truncated_traceId128() { + assertThat(parseTraceparentFormat("00-1" + traceId + "-" + spanId + "-01")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "trace ID", null); + } + + @Test public void parseTraceparentFormat_truncated_spanId() { + assertThat( + parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId.substring(0, 15) + "-00")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "parent ID", null); + } + + @Test public void parseTraceparentFormat_truncated_flags() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-0")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too short", "trace flags", null); + } + + @Test public void parseTraceparentFormat_traceIdTooLong() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "a" + "-" + spanId + "-0")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too long", "trace ID", null); + } + + @Test public void parseTraceparentFormat_spanIdTooLong() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "a-0")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: {0} is too long", "parent ID", null); + } + + @Test public void parseTraceparentFormat_flagsTooLong() { + assertThat(parseTraceparentFormat("00-" + traceIdHigh + traceId + "-" + spanId + "-001")) + .isNull(); // instead of raising exception + + verify(platform).log("Invalid input: too long", null); + } +} diff --git a/propagation/w3c/src/test/resources/log4j2.properties b/propagation/w3c/src/test/resources/log4j2.properties new file mode 100755 index 0000000000..5d75397687 --- /dev/null +++ b/propagation/w3c/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