diff --git a/benchmarks/src/main/java/zipkin/benchmarks/Span2ConverterBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/Span2ConverterBenchmarks.java new file mode 100644 index 00000000000..4c9655ea960 --- /dev/null +++ b/benchmarks/src/main/java/zipkin/benchmarks/Span2ConverterBenchmarks.java @@ -0,0 +1,127 @@ +/** + * Copyright 2015-2017 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 zipkin.benchmarks; + +import java.util.List; +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.Threads; +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 zipkin.Annotation; +import zipkin.BinaryAnnotation; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.TraceKeys; +import zipkin.internal.Span2; +import zipkin.internal.Span2Converter; +import zipkin.internal.Util; + +@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 10, time = 1) +@Fork(3) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Thread) +@Threads(1) +public class Span2ConverterBenchmarks { + Endpoint frontend = Endpoint.create("frontend", 127 << 24 | 1); + Endpoint backend = Endpoint.builder() + .serviceName("backend") + .ipv4(192 << 24 | 168 << 16 | 99 << 8 | 101) + .port(9000) + .build(); + + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, backend)) + .addAnnotation(Annotation.create(1472470996403000L, Constants.WIRE_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/backend", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("srv/finagle.version", "6.44.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + Span server = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, backend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/backend", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("srv/finagle.version", "6.44.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .build(); + + Span2 server2 = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Span2.Kind.SERVER) + .shared(true) + .localEndpoint(backend) + .remoteEndpoint(frontend) + .timestamp(1472470996250000L) + .duration(100000L) + .putTag(TraceKeys.HTTP_PATH, "/backend") + .putTag("srv/finagle.version", "6.44.0") + .build(); + + @Benchmark public List fromSpan_splitShared() { + return Span2Converter.fromSpan(shared); + } + + @Benchmark public List fromSpan() { + return Span2Converter.fromSpan(server); + } + + @Benchmark public Span toSpan() { + return Span2Converter.toSpan(server2); + } + + // Convenience main entry-point + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(".*" + Span2ConverterBenchmarks.class.getSimpleName() + ".*") + .build(); + + new Runner(opt).run(); + } +} diff --git a/zipkin/src/main/java/zipkin/internal/Span2Converter.java b/zipkin/src/main/java/zipkin/internal/Span2Converter.java new file mode 100644 index 00000000000..76768d2425a --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2Converter.java @@ -0,0 +1,325 @@ +/** + * Copyright 2015-2017 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 zipkin.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import zipkin.Annotation; +import zipkin.BinaryAnnotation; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.internal.Span2.Kind; + +import static zipkin.BinaryAnnotation.Type.BOOL; +import static zipkin.BinaryAnnotation.Type.STRING; +import static zipkin.Constants.CLIENT_ADDR; +import static zipkin.Constants.LOCAL_COMPONENT; +import static zipkin.Constants.SERVER_ADDR; + +/** + * This converts {@link zipkin.Span} instances to {@link zipkin.internal.Span2} and visa versa. + */ +public final class Span2Converter { + + /** + * Converts the input, parsing RPC annotations into {@link Span2#kind()}. + * + * @return a span for each unique {@link Annotation#endpoint annotation endpoint} service name. + */ + public static List fromSpan(Span source) { + Builders builders = new Builders(source); + // add annotations unless they are "core" + builders.processAnnotations(source); + // convert binary annotations to tags and addresses + builders.processBinaryAnnotations(source); + return builders.build(); + } + + static final class Builders { + final List spans = new ArrayList<>(); + Annotation cs = null, sr = null, ss = null, cr = null; + + Builders(Span source) { + this.spans.add(newBuilder(source)); + } + + void processAnnotations(Span source) { + for (int i = 0, length = source.annotations.size(); i < length; i++) { + Annotation a = source.annotations.get(i); + Span2.Builder currentSpan = forEndpoint(source, a.endpoint); + // core annotations require an endpoint. Don't give special treatment when that's missing + if (a.value.length() == 2 && a.endpoint != null) { + if (a.value.equals(Constants.CLIENT_SEND)) { + currentSpan.kind(Kind.CLIENT); + cs = a; + } else if (a.value.equals(Constants.SERVER_RECV)) { + currentSpan.kind(Kind.SERVER); + sr = a; + } else if (a.value.equals(Constants.SERVER_SEND)) { + currentSpan.kind(Kind.SERVER); + ss = a; + } else if (a.value.equals(Constants.CLIENT_RECV)) { + currentSpan.kind(Kind.CLIENT); + cr = a; + } else { + currentSpan.addAnnotation(a.timestamp, a.value); + } + } else { + currentSpan.addAnnotation(a.timestamp, a.value); + } + } + + if (cs != null && sr != null) { + // in a shared span, the client side owns span duration by annotations or explicit timestamp + maybeTimestampDuration(source, cs, cr); + + // special-case loopback: We need to make sure on loopback there are two span2s + Span2.Builder client = forEndpoint(source, cs.endpoint); + Span2.Builder server; + if (closeEnough(cs.endpoint, sr.endpoint)) { + client.kind(Kind.CLIENT); + // fork a new span for the server side + server = newSpanBuilder(source, sr.endpoint).kind(Kind.SERVER); + } else { + server = forEndpoint(source, sr.endpoint); + } + + // the server side is smaller than that, we have to read annotations to find out + server.shared(true).timestamp(sr.timestamp); + if (ss != null) server.duration(ss.timestamp - sr.timestamp); + if (cr == null && source.duration == null) client.duration(null); // one-way has no duration + } else if (cs != null && cr != null) { + maybeTimestampDuration(source, cs, cr); + } else if (sr != null && ss != null) { + maybeTimestampDuration(source, sr, ss); + } else { // otherwise, the span is incomplete. revert special-casing + for (Span2.Builder next : spans) { + if (Kind.CLIENT.equals(next.kind)) { + if (cs != null) next.timestamp(cs.timestamp); + } else if (Kind.SERVER.equals(next.kind)) { + if (sr != null) next.timestamp(sr.timestamp); + } + } + revertCoreAnnotation(source, ss); + revertCoreAnnotation(source, cr); + + if (source.timestamp != null) { + spans.get(0).timestamp(source.timestamp).duration(source.duration); + } + } + } + + void revertCoreAnnotation(Span source, Annotation a) { + if (a == null) return; + forEndpoint(source, a.endpoint).kind(null).addAnnotation(a.timestamp, a.value); + } + + void maybeTimestampDuration(Span source, Annotation begin, @Nullable Annotation end) { + Span2.Builder span2 = forEndpoint(source, begin.endpoint); + if (source.timestamp != null && source.duration != null) { + span2.timestamp(source.timestamp).duration(source.duration); + } else { + span2.timestamp(begin.timestamp); + if (end != null) span2.duration(end.timestamp - begin.timestamp); + } + } + + void processBinaryAnnotations(Span source) { + Endpoint ca = null, sa = null; + for (int i = 0, length = source.binaryAnnotations.size(); i < length; i++) { + BinaryAnnotation b = source.binaryAnnotations.get(i); + if (b.type == BOOL) { + if (Constants.CLIENT_ADDR.equals(b.key)) { + ca = b.endpoint; + } else if (Constants.SERVER_ADDR.equals(b.key)) { + sa = b.endpoint; + } + continue; + } + Span2.Builder currentSpan = forEndpoint(source, b.endpoint); + if (b.type == STRING) { + // don't add marker "lc" tags + if (Constants.LOCAL_COMPONENT.equals(b.key) && b.value.length == 0) continue; + currentSpan.putTag(b.key, new String(b.value, Util.UTF_8)); + } + } + + if (cs != null && sa != null && !closeEnough(sa, cs.endpoint)) { + forEndpoint(source, cs.endpoint).remoteEndpoint(sa); + } + + if (sr != null && ca != null && !closeEnough(ca, sr.endpoint)) { + forEndpoint(source, sr.endpoint).remoteEndpoint(ca); + } + + // special-case when we are missing core annotations, but we have both address annotations + if ((cs == null && sr == null) && (ca != null && sa != null)) { + forEndpoint(source, ca).remoteEndpoint(sa); + } + } + + Span2.Builder forEndpoint(Span source, @Nullable Endpoint e) { + if (e == null) return spans.get(0); // allocate missing endpoint data to first span + for (int i = 0, length = spans.size(); i < length; i++) { + Span2.Builder next = spans.get(i); + if (next.localEndpoint == null) { + next.localEndpoint = e; + return next; + } else if (closeEnough(next.localEndpoint, e)) { + return next; + } + } + return newSpanBuilder(source, e); + } + + Span2.Builder newSpanBuilder(Span source, Endpoint e) { + Span2.Builder result = newBuilder(source).localEndpoint(e); + spans.add(result); + return result; + } + + List build() { + int length = spans.size(); + if (length == 1) return Collections.singletonList(spans.get(0).build()); + List result = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + result.add(spans.get(i).build()); + } + return result; + } + } + + static boolean closeEnough(Endpoint left, Endpoint right) { + return left.serviceName.equals(right.serviceName); + } + + static Span2.Builder newBuilder(Span source) { + return Span2.builder() + .traceIdHigh(source.traceIdHigh) + .traceId(source.traceId) + .parentId(source.parentId) + .id(source.id) + .name(source.name) + .debug(source.debug); + } + + /** Converts the input, parsing {@link Span2#kind()} into RPC annotations. */ + public static Span toSpan(Span2 in) { + Span.Builder result = Span.builder() + .traceIdHigh(in.traceIdHigh()) + .traceId(in.traceId()) + .parentId(in.parentId()) + .id(in.id()) + .debug(in.debug()) + .name(in.name() == null ? "" : in.name()); // avoid a NPE + + long timestamp = in.timestamp() == null ? 0L : in.timestamp(); + long duration = in.duration() == null ? 0L : in.duration(); + if (timestamp != 0L) { + result.timestamp(timestamp); + if (duration != 0L) result.duration(duration); + } + + Annotation cs = null, sr = null, ss = null, cr = null; + String remoteEndpointType = null; + + if (in.kind() != null) { + switch (in.kind()) { + case CLIENT: + remoteEndpointType = Constants.SERVER_ADDR; + if (timestamp != 0L) { + cs = Annotation.create(timestamp, Constants.CLIENT_SEND, in.localEndpoint()); + } + if (duration != 0L) { + cr = Annotation.create(timestamp + duration, Constants.CLIENT_RECV, in.localEndpoint()); + } + break; + case SERVER: + remoteEndpointType = Constants.CLIENT_ADDR; + if (timestamp != 0L) { + sr = Annotation.create(timestamp, Constants.SERVER_RECV, in.localEndpoint()); + } + if (duration != 0L) { + ss = Annotation.create(timestamp + duration, Constants.SERVER_SEND, in.localEndpoint()); + } + break; + default: + throw new AssertionError("update kind mapping"); + } + } + + boolean wroteEndpoint = false; + + for (int i = 0, length = in.annotations().size(); i < length; i++) { + Annotation a = in.annotations().get(i); + if (in.localEndpoint() != null) { + a = a.toBuilder().endpoint(in.localEndpoint()).build(); + } + if (a.value.length() == 2) { + if (a.value.equals(Constants.CLIENT_SEND)) { + cs = a; + remoteEndpointType = SERVER_ADDR; + } else if (a.value.equals(Constants.SERVER_RECV)) { + sr = a; + remoteEndpointType = CLIENT_ADDR; + } else if (a.value.equals(Constants.SERVER_SEND)) { + ss = a; + } else if (a.value.equals(Constants.CLIENT_RECV)) { + cr = a; + } else { + wroteEndpoint = true; + result.addAnnotation(a); + } + } else { + wroteEndpoint = true; + result.addAnnotation(a); + } + } + + for (Map.Entry tag : in.tags().entrySet()) { + wroteEndpoint = true; + result.addBinaryAnnotation( + BinaryAnnotation.create(tag.getKey(), tag.getValue(), in.localEndpoint())); + } + + if (cs != null || sr != null || ss != null || cr != null) { + if (cs != null) result.addAnnotation(cs); + if (sr != null) result.addAnnotation(sr); + if (ss != null) result.addAnnotation(ss); + if (cr != null) result.addAnnotation(cr); + wroteEndpoint = true; + } else if (in.localEndpoint() != null && in.remoteEndpoint() != null) { + // special-case when we are missing core annotations, but we have both address annotations + result.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, in.localEndpoint())); + wroteEndpoint = true; + remoteEndpointType = SERVER_ADDR; + } + + if (remoteEndpointType != null && in.remoteEndpoint() != null) { + result.addBinaryAnnotation(BinaryAnnotation.address(remoteEndpointType, in.remoteEndpoint())); + } + + // don't report server-side timestamp on shared or incomplete spans + if (Boolean.TRUE.equals(in.shared()) && sr != null) { + result.timestamp(null).duration(null); + } + if (in.localEndpoint() != null && !wroteEndpoint) { // create a dummy annotation + result.addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "", in.localEndpoint())); + } + return result.build(); + } +} diff --git a/zipkin/src/test/java/zipkin/internal/Span2ConverterTest.java b/zipkin/src/test/java/zipkin/internal/Span2ConverterTest.java new file mode 100644 index 00000000000..6339c96fdd1 --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/Span2ConverterTest.java @@ -0,0 +1,449 @@ +/** + * Copyright 2015-2017 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 zipkin.internal; + +import org.junit.Test; +import zipkin.Annotation; +import zipkin.BinaryAnnotation; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.TraceKeys; +import zipkin.internal.Span2.Kind; + +import static org.assertj.core.api.Assertions.assertThat; +import static zipkin.Constants.LOCAL_COMPONENT; + +public class Span2ConverterTest { + Endpoint frontend = Endpoint.create("frontend", 127 << 24 | 1); + Endpoint backend = Endpoint.builder() + .serviceName("backend") + .ipv4(192 << 24 | 168 << 16 | 99 << 8 | 101) + .port(9000) + .build(); + + @Test public void client() { + Span2 simpleClient = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + Span client = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996403000L, Constants.WIRE_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + assertThat(Span2Converter.toSpan(simpleClient)) + .isEqualTo(client); + assertThat(Span2Converter.fromSpan(client)) + .containsExactly(simpleClient); + } + + // TODO: loopback one-way + + @Test public void client_unfinished() { + Span2 simpleClient = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .timestamp(1472470996199000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .build(); + + Span client = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .build(); + + assertThat(Span2Converter.toSpan(simpleClient)) + .isEqualTo(client); + assertThat(Span2Converter.fromSpan(client)) + .containsExactly(simpleClient); + } + + @Test public void noAnnotationsExceptAddresses() { + Span2 span2 = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .localEndpoint(frontend) + .remoteEndpoint(backend) + .timestamp(1472470996199000L) + .duration(207000L) + .build(); + + Span span = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + assertThat(Span2Converter.toSpan(span2)) + .isEqualTo(span); + assertThat(Span2Converter.fromSpan(span)) + .containsExactly(span2); + } + + @Test public void fromSpan_redundantAddressAnnotations() { + Span2 span2 = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .kind(Kind.CLIENT) + .name("get") + .localEndpoint(frontend) + .timestamp(1472470996199000L) + .duration(207000L) + .build(); + + Span span = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, frontend)) + .build(); + + assertThat(Span2Converter.fromSpan(span)) + .containsExactly(span2); + } + + @Test public void server() { + Span2 simpleServer = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .id("216a2aea45d08fc9") + .name("get") + .kind(Kind.SERVER) + .localEndpoint(backend) + .remoteEndpoint(frontend) + .timestamp(1472470996199000L) + .duration(207000L) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + Span server = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .id(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.SERVER_SEND, backend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .build(); + + assertThat(Span2Converter.toSpan(simpleServer)) + .isEqualTo(server); + assertThat(Span2Converter.fromSpan(server)) + .containsExactly(simpleServer); + } + + /** Buggy instrumentation can send data with missing endpoints. Make sure we can record it. */ + @Test public void missingEndpoints() { + Span2 span2 = Span2.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L) + .duration(207000L) + .build(); + + Span span = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L).duration(207000L) + .build(); + + assertThat(Span2Converter.toSpan(span2)) + .isEqualTo(span); + assertThat(Span2Converter.fromSpan(span)) + .containsExactly(span2); + } + + /** No special treatment for invalid core annotations: missing endpoint */ + @Test public void missingEndpoints_coreAnnotation() { + Span2 span2 = Span2.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(1472470996199000L, "sr") + .build(); + + Span span = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L).duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, "sr", null)) + .build(); + + assertThat(Span2Converter.toSpan(span2)) + .isEqualTo(span); + assertThat(Span2Converter.fromSpan(span)) + .containsExactly(span2); + } + + @Test public void localSpan_emptyComponent() { + Span2 simpleLocal = Span2.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("local") + .localEndpoint(frontend) + .timestamp(1472470996199000L) + .duration(207000L) + .build(); + + Span local = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("local") + .timestamp(1472470996199000L).duration(207000L) + .addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "", frontend)).build(); + + assertThat(Span2Converter.toSpan(simpleLocal)) + .isEqualTo(local); + assertThat(Span2Converter.fromSpan(local)) + .containsExactly(simpleLocal); + } + + @Test public void clientAndServer() { + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, backend)) + .addAnnotation(Annotation.create(1472470996403000L, Constants.WIRE_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/backend", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("srv/finagle.version", "6.44.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + Span2.Builder builder = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get"); + + // the client side owns timestamp and duration + Span2 client = builder.clone() + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + // notice server tags are different than the client, and the client's annotations aren't here + Span2 server = builder.clone() + .kind(Kind.SERVER) + .shared(true) + .localEndpoint(backend) + .remoteEndpoint(frontend) + .timestamp(1472470996250000L) + .duration(100000L) + .putTag(TraceKeys.HTTP_PATH, "/backend") + .putTag("srv/finagle.version", "6.44.0") + .build(); + + assertThat(Span2Converter.fromSpan(shared)) + .containsExactly(client, server); + } + + @Test public void clientAndServer_loopback() { + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .build(); + + Span2.Builder builder = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get"); + + Span2 client = builder.clone() + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .timestamp(1472470996199000L) + .duration(207000L) + .build(); + + Span2 server = builder.clone() + .kind(Kind.SERVER) + .shared(true) + .localEndpoint(frontend) + .timestamp(1472470996250000L) + .duration(100000L) + .build(); + + assertThat(Span2Converter.fromSpan(shared)) + .containsExactly(client, server); + } + + @Test public void oneway_loopback() { + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, frontend)) + .build(); + + Span2.Builder builder = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get"); + + Span2 client = builder.clone() + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .timestamp(1472470996199000L) + .build(); + + Span2 server = builder.clone() + .kind(Kind.SERVER) + .shared(true) + .localEndpoint(frontend) + .timestamp(1472470996250000L) + .build(); + + assertThat(Span2Converter.fromSpan(shared)) + .containsExactly(client, server); + } + + @Test public void dataMissingEndpointGoesOnFirstSpan() { + Span shared = Span.builder() + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("missing") + .addAnnotation(Annotation.create(1472470996199000L, "foo", frontend)) + .addAnnotation(Annotation.create(1472470996238000L, "bar", frontend)) + .addAnnotation(Annotation.create(1472470996250000L, "baz", backend)) + .addAnnotation(Annotation.create(1472470996350000L, "qux", backend)) + .addAnnotation(Annotation.create(1472470996403000L, "missing", null)) + .addBinaryAnnotation(BinaryAnnotation.create("foo", "bar", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("baz", "qux", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("missing", "", null)) + .build(); + + Span2.Builder builder = Span2.builder() + .traceId("216a2aea45d08fc9") + .id("5b4185666d50f68b") + .name("missing"); + + Span2 first = builder.clone() + .localEndpoint(frontend) + .addAnnotation(1472470996199000L, "foo") + .addAnnotation(1472470996238000L, "bar") + .addAnnotation(1472470996403000L, "missing") + .putTag("foo", "bar") + .putTag("missing", "") + .build(); + + Span2 second = builder.clone() + .localEndpoint(backend) + .addAnnotation(1472470996250000L, "baz") + .addAnnotation(1472470996350000L, "qux") + .putTag("baz", "qux") + .build(); + + assertThat(Span2Converter.fromSpan(shared)) + .containsExactly(first, second); + } +}