diff --git a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java index b7214e3c627..7ec53da770a 100644 --- a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java +++ b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java @@ -41,6 +41,8 @@ import zipkin.Codec; import zipkin.Endpoint; import zipkin.Span; +import zipkin.internal.Span2; +import zipkin.internal.Span2Codec; /** * This compares the speed of the bundled java codec with the approach used in the scala @@ -154,6 +156,31 @@ public byte[] writeClientSpan_thrift_libthrift() throws TException { return serialize(clientSpanLibThrift); } + static final byte[] span2Json = read("/span2.json"); + static final Span2 span2 = Span2Codec.JSON.readSpan(span2Json); + static final List tenClientSpan2s = Collections.nCopies(10, span2); + static final byte[] tenClientSpan2sJson = Span2Codec.JSON.writeSpans(tenClientSpan2s); + + @Benchmark + public Span2 readClientSpan_json_span2() { + return Span2Codec.JSON.readSpan(span2Json); + } + + @Benchmark + public List readTenClientSpans_json_span2() { + return Span2Codec.JSON.readSpans(tenClientSpan2sJson); + } + + @Benchmark + public byte[] writeClientSpan_json_span2() { + return Span2Codec.JSON.writeSpan(span2); + } + + @Benchmark + public byte[] writeTenClientSpans_json_span2() { + return Span2Codec.JSON.writeSpans(tenClientSpan2s); + } + static final byte[] rpcSpanJson = read("/span-rpc.json"); static final Span rpcSpan = Codec.JSON.readSpan(rpcSpanJson); static final byte[] rpcSpanThrift = Codec.THRIFT.writeSpan(rpcSpan); @@ -227,7 +254,7 @@ public byte[] writeRpcV6Span_thrift_libthrift() throws TException { // Convenience main entry-point public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() - .include(".*" + CodecBenchmarks.class.getSimpleName() + ".*lientSpan.*") + .include(".*" + CodecBenchmarks.class.getSimpleName()) .build(); new Runner(opt).run(); diff --git a/benchmarks/src/main/resources/span-client.json b/benchmarks/src/main/resources/span-client.json index 701e7b185e7..466eb2a29ae 100644 --- a/benchmarks/src/main/resources/span-client.json +++ b/benchmarks/src/main/resources/span-client.json @@ -40,25 +40,16 @@ } ], "binaryAnnotations": [ - { - "key": "ca", - "value": true, - "endpoint": { - "serviceName": "frontend", - "ipv4": "127.0.0.1", - "port": 49504 - } - }, { "key": "clnt/finagle.version", - "value": "6.36.0", + "value": "6.45.0", "endpoint": { "serviceName": "frontend", "ipv4": "127.0.0.1" } }, { - "key": "http.uri", + "key": "http.path", "value": "/api", "endpoint": { "serviceName": "frontend", @@ -70,10 +61,9 @@ "value": true, "endpoint": { "serviceName": "backend", - "ipv4": "127.0.0.1", + "ipv4": "192.168.99.101", "port": 9000 } } - ], - "debug": false + ] } diff --git a/benchmarks/src/main/resources/span2.json b/benchmarks/src/main/resources/span2.json new file mode 100644 index 00000000000..e210b93413d --- /dev/null +++ b/benchmarks/src/main/resources/span2.json @@ -0,0 +1,32 @@ +{ + "traceId": "86154a4ba6e91385", + "parentId": "86154a4ba6e91385", + "id": "4d1e00c0db9010db", + "kind": "CLIENT", + "name": "get", + "timestamp": 1472470996199000, + "duration": 207000, + "localEndpoint": { + "serviceName": "frontend", + "ipv4": "127.0.0.1" + }, + "remoteEndpoint": { + "serviceName": "backend", + "ipv4": "192.168.99.101", + "port": 9000 + }, + "annotations": [ + { + "timestamp": 1472470996238000, + "value": "ws" + }, + { + "timestamp": 1472470996403000, + "value": "wr" + } + ], + "tags": { + "http.path": "/api", + "clnt/finagle.version": "6.45.0" + } +} diff --git a/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpanIterator.java b/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpan2Iterator.java similarity index 61% rename from zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpanIterator.java rename to zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpan2Iterator.java index b759aa64fa4..6736a8eafc0 100644 --- a/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpanIterator.java +++ b/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/DependencyLinkSpan2Iterator.java @@ -16,28 +16,32 @@ import java.util.Iterator; import org.jooq.Record; import org.jooq.TableField; -import zipkin.internal.DependencyLinkSpan; +import zipkin.Endpoint; import zipkin.internal.Nullable; import zipkin.internal.PeekingIterator; +import zipkin.internal.Span2; import zipkin.storage.mysql.internal.generated.tables.ZipkinSpans; import static zipkin.Constants.CLIENT_ADDR; import static zipkin.Constants.CLIENT_SEND; import static zipkin.Constants.SERVER_ADDR; import static zipkin.Constants.SERVER_RECV; +import static zipkin.internal.Util.equal; import static zipkin.storage.mysql.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS; /** - * Convenience that lazy converts rows into {@linkplain DependencyLinkSpan} objects. + * Lazy converts rows into {@linkplain Span2} objects suitable for dependency links. This takes + * short-cuts to require less data. For example, it folds shared RPC spans into one, and doesn't + * include tags, non-core annotations or time units. * *

Out-of-date schemas may be missing the trace_id_high field. When present, this becomes {@link - * DependencyLinkSpan.TraceId#hi} used as the left-most 16 characters of the traceId in logging + * Span2#traceIdHigh()} used as the left-most 16 characters of the traceId in logging * statements. */ -final class DependencyLinkSpanIterator implements Iterator { +final class DependencyLinkSpan2Iterator implements Iterator { /** Assumes the input records are sorted by trace id, span id */ - static final class ByTraceId implements Iterator> { + static final class ByTraceId implements Iterator> { final PeekingIterator delegate; final boolean hasTraceIdHigh; @@ -53,10 +57,10 @@ static final class ByTraceId implements Iterator> { return delegate.hasNext(); } - @Override public Iterator next() { + @Override public Iterator next() { currentTraceIdHi = hasTraceIdHigh ? traceIdHigh(delegate) : null; currentTraceIdLo = delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID); - return new DependencyLinkSpanIterator(delegate, currentTraceIdHi, currentTraceIdLo); + return new DependencyLinkSpan2Iterator(delegate, currentTraceIdHi, currentTraceIdLo); } @Override public void remove() { @@ -68,7 +72,7 @@ static final class ByTraceId implements Iterator> { @Nullable final Long traceIdHi; final long traceIdLo; - DependencyLinkSpanIterator(PeekingIterator delegate, Long traceIdHi, long traceIdLo) { + DependencyLinkSpan2Iterator(PeekingIterator delegate, Long traceIdHi, long traceIdLo) { this.delegate = delegate; this.traceIdHi = traceIdHi; this.traceIdLo = traceIdLo; @@ -83,17 +87,11 @@ public boolean hasNext() { } @Override - public DependencyLinkSpan next() { + public Span2 next() { Record row = delegate.peek(); long spanId = row.getValue(ZipkinSpans.ZIPKIN_SPANS.ID); - DependencyLinkSpan.Builder result = DependencyLinkSpan.builder( - traceIdHi != null ? traceIdHi : 0L, - traceIdLo, - row.getValue(ZipkinSpans.ZIPKIN_SPANS.PARENT_ID), - spanId - ); - + String srService = null, csService = null, caService = null, saService = null; while (hasNext()) { // there are more values for this trace if (spanId != delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.ID)) { break; // if we are in a new span @@ -105,18 +103,48 @@ public DependencyLinkSpan next() { if (key == null || value == null) continue; // neither client nor server switch (key) { case CLIENT_ADDR: - result.caService(value); + caService = value; break; case CLIENT_SEND: - result.csService(value); + csService = value; break; case SERVER_ADDR: - result.saService(value); + saService = value; break; case SERVER_RECV: - result.srService(value); + srService = value; } } + + // The client address is more authoritative than the client send owner. + if (caService == null) caService = csService; + + // Finagle labels two sides of the same socket ("ca", "sa") with the same name. + // Skip the client side, so it isn't mistaken for a loopback request + if (equal(saService, caService)) caService = null; + + Span2.Builder result = Span2.builder() + .traceIdHigh(traceIdHi != null ? traceIdHi : 0L) + .traceId(traceIdLo) + .parentId(row.getValue(ZipkinSpans.ZIPKIN_SPANS.PARENT_ID)) + .id(spanId); + + if (srService != null) { + return result.kind(Span2.Kind.SERVER) + .localEndpoint(ep(srService)) + .remoteEndpoint(ep(caService)) + .build(); + } else if (saService != null) { + return result + .kind(csService != null ? Span2.Kind.CLIENT : null) + .localEndpoint(ep(caService)) + .remoteEndpoint(ep(saService)) + .build(); + } else if (csService != null) { + return result.kind(Span2.Kind.SERVER) + .localEndpoint(ep(caService)) + .build(); + } return result.build(); } @@ -129,8 +157,12 @@ static long traceIdHigh(PeekingIterator delegate) { return delegate.peek().getValue(ZipkinSpans.ZIPKIN_SPANS.TRACE_ID_HIGH); } - static String emptyToNull(Record next, TableField field) { + static @Nullable String emptyToNull(Record next, TableField field) { String result = next.getValue(field); return result != null && !"".equals(result) ? result : null; } + + static Endpoint ep(@Nullable String serviceName) { + return serviceName != null ? Endpoint.builder().serviceName(serviceName).build() : null; + } } diff --git a/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/MySQLSpanStore.java b/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/MySQLSpanStore.java index 75c5480b657..f3c8b321e3b 100644 --- a/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/MySQLSpanStore.java +++ b/zipkin-storage/mysql/src/main/java/zipkin/storage/mysql/MySQLSpanStore.java @@ -40,11 +40,11 @@ import zipkin.DependencyLink; import zipkin.Endpoint; import zipkin.Span; -import zipkin.internal.DependencyLinkSpan; import zipkin.internal.DependencyLinker; import zipkin.internal.GroupByTraceId; import zipkin.internal.Nullable; import zipkin.internal.Pair; +import zipkin.internal.Span2; import zipkin.storage.QueryRequest; import zipkin.storage.SpanStore; import zipkin.storage.mysql.internal.generated.tables.ZipkinAnnotations; @@ -324,8 +324,8 @@ List aggregateDependencies(long endTs, @Nullable Long lookback, // Grouping so that later code knows when a span or trace is finished. .groupBy(schema.dependencyLinkGroupByFields).fetchLazy(); - Iterator> traces = - new DependencyLinkSpanIterator.ByTraceId(cursor.iterator(), schema.hasTraceIdHigh); + Iterator> traces = + new DependencyLinkSpan2Iterator.ByTraceId(cursor.iterator(), schema.hasTraceIdHigh); if (!traces.hasNext()) return Collections.emptyList(); diff --git a/zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpanIteratorTest.java b/zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpan2IteratorTest.java similarity index 57% rename from zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpanIteratorTest.java rename to zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpan2IteratorTest.java index 7bc7f8b2812..21f2d9cc544 100644 --- a/zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpanIteratorTest.java +++ b/zipkin-storage/mysql/src/test/java/zipkin/storage/mysql/DependencyLinkSpan2IteratorTest.java @@ -19,16 +19,15 @@ import org.jooq.impl.DSL; import org.junit.Test; import zipkin.Constants; -import zipkin.internal.DependencyLinkSpan; import zipkin.internal.PeekingIterator; +import zipkin.internal.Span2; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static zipkin.storage.mysql.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS; import static zipkin.storage.mysql.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS; -// TODO: this class temporarily uses reflection until zipkin2 span replaces DependencyLinkSpan -public class DependencyLinkSpanIteratorTest { +public class DependencyLinkSpan2IteratorTest { Long traceIdHigh = null; long traceId = 1L; Long parentId = null; @@ -36,50 +35,50 @@ public class DependencyLinkSpanIteratorTest { /** You cannot make a dependency link unless you know the the local or peer endpoint. */ @Test public void whenNoServiceLabelsExist_kindIsUnknown() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", null) ); - DependencyLinkSpan span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("UNKNOWN"); - assertThat(span).extracting("service").containsNull(); - assertThat(span).extracting("peerService").containsNull(); + Span2 span = iterator.next(); + assertThat(span.kind()).isNull(); + assertThat(span.localEndpoint()).isNull(); + assertThat(span.remoteEndpoint()).isNull(); } - @Test public void whenOnlyAddressLabelsExist_kindIsClient() { - DependencyLinkSpanIterator iterator = iterator( + @Test public void whenOnlyAddressLabelsExist_kindIsNull() { + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service1"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sa", "service2") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("CLIENT"); - assertThat(span).extracting("service").containsOnly("service1"); - assertThat(span).extracting("peerService").containsOnly("service2"); + assertThat(span.kind()).isNull(); + assertThat(span.localEndpoint().serviceName).isEqualTo("service1"); + assertThat(span.remoteEndpoint().serviceName).isEqualTo("service2"); } /** The linker is biased towards server spans, or client spans that know the peer localEndpoint(). */ @Test public void whenServerLabelsAreMissing_kindIsUnknownAndLabelsAreCleared() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service1") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("UNKNOWN"); - assertThat(span).extracting("service").containsNull(); - assertThat(span).extracting("peerService").containsNull(); + assertThat(span.kind()).isNull(); + assertThat(span.localEndpoint()).isNull(); + assertThat(span.remoteEndpoint()).isNull(); } /** {@link Constants#SERVER_RECV} is only applied when the local span is acting as a server */ @Test public void whenSrServiceExists_kindIsServer() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "service") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service"); - assertThat(span).extracting("peerService").containsNull(); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service"); + assertThat(span.remoteEndpoint()).isNull(); } /** @@ -87,15 +86,15 @@ public class DependencyLinkSpanIteratorTest { * span */ @Test public void whenSrAndCaServiceExists_caIsThePeer() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service1"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "service2") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service2"); - assertThat(span).extracting("peerService").containsOnly("service1"); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service2"); + assertThat(span.remoteEndpoint().serviceName).isEqualTo("service1"); } /** @@ -103,57 +102,58 @@ public class DependencyLinkSpanIteratorTest { * span */ @Test public void whenSrAndCsServiceExists_caIsThePeer() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", "service1"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "service2") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service2"); - assertThat(span).extracting("peerService").containsOnly("service1"); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service2"); + assertThat(span.remoteEndpoint().serviceName).isEqualTo("service1"); } /** {@link Constants#CLIENT_ADDR} is more authoritative than {@link Constants#CLIENT_SEND} */ @Test public void whenCrAndCaServiceExists_caIsThePeer() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", "foo"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service1"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "service2") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service2"); - assertThat(span).extracting("peerService").containsOnly("service1"); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service2"); + assertThat(span.remoteEndpoint().serviceName).isEqualTo("service1"); } /** Finagle labels two sides of the same socket "ca", "sa" with the local endpoint name */ @Test public void specialCasesFinagleLocalSocketLabeling_client() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( + newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", "service"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sa", "service") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); // When there's no "sr" annotation, we assume it is a client. - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("CLIENT"); - assertThat(span).extracting("service").containsNull(); - assertThat(span).extracting("peerService").containsOnly("service"); + assertThat(span.kind()).isEqualTo(Span2.Kind.CLIENT); + assertThat(span.localEndpoint()).isNull(); + assertThat(span.remoteEndpoint().serviceName).isEqualTo("service"); } @Test public void specialCasesFinagleLocalSocketLabeling_server() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", "service"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sa", "service"), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "service") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); // When there is an "sr" annotation, we know it is a server - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service"); - assertThat(span).extracting("peerService").containsNull(); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service"); + assertThat(span.remoteEndpoint()).isNull(); } /** @@ -161,33 +161,33 @@ public class DependencyLinkSpanIteratorTest { * caller, than a client span lacking its receiver. */ @Test public void csWithoutSaIsServer() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", "service1") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("SERVER"); - assertThat(span).extracting("service").containsOnly("service1"); - assertThat(span).extracting("peerService").containsNull(); + assertThat(span.kind()).isEqualTo(Span2.Kind.SERVER); + assertThat(span.localEndpoint().serviceName).isEqualTo("service1"); + assertThat(span.remoteEndpoint()).isNull(); } /** Service links to empty string are confusing and offer no value. */ @Test public void emptyToNull() { - DependencyLinkSpanIterator iterator = iterator( + DependencyLinkSpan2Iterator iterator = iterator( newRecord().values(traceIdHigh, traceId, parentId, spanId, "ca", ""), newRecord().values(traceIdHigh, traceId, parentId, spanId, "cs", ""), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sa", ""), newRecord().values(traceIdHigh, traceId, parentId, spanId, "sr", "") ); - DependencyLinkSpan span = iterator.next(); + Span2 span = iterator.next(); - assertThat(span).extracting("kind").extracting(Object::toString).containsOnly("UNKNOWN"); - assertThat(span).extracting("service").containsNull(); - assertThat(span).extracting("peerService").containsNull(); + assertThat(span.kind()).isNull(); + assertThat(span.localEndpoint()).isNull(); + assertThat(span.remoteEndpoint()).isNull(); } - static DependencyLinkSpanIterator iterator(Record... records) { - return new DependencyLinkSpanIterator( + static DependencyLinkSpan2Iterator iterator(Record... records) { + return new DependencyLinkSpan2Iterator( new PeekingIterator<>(asList(records).iterator()), records[0].get(ZIPKIN_SPANS.TRACE_ID_HIGH), records[0].get(ZIPKIN_SPANS.TRACE_ID) ); diff --git a/zipkin/src/main/java/zipkin/internal/DependencyLinkSpan.java b/zipkin/src/main/java/zipkin/internal/DependencyLinkSpan.java deleted file mode 100644 index 1fc6bc887d2..00000000000 --- a/zipkin/src/main/java/zipkin/internal/DependencyLinkSpan.java +++ /dev/null @@ -1,245 +0,0 @@ -/** - * 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 zipkin.Annotation; -import zipkin.BinaryAnnotation; -import zipkin.Constants; -import zipkin.Span; - -import static zipkin.internal.Util.checkNotNull; -import static zipkin.internal.Util.equal; -import static zipkin.internal.Util.writeHexLong; - -/** - * Internal type used by {@link DependencyLinker linker} that holds the minimum state needed to - * aggregate {@link zipkin.DependencyLink dependency links}. - */ -// fields not exposed as public to further discourage use as a general type -public final class DependencyLinkSpan { - - /** Unique 8 or 16-byte identifier for a trace, set on all spans within it. */ - static final class TraceId { - - /** 0 may imply 8-byte identifiers are in use */ - final long hi; - final long lo; - - TraceId(long hi, long lo) { - this.hi = hi; - this.lo = lo; - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (o instanceof TraceId) { - TraceId that = (TraceId) o; - return (this.hi == that.hi) - && (this.lo == that.lo); - } - return false; - } - - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= (hi >>> 32) ^ hi; - h *= 1000003; - h ^= (lo >>> 32) ^ lo; - return h; - } - - /** Returns the hex representation of the span's trace ID */ - @Override public String toString() { - if (hi != 0) { - char[] result = new char[32]; - writeHexLong(result, 0, hi); - writeHexLong(result, 16, lo); - return new String(result); - } - char[] result = new char[16]; - writeHexLong(result, 0, lo); - return new String(result); - } - } - - /** - * Indicates the primary span type. - */ - enum Kind { - CLIENT, - /** The span includes a {@link zipkin.Constants#SERVER_RECV}. */ - SERVER, - UNKNOWN - } - - final TraceId traceId; - @Nullable - final Long parentId; - final long id; - final Kind kind; - @Nullable - final String service; - @Nullable - final String peerService; - - DependencyLinkSpan(TraceId traceId, Long parentId, long id, Kind kind, String service, - String peerService) { - this.traceId = traceId; - this.parentId = parentId; - this.id = id; - this.kind = checkNotNull(kind, "kind"); - this.service = service; - this.peerService = peerService; - } - - @Override public String toString() { - StringBuilder json = new StringBuilder(); - json.append("{\"traceId\": \"").append(Util.toLowerHex(traceId.hi, traceId.lo)).append('\"'); - if (parentId != null) { - json.append(", \"parentId\": \"").append(Util.toLowerHex(parentId)).append('\"'); - } - json.append(", \"id\": \"").append(Util.toLowerHex(id)).append('\"'); - json.append(", \"kind\": \"").append(kind).append('\"'); - if (service != null) json.append(", \"service\": \"").append(service).append('\"'); - if (peerService != null) json.append(", \"peerService\": \"").append(peerService).append('\"'); - return json.append("}").toString(); - } - - /** Only considers ID fields, as these spans are not expected to repeat */ - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (o instanceof DependencyLinkSpan) { - DependencyLinkSpan that = (DependencyLinkSpan) o; - return equal(this.traceId, that.traceId) - && equal(this.parentId, that.parentId) - && (this.id == that.id); - } - return false; - } - - /** Only considers ID fields, as these spans are not expected to repeat */ - @Override - public int hashCode() { - int h = 1; - h *= 1000003; - h ^= traceId.hashCode(); - h *= 1000003; - h ^= (parentId == null) ? 0 : parentId.hashCode(); - h *= 1000003; - h ^= (id >>> 32) ^ id; - return h; - } - - public static Builder builder(long traceIdHigh, long traceIdLow, Long parentId, long spanId) { - return new Builder(new TraceId(traceIdHigh, traceIdLow), parentId, spanId); - } - - public static DependencyLinkSpan from(Span s) { - TraceId traceId = new TraceId(s.traceIdHigh, s.traceId); - DependencyLinkSpan.Builder linkSpan = new DependencyLinkSpan.Builder(traceId, s.parentId, s.id); - for (BinaryAnnotation a : s.binaryAnnotations) { - if (a.key.equals(Constants.CLIENT_ADDR) && a.endpoint != null) { - linkSpan.caService(a.endpoint.serviceName); - } else if (a.key.equals(Constants.SERVER_ADDR) && a.endpoint != null) { - linkSpan.saService(a.endpoint.serviceName); - } - } - for (Annotation a : s.annotations) { - if (a.value.equals(Constants.SERVER_RECV) && a.endpoint != null) { - linkSpan.srService(a.endpoint.serviceName); - } else if (a.value.equals(Constants.CLIENT_SEND) && a.endpoint != null) { - linkSpan.csService(a.endpoint.serviceName); - } - } - return linkSpan.build(); - } - - public static final class Builder { - final TraceId traceId; - final Long parentId; - final long spanId; - String srService; - String csService; - String caService; - String saService; - - Builder(TraceId traceId, Long parentId, long spanId) { - this.traceId = traceId; - this.spanId = spanId; - this.parentId = parentId; - } - - /** - * {@link zipkin.Constants#SERVER_RECV} is the preferred name of server, and this is a - * traditional span. - */ - public Builder srService(String srService) { - if ("".equals(srService)) srService = null; - this.srService = srService; - return this; - } - - /** - * {@link zipkin.Constants#CLIENT_SEND} is read to see calls into the root span from - * instrumented clients. - */ - public Builder csService(String csService) { - if ("".equals(csService)) csService = null; - this.csService = csService; - return this; - } - - /** - * {@link zipkin.Constants#CLIENT_ADDR} is read to see calls into the root span from - * uninstrumented clients. - */ - public Builder caService(String caService) { - if ("".equals(caService)) caService = null; - this.caService = caService; - return this; - } - - /** - * {@link zipkin.Constants#SERVER_ADDR} is only read at the leaf, when a client calls an - * un-instrumented server. - */ - public Builder saService(String saService) { - if ("".equals(saService)) saService = null; - this.saService = saService; - return this; - } - - public DependencyLinkSpan build() { - // The client address is more authoritative than the client send owner. - if (caService == null) caService = csService; - - // Finagle labels two sides of the same socket ("ca", "sa") with the same name. - // Skip the client side, so it isn't mistaken for a loopback request - if (equal(saService, caService)) caService = null; - - if (srService != null) { - return new DependencyLinkSpan(traceId, parentId, spanId, Kind.SERVER, srService, caService); - } else if (saService != null) { - return new DependencyLinkSpan(traceId, parentId, spanId, Kind.CLIENT, caService, saService); - } else if (csService != null) { - return new DependencyLinkSpan(traceId, parentId, spanId, Kind.SERVER, caService, null); - } - return new DependencyLinkSpan(traceId, parentId, spanId, Kind.UNKNOWN, null, null); - } - } -} diff --git a/zipkin/src/main/java/zipkin/internal/DependencyLinker.java b/zipkin/src/main/java/zipkin/internal/DependencyLinker.java index 3748065ce07..abd9f067d7b 100644 --- a/zipkin/src/main/java/zipkin/internal/DependencyLinker.java +++ b/zipkin/src/main/java/zipkin/internal/DependencyLinker.java @@ -23,17 +23,18 @@ import java.util.logging.Logger; import zipkin.DependencyLink; import zipkin.Span; +import zipkin.internal.Span2.Kind; import static java.util.logging.Level.FINE; -import static zipkin.internal.Util.checkNotNull; /** * This parses a span tree into dependency links used by Web UI. Ex. http://zipkin/dependency * - *

This implementation traverses the tree, and only creates links between {@link - * DependencyLinkSpan.Kind#SERVER server} spans. One exception is at the bottom of the trace tree. - * {@link DependencyLinkSpan.Kind#CLIENT client} spans that record their {@link - * DependencyLinkSpan#peerService peer} are included, as this accounts for uninstrumented services. + *

This implementation traverses the tree, and only creates links between {@link Kind#SERVER + * server} spans. One exception is at the bottom of the trace tree. {@link Kind#CLIENT client} spans + * that record their {@link Span2#remoteEndpoint()} are included, as this accounts for + * uninstrumented services. Spans with {@link Span2#kind()} unset, but {@link + * Span2#remoteEndpoint()} set are treated the same as client spans. */ public final class DependencyLinker { private final Logger logger; @@ -53,34 +54,52 @@ public DependencyLinker() { public DependencyLinker putTrace(Collection spans) { if (spans.isEmpty()) return this; - List linkSpans = new LinkedList<>(); + List linkSpans = new LinkedList<>(); for (Span s : MergeById.apply(spans)) { - linkSpans.add(DependencyLinkSpan.from(s)); + linkSpans.addAll(Span2Converter.fromSpan(s)); } return putTrace(linkSpans.iterator()); } + static final Node.MergeFunction MERGE_RPC = new Node.MergeFunction() { + @Override public Span2 merge(Span2 existing, Span2 update) { + if (existing == null) return update; + if (update == null) return existing; + if (existing.kind() == null) return update; + if (update.kind() == null) return existing; + Span2 server = existing.kind() == Kind.SERVER ? existing : update; + Span2 client = existing == server ? update : existing; + if (server.remoteEndpoint() != null && !"".equals(server.remoteEndpoint().serviceName)) { + return server; + } + return server.toBuilder().remoteEndpoint(client.localEndpoint()).build(); + } + + @Override public String toString() { + return "MergeRpc"; + } + }; + /** * @param spans spans where all spans have the same trace id */ - public DependencyLinker putTrace(Iterator spans) { + public DependencyLinker putTrace(Iterator spans) { if (!spans.hasNext()) return this; - DependencyLinkSpan first = spans.next(); - Node.TreeBuilder builder = - new Node.TreeBuilder<>(logger, first.traceId.toString()); - builder.addNode(first.parentId, first.id, first); - + Span2 first = spans.next(); + Node.TreeBuilder builder = + new Node.TreeBuilder<>(logger, MERGE_RPC, first.traceIdString()); + builder.addNode(first.parentId(), first.id(), first); while (spans.hasNext()) { - DependencyLinkSpan next = spans.next(); - builder.addNode(next.parentId, next.id, next); + Span2 next = spans.next(); + builder.addNode(next.parentId(), next.id(), next); } - Node tree = builder.build(); + Node tree = builder.build(); if (logger.isLoggable(FINE)) logger.fine("traversing trace tree, breadth-first"); - for (Iterator> i = tree.traverse(); i.hasNext(); ) { - Node current = i.next(); - DependencyLinkSpan currentSpan = current.value(); + for (Iterator> i = tree.traverse(); i.hasNext(); ) { + Node current = i.next(); + Span2 currentSpan = current.value(); if (logger.isLoggable(FINE)) { logger.fine("processing " + currentSpan); } @@ -88,12 +107,31 @@ public DependencyLinker putTrace(Iterator spans) { logger.fine("skipping synthetic node for broken span tree"); continue; } + + Kind kind = currentSpan.kind(); + if (Kind.CLIENT.equals(kind) && !current.children().isEmpty()) { + logger.fine("deferring link to rpc child span"); + continue; + } + + String serviceName = serviceName(currentSpan); + String remoteServiceName = remoteServiceName(currentSpan); + if (kind == null) { + // Treat unknown type of span as a client span if we know both sides + if (serviceName != null && remoteServiceName != null) { + kind = Kind.CLIENT; + } else { + logger.fine("non-rpc span; skipping"); + continue; + } + } + String child; String parent; - switch (currentSpan.kind) { + switch (kind) { case SERVER: - child = currentSpan.service; - parent = currentSpan.peerService; + child = serviceName; + parent = remoteServiceName; if (current == tree) { // we are the root-most span. if (parent == null) { logger.fine("root's peer is unknown; skipping"); @@ -102,11 +140,11 @@ public DependencyLinker putTrace(Iterator spans) { } break; case CLIENT: - child = currentSpan.peerService; - parent = currentSpan.service; + parent = serviceName; + child = remoteServiceName; break; default: - logger.fine("non-rpc span; skipping"); + logger.fine("unknown kind; skipping"); continue; } @@ -114,39 +152,60 @@ public DependencyLinker putTrace(Iterator spans) { logger.fine("cannot determine parent, looking for first server ancestor"); } - // Local spans may be between the current node and its remote ancestor - // Look up the stack until we see a service name, and assume that's the client - Node ancestor = current.parent(); - while (ancestor != null && parent == null) { - if (logger.isLoggable(FINE)) { - logger.fine("processing ancestor " + ancestor.value()); - } - DependencyLinkSpan ancestorLink = ancestor.value(); - if (!ancestor.isSyntheticRootForPartialTree() && - ancestorLink.kind == DependencyLinkSpan.Kind.SERVER) { - parent = ancestorLink.service; - break; + String rpcAncestor = findRpcAncestor(current); + if (rpcAncestor != null) { + + // Local spans may be between the current node and its remote parent + if (parent == null) parent = rpcAncestor; + + // Some users accidentally put the remote service name on client annotations. + // Check for this and backfill a link from the nearest remote to that service as necessary. + if (Kind.CLIENT.equals(kind) && serviceName != null && !rpcAncestor.equals(serviceName)) { + logger.fine("detected missing link to client span"); + addLink(rpcAncestor, serviceName); + continue; } - ancestor = ancestor.parent(); } if (parent == null || child == null) { logger.fine("cannot find server ancestor; skipping"); continue; - } else if (logger.isLoggable(FINE)) { - logger.fine("incrementing link " + parent + " -> " + child); } - Pair key = Pair.create(parent, child); - if (linkMap.containsKey(key)) { - linkMap.put(key, linkMap.get(key) + 1); - } else { - linkMap.put(key, 1L); - } + addLink(parent, child); } return this; } + String findRpcAncestor(Node current) { + Node ancestor = current.parent(); + while (ancestor != null) { + if (logger.isLoggable(FINE)) { + logger.fine("processing ancestor " + ancestor.value()); + } + if (!ancestor.isSyntheticRootForPartialTree()) { + Span2 maybeRemote = ancestor.value(); + if (maybeRemote.kind() != null) { + return serviceName(maybeRemote); + } + } + ancestor = ancestor.parent(); + } + return null; + } + + void addLink(String parent, String child) { + if (logger.isLoggable(FINE)) { + logger.fine("incrementing link " + parent + " -> " + child); + } + Pair key = Pair.create(parent, child); + if (linkMap.containsKey(key)) { + linkMap.put(key, linkMap.get(key) + 1); + } else { + linkMap.put(key, 1L); + } + } + public List link() { // links are merged by mapping to parent/child and summing corresponding links List result = new ArrayList<>(linkMap.size()); @@ -173,4 +232,16 @@ public static List merge(Iterable in) { } return result; } + + static String serviceName(Span2 span) { + return span.localEndpoint() != null && !"".equals(span.localEndpoint().serviceName) + ? span.localEndpoint().serviceName + : null; + } + + static String remoteServiceName(Span2 span) { + return span.remoteEndpoint() != null && !"".equals(span.remoteEndpoint().serviceName) + ? span.remoteEndpoint().serviceName + : null; + } } diff --git a/zipkin/src/main/java/zipkin/internal/Node.java b/zipkin/src/main/java/zipkin/internal/Node.java index 5b8b4b97eb0..48ef740e5a6 100644 --- a/zipkin/src/main/java/zipkin/internal/Node.java +++ b/zipkin/src/main/java/zipkin/internal/Node.java @@ -109,6 +109,16 @@ public void remove() { } } + interface MergeFunction { + V merge(@Nullable V existing, @Nullable V update); + } + + static final MergeFunction FIRST_NOT_NULL = new MergeFunction() { + @Override public Object merge(Object existing, Object update) { + return existing != null ? existing : update; + } + }; + /** * Some operations do not require the entire span object. This creates a tree given (parent id, * id) pairs. @@ -117,16 +127,21 @@ public void remove() { */ static final class TreeBuilder { final Logger logger; + final MergeFunction mergeFunction; final String traceId; TreeBuilder(Logger logger, String traceId) { + this(logger, FIRST_NOT_NULL, traceId); + } + + TreeBuilder(Logger logger, MergeFunction mergeFunction, String traceId) { this.logger = logger; + this.mergeFunction = mergeFunction; this.traceId = traceId; } - Node rootNode = null; Long rootId = null; - + Node rootNode = null; // Nodes representing the trace tree Map> idToNode = new LinkedHashMap<>(); // Collect the parent-child relationships between all spans. @@ -158,8 +173,11 @@ public boolean addNode(@Nullable Long parentId, long id, V value) { if (parentId == null && rootNode == null) { rootNode = node; rootId = id; + } else if (parentId == null && rootId == id) { + rootNode.value(mergeFunction.merge(rootNode.value, node.value)); } else { - idToNode.put(id, node); + Node previous = idToNode.put(id, node); + if (previous != null) node.value(mergeFunction.merge(previous.value, node.value)); idToParent.put(id, parentId); } return true; diff --git a/zipkin/src/main/java/zipkin/internal/Span2.java b/zipkin/src/main/java/zipkin/internal/Span2.java index 94de3f57c74..cb1e6bef255 100644 --- a/zipkin/src/main/java/zipkin/internal/Span2.java +++ b/zipkin/src/main/java/zipkin/internal/Span2.java @@ -14,6 +14,9 @@ package zipkin.internal; import com.google.auto.value.AutoValue; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.io.StreamCorruptedException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -27,6 +30,7 @@ import zipkin.Span; import zipkin.TraceKeys; +import static zipkin.internal.Util.UTF_8; import static zipkin.internal.Util.checkNotNull; import static zipkin.internal.Util.lowerHexToUnsignedLong; import static zipkin.internal.Util.sortedList; @@ -54,7 +58,8 @@ * and smaller data. */ @AutoValue -public abstract class Span2 { // TODO: make serializable when needed between stages in Spark jobs +public abstract class Span2 implements Serializable { // for Spark jobs + private static final long serialVersionUID = 0L; /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */ public abstract long traceIdHigh(); @@ -409,4 +414,33 @@ public Span2 build() { ); } } + + @Override + public String toString() { + return new String(Span2Codec.JSON.writeSpan(this), UTF_8); + } + + // Since this is an immutable object, and we have json handy, defer to a serialization proxy. + final Object writeReplace() throws ObjectStreamException { + return new SerializedForm(Span2Codec.JSON.writeSpan(this)); + } + + static final class SerializedForm implements Serializable { + private static final long serialVersionUID = 0L; + + private final byte[] bytes; + + SerializedForm(byte[] bytes) { + this.bytes = bytes; + } + + Object readResolve() throws ObjectStreamException { + try { + return Span2Codec.JSON.readSpan(bytes); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new StreamCorruptedException(e.getMessage()); + } + } + } } diff --git a/zipkin/src/main/java/zipkin/internal/Span2Codec.java b/zipkin/src/main/java/zipkin/internal/Span2Codec.java new file mode 100644 index 00000000000..083f5a915b0 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2Codec.java @@ -0,0 +1,33 @@ +/** + * 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.List; + +/** Utilities for working with {@link Span2} */ +public interface Span2Codec { + Span2Codec JSON = new Span2JsonCodec(); + + /** Serialize a span recorded from instrumentation into its binary form. */ + byte[] writeSpan(Span2 span); + + /** Serialize a list of spans recorded from instrumentation into their binary form. */ + byte[] writeSpans(List spans); + + /** throws {@linkplain IllegalArgumentException} if a span couldn't be decoded */ + Span2 readSpan(byte[] bytes); + + /** throws {@linkplain IllegalArgumentException} if the spans couldn't be decoded */ + List readSpans(byte[] bytes); +} diff --git a/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java b/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java new file mode 100644 index 00000000000..8d3152144e4 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java @@ -0,0 +1,247 @@ +/** + * 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 com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.MalformedJsonException; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import zipkin.internal.JsonCodec.JsonReaderAdapter; + +import static zipkin.internal.Buffer.asciiSizeInBytes; +import static zipkin.internal.Buffer.jsonEscapedSizeInBytes; +import static zipkin.internal.JsonCodec.ANNOTATION_WRITER; +import static zipkin.internal.JsonCodec.ENDPOINT_READER; +import static zipkin.internal.JsonCodec.ENDPOINT_WRITER; +import static zipkin.internal.JsonCodec.writeList; + +/** + * Internal type supporting codec operations in {@link Span2}. Design rationale is the same as + * {@link JsonCodec}. + */ +public final class Span2JsonCodec implements Span2Codec { + + @Override public Span2 readSpan(byte[] bytes) { + return JsonCodec.read(new SimpleSpanReader(), bytes); + } + + /** Serialize a span recorded from instrumentation into its binary form. */ + @Override public byte[] writeSpan(Span2 span) { + return JsonCodec.write(SPAN_WRITER, span); + } + + @Override public List readSpans(byte[] bytes) { + return JsonCodec.readList(new SimpleSpanReader(), bytes); + } + + @Override public byte[] writeSpans(List value) { + return writeList(SPAN_WRITER, value); + } + + static final class SimpleSpanReader implements JsonReaderAdapter { + Span2.Builder builder; + + @Override public Span2 fromJson(JsonReader reader) throws IOException { + if (builder == null) { + builder = Span2.builder(); + } else { + builder.clear(); + } + reader.beginObject(); + while (reader.hasNext()) { + String nextName = reader.nextName(); + if (nextName.equals("traceId")) { + builder.traceId(reader.nextString()); + } else if (nextName.equals("parentId") && reader.peek() != JsonToken.NULL) { + builder.parentId(reader.nextString()); + } else if (nextName.equals("id")) { + builder.id(reader.nextString()); + } else if (nextName.equals("kind")) { + builder.kind(Span2.Kind.valueOf(reader.nextString())); + } else if (nextName.equals("name") && reader.peek() != JsonToken.NULL) { + builder.name(reader.nextString()); + } else if (nextName.equals("timestamp") && reader.peek() != JsonToken.NULL) { + builder.timestamp(reader.nextLong()); + } else if (nextName.equals("duration") && reader.peek() != JsonToken.NULL) { + builder.duration(reader.nextLong()); + } else if (nextName.equals("localEndpoint") && reader.peek() != JsonToken.NULL) { + builder.localEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("remoteEndpoint") && reader.peek() != JsonToken.NULL) { + builder.remoteEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("annotations")) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + Long timestamp = null; + String value = null; + while (reader.hasNext()) { + nextName = reader.nextName(); + if (nextName.equals("timestamp")) { + timestamp = reader.nextLong(); + } else if (nextName.equals("value")) { + value = reader.nextString(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + if (timestamp != null && value != null) builder.addAnnotation(timestamp, value); + } + reader.endArray(); + } else if (nextName.equals("tags")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + throw new MalformedJsonException("No value at " + reader.getPath()); + } + builder.putTag(key, reader.nextString()); + } + reader.endObject(); + } else if (nextName.equals("debug") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.debug(true); + } else if (nextName.equals("shared") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.shared(true); + } else { + reader.skipValue(); + } + } + reader.endObject(); + return builder.build(); + } + + @Override public String toString() { + return "Span2"; + } + } + + static final Buffer.Writer SPAN_WRITER = new Buffer.Writer() { + @Override public int sizeInBytes(Span2 value) { + int sizeInBytes = 0; + if (value.traceIdHigh() != 0) sizeInBytes += 16; + sizeInBytes += asciiSizeInBytes("{\"traceId\":\"") + 16 + 1; + if (value.parentId() != null) { + sizeInBytes += asciiSizeInBytes(",\"parentId\":\"") + 16 + 1; + } + sizeInBytes += asciiSizeInBytes(",\"id\":\"") + 16 + 1; + if (value.kind() != null) { + sizeInBytes += asciiSizeInBytes(",\"kind\":\""); + sizeInBytes += asciiSizeInBytes(value.kind().toString()) + 1; + } + if (value.name() != null) { + sizeInBytes += asciiSizeInBytes(",\"name\":\""); + sizeInBytes += jsonEscapedSizeInBytes(value.name()) + 1; + } + if (value.timestamp() != null) { + sizeInBytes += asciiSizeInBytes(",\"timestamp\":"); + sizeInBytes += asciiSizeInBytes(value.timestamp()); + } + if (value.duration() != null) { + sizeInBytes += asciiSizeInBytes(",\"duration\":"); + sizeInBytes += asciiSizeInBytes(value.duration()); + } + if (value.localEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"localEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.localEndpoint()); + } + if (value.remoteEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"remoteEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.remoteEndpoint()); + } + if (!value.annotations().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"annotations\":"); + sizeInBytes += JsonCodec.sizeInBytes(ANNOTATION_WRITER, value.annotations()); + } + if (!value.tags().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"tags\":"); + sizeInBytes += 2; // curly braces + int tagCount = value.tags().size(); + if (tagCount > 1) sizeInBytes += tagCount - 1; // comma to join elements + for (Map.Entry entry : value.tags().entrySet()) { + sizeInBytes += 5; // 4 quotes and a colon + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getKey()); + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getValue()); + } + } + if (Boolean.TRUE.equals(value.debug())) { + sizeInBytes += asciiSizeInBytes(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + sizeInBytes += asciiSizeInBytes(",\"shared\":true"); + } + return ++sizeInBytes;// end curly-brace + } + + @Override public void write(Span2 value, Buffer b) { + b.writeAscii("{\"traceId\":\""); + if (value.traceIdHigh() != 0) { + b.writeLowerHex(value.traceIdHigh()); + } + b.writeLowerHex(value.traceId()).writeByte('"'); + if (value.parentId() != null) { + b.writeAscii(",\"parentId\":\"").writeLowerHex(value.parentId()).writeByte('"'); + } + b.writeAscii(",\"id\":\"").writeLowerHex(value.id()).writeByte('"'); + if (value.kind() != null) { + b.writeAscii(",\"kind\":\"").writeJsonEscaped(value.kind().toString()).writeByte('"'); + } + if (value.name() != null) { + b.writeAscii(",\"name\":\"").writeJsonEscaped(value.name()).writeByte('"'); + } + if (value.timestamp() != null) { + b.writeAscii(",\"timestamp\":").writeAscii(value.timestamp()); + } + if (value.duration() != null) { + b.writeAscii(",\"duration\":").writeAscii(value.duration()); + } + if (value.localEndpoint() != null) { + b.writeAscii(",\"localEndpoint\":"); + ENDPOINT_WRITER.write(value.localEndpoint(), b); + } + if (value.remoteEndpoint() != null) { + b.writeAscii(",\"remoteEndpoint\":"); + ENDPOINT_WRITER.write(value.remoteEndpoint(), b); + } + if (!value.annotations().isEmpty()) { + b.writeAscii(",\"annotations\":"); + writeList(ANNOTATION_WRITER, value.annotations(), b); + } + if (!value.tags().isEmpty()) { + b.writeAscii(",\"tags\":{"); + Iterator> i = value.tags().entrySet().iterator(); + while (i.hasNext()) { + Map.Entry entry = i.next(); + b.writeByte('"').writeJsonEscaped(entry.getKey()).writeAscii("\":\""); + b.writeJsonEscaped(entry.getValue()).writeByte('"'); + if (i.hasNext()) b.writeByte(','); + } + b.writeByte('}'); + } + if (Boolean.TRUE.equals(value.debug())) { + b.writeAscii(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + b.writeAscii(",\"shared\":true"); + } + b.writeByte('}'); + } + + @Override public String toString() { + return "Span2"; + } + }; +} diff --git a/zipkin/src/test/java/zipkin/internal/DependencyLinkSpanTest.java b/zipkin/src/test/java/zipkin/internal/DependencyLinkSpanTest.java deleted file mode 100644 index d8a1f702055..00000000000 --- a/zipkin/src/test/java/zipkin/internal/DependencyLinkSpanTest.java +++ /dev/null @@ -1,221 +0,0 @@ -/** - * 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.Constants; -import zipkin.internal.DependencyLinkSpan.Kind; - -import static org.assertj.core.api.Assertions.assertThat; - -public class DependencyLinkSpanTest { - - @Test - public void testToString() { - assertThat(DependencyLinkSpan.builder(0L, 1L, null, 1L).build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"id\": \"0000000000000001\", \"kind\": \"UNKNOWN\"}"); - - assertThat(DependencyLinkSpan.builder(0L, 1L, 1L, 2L).build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"parentId\": \"0000000000000001\", \"id\": \"0000000000000002\", \"kind\": \"UNKNOWN\"}"); - - assertThat(DependencyLinkSpan.builder(0L, 1L, 1L, 2L) - .srService("processor") - .caService("kinesis").build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"parentId\": \"0000000000000001\", \"id\": \"0000000000000002\", \"kind\": \"SERVER\", \"service\": \"processor\", \"peerService\": \"kinesis\"}"); - - // It is invalid to log "ca" without "sr", so marked as unknown - assertThat(DependencyLinkSpan.builder(0L, 1L, 1L, 2L) - .caService("kinesis").build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"parentId\": \"0000000000000001\", \"id\": \"0000000000000002\", \"kind\": \"UNKNOWN\"}"); - - assertThat(DependencyLinkSpan.builder(0L, 1L, 1L, 2L) - .saService("mysql").build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"parentId\": \"0000000000000001\", \"id\": \"0000000000000002\", \"kind\": \"CLIENT\", \"peerService\": \"mysql\"}"); - - // arbitrary 2-sided span - assertThat(DependencyLinkSpan.builder(0L, 1L, 1L, 2L) - .caService("shell-script") - .saService("mysql").build()) - .hasToString( - "{\"traceId\": \"0000000000000001\", \"parentId\": \"0000000000000001\", \"id\": \"0000000000000002\", \"kind\": \"CLIENT\", \"service\": \"shell-script\", \"peerService\": \"mysql\"}"); - - // 128-bit trace ID - assertThat(DependencyLinkSpan.builder(3L, 1L, null, 1L).build()) - .hasToString( - "{\"traceId\": \"00000000000000030000000000000001\", \"id\": \"0000000000000001\", \"kind\": \"UNKNOWN\"}"); - } - - @Test - public void parentAndChildApply() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L).build(); - assertThat(span.parentId).isNull(); - assertThat(span.id).isEqualTo(1L); - - span = DependencyLinkSpan.builder(0L, 1L, 1L, 2L).build(); - assertThat(span.parentId).isEqualTo(1L); - assertThat(span.id).isEqualTo(2L); - } - - /** You cannot make a dependency link unless you know the the local or peer service. */ - @Test - public void whenNoServiceLabelsExist_kindIsUnknown() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L).build(); - - assertThat(span.kind).isEqualTo(Kind.UNKNOWN); - assertThat(span.peerService).isNull(); - assertThat(span.service).isNull(); - } - - @Test - public void whenOnlyAddressLabelsExist_kindIsClient() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .caService("service1") - .saService("service2") - .build(); - - assertThat(span.kind).isEqualTo(Kind.CLIENT); - assertThat(span.service).isEqualTo("service1"); - assertThat(span.peerService).isEqualTo("service2"); - } - - /** The linker is biased towards server spans, or client spans that know the peer service. */ - @Test - public void whenServerLabelsAreMissing_kindIsUnknownAndLabelsAreCleared() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .caService("service1") - .build(); - - assertThat(span.kind).isEqualTo(Kind.UNKNOWN); - assertThat(span.service).isNull(); - assertThat(span.peerService).isNull(); - } - - /** {@link Constants#SERVER_RECV} is only applied when the local span is acting as a server */ - @Test - public void whenSrServiceExists_kindIsServer() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .srService("service") - .build(); - - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service"); - assertThat(span.peerService).isNull(); - } - - /** - * {@link Constants#CLIENT_ADDR} indicates the peer, which is a client in the case of a server - * span - */ - @Test - public void whenSrAndCaServiceExists_caIsThePeer() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .caService("service1") - .srService("service2") - .build(); - - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service2"); - assertThat(span.peerService).isEqualTo("service1"); - } - - /** - * {@link Constants#CLIENT_SEND} indicates the peer, which is a client in the case of a server - * span - */ - @Test - public void whenSrAndCsServiceExists_caIsThePeer() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .csService("service1") - .srService("service2") - .build(); - - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service2"); - assertThat(span.peerService).isEqualTo("service1"); - } - - /** {@link Constants#CLIENT_ADDR} is more authoritative than {@link Constants#CLIENT_SEND} */ - @Test - public void whenCrAndCaServiceExists_caIsThePeer() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .csService("foo") - .caService("service1") - .srService("service2") - .build(); - - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service2"); - assertThat(span.peerService).isEqualTo("service1"); - } - - @Test - public void specialCasesFinagleLocalSocketLabeling() { - // Finagle labels two sides of the same socket ("ca", "sa") with the local service name. - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .caService("service") - .saService("service") - .build(); - - // When there's no "sr" annotation, we assume it is a client. - assertThat(span.kind).isEqualTo(Kind.CLIENT); - assertThat(span.service).isNull(); - assertThat(span.peerService).isEqualTo("service"); - - span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .srService("service") - .caService("service") - .saService("service") - .build(); - - // When there is an "sr" annotation, we know it is a server - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service"); - assertThat(span.peerService).isNull(); - } - - /** - *

Dependency linker works backwards: it is easier to treat a "cs" as a server span lacking its - * caller, than a client span lacking its receiver. - */ - @Test - public void csWithoutSaIsServer() { - DependencyLinkSpan span = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .csService("service1") - .build(); - - assertThat(span.kind).isEqualTo(Kind.SERVER); - assertThat(span.service).isEqualTo("service1"); - assertThat(span.peerService).isNull(); - } - - /** Service links to empty string are confusing and offer no value. */ - @Test - public void emptyToNull() { - DependencyLinkSpan.Builder builder = DependencyLinkSpan.builder(0L, 1L, null, 1L) - .caService("") - .csService("") - .saService("") - .srService(""); - - assertThat(builder.caService).isNull(); - assertThat(builder.csService).isNull(); - assertThat(builder.saService).isNull(); - assertThat(builder.srService).isNull(); - } -} diff --git a/zipkin/src/test/java/zipkin/internal/DependencyLinkerTest.java b/zipkin/src/test/java/zipkin/internal/DependencyLinkerTest.java index 362d6a63bda..c38181aa406 100644 --- a/zipkin/src/test/java/zipkin/internal/DependencyLinkerTest.java +++ b/zipkin/src/test/java/zipkin/internal/DependencyLinkerTest.java @@ -20,10 +20,10 @@ import java.util.stream.Collectors; import org.junit.Test; import zipkin.DependencyLink; +import zipkin.Endpoint; import zipkin.Span; import zipkin.TestObjects; -import zipkin.internal.DependencyLinkSpan.Kind; -import zipkin.internal.DependencyLinkSpan.TraceId; +import zipkin.internal.Span2.Kind; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @@ -50,8 +50,8 @@ public void baseCase() { @Test public void linksSpans() { assertThat(new DependencyLinker().putTrace(TestObjects.TRACE).link()).containsExactly( - DependencyLink.create("web", "app", 1L), - DependencyLink.create("app", "db", 1L) + DependencyLink.create("web", "app", 1L), + DependencyLink.create("app", "db", 1L) ); } @@ -69,71 +69,48 @@ public void dropsSelfReferencingSpans() { ); } - /** - * The linker links a directed graph, if the span kind is unknown, we don't know the direction to - * link. - */ - @Test - public void doesntLinkUnknownRootSpans() { - List unknownRootSpans = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.UNKNOWN, null, null), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.UNKNOWN, "server", "client"), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.UNKNOWN, "client", "server") - ); - - for (DependencyLinkSpan span : unknownRootSpans) { - assertThat(new DependencyLinker(logger) - .putTrace(asList(span).iterator()).link()) - .isEmpty(); - } - - assertThat(messages).contains( - "non-rpc span; skipping" - ); - } - /** * A root span can be a client-originated trace or a server receipt which knows its peer. In these * cases, the peer is known and kind establishes the direction. */ @Test public void linksSpansDirectedByKind() { - List validRootSpans = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "server", "client"), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.CLIENT, "client", "server") + List validRootSpans = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "server", "client"), + span2(0L, 1L, null, 1L, Kind.CLIENT, "client", "server") ); - for (DependencyLinkSpan span : validRootSpans) { + for (Span2 span : validRootSpans) { assertThat(new DependencyLinker() - .putTrace(asList(span).iterator()).link()) - .containsOnly(DependencyLink.create("client", "server", 1L)); + .putTrace(asList(span).iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 1L)); } } @Test public void callsAgainstTheSameLinkIncreasesCallCount_span() { - List trace = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "client", null), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 2L, Kind.CLIENT, null, "server"), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 3L, Kind.CLIENT, null, "server") + List trace = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "client", null), + span2(0L, 1L, 1L, 2L, Kind.CLIENT, null, "server"), + span2(0L, 1L, 1L, 3L, Kind.CLIENT, null, "server") ); assertThat(new DependencyLinker() - .putTrace(trace.iterator()).link()) - .containsOnly(DependencyLink.create("client", "server", 2L)); + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 2L)); } @Test public void callsAgainstTheSameLinkIncreasesCallCount_trace() { - List trace = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "client", null), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 2L, Kind.CLIENT, null, "server") + List trace = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "client", null), + span2(0L, 1L, 1L, 2L, Kind.CLIENT, null, "server") ); assertThat(new DependencyLinker() - .putTrace(trace.iterator()) - .putTrace(trace.iterator()).link()) - .containsOnly(DependencyLink.create("client", "server", 2L)); + .putTrace(trace.iterator()) + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 2L)); } /** @@ -142,22 +119,57 @@ public void callsAgainstTheSameLinkIncreasesCallCount_trace() { */ @Test public void singleHostSpansResultInASingleCallCount() { - List> singleLinks = asList( - asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.CLIENT, "client", "server"), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 2L, Kind.SERVER, "server", null) - ), - asList( - new DependencyLinkSpan(new TraceId(0L, 3L), null, 3L, Kind.SERVER, "client", null), - new DependencyLinkSpan(new TraceId(0L, 3L), 3L, 4L, Kind.CLIENT, "client", "server") - ) + List trace = asList( + span2(0L, 3L, null, 3L, Kind.CLIENT, "client", null), + span2(0L, 3L, 3L, 4L, Kind.SERVER, "server", "client") ); - for (List trace : singleLinks) { - assertThat(new DependencyLinker() - .putTrace(trace.iterator()).link()) - .containsOnly(DependencyLink.create("client", "server", 1L)); - } + assertThat(new DependencyLinker() + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 1L)); + } + + @Test + public void singleHostSpansResultInASingleCallCount_defersNameToServer() { + List trace = asList( + span2(0L, 1L, null, 1L, Kind.CLIENT, "client", "server"), + span2(0L, 1L, 1L, 2L, Kind.SERVER, "server", null) + ); + + assertThat(new DependencyLinker(logger) + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 1L)); + + assertThat(messages).contains("deferring link to rpc child span"); + messages.clear(); + } + + @Test + public void singleHostSpans_multipleChildren() { + List trace = asList( + span2(0L, 4L, null, 4L, Kind.CLIENT, "client", null), + span2(0L, 4L, 4L, 5L, Kind.SERVER, "server", "client"), + span2(0L, 4L, 4L, 6L, Kind.SERVER, "server", "client") + ); + + assertThat(new DependencyLinker() + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 2L)); + } + + @Test + public void singleHostSpans_multipleChildren_defersNameToServer() { + List trace = asList( + span2(0L, 1L, null, 1L, Kind.CLIENT, "client", "server"), + span2(0L, 1L, 1L, 2L, Kind.SERVER, "server", null), + span2(0L, 1L, 1L, 3L, Kind.SERVER, "server", null) + ); + + assertThat(new DependencyLinker(logger) + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 2L)); + + assertThat(messages).contains("deferring link to rpc child span"); } /** @@ -166,67 +178,82 @@ public void singleHostSpansResultInASingleCallCount() { */ @Test public void intermediatedClientSpansMissingLocalServiceNameLinkToNearestServer() { - List trace = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "client", null), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 2L, Kind.UNKNOWN, null, null), - // possibly a local fan-out span - new DependencyLinkSpan(new TraceId(0L, 1L), 2L, 3L, Kind.CLIENT, null, "server"), - new DependencyLinkSpan(new TraceId(0L, 1L), 2L, 4L, Kind.CLIENT, null, "server") + List trace = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "client", null), + span2(0L, 1L, 1L, 2L, null, null, null), + // possibly a local fan-out span + span2(0L, 1L, 2L, 3L, Kind.CLIENT, null, "server"), + span2(0L, 1L, 2L, 4L, Kind.CLIENT, null, "server") ); assertThat(new DependencyLinker() - .putTrace(trace.iterator()).link()) - .containsOnly(DependencyLink.create("client", "server", 2L)); + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("client", "server", 2L)); } /** A loopback span is direction-agnostic, so can be linked properly regardless of kind. */ @Test public void linksLoopbackSpans() { - List validRootSpans = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "service", "service"), - new DependencyLinkSpan(new TraceId(0L, 2L), null, 2L, Kind.CLIENT, "service", "service") + List validRootSpans = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "service", "service"), + span2(0L, 2L, null, 2L, Kind.CLIENT, "service", "service") ); - for (DependencyLinkSpan span : validRootSpans) { + for (Span2 span : validRootSpans) { assertThat(new DependencyLinker() - .putTrace(asList(span).iterator()).link()) - .containsOnly(DependencyLink.create("service", "service", 1L)); + .putTrace(asList(span).iterator()).link()) + .containsOnly(DependencyLink.create("service", "service", 1L)); } } + @Test + public void noSpanKindTreatedSameAsClient() { + List trace = asList( + span2(0L, 1L, null, 1L, null, "some-client", "web"), + span2(0L, 1L, 1L, 2L, null, "web", "app"), + span2(0L, 1L, 2L, 3L, null, "app", "db") + ); + + assertThat(new DependencyLinker().putTrace(trace.iterator()).link()).containsOnly( + DependencyLink.create("some-client", "web", 1L), + DependencyLink.create("web", "app", 1L), + DependencyLink.create("app", "db", 1L) + ); + } + /** * A dependency link is between two services. Given only one span, we cannot link if we don't know * both service names. */ @Test public void cannotLinkSingleSpanWithoutBothServiceNames() { - List incompleteRootSpans = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, null, null), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "server", null), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, null, "client"), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.CLIENT, null, null), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.CLIENT, "client", null), - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.CLIENT, null, "server") + List incompleteRootSpans = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, null, null), + span2(0L, 1L, null, 1L, Kind.SERVER, "server", null), + span2(0L, 1L, null, 1L, Kind.SERVER, null, "client"), + span2(0L, 1L, null, 1L, Kind.CLIENT, null, null), + span2(0L, 1L, null, 1L, Kind.CLIENT, "client", null), + span2(0L, 1L, null, 1L, Kind.CLIENT, null, "server") ); - for (DependencyLinkSpan span : incompleteRootSpans) { + for (Span2 span : incompleteRootSpans) { assertThat(new DependencyLinker(logger) - .putTrace(asList(span).iterator()).link()) - .isEmpty(); + .putTrace(asList(span).iterator()).link()) + .isEmpty(); } } @Test public void doesntLinkUnrelatedSpansWhenMissingRootSpan() { long missingParentId = 1; - List trace = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), missingParentId, 2L, Kind.SERVER, "service1", null), - new DependencyLinkSpan(new TraceId(0L, 1L), missingParentId, 3L, Kind.SERVER, "service2", null) + List trace = asList( + span2(0L, 1L, missingParentId, 2L, Kind.SERVER, "service1", null), + span2(0L, 1L, missingParentId, 3L, Kind.SERVER, "service2", null) ); assertThat(new DependencyLinker(logger) - .putTrace(trace.iterator()).link()) - .isEmpty(); + .putTrace(trace.iterator()).link()) + .isEmpty(); assertThat(messages).contains( "skipping synthetic node for broken span tree" @@ -236,14 +263,14 @@ public void doesntLinkUnrelatedSpansWhenMissingRootSpan() { @Test public void linksRelatedSpansWhenMissingRootSpan() { long missingParentId = 1; - List trace = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), missingParentId, 2L, Kind.SERVER, "service1", null), - new DependencyLinkSpan(new TraceId(0L, 1L), 2L, 3L, Kind.SERVER, "service2", null) + List trace = asList( + span2(0L, 1L, missingParentId, 2L, Kind.SERVER, "service1", null), + span2(0L, 1L, 2L, 3L, Kind.SERVER, "service2", null) ); assertThat(new DependencyLinker(logger) - .putTrace(trace.iterator()).link()) - .containsOnly(DependencyLink.create("service1", "service2", 1L)); + .putTrace(trace.iterator()).link()) + .containsOnly(DependencyLink.create("service1", "service2", 1L)); assertThat(messages).contains( "skipping synthetic node for broken span tree" @@ -253,9 +280,9 @@ public void linksRelatedSpansWhenMissingRootSpan() { /** Client+Server spans that don't share IDs are treated as server spans missing their peer */ @Test public void linksSingleHostSpans() { - List singleHostSpans = asList( - new DependencyLinkSpan(new TraceId(0L, 1L), null, 1L, Kind.SERVER, "web", null), - new DependencyLinkSpan(new TraceId(0L, 1L), 1L, 2L, Kind.SERVER, "app", null) + List singleHostSpans = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "web", null), + span2(0L, 1L, 1L, 2L, Kind.SERVER, "app", null) ); assertThat(new DependencyLinker() @@ -263,17 +290,45 @@ public void linksSingleHostSpans() { .containsOnly(DependencyLink.create("web", "app", 1L)); } + /** Creates a link when there's a span missing, in this case 2L which is an RPC from web to app */ + @Test + public void missingSpan() { + List singleHostSpans = asList( + span2(0L, 1L, null, 1L, Kind.SERVER, "web", null), + span2(0L, 1L, 1L, 2L, Kind.CLIENT, "app", null) + ); + + assertThat(new DependencyLinker(logger) + .putTrace(singleHostSpans.iterator()).link()) + .containsOnly(DependencyLink.create("web", "app", 1L)); + + assertThat(messages).contains( + "detected missing link to client span" + ); + } + @Test public void merge() { List links = asList( - DependencyLink.create("client", "server", 2L), - DependencyLink.create("client", "server", 2L), - DependencyLink.create("client", "client", 1L) + DependencyLink.create("client", "server", 2L), + DependencyLink.create("client", "server", 2L), + DependencyLink.create("client", "client", 1L) ); assertThat(DependencyLinker.merge(links)).containsExactly( - DependencyLink.create("client", "server", 4L), - DependencyLink.create("client", "client", 1L) + DependencyLink.create("client", "server", 4L), + DependencyLink.create("client", "client", 1L) ); } + + static Span2 span2(long traceIdHigh, long traceId, @Nullable Long parentId, long id, + @Nullable Kind kind, + @Nullable String local, @Nullable String remote) { + Span2.Builder result = Span2.builder(); + result.traceIdHigh(traceIdHigh).traceId(traceId).parentId(parentId).id(id); + result.kind(kind); + if (local != null) result.localEndpoint(Endpoint.builder().serviceName(local).build()); + if (remote != null) result.remoteEndpoint(Endpoint.builder().serviceName(remote).build()); + return result.build(); + } } diff --git a/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java b/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java new file mode 100644 index 00000000000..2fb11e91473 --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java @@ -0,0 +1,294 @@ +/** + * 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.io.IOException; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.TraceKeys; + +import static org.assertj.core.api.Assertions.assertThat; +import static zipkin.internal.Util.UTF_8; + +public class Span2JsonCodecTest { + Span2JsonCodec codec = new Span2JsonCodec(); + + 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(); + + Span2 span = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Span2.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(); + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test public void spanRoundTrip() throws IOException { + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes() throws IOException { + assertThat(Span2JsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + @Test public void spanRoundTrip_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void spanRoundTrip_shared() throws IOException { + span = span.toBuilder().shared(true).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + assertThat(Span2JsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + /** + * This isn't a test of what we "should" accept as a span, rather that characters that trip-up + * json don't fail in codec. + */ + @Test public void specialCharsInJson() throws IOException { + // service name is surrounded by control characters + Span2 worstSpanInTheWorld = Span2.builder().traceId(1L).id(1L) + // name is terrible + .name(new String(new char[] {'"', '\\', '\t', '\b', '\n', '\r', '\f'})) + .localEndpoint(Endpoint.create(new String(new char[] {0, 'a', 1}), 0)) + // annotation value includes some json newline characters + .addAnnotation(1L, "\u2028 and \u2029") + // tag key includes a quote and value newlines + .putTag("\"foo", "Database error: ORA-00942:\u2028 and \u2029 table or view does not exist\n") + .build(); + + byte[] bytes = codec.writeSpan(worstSpanInTheWorld); + assertThat(codec.readSpan(bytes)) + .isEqualTo(worstSpanInTheWorld); + } + + @Test public void decentErrorMessageOnEmptyInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading Span2"); + + codec.readSpan(new byte[0]); + } + + @Test public void decentErrorMessageOnEmptyInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading List"); + + codec.readSpans(new byte[0]); + } + + @Test public void decentErrorMessageOnMalformedInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading Span2 from "); + + codec.readSpan(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + /** + * Particulary, thrift can mistake malformed content as a huge list. Let's not blow up. + */ + @Test public void decentErrorMessageOnMalformedInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading List from "); + + codec.readSpans(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + @Test public void spansRoundTrip() throws IOException { + List tenClientSpans = Collections.nCopies(10, span); + + byte[] bytes = codec.writeSpans(tenClientSpans); + assertThat(codec.readSpans(bytes)) + .isEqualTo(tenClientSpans); + } + + @Test public void writesTraceIdHighIntoTraceIdField() { + Span2 with128BitTraceId = Span2.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")) + .traceId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .localEndpoint(frontend) + .id(1).name("").build(); + + assertThat(new String(codec.writeSpan(with128BitTraceId), Util.UTF_8)) + .startsWith("{\"traceId\":\"48485a3953bb61246b221d5bc9e6496c\""); + } + + @Test public void readsTraceIdHighFromTraceIdField() { + byte[] with128BitTraceId = ("{\n" + + " \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + byte[] withLower64bitsTraceId = ("{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + + assertThat(codec.readSpan(with128BitTraceId)) + .isEqualTo(codec.readSpan(withLower64bitsTraceId).toBuilder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")).build()); + } + + @Test public void ignoreNull_parentId() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"parentId\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_timestamp() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"timestamp\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_duration() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"duration\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_debug() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"debug\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_shared() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"shared\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_localEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"localEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_remoteEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"remoteEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_traceId() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": null,\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_id() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void missingValue() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("No value at $.tags.foo"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"tags\": {\n" + + " \"foo\": NULL\n" + + " }\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } +} diff --git a/zipkin/src/test/java/zipkin/internal/Span2Test.java b/zipkin/src/test/java/zipkin/internal/Span2Test.java index 0deae2ca19c..05d0ce5b19c 100644 --- a/zipkin/src/test/java/zipkin/internal/Span2Test.java +++ b/zipkin/src/test/java/zipkin/internal/Span2Test.java @@ -13,6 +13,10 @@ */ package zipkin.internal; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import okio.Buffer; +import okio.ByteString; import org.junit.Test; import zipkin.Annotation; @@ -109,9 +113,25 @@ public class Span2Test { .isNull(); } - // TODO: toString_isJson + @Test public void toString_isJson() { + assertThat(base.toString()).hasToString( + "{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"localEndpoint\":{\"serviceName\":\"app\",\"ipv4\":\"172.17.0.2\",\"port\":8080}}" + ); + } + + @Test public void serialization() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); - // TODO: serialization + assertThat(new ObjectInputStream(buffer.inputStream()).readObject()) + .isEqualTo(base); + } + + @Test public void serializationUsesJson() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); - // TODO: serializationUsesJson + assertThat(buffer.indexOf(ByteString.encodeUtf8(base.toString()))) + .isPositive(); + } }