From 6e3f3346b87150b1a88c689000ad05e603f4170f Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Wed, 4 Oct 2017 12:53:15 +0800 Subject: [PATCH] AWS propagation support --- .../main/java/brave/internal/HexCodec.java | 10 +- instrumentation/benchmarks/pom.xml | 5 + .../main/java/brave/EndToEndBenchmarks.java | 13 + .../java/brave/http/HttpServerBenchmarks.java | 12 + .../brave/internal/PropagationBenchmarks.java | 96 ++++++ pom.xml | 1 + propagation/aws/README.md | 45 +++ propagation/aws/pom.xml | 18 + .../brave/propagation/aws/AWSPropagation.java | 326 ++++++++++++++++++ .../propagation/aws/AWSPropagationTest.java | 153 ++++++++ propagation/pom.xml | 34 ++ 11 files changed, 708 insertions(+), 5 deletions(-) create mode 100644 instrumentation/benchmarks/src/main/java/brave/internal/PropagationBenchmarks.java create mode 100644 propagation/aws/README.md create mode 100644 propagation/aws/pom.xml create mode 100644 propagation/aws/src/main/java/brave/propagation/aws/AWSPropagation.java create mode 100644 propagation/aws/src/test/java/brave/propagation/aws/AWSPropagationTest.java create mode 100644 propagation/pom.xml diff --git a/brave/src/main/java/brave/internal/HexCodec.java b/brave/src/main/java/brave/internal/HexCodec.java index ae84a398d5..8c3482b7e0 100644 --- a/brave/src/main/java/brave/internal/HexCodec.java +++ b/brave/src/main/java/brave/internal/HexCodec.java @@ -7,7 +7,7 @@ public final class HexCodec { * Parses a 1 to 32 character lower-hex string with no prefix into an unsigned long, tossing any * bits higher than 64. */ - public static long lowerHexToUnsignedLong(String lowerHex) { + public static long lowerHexToUnsignedLong(CharSequence lowerHex) { int length = lowerHex.length(); if (length < 1 || length > 32) throw isntLowerHexLong(lowerHex); @@ -19,9 +19,9 @@ public static long lowerHexToUnsignedLong(String lowerHex) { /** * Parses a 16 character lower-hex string with no prefix into an unsigned long, starting at the - * spe index. + * specified index. */ - public static long lowerHexToUnsignedLong(String lowerHex, int index) { + public static long lowerHexToUnsignedLong(CharSequence lowerHex, int index) { long result = 0; for (int endIndex = Math.min(index + 16, lowerHex.length()); index < endIndex; index++) { char c = lowerHex.charAt(index); @@ -37,7 +37,7 @@ public static long lowerHexToUnsignedLong(String lowerHex, int index) { return result; } - static NumberFormatException isntLowerHexLong(String lowerHex) { + static NumberFormatException isntLowerHexLong(CharSequence lowerHex) { throw new NumberFormatException( lowerHex + " should be a 1 to 32 character lower-hex string with no prefix"); } @@ -76,7 +76,7 @@ public static void writeHexLong(char[] data, int pos, long v) { static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - static void writeHexByte(char[] data, int pos, byte b) { + public static void writeHexByte(char[] data, int pos, byte b) { data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf]; data[pos + 1] = HEX_DIGITS[b & 0xf]; } diff --git a/instrumentation/benchmarks/pom.xml b/instrumentation/benchmarks/pom.xml index 9d1f2858f6..49780943b4 100644 --- a/instrumentation/benchmarks/pom.xml +++ b/instrumentation/benchmarks/pom.xml @@ -130,6 +130,11 @@ resteasy-undertow ${resteasy.version} + + io.zipkin.brave + brave-propagation-aws + ${project.version} + diff --git a/instrumentation/benchmarks/src/main/java/brave/EndToEndBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/EndToEndBenchmarks.java index c4dd20ef85..de8c7dd53e 100644 --- a/instrumentation/benchmarks/src/main/java/brave/EndToEndBenchmarks.java +++ b/instrumentation/benchmarks/src/main/java/brave/EndToEndBenchmarks.java @@ -2,6 +2,7 @@ import brave.http.HttpServerBenchmarks; import brave.okhttp3.TracingCallFactory; +import brave.propagation.aws.AWSPropagation; import brave.sampler.Sampler; import brave.servlet.TracingFilter; import io.undertow.servlet.Servlets; @@ -78,6 +79,15 @@ public Traced128() { } } + public static class TracedAWS extends ForwardingTracingFilter { + public TracedAWS() { + super(Tracing.newBuilder() + .propagationFactory(new AWSPropagation.Factory()) + .spanReporter(Reporter.NOOP) + .build()); + } + } + @Override protected void init(DeploymentInfo servletBuilder) { servletBuilder.addFilter(new FilterInfo("Unsampled", Unsampled.class)) .addFilterUrlMapping("Unsampled", "/unsampled", REQUEST) @@ -88,6 +98,9 @@ public Traced128() { .addFilter(new FilterInfo("Traced128", Traced128.class)) .addFilterUrlMapping("Traced128", "/traced128", REQUEST) .addFilterUrlMapping("Traced128", "/traced128/api", REQUEST) + .addFilter(new FilterInfo("TracedAWS", TracedAWS.class)) + .addFilterUrlMapping("TracedAWS", "/tracedaws", REQUEST) + .addFilterUrlMapping("TracedAWS", "/tracedaws/api", REQUEST) .addServlets(Servlets.servlet("HelloServlet", HelloServlet.class).addMapping("/*")); } diff --git a/instrumentation/benchmarks/src/main/java/brave/http/HttpServerBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/http/HttpServerBenchmarks.java index 97d1b6ff46..df1a441d0b 100644 --- a/instrumentation/benchmarks/src/main/java/brave/http/HttpServerBenchmarks.java +++ b/instrumentation/benchmarks/src/main/java/brave/http/HttpServerBenchmarks.java @@ -106,6 +106,18 @@ protected int initServer() throws ServletException { .execute().body().close(); } + @Benchmark public void tracedawsServer_get() throws Exception { + get("/tracedaws"); + } + + @Benchmark public void tracedawsServer_get_resumeTrace() throws Exception { + client.newCall(new Request.Builder().url(baseUrl() + "/traced128") + .header("X-Amzn-Trace-Id", + "Root=1-67891233-abcdef012345678912345678;Parent=463ac35c9f6413ad;Sampled=1") + .build()) + .execute().body().close(); + } + void get(String path) throws IOException { client.newCall(new Request.Builder().url(baseUrl() + path).build()).execute().body().close(); } diff --git a/instrumentation/benchmarks/src/main/java/brave/internal/PropagationBenchmarks.java b/instrumentation/benchmarks/src/main/java/brave/internal/PropagationBenchmarks.java new file mode 100644 index 0000000000..c516c92d86 --- /dev/null +++ b/instrumentation/benchmarks/src/main/java/brave/internal/PropagationBenchmarks.java @@ -0,0 +1,96 @@ +/** + * Copyright 2015-2016 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.internal; + +import brave.propagation.Propagation; +import brave.propagation.TraceContext; +import brave.propagation.TraceContext.Extractor; +import brave.propagation.TraceContext.Injector; +import brave.propagation.TraceContextOrSamplingFlags; +import brave.propagation.aws.AWSPropagation; +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.Scope; +import org.openjdk.jmh.annotations.State; +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.Throughput) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Thread) +public class PropagationBenchmarks { + + static final Injector> b3Injector = + Propagation.B3_STRING.injector(Map::put); + static final Injector> awsInjector = + new AWSPropagation.Factory().create(Propagation.KeyFactory.STRING).injector(Map::put); + static final Extractor> b3Extractor = + Propagation.B3_STRING.extractor(Map::get); + static final Extractor> awsExtractor = + new AWSPropagation.Factory().create(Propagation.KeyFactory.STRING).extractor(Map::get); + + static final TraceContext context = TraceContext.newBuilder() + .traceIdHigh(HexCodec.lowerHexToUnsignedLong("67891233abcdef01")) + .traceId(HexCodec.lowerHexToUnsignedLong("2345678912345678")) + .spanId(HexCodec.lowerHexToUnsignedLong("463ac35c9f6413ad")) + .sampled(true) + .build(); + + static final Map incoming = new LinkedHashMap() { + { + b3Injector.inject(context, this); + awsInjector.inject(context, this); + } + }; + + Map carrier = new LinkedHashMap<>(); + + @Benchmark public void inject_b3() { + b3Injector.inject(context, carrier); + } + + @Benchmark public void inject_aws() { + awsInjector.inject(context, carrier); + } + + @Benchmark public TraceContextOrSamplingFlags extract_b3() { + return b3Extractor.extract(incoming); + } + + @Benchmark public TraceContextOrSamplingFlags extract_aws() { + return awsExtractor.extract(incoming); + } + + // Convenience main entry-point + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(".*" + PropagationBenchmarks.class.getSimpleName() + ".extract_aws") + .build(); + + new Runner(opt).run(); + } +} diff --git a/pom.xml b/pom.xml index a7eb54a2ff..61b8fa953b 100755 --- a/pom.xml +++ b/pom.xml @@ -100,6 +100,7 @@ brave-tests context instrumentation + propagation spring-beans archive/brave-core archive/brave-http diff --git a/propagation/aws/README.md b/propagation/aws/README.md new file mode 100644 index 0000000000..0ea4f483ce --- /dev/null +++ b/propagation/aws/README.md @@ -0,0 +1,45 @@ +# brave-propagation-aws +This changes brave to use "x-amzn-trace-id" as opposed to "x-b3" prefixed headers to propagate trace +context across processes. + +To enable this, configure `brave.Tracing` with `AWSPropagation.Factory` like so: + +```java +tracing = Tracing.newBuilder() + .propagationFactory(new AWSPropagation.Factory()) + ... + .build(); +``` + +## Notes +* This does not send spans to Amazon. If you want to do that, use [io.zipkin.aws:reporter-xray-udp](https://github.com/openzipkin/zipkin-aws). + * Unless you send spans to amazon, the impact is only which headers are used by Brave. +* This neither depends on, nor coordinates with [com.amazonaws:aws-xray-recorder-sdk-core](http://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java.html). + * If you are using Amazon X-Ray SDK in the same app, Brave will not "see" those traces and visa versa. + * If you would like X-Ray SDK integration (such that traces are mutually visible), please raise an issue. +* This implicitly switches Brave to use 128-bit trace IDS + * Internally, Amazon's root timestamp is encoded in the first 32-bits of the 128-bit trace ID. + +## Utilities +There are a couple added utilities for parsing and generating an AWS trace ID string: + +* `AWSPropagation.traceIdString` - used to generate a formatted trace ID for correlation purposes. +* `AWSPropagation.extract` - extracts a trace context from a string such as an environment variable. + +Ex to extract the trace ID from the built-in AWS Lambda variable +```java +extracted = AWSPropagation.extract(System.getenv("_X_AMZN_TRACE_ID")); +``` + +## Extra fields +Amazon's trace ID format allows propagation of "extra" fields. For example, someone can add +diagnostic variables by appending them to the trace header as discussed in the [ALB blog](https://aws.amazon.com/blogs/aws/application-performance-percentiles-and-request-tracing-for-aws-application-load-balancer/). + +Ex: the below header includes a custom field `CalledFrom=Foo`, which is non-standard, but will be +propagated throughout the trace. +``` +X-Amzn-Trace-Id: Root=1-58211399-36d228ad5d99923122bbe354;CalledFrom=Foo +``` + +Internally, this field is stored in `TraceContext.extra()`, which allows it to be carried from the +point a trace context is extracted until where it is injected into an outgoing request or message. diff --git a/propagation/aws/pom.xml b/propagation/aws/pom.xml new file mode 100644 index 0000000000..6d8b992dcf --- /dev/null +++ b/propagation/aws/pom.xml @@ -0,0 +1,18 @@ + + + + io.zipkin.brave + brave-propagation-parent + 4.8.2-SNAPSHOT + + 4.0.0 + + brave-propagation-aws + Brave Propagation: Amazon Web Services (AWS) + + + ${project.basedir}/../.. + 1.6 + java16 + + diff --git a/propagation/aws/src/main/java/brave/propagation/aws/AWSPropagation.java b/propagation/aws/src/main/java/brave/propagation/aws/AWSPropagation.java new file mode 100644 index 0000000000..f6b304372f --- /dev/null +++ b/propagation/aws/src/main/java/brave/propagation/aws/AWSPropagation.java @@ -0,0 +1,326 @@ +package brave.propagation.aws; + +import brave.propagation.Propagation; +import brave.propagation.SamplingFlags; +import brave.propagation.TraceContext; +import brave.propagation.TraceContextOrSamplingFlags; +import brave.propagation.TraceIdContext; +import java.util.Collections; +import java.util.List; + +import static brave.internal.HexCodec.writeHexByte; +import static brave.internal.HexCodec.writeHexLong; + +/** + * Utility for working with Amazon Web Services Trace IDs, for example reading from headers or + * environment variables. {@code x-amzn-trace-id} is primarily for Amazon's X-Ray service, but it is + * also integrated with AWS ALB, API Gateway and Lambda. + * + *

For example, if you are in a lambda environment, you can read the incoming context like this: + *

{@code
+ * extracted = AWSPropagation.extract(System.getenv("_X_AMZN_TRACE_ID"));
+ * }
+ * + *

Details

{@code x-amzn-trace-id} or {@code _X_AMZN_TRACE_ID} follows RFC 6265 style + * syntax (https://tools.ietf.org/html/rfc6265#section-2.2): fields are split on semicolon and + * optional whitespace. + * + *

Description of the {@code Root} (or {@code Self}) field from AWS CLI help: + * + *

A trace_id consists of three numbers separated by hyphens. For example, {@code + * 1-58406520-a006649127e371903a2de979}. This includes: + *

+ * 
    + *
  • The version number, i.e. 1
  • + *
  • The time of the original request, in Unix epoch time, in 8 hexadecimal digits. For example, + * 10:00AM December 2nd, 2016 PST in epoch timeis 1480615200 seconds, or 58406520 in + * hexadecimal.
  • + *
  • A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits.
  • + *
+ *
+ */ +public final class AWSPropagation implements Propagation { + public static final class Factory extends Propagation.Factory { + @Override public Propagation create(KeyFactory keyFactory) { + return AWSPropagation.create(keyFactory); + } + + @Override public boolean requires128BitTraceId() { + return true; + } + } + + public static AWSPropagation create(KeyFactory keyFactory) { + return new AWSPropagation<>(keyFactory); + } + + // Using lowercase field name as http is case-insensitive, but http/2 transport downcases */ + static final String TRACE_ID_NAME = "x-amzn-trace-id"; + static final char[] ROOT = "Root=".toCharArray(); + static final char[] PARENT = ";Parent=".toCharArray(); + static final char[] SAMPLED = ";Sampled=".toCharArray(); + + final K traceIdKey; + final List fields; + + AWSPropagation(KeyFactory keyFactory) { + this.traceIdKey = keyFactory.create(TRACE_ID_NAME); + this.fields = Collections.singletonList(traceIdKey); + } + + /** returns the name of the header field: "x-amzn-trace-id" */ + @Override public List keys() { + return fields; + } + + @Override public TraceContext.Injector injector(Setter setter) { + if (setter == null) throw new NullPointerException("setter == null"); + return new AWSInjector<>(this, setter); + } + + static final class AWSInjector implements TraceContext.Injector { + final AWSPropagation propagation; + final Setter setter; + + AWSInjector(AWSPropagation propagation, Setter setter) { + this.propagation = propagation; + this.setter = setter; + } + + /** + * This version of propagation contains at least 74 characters corresponding to identifiers and + * the sampling bit. It will also include extra fields where present. + * + *

Ex 74 characters: {@code Root=1-67891233-abcdef012345678912345678;Parent=463ac35c9f6413ad;Sampled=1} + * + *

{@inheritDoc} + */ + @Override public void inject(TraceContext traceContext, C carrier) { + CharSequence extra = null; + for (int i = 0, length = traceContext.extra().size(); i < length; i++) { + Object next = traceContext.extra().get(i); + if (next instanceof Extra) { + extra = ((Extra) next).fields; + break; + } + } + int extraLength = extra == null ? 0 : extra.length(); + //Root=1-67891233-abcdef012345678912345678;Parent=463ac35c9f6413ad;Sampled=1 + char[] result = new char[74 + extraLength]; + System.arraycopy(ROOT, 0, result, 0, 5); + writeTraceId(traceContext, result, 5); + System.arraycopy(PARENT, 0, result, 40, 8); + writeHexLong(result, 48, traceContext.spanId()); + System.arraycopy(SAMPLED, 0, result, 64, 9); + Boolean sampled = traceContext.sampled(); + // Sampled status is same as B3, but ? means downstream decides (like omitting X-B3-Sampled) + // https://github.com/aws/aws-xray-sdk-go/blob/391885218b556c43ed05a1e736a766d70fc416f1/header/header.go#L50 + result[73] = sampled == null ? '?' : sampled ? '1' : '0'; + for (int i = 0; i < extraLength; i++) { + result[i + 74] = extra.charAt(i); + } + setter.put(carrier, propagation.traceIdKey, new String(result)); + } + } + + /** Used for log correlation or {@link brave.Span#tag(String, String) tag values} */ + public static String traceIdString(TraceContext context) { + char[] result = new char[35]; + writeTraceId(context, result, 0); + return new String(result); + } + + /** Writes 35 characters representing the input trace ID to the buffer at the given offset */ + static void writeTraceId(TraceContext context, char[] result, int offset) { + result[offset] = '1'; // version + result[offset + 1] = '-'; // delimiter + long high = context.traceIdHigh(); + writeHexByte(result, offset + 2, (byte) ((high >>> 56L) & 0xff)); + writeHexByte(result, offset + 4, (byte) ((high >>> 48L) & 0xff)); + writeHexByte(result, offset + 6, (byte) ((high >>> 40L) & 0xff)); + writeHexByte(result, offset + 8, (byte) ((high >>> 32L) & 0xff)); + result[offset + 10] = '-'; + writeHexByte(result, offset + 11, (byte) ((high >>> 24L) & 0xff)); + writeHexByte(result, offset + 13, (byte) ((high >>> 16L) & 0xff)); + writeHexByte(result, offset + 15, (byte) ((high >>> 8L) & 0xff)); + writeHexByte(result, offset + 17, (byte) (high & 0xff)); + writeHexLong(result, offset + 19, context.traceId()); + } + + @Override public TraceContext.Extractor extractor(Getter getter) { + if (getter == null) throw new NullPointerException("getter == null"); + return new AWSExtractor(this, getter); + } + + static final AWSExtractor STRING_EXTRACTOR = + new AWSExtractor<>(new AWSPropagation<>(KeyFactory.STRING), (carrier, key) -> carrier); + + /** + * Like {@link TraceContext.Extractor#extract(Object)} except reading from a single field. + * + *

This is used for extracting from the AWS lambda environment variable {@code + * X_AMZN_TRACE_ID}. + */ + public static TraceContextOrSamplingFlags extract(String amznTraceId) { + if (amznTraceId == null) throw new NullPointerException("amznTraceId == null"); + return STRING_EXTRACTOR.extract(amznTraceId); + } + + static final class AWSExtractor implements TraceContext.Extractor { + final AWSPropagation propagation; + final Getter getter; + + AWSExtractor(AWSPropagation propagation, Getter getter) { + this.propagation = propagation; + this.getter = getter; + } + + enum Op { + SKIP, + ROOT, + PARENT, + SAMPLED, + EXTRA + } + + @Override public TraceContextOrSamplingFlags extract(C carrier) { + if (carrier == null) throw new NullPointerException("carrier == null"); + String traceIdString = getter.get(carrier, propagation.traceIdKey); + if (traceIdString == null) return TraceContextOrSamplingFlags.create(SamplingFlags.EMPTY); + + Boolean sampled = null; + long traceIdHigh = 0L, traceId = 0L; + Long parent = null; + StringBuilder currentString = new StringBuilder(7 /* Sampled.length */), currentExtra = null; + Op op = null; + OUTER: + for (int i = 0, length = traceIdString.length(); i < length; i++) { + char c = traceIdString.charAt(i); + if (c == ' ') continue; // trim whitespace + if (c == '=') { // we reached a field name + if (++i == length) break; // skip '=' character + if (currentString.indexOf("Root") == 0) { + op = Op.ROOT; + } else if (currentString.indexOf("Parent") == 0) { + op = Op.PARENT; + } else if (currentString.indexOf("Sampled") == 0) { + op = Op.SAMPLED; + } else if (currentString.indexOf("Self") == 0) { + // ALB implements Trace ID chaining using self so that customers not using X-Ray + // (I.e. request logs) can do the correlation themselves. We drop these + op = Op.SKIP; + } else { + op = Op.EXTRA; + if (currentExtra == null) currentExtra = new StringBuilder(); + currentExtra.append(';').append(currentString); + } + currentString.setLength(0); + } else if (op == null) { + currentString.append(c); + continue; + } + // no longer whitespace + switch (op) { + case EXTRA: + currentExtra.append(c); + while (i < length && (c = traceIdString.charAt(i)) != ';') { + currentExtra.append(c); + i++; + } + break; + case SKIP: + while (++i < length && traceIdString.charAt(i) != ';') { + // skip until we hit a delimiter + } + break; + case ROOT: + if (i + 35 > length // 35 = length of 1-67891233-abcdef012345678912345678 + || traceIdString.charAt(i++) != '1' + || traceIdString.charAt(i++) != '-') { + break OUTER; // invalid version or format + } + // Parse the epoch seconds and high 32 of the 96 bit trace ID into traceID high + for (int hyphenIndex = i + 8, endIndex = hyphenIndex + 1 + 8; i < endIndex; i++) { + c = traceIdString.charAt(i); + if (c == '-' && i == hyphenIndex) continue; // skip delimiter between epoch and random + traceIdHigh <<= 4; + if (c >= '0' && c <= '9') { + traceIdHigh |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + traceIdHigh |= c - 'a' + 10; + } else { + break OUTER; // invalid format + } + } + // Parse the low 64 of the 96 bit trace ID into traceId + for (int endIndex = i + 16; i < endIndex; i++) { + c = traceIdString.charAt(i); + traceId <<= 4; + if (c >= '0' && c <= '9') { + traceId |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + traceId |= c - 'a' + 10; + } else { + break OUTER; // invalid format + } + } + break; + case PARENT: + long parentId = 0L; + for (int endIndex = i + 16; i < endIndex; i++) { + c = traceIdString.charAt(i); + parentId <<= 4; + if (c >= '0' && c <= '9') { + parentId |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + parentId |= c - 'a' + 10; + } else { + break OUTER; // invalid format + } + } + parent = parentId; + break; + case SAMPLED: + c = traceIdString.charAt(i++); + if (c == '1') { + sampled = true; + } else if (c == '0') { + sampled = false; + } + break; + } + op = null; + } + TraceContextOrSamplingFlags result; + + if (traceIdHigh == 0L) { // traceIdHigh cannot be null, so just return sampled + result = TraceContextOrSamplingFlags.create( + new SamplingFlags.Builder().sampled(sampled).build() + ); + } else if (parent == null) { + result = TraceContextOrSamplingFlags.create(TraceIdContext.newBuilder() + .traceIdHigh(traceIdHigh) + .traceId(traceId) + .sampled(sampled) + .build() + ); + } else { + result = TraceContextOrSamplingFlags.create(TraceContext.newBuilder() + .traceIdHigh(traceIdHigh) + .traceId(traceId) + .spanId(parent) + .sampled(sampled) + .build() + ); + } + if (currentExtra == null) return result; + Extra extra = new Extra(); + extra.fields = currentExtra; + return result.toBuilder().addExtra(extra).build(); + } + } + + static final class Extra { // hidden intentionally + CharSequence fields; + } +} diff --git a/propagation/aws/src/test/java/brave/propagation/aws/AWSPropagationTest.java b/propagation/aws/src/test/java/brave/propagation/aws/AWSPropagationTest.java new file mode 100644 index 0000000000..c902fb7df9 --- /dev/null +++ b/propagation/aws/src/test/java/brave/propagation/aws/AWSPropagationTest.java @@ -0,0 +1,153 @@ +package brave.propagation.aws; + +import brave.internal.HexCodec; +import brave.propagation.Propagation; +import brave.propagation.SamplingFlags; +import brave.propagation.TraceContext; +import brave.propagation.TraceContextOrSamplingFlags; +import brave.propagation.TraceIdContext; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AWSPropagationTest { + Map carrier = new LinkedHashMap<>(); + TraceContext.Injector> injector = + new AWSPropagation.Factory().create(Propagation.KeyFactory.STRING).injector(Map::put); + TraceContext.Extractor> extractor = + new AWSPropagation.Factory().create(Propagation.KeyFactory.STRING).extractor(Map::get); + + String sampledTraceId = + "Root=1-67891233-abcdef012345678912345678;Parent=463ac35c9f6413ad;Sampled=1"; + TraceContext sampledContext = TraceContext.newBuilder() + .traceIdHigh(HexCodec.lowerHexToUnsignedLong("67891233abcdef01")) + .traceId(HexCodec.lowerHexToUnsignedLong("2345678912345678")) + .spanId(HexCodec.lowerHexToUnsignedLong("463ac35c9f6413ad")) + .sampled(true) + .build(); + + @Test public void traceIdString() throws Exception { + assertThat(AWSPropagation.traceIdString(sampledContext)) + .isEqualTo("1-67891233-abcdef012345678912345678"); + } + + @Test public void inject() throws Exception { + injector.inject(sampledContext, carrier); + + assertThat(carrier).containsEntry("x-amzn-trace-id", sampledTraceId); + } + + @Test public void extract() throws Exception { + carrier.put("x-amzn-trace-id", sampledTraceId); + + assertThat(extractor.extract(carrier).context()) + .isEqualTo(sampledContext); + } + + @Test public void extract_static() throws Exception { + assertThat(AWSPropagation.extract(sampledTraceId).context()) + .isEqualTo(sampledContext); + } + + @Test public void extractDifferentOrder() throws Exception { + carrier.put("x-amzn-trace-id", + "Sampled=1;Parent=463ac35c9f6413ad;Root=1-67891233-abcdef012345678912345678"); + + assertThat(extractor.extract(carrier).context()) + .isEqualTo(sampledContext); + } + + @Test public void extract_noParent() throws Exception { + carrier.put("x-amzn-trace-id", "Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1"); + + assertThat(extractor.extract(carrier).traceIdContext()) + .isEqualTo(TraceIdContext.newBuilder() + .traceIdHigh(HexCodec.lowerHexToUnsignedLong("5759e988bd862e3f")) + .traceId(HexCodec.lowerHexToUnsignedLong("e1be46a994272793")) + .sampled(true) + .build()); + } + + @Test public void extract_noSamplingDecision() throws Exception { + carrier.put("x-amzn-trace-id", sampledTraceId.replace("Sampled=1", "Sampled=?")); + + assertThat(extractor.extract(carrier).context()) + .isEqualTo(sampledContext.toBuilder().sampled(null).build()); + } + + @Test public void extract_sampledFalse() throws Exception { + carrier.put("x-amzn-trace-id", sampledTraceId.replace("Sampled=1", "Sampled=0")); + + assertThat(extractor.extract(carrier).context()) + .isEqualTo(sampledContext.toBuilder().sampled(false).build()); + } + + /** Shows we skip whitespace and extra fields like self or custom ones */ + // https://aws.amazon.com/blogs/aws/application-performance-percentiles-and-request-tracing-for-aws-application-load-balancer/ + @Test public void extract_skipsSelfField() throws Exception { + // TODO: check with AWS if it is valid to have arbitrary fields in front of standard ones. + // we currently permit them + carrier.put("x-amzn-trace-id", "Robot=Hello;Self=1-582113d1-1e48b74b3603af8479078ed6; " + + "Root=1-58211399-36d228ad5d99923122bbe354; " + + "TotalTimeSoFar=112ms;CalledFrom=Foo"); + + TraceContextOrSamplingFlags extracted = extractor.extract(carrier); + assertThat(extracted.traceIdContext()) + .isEqualTo(TraceIdContext.newBuilder() + .traceIdHigh(HexCodec.lowerHexToUnsignedLong("5821139936d228ad")) + .traceId(HexCodec.lowerHexToUnsignedLong("5d99923122bbe354")) + .build()); + + assertThat(((AWSPropagation.Extra) extracted.extra().get(0)).fields) + .contains(new StringBuilder(";Robot=Hello;TotalTimeSoFar=112ms;CalledFrom=Foo")); + } + + @Test public void injectExtraStuff() throws Exception { + AWSPropagation.Extra extra = new AWSPropagation.Extra(); + extra.fields = ";Robot=Hello;TotalTimeSoFar=112ms;CalledFrom=Foo"; + TraceContext extraContext = sampledContext.toBuilder().extra(Arrays.asList(extra)).build(); + injector.inject(extraContext, carrier); + + assertThat(carrier) + .containsEntry("x-amzn-trace-id", + "Root=1-67891233-abcdef012345678912345678;Parent=463ac35c9f6413ad;Sampled=1;Robot=Hello;TotalTimeSoFar=112ms;CalledFrom=Foo"); + } + + @Test public void extract_skipsLaterVersion() throws Exception { + carrier.put("x-amzn-trace-id", "Root=2-58211399-36d228ad5d99923122bbe354"); + + assertThat(extractor.extract(carrier).samplingFlags()) + .isEqualTo(SamplingFlags.EMPTY); + } + + @Test public void extract_skipsTruncatedId() throws Exception { + carrier.put("x-amzn-trace-id", "Root=1-58211399-36d228ad5d99923122bbe35"); + + assertThat(extractor.extract(carrier).samplingFlags()) + .isEqualTo(SamplingFlags.EMPTY); + } + + @Test public void extract_skips_leadingEquals() throws Exception { + carrier.put("x-amzn-trace-id", "=Root=1-58211399-36d228ad5d99923122bbe354"); + + assertThat(extractor.extract(carrier).samplingFlags()) + .isEqualTo(SamplingFlags.EMPTY); + } + + @Test public void extract_skips_doubleEquals() throws Exception { + carrier.put("x-amzn-trace-id", "Root==1-58211399-36d228ad5d99923122bbe354"); + + assertThat(extractor.extract(carrier).samplingFlags()) + .isEqualTo(SamplingFlags.EMPTY); + } + + @Test public void extract_skips_noEquals() throws Exception { + carrier.put("x-amzn-trace-id", "1-58211399-36d228ad5d99923122bbe354"); + + assertThat(extractor.extract(carrier).samplingFlags()) + .isEqualTo(SamplingFlags.EMPTY); + } +} diff --git a/propagation/pom.xml b/propagation/pom.xml new file mode 100644 index 0000000000..9f25e25756 --- /dev/null +++ b/propagation/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + io.zipkin.brave + brave-parent + 4.8.2-SNAPSHOT + + + brave-propagation-parent + Brave: Trace Propagation Formats + pom + + + ${project.basedir}/.. + + + + aws + + + + + ${project.groupId} + brave + + + ${project.groupId} + brave-tests + test + + +