diff --git a/brave-bom/pom.xml b/brave-bom/pom.xml index 27149e3a30..193287b3e3 100644 --- a/brave-bom/pom.xml +++ b/brave-bom/pom.xml @@ -277,6 +277,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 6e0351d0f1..089008f2d1 100644 --- a/instrumentation/benchmarks/pom.xml +++ b/instrumentation/benchmarks/pom.xml @@ -239,6 +239,12 @@ grpc-testing ${grpc.version} + + + ${project.groupId} + brave-propagation-w3c + ${project.version} + 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..659c020171 --- /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.codec.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.newFactory().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 incoming = 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() { + return tcExtractor.extract(incoming); + } + + @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/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TracestateFormatBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TracestateFormatBenchmarks.java new file mode 100644 index 0000000000..2c8396428d --- /dev/null +++ b/instrumentation/benchmarks/src/main/java/brave/propagation/w3c/TracestateFormatBenchmarks.java @@ -0,0 +1,86 @@ +/* + * 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 java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +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; + +import static brave.propagation.w3c.TracestateFormat.validateKey; + +/** + * This mainly shows the impact of much slower approaches, such as regular expressions. However, + * this is also used to help us evaluate efficiencies beyond that. + */ +@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 10, time = 1) +@Fork(3) +@BenchmarkMode(Mode.Throughput) // simpler to interpret vs sample time +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class TracestateFormatBenchmarks { + // see https://github.com/w3c/trace-context/pull/386 for clearer definition of this stuff + static final String KEY_CHAR = "[a-z0-9_\\-*/]"; + static final Pattern KEY_PATTERN = Pattern.compile("^(" + + "[a-z]" + KEY_CHAR + "{0,255}" + // Basic Key + "|" + // OR + "[a-z0-9]" + KEY_CHAR + "{0,240}@[a-z]" + KEY_CHAR + "{0,13}" + // Tenant Key + ")$"); + + // copied from TracestateFormatTest as we don't share classpath + static final String FORTY_KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_-*/"; + static final String TWO_HUNDRED_FORTY_KEY_CHARS = + FORTY_KEY_CHARS + FORTY_KEY_CHARS + FORTY_KEY_CHARS + + FORTY_KEY_CHARS + FORTY_KEY_CHARS + FORTY_KEY_CHARS; + + static final String LONGEST_BASIC_KEY = + TWO_HUNDRED_FORTY_KEY_CHARS + FORTY_KEY_CHARS.substring(0, 16); + + static final String LONGEST_TENANT_KEY = + "1" + TWO_HUNDRED_FORTY_KEY_CHARS + "@" + FORTY_KEY_CHARS.substring(0, 13); + + @Benchmark public boolean validateKey_brave_longest_basic() { + return validateKey(LONGEST_BASIC_KEY, false); + } + + @Benchmark public boolean validateKey_brave_longest_tenant() { + return validateKey(LONGEST_TENANT_KEY, false); + } + + @Benchmark public boolean validateKey_regex_longest_basic() { + return KEY_PATTERN.matcher(LONGEST_BASIC_KEY).matches(); + } + + @Benchmark public boolean validateKey_regex_longest_tenant() { + return KEY_PATTERN.matcher(LONGEST_TENANT_KEY).matches(); + } + + // Convenience main entry-point + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(".*" + TracestateFormatBenchmarks.class.getSimpleName()) + .build(); + + new Runner(opt).run(); + } +} diff --git a/pom.xml b/pom.xml index e9d58da0af..311fea4e75 100755 --- a/pom.xml +++ b/pom.xml @@ -136,6 +136,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..e12bdd0bd6 --- /dev/null +++ b/propagation/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + + io.zipkin.brave + brave-parent + 5.11.3-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..e16b5ab9d5 --- /dev/null +++ b/propagation/w3c/RATIONALE.md @@ -0,0 +1,68 @@ +# 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. + +#### `TraceContext.sampled() == null` +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. + +#### `TraceContext.parentId()` +The parentId is a propagated field and also used in logging expressions. This is important for RPC +spans as downstream usually finishes before upstream. This obviates a data race even if Zipkin's UI +can tolerate lack of parent ID. What `traceparent` calls `parent-id` is not the parent, rather the +span ID. It has no field for the actual parent ID. + +#### 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..3f8b5947cb --- /dev/null +++ b/propagation/w3c/pom.xml @@ -0,0 +1,64 @@ + + + + + io.zipkin.brave + brave-propagation-parent + 5.11.3-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..5a5cb89997 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextExtractor.java @@ -0,0 +1,67 @@ +/* + * 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.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContextOrSamplingFlags; + +import static brave.propagation.B3SingleFormat.parseB3SingleFormat; +import static brave.propagation.w3c.TraceContextPropagation.TRACEPARENT; +import static brave.propagation.w3c.TraceContextPropagation.TRACESTATE; + +// TODO: this class has no useful tests wrt traceparent yet +final class TraceContextExtractor implements Extractor { + final Getter getter; + final TraceContextPropagation propagation; + + TraceContextExtractor(TraceContextPropagation propagation, Getter getter) { + this.getter = getter; + this.propagation = propagation; + } + + @Override public TraceContextOrSamplingFlags extract(R request) { + if (request == null) throw new NullPointerException("request == null"); + String traceparentString = getter.get(request, TRACEPARENT); + if (traceparentString == null) return TraceContextOrSamplingFlags.EMPTY; + + // TODO: add link that says tracestate itself is optional + String tracestateString = getter.get(request, TRACESTATE); + if (tracestateString == null) { + // NOTE: we may not want to pay attention to the sampled flag. Since it conflates + // not-yet-sampled with sampled=false, implementations that always set flags to -00 would + // never be traced! + // + // NOTE: We are required to use the same trace ID, there's some vagueness about the parent + // span ID. Ex we don't know if upstream are sending to the same system or not, when we can't + // read the tracestate header. Trusting the span ID (traceparent calls the span ID parent-id) + // could result in a headless trace. + TraceContext maybeUpstream = TraceparentFormat.parseTraceparentFormat(traceparentString); + return TraceContextOrSamplingFlags.create(maybeUpstream); + } + + Tracestate tracestate = propagation.tracestateFactory.create(); + TraceContextOrSamplingFlags extracted = null; + if (TracestateFormat.INSTANCE.parseInto(tracestateString, tracestate)) { + String b3 = tracestate.get(propagation.tracestateKey); + if (b3 != null) { + tracestate.put(propagation.tracestateKey, null); + extracted = parseB3SingleFormat(b3); + } + } + if (extracted == null) extracted = TraceContextOrSamplingFlags.EMPTY; + return extracted.toBuilder().addExtra(tracestate).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..bf81025d85 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextInjector.java @@ -0,0 +1,40 @@ +/* + * 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.B3SingleFormat; +import brave.propagation.Propagation.Setter; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Injector; + +import static brave.propagation.w3c.TraceContextPropagation.TRACEPARENT; +import static brave.propagation.w3c.TraceContextPropagation.TRACESTATE; +import static brave.propagation.w3c.TraceparentFormat.writeTraceparentFormat; + +final class TraceContextInjector implements Injector { + final Setter setter; + final String tracestateKey; + + TraceContextInjector(TraceContextPropagation propagation, Setter setter) { + this.setter = setter; + this.tracestateKey = propagation.tracestateKey; + } + + @Override public void inject(TraceContext context, R request) { + setter.put(request, TRACEPARENT, writeTraceparentFormat(context)); + Tracestate tracestate = context.findExtra(Tracestate.class); + tracestate.put(tracestateKey, B3SingleFormat.writeB3SingleFormat(context)); + setter.put(request, TRACESTATE, tracestate.stateString()); + } +} 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..2be7c177c8 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceContextPropagation.java @@ -0,0 +1,101 @@ +/* + * 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.propagation.StringPropagationAdapter; +import brave.propagation.Propagation; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import java.util.Collections; +import java.util.List; + +import static java.util.Arrays.asList; + +public final class TraceContextPropagation extends Propagation.Factory + implements Propagation { + static final String TRACEPARENT = "traceparent", TRACESTATE = "tracestate"; + + public static Propagation.Factory create() { + return new Builder().build(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + static final TracestateFormat THROWING_VALIDATOR = new TracestateFormat(true); + String tracestateKey = "b3"; + + /** + * The key to use inside the {@code tracestate} value. Defaults to "b3". + * + * @throws IllegalArgumentException if the key doesn't conform to ABNF rules defined by the + * trace-context specification. + */ + public Builder tracestateKey(String key) { + if (key == null) throw new NullPointerException("key == null"); + THROWING_VALIDATOR.validateKey(key, 0, key.length()); + this.tracestateKey = key; + return this; + } + + public Propagation.Factory build() { + return new TraceContextPropagation(this); + } + + Builder() { + } + } + + final String tracestateKey; + final Tracestate.Factory tracestateFactory; + final List keys = Collections.unmodifiableList(asList(TRACEPARENT, TRACESTATE)); + + TraceContextPropagation(Builder builder) { + this.tracestateKey = builder.tracestateKey; + this.tracestateFactory = Tracestate.newFactory(tracestateKey); + } + + @Override public List keys() { + return keys; + } + + @Override public boolean requires128BitTraceId() { + return true; + } + + @Override public TraceContext decorate(TraceContext context) { + return tracestateFactory.decorate(context); + } + + @Override public Propagation get() { + return this; + } + + @Override public Propagation create(KeyFactory keyFactory) { + return StringPropagationAdapter.create(this, keyFactory); + } + + @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); + } +} 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..063d0a767a --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TraceparentFormat.java @@ -0,0 +1,280 @@ +/* + * 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.nio.ByteBuffer; + +import static brave.internal.codec.HexCodec.writeHexLong; + +/** Implements https://w3c.github.io/trace-context/#traceparent-header */ +// TODO: this uses the internal Platform class as it defers access to the logger and makes JUL less +// expensive. We should inline that here to to unhook the internal dep. +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 requests 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; + } + + // Benchmarks show no difference in memory usage re-using with thread local vs newing each time. + TraceContext.Builder builder = TraceContext.newBuilder(); + + int version = 0; + 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; + } + } + + return builder.build(); + } + + static boolean validateFieldLength(int field, int length) { + int expectedLength = (field == FIELD_VERSION || field == FIELD_TRACE_FLAGS) + ? 2 // There are two fields that are 2 characters long: version and flags + : field == FIELD_TRACE_ID ? 32 : 16; // trace ID or span ID + 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 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/Tracestate.java b/propagation/w3c/src/main/java/brave/propagation/w3c/Tracestate.java new file mode 100644 index 0000000000..b97afe7dcc --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/Tracestate.java @@ -0,0 +1,68 @@ +/* + * 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.extra.MapExtra; +import brave.internal.extra.MapExtraFactory; + +final class Tracestate extends MapExtra { + static Factory newFactory(String tracestateKey) { + // max is total initial + dynamic + return new FactoryBuilder().addInitialKey(tracestateKey).maxDynamicEntries(31).build(); + } + + static final class FactoryBuilder extends + MapExtraFactory.Builder { + @Override protected Factory build() { + return new Factory(this); + } + } + + static final class Factory extends MapExtraFactory { + Factory(FactoryBuilder builder) { + super(builder); + } + + @Override protected Tracestate create() { + return new Tracestate(this); + } + } + + Tracestate(Factory factory) { + super(factory); + } + + @Override protected String get(String key) { + return super.get(key); + } + + @Override protected String stateString() { + Object[] array = (Object[]) state; + // TODO: SHOULD on 512 char limit https://w3c.github.io/trace-context/#tracestate-limits + StringBuilder result = new StringBuilder(); + boolean empty = true; + for (int i = 0; i < array.length; i += 2) { + String key = (String) array[i], value = (String) array[i + 1]; + if (value == null) continue; + if (!empty) result.append(','); + result.append(key).append('=').append(value); + empty = false; + } + return result.toString(); + } + + @Override protected boolean put(String key, String value) { + return super.put(key, value); + } +} 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..802690fc01 --- /dev/null +++ b/propagation/w3c/src/main/java/brave/propagation/w3c/TracestateFormat.java @@ -0,0 +1,119 @@ +/* + * 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.internal.codec.EntrySplitter; + +/** + * 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 implements EntrySplitter.Handler { + static final TracestateFormat INSTANCE = new TracestateFormat(false); + + final boolean shouldThrow; + final EntrySplitter entrySplitter; + + TracestateFormat(boolean shouldThrow) { + this.shouldThrow = shouldThrow; + entrySplitter = EntrySplitter.newBuilder() + .maxEntries(32) // https://w3c.github.io/trace-context/#tracestate-header-field-values + .entrySeparator(',') + .trimOWSAroundEntrySeparator(true) // https://w3c.github.io/trace-context/#list + .keyValueSeparator('=') + .trimOWSAroundKeyValueSeparator(false) // https://github.com/w3c/trace-context/issues/409 + .shouldThrow(shouldThrow) + .build(); + } + + // Simplify parsing rules by allowing value-based lookup on an ASCII value. + // + // This approach is similar to io.netty.util.internal.StringUtil.HEX2B as it uses an array to + // cache values. Unlike HEX2B, this requires a bounds check when using the character's integer + // value as a key. + // + // The performance cost of a bounds check is still better than using BitSet, and avoids allocating + // an array of 64 thousand booleans: that could be problematic in old JREs or Android. + static int LAST_VALID_KEY_CHAR = 'z'; + static boolean[] VALID_KEY_CHARS = new boolean[LAST_VALID_KEY_CHAR + 1]; + + static { + for (char c = 0; c < VALID_KEY_CHARS.length; c++) { + VALID_KEY_CHARS[c] = isValidTracestateKeyChar(c); + } + } + + static boolean isValidTracestateKeyChar(char c) { + return isLetterOrNumber(c) || c == '@' || c == '_' || c == '-' || c == '*' || c == '/'; + } + + static boolean isLetterOrNumber(char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + } + + @Override + public boolean onEntry( + Tracestate target, String buffer, int beginKey, int endKey, int beginValue, int endValue) { + if (!validateKey(buffer, beginKey, endKey)) return false; + if (!validateValue(buffer, beginValue, beginValue)) return false; + return target.put(buffer.substring(beginKey, endKey), buffer.substring(beginValue, endValue)); + } + + boolean parseInto(String tracestateString, Tracestate tracestate) { + return entrySplitter.parse(this, tracestate, tracestateString); + } + + /** + * Performs validation according to the ABNF of the {@code tracestate} key. + * + *

See https://www.w3.org/TR/trace-context-1/#key + */ + // Logic to narrow error messages is intentionally deferred. + // Performance matters as this could be called up to 32 times per header. + boolean validateKey(CharSequence buffer, int beginKey, int endKey) { + int length = endKey - beginKey; + if (length == 0) return logOrThrow("Invalid key: empty", shouldThrow); + if (length > 256) return logOrThrow("Invalid key: too large", shouldThrow); + char first = buffer.charAt(beginKey); + if (!isLetterOrNumber(first)) { + return logOrThrow("Invalid key: must start with a-z 0-9", shouldThrow); + } + + for (int i = beginKey + 1; i < endKey; i++) { + char c = buffer.charAt(i); + + if (c > LAST_VALID_KEY_CHAR || !VALID_KEY_CHARS[c]) { + return logOrThrow("Invalid key: valid characters are: a-z 0-9 _ - * / @", shouldThrow); + } + } + return true; + } + + boolean validateValue(CharSequence buffer, int beginValue, int endValue) { + // TODO: empty and whitespace-only allowed Ex. 'foo=' or 'foo= ' + // There are likely other rules, so figure out what they are and implement. + return true; + } + + static boolean logOrThrow(String msg, boolean shouldThrow) { + if (shouldThrow) throw new IllegalArgumentException(msg); + Platform.get().log(msg, null); + return false; + } +} diff --git a/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationClassLoaderTest.java b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationClassLoaderTest.java new file mode 100644 index 0000000000..565cd0dee5 --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationClassLoaderTest.java @@ -0,0 +1,53 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +import static brave.test.util.ClassLoaders.assertRunIsUnloadable; + +public class TraceContextPropagationClassLoaderTest { + @Test public void unloadable_afterBasicUsage() { + assertRunIsUnloadable(BasicUsage.class, getClass().getClassLoader()); + } + + static class BasicUsage implements Runnable { + @Override public void run() { + Propagation.Factory propagation = TraceContextPropagation.create(); + Injector> injector = propagation.get().injector(Map::put); + Extractor> extractor = propagation.get().extractor(Map::get); + + TraceContext context = + propagation.decorate(TraceContext.newBuilder().traceId(1L).spanId(2L).build()); + + Map headers = new LinkedHashMap<>(); + injector.inject(context, headers); + + String traceparent = headers.get("traceparent"); + if (!"00-00000000000000000000000000000001-0000000000000002-00".equals(traceparent)) { + throw new AssertionError(); + } + + if (!context.equals(extractor.extract(headers).context())) { + throw new AssertionError(); + } + } + } +} 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..9b3e1e0ec4 --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceContextPropagationTest.java @@ -0,0 +1,102 @@ +/* + * 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 brave.propagation.TraceContextOrSamplingFlags; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +import static brave.internal.codec.HexCodec.lowerHexToUnsignedLong; +import static org.assertj.core.api.Assertions.assertThat; + +public class TraceContextPropagationTest { + Map request = new LinkedHashMap<>(); + Propagation.Factory propagation = TraceContextPropagation.create(); + Injector> injector = propagation.get().injector(Map::put); + Extractor> extractor = propagation.get().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=t61rcWkgMzE"; + + @Test public void injects_b3_when_no_other_tracestate() { + sampledContext = propagation.decorate(sampledContext); + + injector.inject(sampledContext, request); + + assertThat(request).containsEntry("tracestate", "b3=" + validB3Single); + } + + @Test public void injects_b3_before_other_tracestate() { + sampledContext = propagation.decorate(sampledContext); + TracestateFormat.INSTANCE.parseInto(otherState, sampledContext.findExtra(Tracestate.class)); + + injector.inject(sampledContext, request); + + assertThat(request).containsEntry("tracestate", "b3=" + validB3Single + "," + otherState); + } + + @Test public void extracts_b3_when_no_other_tracestate() { + request.put("traceparent", validTraceparent); + request.put("tracestate", "b3=" + validB3Single); + + assertThat(extractor.extract(request)).isEqualTo( + TraceContextOrSamplingFlags.create(propagation.decorate(sampledContext))); + } + + @Test public void extracts_b3_before_other_tracestate() { + request.put("traceparent", validTraceparent); + request.put("tracestate", "b3=" + validB3Single + "," + otherState); + + sampledContext = propagation.decorate(sampledContext); + TracestateFormat.INSTANCE.parseInto(otherState, sampledContext.findExtra(Tracestate.class)); + + assertThat(extractor.extract(request)) + .isEqualTo(TraceContextOrSamplingFlags.create(sampledContext)); + } + + @Test public void extracted_toString() { + request.put("traceparent", validTraceparent); + request.put("tracestate", "b3=" + validB3Single + "," + otherState); + + assertThat(extractor.extract(request)).hasToString( + "Extracted{" + + "traceContext=" + sampledContext + ", " + + "samplingFlags=SAMPLED_REMOTE, " + + "extra=[Tracestate{" + otherState + "}]" + + "}"); + } + + @Test public void extracts_b3_after_other_tracestate() { + request.put("traceparent", validTraceparent); + request.put("tracestate", otherState + ",b3=" + validB3Single); + + sampledContext = propagation.decorate(sampledContext); + TracestateFormat.INSTANCE.parseInto(otherState, sampledContext.findExtra(Tracestate.class)); + + assertThat(extractor.extract(request)) + .isEqualTo(TraceContextOrSamplingFlags.create(sampledContext)); + } +} 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..5d73a8e29f --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TraceparentFormatTest.java @@ -0,0 +1,384 @@ +/* + * 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/java/brave/propagation/w3c/TracestateFormatTest.java b/propagation/w3c/src/test/java/brave/propagation/w3c/TracestateFormatTest.java new file mode 100644 index 0000000000..12b16de263 --- /dev/null +++ b/propagation/w3c/src/test/java/brave/propagation/w3c/TracestateFormatTest.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 java.util.Arrays; +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.AbstractThrowableAssert; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TracestateFormatTest { + static final String FORTY_KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_-*/"; + static final String TWO_HUNDRED_FORTY_KEY_CHARS = + FORTY_KEY_CHARS + FORTY_KEY_CHARS + FORTY_KEY_CHARS + + FORTY_KEY_CHARS + FORTY_KEY_CHARS + FORTY_KEY_CHARS; + + static final String LONGEST_BASIC_KEY = + TWO_HUNDRED_FORTY_KEY_CHARS + FORTY_KEY_CHARS.substring(0, 16); + + static final String LONGEST_TENANT_KEY = + "1" + TWO_HUNDRED_FORTY_KEY_CHARS + "@" + FORTY_KEY_CHARS.substring(0, 13); + + TracestateFormat tracestateFormat = new TracestateFormat(true); + + // all these need log assertions + @Test public void validateKey_empty() { + assertThatThrownByValidateKey("") + .hasMessage("Invalid key: empty"); + } + + @Test public void validateKey_tooLong() { + char[] tooMany = new char[257]; + Arrays.fill(tooMany, 'a'); + assertThatThrownByValidateKey(new String(tooMany)) + .hasMessage("Invalid key: too large"); + } + + @Test public void validateKey_specialCharacters() { + for (char allowedSpecial : Arrays.asList('@', '_', '-', '*', '/')) { + assertThatThrownByValidateKey(allowedSpecial + "") + .hasMessage("Invalid key: must start with a-z 0-9"); + assertThatValidateKey("a" + allowedSpecial).isTrue(); + // Any number of special characters are allowed. ex "a*******", "a@@@@@@@" + // https://github.com/w3c/trace-context/pull/386 + assertThatValidateKey("a" + allowedSpecial + allowedSpecial).isTrue(); + assertThatValidateKey("a" + allowedSpecial + "1").isTrue(); + } + } + + @Test public void validateKey_longest_basic() { + assertThatValidateKey(LONGEST_BASIC_KEY).isTrue(); + } + + @Test public void validateKey_longest_tenant() { + assertThatValidateKey(LONGEST_TENANT_KEY).isTrue(); + } + + @Test public void validateKey_shortest() { + for (char n = '0'; n <= '9'; n++) { + assertThatValidateKey(String.valueOf(n)).isTrue(); + } + for (char l = 'a'; l <= 'z'; l++) { + assertThatValidateKey(String.valueOf(l)).isTrue(); + } + } + + @Test public void validateKey_invalid_unicode() { + assertThatThrownByValidateKey("a💩") + .hasMessage("Invalid key: valid characters are: a-z 0-9 _ - * / @"); + assertThatThrownByValidateKey("💩a") + .hasMessage("Invalid key: must start with a-z 0-9"); + } + + AbstractBooleanAssert assertThatValidateKey(String key) { + return assertThat(tracestateFormat.validateKey(key, 0, key.length())); + } + + AbstractThrowableAssert assertThatThrownByValidateKey(String key) { + return assertThatThrownBy(() -> tracestateFormat.validateKey(key, 0, key.length())) + .isInstanceOf(IllegalArgumentException.class); + } +} 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