From 068d0f0f668384987741f077f81a119ac095ee29 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 12 Nov 2015 21:11:02 -0800 Subject: [PATCH] Updates to Spring Boot 1.3 and upstream 1.25 * Adds duration and lookback queries * Added docs for all core datatypes * Uses "on duplicate key update" to merge late arriving span metadata * Adds gzip compression of api traffic * Adds clock skew adjustment --- README.md | 10 +- docker-compose.yml | 4 +- pom.xml | 7 +- .../src/main/java/io/zipkin/Annotation.java | 38 +-- .../main/java/io/zipkin/BinaryAnnotation.java | 77 +++++- .../src/main/java/io/zipkin/Constants.java | 133 ++++++++++- .../src/main/java/io/zipkin/Endpoint.java | 16 +- .../src/main/java/io/zipkin/QueryRequest.java | 103 +++++++- .../src/main/java/io/zipkin/Span.java | 222 +++++++++++++++--- .../src/main/java/io/zipkin/SpanStore.java | 19 +- .../zipkin/internal/CorrectForClockSkew.java | 144 ++++++++++++ .../java/io/zipkin/internal/JsonCodec.java | 14 +- .../java/io/zipkin/internal/SpanNode.java | 79 +++++++ .../java/io/zipkin/internal/ThriftCodec.java | 28 +++ .../src/test/java/io/zipkin/SpanTest.java | 71 ++++++ .../src/test/java/io/zipkin/TestObjects.java | 28 ++- .../java/io/zipkin/assertj/SpanAssert.java | 70 ++++++ .../io/zipkin/assertj/ZipkinAssertions.java | 25 ++ zipkin-java-interop/pom.xml | 2 +- .../interop/ScalaDependencyStoreAdapter.java | 17 +- .../zipkin/interop/ScalaSpanStoreAdapter.java | 3 + .../test/java/io/zipkin/SpanStoreTest.java | 179 ++++++++++++++ .../io/zipkin/jdbc/JDBCSpanStoreTest.java | 32 +++ .../server/InMemoryScalaSpanStoreTest.java | 1 - .../zipkin/server/InMemorySpanStoreTest.java | 26 ++ .../java/io/zipkin/jdbc/JDBCSpanStore.java | 92 +++++--- .../generated/tables/ZipkinSpans.java | 11 +- zipkin-java-jdbc/src/main/resources/mysql.sql | 4 +- zipkin-java-server/Dockerfile | 2 +- zipkin-java-server/pom.xml | 4 +- .../io/zipkin/server/InMemorySpanStore.java | 50 +++- .../io/zipkin/server/ZipkinQueryApiV1.java | 22 +- .../server/brave/JDBCTracerConfiguration.java | 27 +-- .../src/main/resources/zipkin-server.yml | 2 + .../brave/SpanStoreSpanCollectorTest.java | 1 + 35 files changed, 1373 insertions(+), 190 deletions(-) create mode 100644 zipkin-java-core/src/main/java/io/zipkin/internal/CorrectForClockSkew.java create mode 100644 zipkin-java-core/src/main/java/io/zipkin/internal/SpanNode.java create mode 100644 zipkin-java-core/src/test/java/io/zipkin/SpanTest.java create mode 100644 zipkin-java-core/src/test/java/io/zipkin/assertj/SpanAssert.java create mode 100644 zipkin-java-core/src/test/java/io/zipkin/assertj/ZipkinAssertions.java create mode 100644 zipkin-java-interop/src/test/java/io/zipkin/SpanStoreTest.java create mode 100644 zipkin-java-interop/src/test/java/io/zipkin/jdbc/JDBCSpanStoreTest.java create mode 100644 zipkin-java-interop/src/test/java/io/zipkin/server/InMemorySpanStoreTest.java diff --git a/README.md b/README.md index fe04d76c28a..99aeabae5e2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # zipkin-java Experimental java zipkin backend. Please look at issues as we are currently working on scope. +## Server +You can run the experimental server to power the zipkin web UI. -## Docker -You can run the experimental docker server to power the zipkin web UI. The following is a hack of the [docker-zipkin](https://github.com/openzipkin/docker-zipkin) docker-compose instructions. +```bash +$ (cd zipkin-java-server; mvn spring-boot:run) +``` + +### Docker +You can also run the java server with docker. The following is a hack of the [docker-zipkin](https://github.com/openzipkin/docker-zipkin) docker-compose instructions. ```bash # build zipkin-java-server/target/zipkin-java-server-0.1.0-SNAPSHOT.jar diff --git a/docker-compose.yml b/docker-compose.yml index 9e1bde67542..c4d095c2b7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ # mysql: - image: openzipkin/zipkin-mysql:1.21.1 + image: openzipkin/zipkin-mysql:1.25.0 ports: - 3306:3306 query: @@ -27,7 +27,7 @@ query: links: - mysql:storage web: - image: openzipkin/zipkin-web:1.21.1 + image: openzipkin/zipkin-web:1.25.0 ports: - 8080:8080 environment: diff --git a/pom.xml b/pom.xml index 5a13040fa76..bdeedc2412d 100644 --- a/pom.xml +++ b/pom.xml @@ -54,13 +54,13 @@ 2.10.3 2.11 2.6 - 2.5.2 - 2.4.1 + 2.5.3 + 2.4.2 1.6.6 1.6 1.0.0 1.6.0 - 1.2.7.RELEASE + 1.3.0.RELEASE 0.9.3 @@ -187,6 +187,7 @@ test + diff --git a/zipkin-java-core/src/main/java/io/zipkin/Annotation.java b/zipkin-java-core/src/main/java/io/zipkin/Annotation.java index 558f7b974dc..b0506125f46 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/Annotation.java +++ b/zipkin-java-core/src/main/java/io/zipkin/Annotation.java @@ -20,14 +20,9 @@ import static io.zipkin.internal.Util.equal; /** - * The endpoint associated with this annotation depends on {@link #value}. + * Associates an event that explains latency with a timestamp. * - *

When {@link #value} is... - *

    - *
  • {@link Constants#CLIENT_ADDR}, this is the client endpoint of an RPC call
  • - *
  • {@link Constants#SERVER_ADDR}, this is the server endpoint of an RPC call
  • - *
  • Otherwise, this is the endpoint that recorded this annotation
  • - *
+ *

Unlike log statements, annotations are often codes: Ex. {@link Constants#SERVER_RECV "sr"}. */ public final class Annotation implements Comparable { @@ -35,12 +30,19 @@ public static Annotation create(long timestamp, String value, @Nullable Endpoint return new Annotation(timestamp, value, endpoint); } - /** Microseconds from epoch */ + /** + * Microseconds from epoch. + * + *

This value should be set directly by instrumentation, using the most precise value + * possible. For example, {@code gettimeofday} or syncing {@link System#nanoTime} against a tick + * of {@link System#currentTimeMillis}. + */ public final long timestamp; - /** What happened at the timestamp? */ + /** Usually a short tag indicating an event, like {@link Constants#SERVER_RECV "sr"}. or "finagle.retry" */ public final String value; + /** The host that recorded {@link #value}, primarily for query by service name. */ @Nullable public final Endpoint endpoint; @@ -64,17 +66,20 @@ public Builder(Annotation source) { this.endpoint = source.endpoint; } - public Annotation.Builder timestamp(long timestamp) { + /** @see Annotation#timestamp */ + public Builder timestamp(long timestamp) { this.timestamp = timestamp; return this; } - public Annotation.Builder value(String value) { + /** @see Annotation#value */ + public Builder value(String value) { this.value = value; return this; } - public Annotation.Builder endpoint(Endpoint endpoint) { + /** @see Annotation#endpoint */ + public Builder endpoint(Endpoint endpoint) { this.endpoint = endpoint; return this; } @@ -115,11 +120,12 @@ public int hashCode() { return h; } + /** Compares by {@link #timestamp}, then {@link #value}. */ @Override public int compareTo(Annotation that) { - if (this == that) { - return 0; - } - return Long.compare(timestamp, that.timestamp); + if (this == that) return 0; + int byTimestamp = Long.compare(timestamp, that.timestamp); + if (byTimestamp != 0) return byTimestamp; + return value.compareTo(that.value); } } diff --git a/zipkin-java-core/src/main/java/io/zipkin/BinaryAnnotation.java b/zipkin-java-core/src/main/java/io/zipkin/BinaryAnnotation.java index 41f57979827..791c5851f5a 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/BinaryAnnotation.java +++ b/zipkin-java-core/src/main/java/io/zipkin/BinaryAnnotation.java @@ -15,15 +15,42 @@ import io.zipkin.internal.JsonCodec; import io.zipkin.internal.Nullable; +import io.zipkin.internal.Util; import java.util.Arrays; import static io.zipkin.internal.Util.checkNotNull; import static io.zipkin.internal.Util.equal; +/** + * Binary annotations are tags applied to a Span to give it context. For example, a binary + * annotation of "http.uri" could the path to a resource in a RPC call. + * + *

Binary annotations of type {@link Type#STRING} are always queryable, though more a historical + * implementation detail than a structural concern. + * + *

Binary annotations can repeat, and vary on the host. Similar to Annotation, the host + * indicates who logged the event. This allows you to tell the difference between the client and + * server side of the same key. For example, the key "http.uri" might be different on the client and + * server side due to rewriting, like "/api/v1/myresource" vs "/myresource. Via the host field, you + * can see the different points of view, which often help in debugging. + */ public final class BinaryAnnotation { + /** A subset of thrift base types, except BYTES. */ public enum Type { - BOOL(0), BYTES(1), I16(2), I32(3), I64(4), DOUBLE(5), STRING(6); + /** + * Set to 0x01 when {@link BinaryAnnotation#key} is {@link Constants#CLIENT_ADDR} or {@link + * Constants#SERVER_ADDR} + */ + BOOL(0), + /** No encoding, or type is unknown. */ + BYTES(1), + I16(2), + I32(3), + I64(4), + DOUBLE(5), + /** The only type zipkin v1 supports search against. */ + STRING(6); public final int value; @@ -54,17 +81,50 @@ public static Type fromValue(int value) { } } + /** + * Special-cased form supporting {@link Constants#CLIENT_ADDR} and + * {@link Constants#SERVER_ADDR}. + * + * @param key {@link Constants#CLIENT_ADDR} or {@link Constants#SERVER_ADDR} + * @param endpoint associated endpoint. + */ + public static BinaryAnnotation address(String key, Endpoint endpoint) { + return new BinaryAnnotation(key, new byte[]{1}, Type.BOOL, checkNotNull(endpoint, "endpoint")); + } + + /** String values are the only queryable type of binary annotation. */ + public static BinaryAnnotation create(String key, String value, @Nullable Endpoint endpoint) { + return new BinaryAnnotation(key, value.getBytes(Util.UTF_8), Type.STRING, endpoint); + } + public static BinaryAnnotation create(String key, byte[] value, Type type, @Nullable Endpoint endpoint) { return new BinaryAnnotation(key, value, type, endpoint); } + /** + * Name used to lookup spans, such as "http.uri" or "finagle.version". + */ public final String key; - + /** + * Serialized thrift bytes, in TBinaryProtocol format. + * + *

For legacy reasons, byte order is big-endian. See THRIFT-3217. + */ public final byte[] value; - + /** + * The thrift type of value, most often STRING. + * + *

Note: type shouldn't vary for the same key. + */ public final Type type; - /** The endpoint that recorded this annotation */ + /** + * The host that recorded {@link #value}, allowing query by service name or address. + * + *

There are two exceptions: when {@link #key} is {@link Constants#CLIENT_ADDR} or {@link + * Constants#SERVER_ADDR}, this is the source or destination of an RPC. This exception allows + * zipkin to display network context of uninstrumented services, such as browsers or databases. + */ @Nullable public final Endpoint endpoint; @@ -91,23 +151,26 @@ public Builder(BinaryAnnotation source) { this.endpoint = source.endpoint; } + /** @see BinaryAnnotation#key */ public BinaryAnnotation.Builder key(String key) { this.key = key; return this; } + /** @see BinaryAnnotation#value */ public BinaryAnnotation.Builder value(byte[] value) { this.value = value.clone(); return this; } - public BinaryAnnotation.Builder type(Type type) { + /** @see BinaryAnnotation#type */ + public Builder type(Type type) { this.type = type; return this; } - @Nullable - public BinaryAnnotation.Builder endpoint(Endpoint endpoint) { + /** @see BinaryAnnotation#endpoint */ + public BinaryAnnotation.Builder endpoint(@Nullable Endpoint endpoint) { this.endpoint = endpoint; return this; } diff --git a/zipkin-java-core/src/main/java/io/zipkin/Constants.java b/zipkin-java-core/src/main/java/io/zipkin/Constants.java index d2cc115494f..31b2aa89b95 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/Constants.java +++ b/zipkin-java-core/src/main/java/io/zipkin/Constants.java @@ -14,21 +14,140 @@ package io.zipkin; public final class Constants { - /* Common annotation values */ - public static final String CLIENT_RECV = "cr"; + /** + * The client sent ("cs") a request to a server. There is only one send per span. For example, if + * there's a transport error, each attempt can be logged as a {@link #WIRE_SEND} annotation. + * + *

If chunking is involved, each chunk could be logged as a separate {@link + * #CLIENT_SEND_FRAGMENT} in the same span. + * + *

{@link Annotation#endpoint} is not the server. It is the host which logged the send event, + * almost always the client. When logging CLIENT_SEND, instrumentation should also log the {@link + * #SERVER_ADDR}. + */ public static final String CLIENT_SEND = "cs"; - public static final String SERVER_RECV = "sr"; + + /** + * The client received ("cr") a response from a server. There is only one receive per span. For + * example, if duplicate responses were received, each can be logged as a {@link #WIRE_RECV} + * annotation. + * + *

If chunking is involved, each chunk could be logged as a separate {@link + * #CLIENT_RECV_FRAGMENT} in the same span. + * + *

{@link Annotation#endpoint} is not the server. It is the host which logged the receive + * event, almost always the client. The actual endpoint of the server is recorded separately as + * {@link #SERVER_ADDR} when {@link #CLIENT_SEND} is logged. + */ + public static final String CLIENT_RECV = "cr"; + + /** + * The server sent ("ss") a response to a client. There is only one response per span. If there's + * a transport error, each attempt can be logged as a {@link #WIRE_SEND} annotation. + * + *

Typically, a trace ends with a server send, so the last timestamp of a trace is often the + * timestamp of the root span's server send. + * + *

If chunking is involved, each chunk could be logged as a separate {@link + * #SERVER_SEND_FRAGMENT} in the same span. + * + *

{@link Annotation#endpoint} is not the client. It is the host which logged the send event, + * almost always the server. The actual endpoint of the client is recorded separately as {@link + * #CLIENT_ADDR} when {@link #SERVER_RECV} is logged. + */ public static final String SERVER_SEND = "ss"; /** - * The endpoint associated with "CLIENT_" annotations is not necessarily {@link - * Annotation#endpoint} + * The server received ("sr") a request from a client. There is only one request per span. For + * example, if duplicate responses were received, each can be logged as a {@link #WIRE_RECV} + * annotation. + * + *

Typically, a trace starts with a server receive, so the first timestamp of a trace is often + * the timestamp of the root span's server receive. + * + *

If chunking is involved, each chunk could be logged as a separate {@link + * #SERVER_RECV_FRAGMENT} in the same span. + * + *

{@link Annotation#endpoint} is not the client. It is the host which logged the receive + * event, almost always the server. When logging SERVER_RECV, instrumentation should also log the + * {@link #CLIENT_ADDR}. + */ + public static final String SERVER_RECV = "sr"; + + /** + * Optionally logs an attempt to send a message on the wire. Multiple wire send events could + * indicate network retries. A lag between client or server send and wire send might indicate + * queuing or processing delay. + */ + public static final String WIRE_SEND = "ws"; + + /** + * Optionally logs an attempt to receive a message from the wire. Multiple wire receive events + * could indicate network retries. A lag between wire receive and client or server receive might + * indicate queuing or processing delay. + */ + public static final String WIRE_RECV = "wr"; + + /** + * Optionally logs progress of a ({@linkplain #CLIENT_SEND}, {@linkplain #WIRE_SEND}). For + * example, this could be one chunk in a chunked request. + */ + public static final String CLIENT_SEND_FRAGMENT = "csf"; + + /** + * Optionally logs progress of a ({@linkplain #CLIENT_RECV}, {@linkplain #WIRE_RECV}). For + * example, this could be one chunk in a chunked response. + */ + public static final String CLIENT_RECV_FRAGMENT = "crf"; + + /** + * Optionally logs progress of a ({@linkplain #SERVER_SEND}, {@linkplain #WIRE_SEND}). For + * example, this could be one chunk in a chunked response. + */ + public static final String SERVER_SEND_FRAGMENT = "ssf"; + + /** + * Optionally logs progress of a ({@linkplain #SERVER_RECV}, {@linkplain #WIRE_RECV}). For + * example, this could be one chunk in a chunked request. + */ + public static final String SERVER_RECV_FRAGMENT = "srf"; + + /** + * The {@link BinaryAnnotation#value value} of "lc" is the component or namespace of a local + * span. + * + *

{@link BinaryAnnotation#endpoint} adds service context needed to support queries. + * + *

Local Component("lc") supports three key features: flagging, query by service and filtering + * Span.name by namespace. + * + *

While structurally the same, local spans are fundamentally different than RPC spans in how + * they should be interpreted. For example, zipkin v1 tools center on RPC latency and service + * graphs. Root local-spans are neither indicative of critical path RPC latency, nor have impact + * on the shape of a service graph. By flagging with "lc", tools can special-case local spans. + * + *

Zipkin v1 Spans are unqueryable unless they can be indexed by service name. The only path + * to a {@link Endpoint#serviceName service name} is via {@link BinaryAnnotation#endpoint + * host}. By logging "lc", a local span can be queried even if no other annotations are logged. + * + *

The value of "lc" is the namespace of {@link Span#name}. For example, it might be + * "finatra2", for a span named "bootstrap". "lc" allows you to resolves conflicts for the same + * Span.name, for example "finatra/bootstrap" vs "finch/bootstrap". Using local component, you'd + * search for spans named "bootstrap" where "lc=finch" + */ + public static final String LOCAL_COMPONENT = "lc"; + + /** + * When present, {@link BinaryAnnotation#endpoint} indicates a client address ("ca") in a span. + * Most likely, there's only one. Multiple addresses are possible when a client changes its ip or + * port within a span. */ public static final String CLIENT_ADDR = "ca"; /** - * The endpoint associated with "SERVER_" annotations is not necessarily {@link - * Annotation#endpoint} + * When present, {@link BinaryAnnotation#endpoint} indicates a server address ("sa") in a span. + * Most likely, there's only one. Multiple addresses are possible when a client is redirected, or + * fails to a different server ip or port. */ public static final String SERVER_ADDR = "sa"; diff --git a/zipkin-java-core/src/main/java/io/zipkin/Endpoint.java b/zipkin-java-core/src/main/java/io/zipkin/Endpoint.java index 133a3998869..4de6605968e 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/Endpoint.java +++ b/zipkin-java-core/src/main/java/io/zipkin/Endpoint.java @@ -20,7 +20,7 @@ import static io.zipkin.internal.Util.checkNotNull; -/** Indicates the network context of a service recording an annotation. */ +/** Indicates the network context of a service involved in a span. */ public final class Endpoint { public static Endpoint create(String serviceName, int ipv4, int port) { @@ -32,9 +32,16 @@ public static Endpoint create(String serviceName, int ipv4) { } /** - * Service name, such as "memcache" or "zipkin-web" + * Classifier of a source or destination in lowercase, such as "zipkin-web". * - *

Note: Some implementations set this to "Unknown" or "Unknown Service" + *

Conventionally, when the service name isn't known, service_name = "unknown". + * + *

This is the primary parameter for trace lookup, so should be intuitive as possible, for + * example, matching names in service discovery. + * + *

Particularly clients may not have a reliable service name at ingest. One approach is to set + * serviceName to "unknown" at ingest, and later assign a better label based on binary + * annotations, such as user agent. */ public final String serviceName; @@ -77,16 +84,19 @@ public Builder(Endpoint source) { this.port = source.port; } + /** @see Endpoint#serviceName */ public Builder serviceName(String serviceName) { this.serviceName = serviceName; return this; } + /** @see Endpoint#ipv4 */ public Builder ipv4(int ipv4) { this.ipv4 = ipv4; return this; } + /** @see Endpoint#port */ public Builder port(short port) { if (port != 0) { this.port = port; diff --git a/zipkin-java-core/src/main/java/io/zipkin/QueryRequest.java b/zipkin-java-core/src/main/java/io/zipkin/QueryRequest.java index ae638656e06..fb53cde274e 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/QueryRequest.java +++ b/zipkin-java-core/src/main/java/io/zipkin/QueryRequest.java @@ -21,9 +21,20 @@ import static io.zipkin.internal.Util.checkArgument; +/** + * Invoking this request retrieves traces matching the below filters. + * + *

Results should be filtered against {@link #endTs}, subject to {@link #limit} and {@link + * #lookback}. For example, if endTs is 10:20 today, limit is 10, and lookback is 7 days, traces + * returned should be those nearest to 10:20 today, not 10:20 a week ago. + * + *

Time units of {@link #endTs} and {@link #lookback} are milliseconds as opposed to + * microseconds, the grain of {@link Span#timestamp}. Milliseconds is a more familiar and supported + * granularity for query, index and windowing functions. + */ public final class QueryRequest { - /** Only include traces whose annotation includes this {@link io.zipkin.Endpoint#serviceName} */ + /** Mandatory {@link io.zipkin.Endpoint#serviceName} and constrains. */ public final String serviceName; /** When present, only include traces with this {@link io.zipkin.Span#name} */ @@ -46,11 +57,31 @@ public final class QueryRequest { public final Map binaryAnnotations; /** - * Only return traces where all {@link io.zipkin.Span#endTs} are at or before this time in epoch - * microseconds. Defaults to current time. + * Only return traces whose {@link io.zipkin.Span#duration} is greater than or equal to + * minDuration microseconds. + */ + @Nullable + public final Long minDuration; + + /** + * Only return traces whose {@link io.zipkin.Span#duration} is less than or equal to maxDuration + * microseconds. Only valid with {@link #minDuration}. + */ + @Nullable + public final Long maxDuration; + + /** + * Only return traces where all {@link io.zipkin.Span#timestamp} are at or before this time in + * epoch milliseconds. Defaults to current time. */ public final long endTs; + /** + * Only return traces where all {@link io.zipkin.Span#timestamp} are at or after (endTs - + * lookback) in milliseconds. Defaults to endTs. + */ + public final long lookback; + /** Maximum number of traces to return. Defaults to 10 */ public final int limit; @@ -59,7 +90,10 @@ private QueryRequest( String spanName, List annotations, Map binaryAnnotations, + Long minDuration, + Long maxDuration, long endTs, + long lookback, int limit) { checkArgument(serviceName != null && !serviceName.isEmpty(), "serviceName was empty"); checkArgument(spanName == null || !spanName.isEmpty(), "spanName was empty"); @@ -76,7 +110,10 @@ private QueryRequest( checkArgument(!entry.getKey().isEmpty(), "binary annotation key was empty"); checkArgument(!entry.getValue().isEmpty(), "binary annotation value was empty"); } + this.minDuration = minDuration; + this.maxDuration = maxDuration; this.endTs = endTs; + this.lookback = lookback; this.limit = limit; } @@ -85,7 +122,10 @@ public static final class Builder { private String spanName; private List annotations = new LinkedList<>(); private Map binaryAnnotations = new LinkedHashMap<>(); + private Long minDuration; + private Long maxDuration; private Long endTs; + private Long lookback; private Integer limit; public Builder() { @@ -96,47 +136,78 @@ public Builder(QueryRequest source) { this.spanName = source.spanName; this.annotations = source.annotations; this.binaryAnnotations = source.binaryAnnotations; + this.minDuration = source.minDuration; + this.maxDuration = source.maxDuration; this.endTs = source.endTs; + this.lookback = source.lookback; this.limit = source.limit; } - public QueryRequest.Builder serviceName(String serviceName) { + /** @see QueryRequest#serviceName */ + public Builder serviceName(String serviceName) { this.serviceName = serviceName; return this; } - public QueryRequest.Builder spanName(@Nullable String spanName) { + /** @see QueryRequest#spanName */ + public Builder spanName(@Nullable String spanName) { this.spanName = spanName; return this; } - public QueryRequest.Builder addAnnotation(String annotation) { + /** @see QueryRequest#annotations */ + public Builder addAnnotation(String annotation) { this.annotations.add(annotation); return this; } - public QueryRequest.Builder addBinaryAnnotation(String key, String value) { + /** @see QueryRequest#binaryAnnotations */ + public Builder addBinaryAnnotation(String key, String value) { this.binaryAnnotations.put(key, value); return this; } - public QueryRequest.Builder endTs(Long endTs) { + /** @see QueryRequest#minDuration */ + public Builder minDuration(Long minDuration) { + this.minDuration = minDuration; + return this; + } + + /** @see QueryRequest#maxDuration */ + public Builder maxDuration(Long maxDuration) { + this.maxDuration = maxDuration; + return this; + } + + /** @see QueryRequest#endTs */ + public Builder endTs(Long endTs) { this.endTs = endTs; return this; } - public QueryRequest.Builder limit(Integer limit) { + /** @see QueryRequest#lookback */ + public Builder lookback(Long lookback) { + this.lookback = lookback; + return this; + } + + /** @see QueryRequest#limit */ + public Builder limit(Integer limit) { this.limit = limit; return this; } public QueryRequest build() { + long selectedEndTs = endTs == null ? System.currentTimeMillis() * 1000 : endTs; return new QueryRequest( serviceName, spanName, annotations, binaryAnnotations, - endTs == null ? System.currentTimeMillis() * 1000 : endTs, + minDuration, + maxDuration, + selectedEndTs, + Math.min(lookback == null ? selectedEndTs : lookback, selectedEndTs), limit == null ? 10 : limit); } } @@ -148,7 +219,10 @@ public String toString() { + "spanName=" + spanName + ", " + "annotations=" + annotations + ", " + "binaryAnnotations=" + binaryAnnotations + ", " + + "minDuration=" + minDuration + ", " + + "maxDuration=" + maxDuration + ", " + "endTs=" + endTs + ", " + + "lookback=" + lookback + ", " + "limit=" + limit + "}"; } @@ -164,7 +238,10 @@ public boolean equals(Object o) { && ((this.spanName == null) ? (that.spanName == null) : this.spanName.equals(that.spanName)) && ((this.annotations == null) ? (that.annotations == null) : this.annotations.equals(that.annotations)) && ((this.binaryAnnotations == null) ? (that.binaryAnnotations == null) : this.binaryAnnotations.equals(that.binaryAnnotations)) + && ((this.minDuration == null) ? (that.minDuration == null) : this.minDuration.equals(that.minDuration)) + && ((this.maxDuration == null) ? (that.maxDuration == null) : this.maxDuration.equals(that.maxDuration)) && (this.endTs == that.endTs) + && (this.lookback == that.lookback) && (this.limit == that.limit); } return false; @@ -182,8 +259,14 @@ public int hashCode() { h *= 1000003; h ^= (binaryAnnotations == null) ? 0 : binaryAnnotations.hashCode(); h *= 1000003; + h ^= (minDuration == null) ? 0 : minDuration.hashCode(); + h *= 1000003; + h ^= (maxDuration == null) ? 0 : maxDuration.hashCode(); + h *= 1000003; h ^= (endTs >>> 32) ^ endTs; h *= 1000003; + h ^= (lookback >>> 32) ^ lookback; + h *= 1000003; h ^= limit; return h; } diff --git a/zipkin-java-core/src/main/java/io/zipkin/Span.java b/zipkin-java-core/src/main/java/io/zipkin/Span.java index 5b6cef78beb..a320e86fd50 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/Span.java +++ b/zipkin-java-core/src/main/java/io/zipkin/Span.java @@ -20,41 +20,133 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.TreeSet; import static io.zipkin.internal.Util.checkNotNull; import static io.zipkin.internal.Util.equal; import static io.zipkin.internal.Util.sortedList; +/** + * A trace is a series of spans (often RPC calls) which form a latency tree. + * + *

Spans are usually created by instrumentation in RPC clients or servers, but can also + * represent in-process activity. Annotations in spans are similar to log statements, and are + * sometimes created directly by application developers to indicate events of interest, such as a + * cache miss. + * + *

The root span is where {@link #parentId} is null; it usually has the longest {@link #duration} in the + * trace. + * + *

Span identifiers are packed into longs, but should be treated opaquely. String encoding is + * fixed-width lower-hex, to avoid signed interpretation. + */ public final class Span implements Comparable { - + /** + * Unique 8-byte identifier for a trace, set on all spans within it. + */ public final long traceId; + /** + * Span name in lowercase, rpc method for example. + * + *

Conventionally, when the span name isn't known, name = "unknown". + */ public final String name; + /** + * Unique 8-byte identifier of this span within a trace. + * + *

A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #id}). + */ public final long id; + /** + * The parent's {@link #id} or null if this the root span in a trace. + */ @Nullable public final Long parentId; + /** + * Epoch microseconds of the start of this span, possibly absent if this an incomplete span. + * + *

This value should be set directly by instrumentation, using the most precise value + * possible. For example, {@code gettimeofday} or syncing {@link System#nanoTime} against a tick + * of {@link System#currentTimeMillis}. + * + *

For compatibilty with instrumentation that precede this field, collectors or span stores + * can derive this via Annotation.timestamp. For example, {@link Constants#SERVER_RECV}.timestamp + * or {@link Constants#CLIENT_SEND}.timestamp. + * + *

Timestamp is nullable for input only. Spans without a timestamp cannot be presented in a + * timeline: Span stores should not output spans missing a timestamp. + * + *

There are two known edge-cases where this could be absent: both cases exist when a + * collector receives a span in parts and a binary annotation precedes a timestamp. This is + * possible when.. + *

    + *
  • The span is in-flight (ex not yet received a timestamp)
  • + *
  • The span's start event was lost
  • + *
+ */ + @Nullable + public final Long timestamp; + + /** + * Measurement in microseconds of the critical path, if known. + * + *

This value should be set directly, as opposed to implicitly via annotation timestamps. + * Doing so encourages precision decoupled from problems of clocks, such as skew or NTP updates + * causing time to move backwards. + * + *

For compatibility with instrumentation that precede this field, collectors or span stores + * can derive this by subtracting {@link Annotation#timestamp}. For example, {@link + * Constants#SERVER_SEND}.timestamp - {@link Constants#SERVER_RECV}.timestamp. + * + *

If this field is persisted as unset, zipkin will continue to work, except duration query + * support will be implementation-specific. Similarly, setting this field non-atomically is + * implementation-specific. + * + *

This field is i64 vs i32 to support spans longer than 35 minutes. + */ + @Nullable + public final Long duration; + + /** + * Associates events that explain latency with a timestamp. + * + *

Unlike log statements, annotations are often codes: for example {@link + * Constants#SERVER_RECV}. Annotations are sorted ascending by timestamp. + */ public final List annotations; + /** + * Tags a span with context, usually to support query or aggregation. + * + *

example, a binary annotation key could be "http.uri". + */ public final List binaryAnnotations; + /** + * True is a request to store this span even if it overrides sampling policy. + */ @Nullable public final Boolean debug; - Span( - long traceId, - String name, - long id, - @Nullable Long parentId, - Collection annotations, - Collection binaryAnnotations, - @Nullable Boolean debug) { + private Span(long traceId, + String name, + long id, + @Nullable Long parentId, + @Nullable Long timestamp, + @Nullable Long duration, + Collection annotations, + Collection binaryAnnotations, + @Nullable Boolean debug) { this.traceId = traceId; this.name = checkNotNull(name, "name").toLowerCase(); this.id = id; this.parentId = parentId; + this.timestamp = timestamp; + this.duration = duration; this.annotations = sortedList(annotations); this.binaryAnnotations = Collections.unmodifiableList(new ArrayList<>(binaryAnnotations)); this.debug = debug; @@ -65,7 +157,9 @@ public static final class Builder { private String name; private Long id; private Long parentId; - private LinkedHashSet annotations = new LinkedHashSet<>(); + private Long timestamp; + private Long duration; + private TreeSet annotations = new TreeSet<>(); private LinkedHashSet binaryAnnotations = new LinkedHashSet<>(); private Boolean debug; @@ -77,6 +171,8 @@ public Builder(Span source) { this.name = source.name; this.id = source.id; this.parentId = source.parentId; + this.timestamp = source.timestamp; + this.duration = source.duration; this.annotations.addAll(source.annotations); this.binaryAnnotations.addAll(source.binaryAnnotations); this.debug = source.debug; @@ -86,7 +182,7 @@ public Builder merge(Span that) { if (this.traceId == null) { this.traceId = that.traceId; } - if (this.name == null) { + if (this.name == null || this.name.length() == 0 || this.name.equals("unknown")) { this.name = that.name; } if (this.id == null) { @@ -95,6 +191,22 @@ public Builder merge(Span that) { if (this.parentId == null) { this.parentId = that.parentId; } + + // Single timestamp makes duration easy: just choose max + if (this.timestamp == null || that.timestamp == null || this.timestamp.equals(that.timestamp)) { + this.timestamp = this.timestamp != null ? this.timestamp : that.timestamp; + if (this.duration == null) { + this.duration = that.duration; + } else if (that.duration != null) { + this.duration = Math.max(this.duration, that.duration); + } + } else { // duration might need to be recalculated, since we have 2 different timestamps + long thisEndTs = this.duration != null ? this.timestamp + this.duration : this.timestamp; + long thatEndTs = that.duration != null ? that.timestamp + that.duration : that.timestamp; + this.timestamp = Math.min(this.timestamp, that.timestamp); + this.duration = Math.max(thisEndTs, thatEndTs) - this.timestamp; + } + this.annotations.addAll(that.annotations); this.binaryAnnotations.addAll(that.binaryAnnotations); if (this.debug == null) { @@ -103,46 +215,91 @@ public Builder merge(Span that) { return this; } - public Span.Builder name(String name) { + /** @see Span#name */ + public Builder name(String name) { this.name = name; return this; } - public Span.Builder traceId(long traceId) { + /** @see Span#traceId */ + public Builder traceId(long traceId) { this.traceId = traceId; return this; } - - public Span.Builder id(long id) { + /** @see Span#id */ + public Builder id(long id) { this.id = id; return this; } - @Nullable - public Span.Builder parentId(Long parentId) { + /** @see Span#parentId */ + public Builder parentId(@Nullable Long parentId) { this.parentId = parentId; return this; } - public Span.Builder addAnnotation(Annotation annotation) { + /** @see Span#timestamp */ + public Builder timestamp(@Nullable Long timestamp) { + this.timestamp = timestamp; + return this; + } + + /** @see Span#duration */ + public Builder duration(@Nullable Long duration) { + this.duration = duration; + return this; + } + + /** + * Replaces currently collected annotations. + * + * @see Span#annotations + */ + public Builder annotations(Collection annotations) { + this.annotations.clear(); + this.annotations.addAll(annotations); + return this; + } + + /** @see Span#annotations */ + public Builder addAnnotation(Annotation annotation) { this.annotations.add(annotation); return this; } - public Span.Builder addBinaryAnnotation(BinaryAnnotation binaryAnnotation) { + /** @see Span#binaryAnnotations */ + public Builder addBinaryAnnotation(BinaryAnnotation binaryAnnotation) { this.binaryAnnotations.add(binaryAnnotation); return this; } - @Nullable - public Span.Builder debug(Boolean debug) { + /** @see Span#debug */ + public Builder debug(@Nullable Boolean debug) { this.debug = debug; return this; } + /** + *

Derived timestamp and duration

+ * + *

Instrumentation should log timestamp and duration, but since these fields are recent + * (Nov-2015), a lot of tracers will not. Accordingly, this will backfill timestamp and duration + * to if possible, based on interpretation of annotations. + */ public Span build() { - return new Span(this.traceId, this.name, this.id, this.parentId, this.annotations, this.binaryAnnotations, this.debug); + Long ts = timestamp; + Long dur = duration; + if ((timestamp == null || duration == null) && !annotations.isEmpty()) { + ts = ts != null ? ts : annotations.first().timestamp; + if (dur == null) { + long lastTs = annotations.last().timestamp; + if (ts.longValue() != lastTs) { + dur = lastTs - ts; + } + } + } + return new Span(this.traceId, this.name, this.id, this.parentId, ts, dur, this.annotations, this.binaryAnnotations, this.debug); } } @@ -162,6 +319,8 @@ public boolean equals(Object o) { && (this.name.equals(that.name)) && (this.id == that.id) && equal(this.parentId, that.parentId) + && equal(this.timestamp, that.timestamp) + && equal(this.duration, that.duration) && (this.annotations.equals(that.annotations)) && (this.binaryAnnotations.equals(that.binaryAnnotations)) && equal(this.debug, that.debug); @@ -181,6 +340,10 @@ public int hashCode() { h *= 1000003; h ^= (parentId == null) ? 0 : parentId.hashCode(); h *= 1000003; + h ^= (timestamp == null) ? 0 : timestamp.hashCode(); + h *= 1000003; + h ^= (duration == null) ? 0 : duration.hashCode(); + h *= 1000003; h ^= annotations.hashCode(); h *= 1000003; h ^= binaryAnnotations.hashCode(); @@ -189,15 +352,14 @@ public int hashCode() { return h; } + /** Compares by {@link #timestamp}, then {@link #name}. */ @Override public int compareTo(Span that) { - if (this == that) { - return 0; - } - return Long.compare(this.timestamp(), that.timestamp()); - } - - @Nullable public Long timestamp() { - return annotations.isEmpty() ? null : annotations.get(0).timestamp; + if (this == that) return 0; + int byTimestamp = Long.compare( + this.timestamp == null ? Long.MIN_VALUE : this.timestamp, + that.timestamp == null ? Long.MIN_VALUE : that.timestamp); + if (byTimestamp != 0) return byTimestamp; + return name.compareTo(that.name); } } diff --git a/zipkin-java-core/src/main/java/io/zipkin/SpanStore.java b/zipkin-java-core/src/main/java/io/zipkin/SpanStore.java index 0dcc4cc8b61..5abd58d7d24 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/SpanStore.java +++ b/zipkin-java-core/src/main/java/io/zipkin/SpanStore.java @@ -57,12 +57,21 @@ public interface SpanStore extends Closeable { List getSpanNames(String serviceName); /** - * @param startTs microseconds from epoch, defaults to one day before end_time - * @param endTs microseconds from epoch, defaults to now - * @return dependency links in an interval contained by startTs and endTs, or empty if none are - * found + * Returns dependency links derived from spans. + * + *

Implementations may bucket aggregated data, for example daily. When this is the case, endTs + * may be floored to align with that bucket, for example midnight if daily. lookback applies to + * the original endTs, even when bucketed. Using the daily example, if endTs was 11pm and lookback + * was 25 hours, the implementation would query against 2 buckets. + * + * @param endTs only return links from spans where {@link Span#timestamp} are at or before this + * time in epoch milliseconds. + * @param lookback only return links from spans where {@link Span#timestamp} are at or after + * (endTs - lookback) in milliseconds. Defaults to endTs. + * @return dependency links in an interval contained by (endTs - lookback) or empty if none are + * found */ - List getDependencies(@Nullable Long startTs, @Nullable Long endTs); + List getDependencies(long endTs, @Nullable Long lookback); @Override void close(); diff --git a/zipkin-java-core/src/main/java/io/zipkin/internal/CorrectForClockSkew.java b/zipkin-java-core/src/main/java/io/zipkin/internal/CorrectForClockSkew.java new file mode 100644 index 00000000000..fb2494b52b0 --- /dev/null +++ b/zipkin-java-core/src/main/java/io/zipkin/internal/CorrectForClockSkew.java @@ -0,0 +1,144 @@ +/** + * Copyright 2015 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 io.zipkin.internal; + +import io.zipkin.Annotation; +import io.zipkin.Constants; +import io.zipkin.Endpoint; +import io.zipkin.Span; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * Adjusts spans + */ +public enum CorrectForClockSkew implements Function, List> { + INSTANCE; + + static class ClockSkew { + final Endpoint endpoint; + final long skew; + + public ClockSkew(Endpoint endpoint, long skew) { + this.endpoint = endpoint; + this.skew = skew; + } + } + + public List apply(List spans) { + for (Span s : spans) { + if (s.parentId == null) { + SpanNode tree = SpanNode.create(s, spans); + adjust(tree, null); + return tree.toSpans(); + } + } + return spans; + } + + /** + * Recursively adjust the timestamps on the span tree. Root span is the reference point, all + * children's timestamps gets adjusted based on that span's timestamps. + */ + private void adjust(SpanNode node, @Nullable ClockSkew skewFromParent) { + // adjust skew for the endpoint brought over from the parent span + if (skewFromParent != null) { + node.span = adjustTimestamps(node.span, skewFromParent); + } + + // Is there any skew in the current span? + ClockSkew skew = getClockSkew(node.span); + if (skew != null) { + // the current span's skew may be a different endpoint than skewFromParent, adjust again. + node.span = adjustTimestamps(node.span, skew); + + // propagate skew to any children + for (SpanNode child : node.children) { + adjust(child, skew); + } + } + } + + /** If any annotation has an IP with skew associated, adjust accordingly. */ + private Span adjustTimestamps(Span span, ClockSkew clockSkew) { + List annotations = null; + for (int i = 0; i < span.annotations.size(); i++) { + Annotation a = span.annotations.get(i); + if (a.endpoint == null) continue; + if (clockSkew.endpoint.ipv4 == a.endpoint.ipv4) { + if (annotations == null) annotations = new ArrayList<>(span.annotations); + annotations.set(i, new Annotation.Builder(a).timestamp(a.timestamp - clockSkew.skew).build()); + } + } + if (annotations == null) return span; + // reset timestamp and duration as if there's skew, these will change. + long first = annotations.get(0).timestamp; + long last = annotations.get(annotations.size() - 1).timestamp; + long duration = last - first; + return new Span.Builder(span).timestamp(first).duration(duration).annotations(annotations).build(); + } + + /** Use client/server annotations to determine if there's clock skew. */ + @Nullable + private ClockSkew getClockSkew(Span span) { + Map annotations = asMap(span.annotations); + + Long clientSend = getTimestamp(annotations, Constants.CLIENT_SEND); + Long clientRecv = getTimestamp(annotations, Constants.CLIENT_RECV); + Long serverRecv = getTimestamp(annotations, Constants.SERVER_RECV); + Long serverSend = getTimestamp(annotations, Constants.SERVER_SEND); + + if (clientSend == null || clientRecv == null || serverRecv == null || serverSend == null) { + return null; + } + + Endpoint server = annotations.get(Constants.SERVER_RECV).endpoint; + server = server == null ? annotations.get(Constants.SERVER_SEND).endpoint : server; + if (server == null) return null; + + long clientDuration = clientRecv - clientSend; + long serverDuration = serverSend - serverRecv; + + // There is only clock skew if CS is after SR or CR is before SS + boolean csAhead = clientSend < serverRecv; + boolean crAhead = clientRecv > serverSend; + if (serverDuration > clientDuration || (csAhead && crAhead)) { + return null; + } + long latency = (clientDuration - serverDuration) / 2; + long skew = serverRecv - latency - clientSend; + if (skew != 0L) { + return new ClockSkew(server, skew); + } + return null; + } + + /** Get the annotations as a map with value to annotation bindings. */ + private static Map asMap(List annotations) { + Map result = new LinkedHashMap<>(annotations.size()); + for (Annotation a : annotations) { + result.put(a.value, a); + } + return result; + } + + @Nullable + private Long getTimestamp(Map annotations, String value) { + Annotation result = annotations.get(value); + return result != null ? result.timestamp : null; + } +} diff --git a/zipkin-java-core/src/main/java/io/zipkin/internal/JsonCodec.java b/zipkin-java-core/src/main/java/io/zipkin/internal/JsonCodec.java index 23dd605adfd..8a74f0de45e 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/internal/JsonCodec.java +++ b/zipkin-java-core/src/main/java/io/zipkin/internal/JsonCodec.java @@ -144,7 +144,7 @@ public BinaryAnnotation fromJson(JsonReader reader) throws IOException { switch (reader.peek()) { case BOOLEAN: type = BinaryAnnotation.Type.BOOL; - result.value(reader.nextBoolean() ? new byte[]{0} : new byte[]{1}); + result.value(reader.nextBoolean() ? new byte[]{1} : new byte[]{0}); break; case STRING: string = reader.nextString(); @@ -260,6 +260,12 @@ public Span fromJson(JsonReader reader) throws IOException { case "parentId": result.parentId(HEX_LONG_ADAPTER.fromJson(reader)); break; + case "timestamp": + result.timestamp(reader.nextLong()); + break; + case "duration": + result.duration(reader.nextLong()); + break; case "annotations": reader.beginArray(); while (reader.hasNext()) { @@ -297,6 +303,12 @@ public void toJson(JsonWriter writer, Span value) throws IOException { writer.name("parentId"); HEX_LONG_ADAPTER.toJson(writer, value.parentId); } + if (value.timestamp != null) { + writer.name("timestamp").value(value.timestamp); + } + if (value.duration != null) { + writer.name("duration").value(value.duration); + } writer.name("annotations"); writer.beginArray(); for (int i = 0, length = value.annotations.size(); i < length; i++) { diff --git a/zipkin-java-core/src/main/java/io/zipkin/internal/SpanNode.java b/zipkin-java-core/src/main/java/io/zipkin/internal/SpanNode.java new file mode 100644 index 00000000000..7f5ab3eb5ac --- /dev/null +++ b/zipkin-java-core/src/main/java/io/zipkin/internal/SpanNode.java @@ -0,0 +1,79 @@ +/** + * Copyright 2015 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 io.zipkin.internal; + +import io.zipkin.Span; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static io.zipkin.internal.Util.checkNotNull; + +final class SpanNode { + /** mutable to avoid allocating lists for no reason */ + Span span; + List children = Collections.emptyList(); + + private SpanNode(Span span) { + this.span = checkNotNull(span, "span"); + } + + void addChild(SpanNode node) { + if (children.equals(Collections.emptyList())) children = new LinkedList<>(); + children.add(node); + } + + static SpanNode create(Span span, List spans) { + SpanNode rootNode = new SpanNode(span); + + // Initialize nodes representing the trace tree + Map idToNode = new LinkedHashMap<>(); + for (Span s : spans) { + if (s.parentId == null) continue; // special-case root + idToNode.put(s.id, new SpanNode(s)); + } + + // Collect the parent-child relationships between all spans. + Map idToParent = new LinkedHashMap<>(); + for (Map.Entry entry : idToNode.entrySet()) { + idToParent.put(entry.getKey(), entry.getValue().span.parentId); + } + + // Materialize the tree using parent - child relationships + for (Map.Entry entry : idToParent.entrySet()) { + SpanNode node = idToNode.get(entry.getKey()); + SpanNode parent = idToNode.get(entry.getValue()); + if (parent == null) { // attribute missing parents to root + rootNode.addChild(node); + } else { + parent.addChild(node); + } + } + return rootNode; + } + + List toSpans() { + if (children.isEmpty()) { + return Collections.singletonList(span); + } + List result = new LinkedList<>(); + result.add(span); + for (SpanNode child : children) { + result.addAll(child.toSpans()); + } + return result; + } +} diff --git a/zipkin-java-core/src/main/java/io/zipkin/internal/ThriftCodec.java b/zipkin-java-core/src/main/java/io/zipkin/internal/ThriftCodec.java index 695131bc21c..428fc45d21e 100644 --- a/zipkin-java-core/src/main/java/io/zipkin/internal/ThriftCodec.java +++ b/zipkin-java-core/src/main/java/io/zipkin/internal/ThriftCodec.java @@ -319,6 +319,8 @@ enum SpanAdapter implements ThriftAdapter { private static final TField ANNOTATIONS_FIELD_DESC = new TField("annotations", TType.LIST, (short) 6); private static final TField BINARY_ANNOTATIONS_FIELD_DESC = new TField("binary_annotations", TType.LIST, (short) 8); private static final TField DEBUG_FIELD_DESC = new TField("debug", TType.BOOL, (short) 9); + private static final TField TIMESTAMP_FIELD_DESC = new TField("timestamp", TType.I64, (short) 10); + private static final TField DURATION_FIELD_DESC = new TField("duration", TType.I64, (short) 11); @Override public Span read(TProtocol iprot) throws TException { @@ -388,6 +390,20 @@ public Span read(TProtocol iprot) throws TException { skip(iprot, field.type); } break; + case 10: // TIMESTAMP + if (field.type == TType.I64) { + result.timestamp(iprot.readI64()); + } else { + skip(iprot, field.type); + } + break; + case 11: // DURATION + if (field.type == TType.I64) { + result.duration(iprot.readI64()); + } else { + skip(iprot, field.type); + } + break; default: skip(iprot, field.type); } @@ -441,6 +457,18 @@ public void write(Span value, TProtocol oprot) throws TException { oprot.writeFieldEnd(); } + if (value.timestamp != null) { + oprot.writeFieldBegin(TIMESTAMP_FIELD_DESC); + oprot.writeI64(value.timestamp); + oprot.writeFieldEnd(); + } + + if (value.duration != null) { + oprot.writeFieldBegin(DURATION_FIELD_DESC); + oprot.writeI64(value.duration); + oprot.writeFieldEnd(); + } + oprot.writeFieldStop(); oprot.writeStructEnd(); } diff --git a/zipkin-java-core/src/test/java/io/zipkin/SpanTest.java b/zipkin-java-core/src/test/java/io/zipkin/SpanTest.java new file mode 100644 index 00000000000..74fb843f43a --- /dev/null +++ b/zipkin-java-core/src/test/java/io/zipkin/SpanTest.java @@ -0,0 +1,71 @@ +/** + * Copyright 2015 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 io.zipkin; + +import java.util.Arrays; +import org.junit.Test; + +import static io.zipkin.TestObjects.WEB_ENDPOINT; +import static io.zipkin.assertj.ZipkinAssertions.assertThat; + +public class SpanTest { + + @Test + public void spanNamesLowercase() { + assertThat(new Span.Builder().traceId(1L).id(1L).name("GET").build()) + .hasName("get"); + } + + @Test + public void mergeWhenBinaryAnnotationsSentSeparately() { + Span part1 = new Span.Builder() + .traceId(1L) + .name("") + .id(1L) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, WEB_ENDPOINT)) + .build(); + + Span part2 = new Span.Builder() + .traceId(1L) + .name("get") + .id(1L) + .timestamp(1444438900939000L) + .duration(376000L) + .addAnnotation(Annotation.create(1444438900939000L, Constants.SERVER_RECV, WEB_ENDPOINT)) + .addAnnotation(Annotation.create(1444438901315000L, Constants.SERVER_SEND, WEB_ENDPOINT)) + .build(); + + Span expected = new Span.Builder(part2) + .addBinaryAnnotation(part1.binaryAnnotations.get(0)) + .build(); + + assertThat(new Span.Builder(part1).merge(part2).build()).isEqualTo(expected); + assertThat(new Span.Builder(part2).merge(part1).build()).isEqualTo(expected); + } + + /** + * Some instrumentation set name to "unknown" or empty. This ensures dummy span names lose on + * merge. + */ + @Test + public void mergeOverridesDummySpanNames() { + for (String nonName : Arrays.asList("", "unknown")) { + Span unknown = new Span.Builder().traceId(1).id(2).name(nonName).build(); + Span get = new Span.Builder(unknown).name("get").build(); + + assertThat(new Span.Builder(unknown).merge(get).build()).hasName("get"); + assertThat(new Span.Builder(get).merge(unknown).build()).hasName("get"); + } + } +} diff --git a/zipkin-java-core/src/test/java/io/zipkin/TestObjects.java b/zipkin-java-core/src/test/java/io/zipkin/TestObjects.java index ddf20b14b76..43134dc47b2 100644 --- a/zipkin-java-core/src/test/java/io/zipkin/TestObjects.java +++ b/zipkin-java-core/src/test/java/io/zipkin/TestObjects.java @@ -27,30 +27,40 @@ public final class TestObjects { public static final Endpoint JDBC_ENDPOINT = Endpoint.create("zipkin-jdbc", 172 << 24 | 17 << 16 | 2); public static final List TRACE = asList( - new Span.Builder() + new Span.Builder() // browser calls web .traceId(WEB_SPAN_ID) - .name("GET") + .name("get") .id(WEB_SPAN_ID) + .timestamp(1444438900939000L) + .duration(376000L) .addAnnotation(Annotation.create(1444438900939000L, Constants.SERVER_RECV, WEB_ENDPOINT)) .addAnnotation(Annotation.create(1444438901315000L, Constants.SERVER_SEND, WEB_ENDPOINT)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, WEB_ENDPOINT)) .build(), - new Span.Builder() + new Span.Builder() // web calls query .traceId(WEB_SPAN_ID) - .name("GET") + .name("get") .id(QUERY_SPAN_ID) .parentId(WEB_SPAN_ID) - .addAnnotation(Annotation.create(1444438900941000L, Constants.CLIENT_SEND, Endpoint.create("zipkin-query", 127 << 24 | 1))) + .timestamp(1444438900941000L) + .duration(77000L) + .addAnnotation(Annotation.create(1444438900941000L, Constants.CLIENT_SEND, WEB_ENDPOINT)) .addAnnotation(Annotation.create(1444438900947000L, Constants.SERVER_RECV, QUERY_ENDPOINT)) .addAnnotation(Annotation.create(1444438901017000L, Constants.SERVER_SEND, QUERY_ENDPOINT)) - .addAnnotation(Annotation.create(1444438901018000L, Constants.CLIENT_RECV, Endpoint.create("zipkin-query", 127 << 24 | 1))) + .addAnnotation(Annotation.create(1444438901018000L, Constants.CLIENT_RECV, WEB_ENDPOINT)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, QUERY_ENDPOINT)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, WEB_ENDPOINT)) .build(), - new Span.Builder() + new Span.Builder() // query calls jdbc .traceId(WEB_SPAN_ID) .name("query") .id(JDBC_SPAN_ID) .parentId(QUERY_SPAN_ID) - .addAnnotation(Annotation.create(1444438900948000L, Constants.CLIENT_SEND, JDBC_ENDPOINT)) - .addAnnotation(Annotation.create(1444438900979000L, Constants.CLIENT_RECV, JDBC_ENDPOINT)) + .timestamp(1444438900948000L) + .duration(31000L) + .addAnnotation(Annotation.create(1444438900948000L, Constants.CLIENT_SEND, QUERY_ENDPOINT)) + .addAnnotation(Annotation.create(1444438900979000L, Constants.CLIENT_RECV, QUERY_ENDPOINT)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, JDBC_ENDPOINT)) .build() ); diff --git a/zipkin-java-core/src/test/java/io/zipkin/assertj/SpanAssert.java b/zipkin-java-core/src/test/java/io/zipkin/assertj/SpanAssert.java new file mode 100644 index 00000000000..5610a65dd19 --- /dev/null +++ b/zipkin-java-core/src/test/java/io/zipkin/assertj/SpanAssert.java @@ -0,0 +1,70 @@ +/** + * Copyright 2015 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 io.zipkin.assertj; + +import io.zipkin.BinaryAnnotation; +import io.zipkin.Span; +import io.zipkin.internal.Util; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.internal.ByteArrays; +import org.assertj.core.internal.Objects; + +public final class SpanAssert extends AbstractAssert { + + ByteArrays arrays = ByteArrays.instance(); + Objects objects = Objects.instance(); + + public SpanAssert(Span actual) { + super(actual, SpanAssert.class); + } + + public SpanAssert hasName(String expected) { + isNotNull(); + objects.assertEqual(info, actual.name, expected); + return this; + } + + public SpanAssert hasTimestamp(long expected) { + isNotNull(); + objects.assertEqual(info, actual.timestamp, expected); + return this; + } + + public SpanAssert hasDuration(long expected) { + isNotNull(); + objects.assertEqual(info, actual.duration, expected); + return this; + } + + public SpanAssert hasBinaryAnnotation(String key, String utf8Expected) { + isNotNull(); + return hasBinaryAnnotation(key, utf8Expected.getBytes(Util.UTF_8)); + } + + public SpanAssert hasBinaryAnnotation(final String key, byte[] expected) { + isNotNull(); + List keys = new ArrayList<>(); + for (BinaryAnnotation b : actual.binaryAnnotations) { + if (b.key.equals(key)) { + arrays.assertContains(info, b.value, expected); + return this; + } + keys.add(b.key); + } + failWithMessage("\nExpecting binaryAnnotation keys to contain %s, was: <%s>", key, keys); + return this; + } +} diff --git a/zipkin-java-core/src/test/java/io/zipkin/assertj/ZipkinAssertions.java b/zipkin-java-core/src/test/java/io/zipkin/assertj/ZipkinAssertions.java new file mode 100644 index 00000000000..6d6e8da17de --- /dev/null +++ b/zipkin-java-core/src/test/java/io/zipkin/assertj/ZipkinAssertions.java @@ -0,0 +1,25 @@ +/** + * Copyright 2015 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 io.zipkin.assertj; + +import org.assertj.core.api.Assertions; + +import io.zipkin.Span; + +public class ZipkinAssertions extends Assertions { + + public static SpanAssert assertThat(Span actual) { + return new SpanAssert(actual); + } +} diff --git a/zipkin-java-interop/pom.xml b/zipkin-java-interop/pom.xml index 77caccd9752..fc8f055e40a 100644 --- a/zipkin-java-interop/pom.xml +++ b/zipkin-java-interop/pom.xml @@ -31,7 +31,7 @@ ${project.basedir}/.. - 1.21.1 + 1.25.0 2.2.5 diff --git a/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaDependencyStoreAdapter.java b/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaDependencyStoreAdapter.java index 98dbf861244..43d309e97b7 100644 --- a/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaDependencyStoreAdapter.java +++ b/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaDependencyStoreAdapter.java @@ -40,10 +40,14 @@ public ScalaDependencyStoreAdapter(SpanStore spanStore) { } @Override - public Future> getDependencies(Option startTs, Option endTs) { - List input = spanStore.getDependencies( - startTs.isDefined() ? (Long) startTs.get() : null, - endTs.isDefined() ? (Long) endTs.get() : null + public Option getDependencies$default$2() { + return Option.empty(); + } + + @Override + public Future> getDependencies(long endTs, Option lookback) { + List input = spanStore.getDependencies(endTs, + lookback.isDefined() ? (Long) lookback.get() : null ); List links = new ArrayList<>(input.size()); for (io.zipkin.DependencyLink link : input) { @@ -55,11 +59,6 @@ public Future> getDependencies(Option startTs, Optio return Future.value(JavaConversions.asScalaBuffer(links).seq()); } - @Override - public Option getDependencies$default$2() { - return Option.empty(); - } - @Override public Future storeDependencies(Dependencies dependencies) { return Future.Unit(); diff --git a/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaSpanStoreAdapter.java b/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaSpanStoreAdapter.java index 0a2b7b477b9..3209812e914 100644 --- a/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaSpanStoreAdapter.java +++ b/zipkin-java-interop/src/main/java/io/zipkin/interop/ScalaSpanStoreAdapter.java @@ -54,7 +54,10 @@ public Future>> getTraces(QueryRequest input) { io.zipkin.QueryRequest.Builder request = new io.zipkin.QueryRequest.Builder() .serviceName(input.serviceName()) .spanName(input.spanName().isDefined() ? input.spanName().get() : null) + .minDuration(input.minDuration().isDefined() ? (Long) input.minDuration().get() : null) + .maxDuration(input.maxDuration().isDefined() ? (Long) input.maxDuration().get() : null) .endTs(input.endTs()) + .lookback(input.lookback()) .limit(input.limit()); for (Iterator i = input.annotations().iterator(); i.hasNext(); ) { diff --git a/zipkin-java-interop/src/test/java/io/zipkin/SpanStoreTest.java b/zipkin-java-interop/src/test/java/io/zipkin/SpanStoreTest.java new file mode 100644 index 00000000000..b90cb8549b2 --- /dev/null +++ b/zipkin-java-interop/src/test/java/io/zipkin/SpanStoreTest.java @@ -0,0 +1,179 @@ +/** + * Copyright 2015 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 io.zipkin; + +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Base test for {@link SpanStore} implementations. Subtypes should create a connection to a real + * backend, even if that backend is in-process. + * + *

Incrementally, this will replace {@code com.twitter.zipkin.storage.SpanStoreSpec}. + */ +public abstract class SpanStoreTest { + + /** Should maintain state between multiple calls within a test. */ + protected final T store; + + protected SpanStoreTest(T store) { + this.store = store; + } + + /** Clears the span store between tests. */ + @Before + public abstract void clear(); + + Endpoint ep = Endpoint.create("service", 127 << 24 | 1, 8080); + + long spanId = 456; + Annotation ann1 = Annotation.create(1000L, "cs", ep); + Annotation ann2 = Annotation.create(2000L, "sr", null); + Annotation ann3 = Annotation.create(10000L, "custom", ep); + Annotation ann4 = Annotation.create(20000L, "custom", ep); + Annotation ann5 = Annotation.create(5000L, "custom", ep); + Annotation ann6 = Annotation.create(6000L, "custom", ep); + Annotation ann7 = Annotation.create(7000L, "custom", ep); + Annotation ann8 = Annotation.create(8000L, "custom", ep); + + Span span1 = new Span.Builder() + .traceId(123) + .name("methodcall") + .id(spanId) + .addAnnotation(ann1) + .addAnnotation(ann3) + .addBinaryAnnotation(BinaryAnnotation.create("BAH", "BEH", ep)).build(); + + Span span2 = new Span.Builder() + .traceId(456) + .name("methodcall") + .id(spanId) + .timestamp(2L) + .addAnnotation(ann2) + .addBinaryAnnotation(BinaryAnnotation.create("BAH2", "BEH2", ep)).build(); + + Span span3 = new Span.Builder() + .traceId(789) + .name("methodcall") + .id(spanId) + .addAnnotation(ann2) + .addAnnotation(ann3) + .addAnnotation(ann4) + .addBinaryAnnotation(BinaryAnnotation.create("BAH2", "BEH2", ep)).build(); + + Span span4 = new Span.Builder() + .traceId(999) + .name("methodcall") + .id(spanId) + .addAnnotation(ann6) + .addAnnotation(ann7).build(); + + Span span5 = new Span.Builder() + .traceId(999) + .name("methodcall") + .id(spanId) + .addAnnotation(ann5) + .addAnnotation(ann8) + .addBinaryAnnotation(BinaryAnnotation.create("BAH2", "BEH2", ep)).build(); + + Span spanEmptySpanName = new Span.Builder() + .traceId(123) + .name("") + .id(spanId) + .parentId(1L) + .addAnnotation(ann1) + .addAnnotation(ann2).build(); + + Span spanEmptyServiceName = new Span.Builder() + .traceId(123) + .name("spanname") + .id(spanId).build(); + + Span mergedSpan = new Span.Builder() + .traceId(123) + .name("methodcall") + .id(spanId) + .addAnnotation(ann1) + .addAnnotation(ann2) + .addBinaryAnnotation(BinaryAnnotation.create("BAH2", "BEH2", ep)).build(); + + /** + * Basic clock skew correction is something span stores should support, until the UI supports + * happens-before without using timestamps. The easiest clock skew to correct is where a child + * appears to happen before the parent. + * + *

It doesn't matter if clock-skew correction happens at storage or query time, as long as it + * occurs by the time results are returned. + * + *

Span stores who don't support this can override and disable this test, noting in the README + * the limitation. + */ + @Test + public void correctsClockSkew() { + Endpoint client = Endpoint.create("client", 192 << 24 | 168 << 16 | 1, 8080); + Endpoint frontend = Endpoint.create("frontend", 192 << 24 | 168 << 16 | 2, 8080); + Endpoint backend = Endpoint.create("backend", 192 << 24 | 168 << 16 | 3, 8080); + + Span parent = new Span.Builder() + .traceId(1) + .name("method1") + .id(666) + .addAnnotation(Annotation.create(100000, Constants.CLIENT_SEND, client)) + .addAnnotation(Annotation.create(95000, Constants.SERVER_RECV, frontend)) // before client sends + .addAnnotation(Annotation.create(120000, Constants.SERVER_SEND, frontend)) // before client receives + .addAnnotation(Annotation.create(135000, Constants.CLIENT_RECV, client)).build(); + + Span child = new Span.Builder() + .traceId(1) + .name("method2") + .id(777) + .parentId(666L) + .addAnnotation(Annotation.create(100000, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(115000, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(120000, Constants.SERVER_SEND, backend)) + .addAnnotation(Annotation.create(115000, Constants.CLIENT_RECV, frontend)) // before server sent + .build(); + + List skewed = asList(parent, child); + + // There's clock skew when the child doesn't happen after the parent + assertThat(skewed.get(0).timestamp) + .isLessThanOrEqualTo(skewed.get(1).timestamp); + + // Regardless of when clock skew is corrected, it should be corrected before traces return + store.accept(asList(parent, child)); + List adjusted = store.getTracesByIds(asList(1L)).get(0); + + // After correction, the child happens after the parent + assertThat(adjusted.get(0).timestamp) + .isLessThanOrEqualTo(adjusted.get(1).timestamp); + + // .. because the child is shifted to a later date + assertThat(adjusted.get(1).timestamp) + .isGreaterThan(skewed.get(1).timestamp); + + // Since we've shifted the child to a later timestamp, the total duration appears shorter + assertThat(adjusted.get(0).duration) + .isLessThan(skewed.get(0).duration); + + // .. but that change in duration should be accounted for + long shift = adjusted.get(0).timestamp - skewed.get(0).timestamp; + assertThat(adjusted.get(0).duration) + .isEqualTo(skewed.get(0).duration - shift); + } +} diff --git a/zipkin-java-interop/src/test/java/io/zipkin/jdbc/JDBCSpanStoreTest.java b/zipkin-java-interop/src/test/java/io/zipkin/jdbc/JDBCSpanStoreTest.java new file mode 100644 index 00000000000..b22fa11c679 --- /dev/null +++ b/zipkin-java-interop/src/test/java/io/zipkin/jdbc/JDBCSpanStoreTest.java @@ -0,0 +1,32 @@ +/** + * Copyright 2015 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 io.zipkin.jdbc; + +import io.zipkin.SpanStoreTest; +import java.sql.SQLException; + +public class JDBCSpanStoreTest extends SpanStoreTest { + + public JDBCSpanStoreTest() throws SQLException { + super(new JDBCTestGraph().spanStore); + } + + public void clear() { + try { + store.clear(); + } catch (SQLException e) { + throw new AssertionError(e); + } + } +} diff --git a/zipkin-java-interop/src/test/java/io/zipkin/server/InMemoryScalaSpanStoreTest.java b/zipkin-java-interop/src/test/java/io/zipkin/server/InMemoryScalaSpanStoreTest.java index cbd763527d3..3e144099996 100644 --- a/zipkin-java-interop/src/test/java/io/zipkin/server/InMemoryScalaSpanStoreTest.java +++ b/zipkin-java-interop/src/test/java/io/zipkin/server/InMemoryScalaSpanStoreTest.java @@ -16,7 +16,6 @@ import com.twitter.zipkin.storage.SpanStore; import com.twitter.zipkin.storage.SpanStoreSpec; import io.zipkin.interop.ScalaSpanStoreAdapter; -import io.zipkin.server.InMemorySpanStore; public class InMemoryScalaSpanStoreTest extends SpanStoreSpec { private InMemorySpanStore mem = new InMemorySpanStore(); diff --git a/zipkin-java-interop/src/test/java/io/zipkin/server/InMemorySpanStoreTest.java b/zipkin-java-interop/src/test/java/io/zipkin/server/InMemorySpanStoreTest.java new file mode 100644 index 00000000000..4ebd601b47b --- /dev/null +++ b/zipkin-java-interop/src/test/java/io/zipkin/server/InMemorySpanStoreTest.java @@ -0,0 +1,26 @@ +/** + * Copyright 2015 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 io.zipkin.server; + +import io.zipkin.SpanStoreTest; + +public class InMemorySpanStoreTest extends SpanStoreTest { + public InMemorySpanStoreTest() { + super(new InMemorySpanStore()); + } + + public void clear() { + store.clear(); + } +} diff --git a/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/JDBCSpanStore.java b/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/JDBCSpanStore.java index 23e3d996e50..1e36e54c2f6 100644 --- a/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/JDBCSpanStore.java +++ b/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/JDBCSpanStore.java @@ -21,6 +21,7 @@ import io.zipkin.QueryRequest; import io.zipkin.Span; import io.zipkin.SpanStore; +import io.zipkin.internal.CorrectForClockSkew; import io.zipkin.internal.Nullable; import io.zipkin.internal.Pair; import io.zipkin.internal.Util; @@ -29,6 +30,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -46,6 +48,7 @@ import org.jooq.SelectConditionStep; import org.jooq.SelectOffsetStep; import org.jooq.Table; +import org.jooq.TableField; import org.jooq.conf.Settings; import org.jooq.impl.DSL; import org.jooq.impl.DefaultConfiguration; @@ -56,8 +59,6 @@ import static io.zipkin.jdbc.internal.generated.tables.ZipkinAnnotations.ZIPKIN_ANNOTATIONS; import static io.zipkin.jdbc.internal.generated.tables.ZipkinSpans.ZIPKIN_SPANS; import static java.util.Collections.emptyList; -import static java.util.concurrent.TimeUnit.DAYS; -import static java.util.concurrent.TimeUnit.MICROSECONDS; import static java.util.logging.Level.FINEST; import static java.util.stream.Collectors.groupingBy; @@ -95,20 +96,34 @@ public void accept(List spans) { List inserts = new ArrayList<>(); for (Span span : spans) { - Long timestamp = span.timestamp(); - if (timestamp == null) { // possible if only contains binary annotations - timestamp = System.currentTimeMillis() * 1000; + Long binaryAnnotationTimestamp = span.timestamp; + if (binaryAnnotationTimestamp == null) { // fallback if we have no timestamp, yet + binaryAnnotationTimestamp = System.currentTimeMillis() * 1000; } - inserts.add(create.insertInto(ZIPKIN_SPANS) - .set(ZIPKIN_SPANS.TRACE_ID, span.traceId) - .set(ZIPKIN_SPANS.ID, span.id) - .set(ZIPKIN_SPANS.NAME, span.name) - .set(ZIPKIN_SPANS.PARENT_ID, span.parentId) - .set(ZIPKIN_SPANS.DEBUG, span.debug) - .set(ZIPKIN_SPANS.START_TS, timestamp) - .onDuplicateKeyIgnore() - ); + Map, Object> updateFields = new LinkedHashMap<>(); + if (!span.name.equals("") && !span.name.equals("unknown")) { + updateFields.put(ZIPKIN_SPANS.NAME, span.name); + } + if (span.timestamp != null) { + updateFields.put(ZIPKIN_SPANS.START_TS, span.timestamp); + } + if (span.duration != null) { + updateFields.put(ZIPKIN_SPANS.DURATION, span.duration); + } + + InsertSetMoreStep insertSpan = create.insertInto(ZIPKIN_SPANS) + .set(ZIPKIN_SPANS.TRACE_ID, span.traceId) + .set(ZIPKIN_SPANS.ID, span.id) + .set(ZIPKIN_SPANS.PARENT_ID, span.parentId) + .set(ZIPKIN_SPANS.NAME, span.name) + .set(ZIPKIN_SPANS.DEBUG, span.debug) + .set(ZIPKIN_SPANS.START_TS, span.timestamp) + .set(ZIPKIN_SPANS.DURATION, span.duration); + + inserts.add(updateFields.isEmpty() ? + insertSpan.onDuplicateKeyIgnore() : + insertSpan.onDuplicateKeyUpdate().set(updateFields)); for (Annotation annotation : span.annotations) { InsertSetMoreStep insert = create.insertInto(ZIPKIN_ANNOTATIONS) @@ -132,7 +147,7 @@ public void accept(List spans) { .set(ZIPKIN_ANNOTATIONS.A_KEY, annotation.key) .set(ZIPKIN_ANNOTATIONS.A_VALUE, annotation.value) .set(ZIPKIN_ANNOTATIONS.A_TYPE, annotation.type.value) - .set(ZIPKIN_ANNOTATIONS.A_TIMESTAMP, timestamp); + .set(ZIPKIN_ANNOTATIONS.A_TIMESTAMP, binaryAnnotationTimestamp); if (annotation.endpoint != null) { insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME, annotation.endpoint.serviceName); insert.set(ZIPKIN_ANNOTATIONS.ENDPOINT_IPV4, annotation.endpoint.ipv4); @@ -156,13 +171,14 @@ private List> getTraces(@Nullable QueryRequest request, @Nullable Lis } spansWithoutAnnotations = context(conn) .selectFrom(ZIPKIN_SPANS).where(ZIPKIN_SPANS.TRACE_ID.in(traceIds)) - .orderBy(ZIPKIN_SPANS.START_TS.asc()) .stream() .map(r -> new Span.Builder() .traceId(r.getValue(ZIPKIN_SPANS.TRACE_ID)) .name(r.getValue(ZIPKIN_SPANS.NAME)) .id(r.getValue(ZIPKIN_SPANS.ID)) .parentId(r.getValue(ZIPKIN_SPANS.PARENT_ID)) + .timestamp(r.getValue(ZIPKIN_SPANS.START_TS)) + .duration(r.getValue(ZIPKIN_SPANS.DURATION)) .debug(r.getValue(ZIPKIN_SPANS.DEBUG)) .build()) .collect(groupingBy((Span s) -> s.traceId, LinkedHashMap::new, Collectors.toList())); @@ -207,8 +223,9 @@ private List> getTraces(@Nullable QueryRequest request, @Nullable Lis } trace.add(span.build()); } - result.add(Util.merge(trace)); + result.add(CorrectForClockSkew.INSTANCE.apply(Util.merge(trace))); } + Collections.sort(result, (left, right) -> right.get(0).compareTo(left.get(0))); return result; } @@ -263,18 +280,15 @@ public List getSpanNames(String serviceName) { } @Override - public List getDependencies(@Nullable Long startTs, @Nullable Long endTs) { - if (endTs == null) { - endTs = System.currentTimeMillis() * 1000; - } - if (startTs == null) { - startTs = endTs - MICROSECONDS.convert(1, DAYS); - } + public List getDependencies(long endTs, @Nullable Long lookback) { + endTs = endTs * 1000; try (Connection conn = this.datasource.getConnection()) { Map>> parentChild = context(conn) .select(ZIPKIN_SPANS.TRACE_ID, ZIPKIN_SPANS.PARENT_ID, ZIPKIN_SPANS.ID) .from(ZIPKIN_SPANS) - .where(ZIPKIN_SPANS.START_TS.between(startTs, endTs)) + .where(lookback == null ? + ZIPKIN_SPANS.START_TS.lessOrEqual(endTs) : + ZIPKIN_SPANS.START_TS.between(endTs - lookback * 1000, endTs)) .and(ZIPKIN_SPANS.PARENT_ID.isNotNull()) .stream().collect(Collectors.groupingBy(r -> r.value1())); @@ -304,7 +318,7 @@ public List getDependencies(@Nullable Long startTs, @Nullable Lo } return result; } catch (SQLException e) { - throw new RuntimeException("Error querying dependencies between " + startTs + " and " + endTs + ": " + e.getMessage()); + throw new RuntimeException("Error querying dependencies for endTs " + endTs + " and lookback " + lookback + ": " + e.getMessage()); } } @@ -335,17 +349,12 @@ static Endpoint endpoint(Record a) { } static SelectOffsetStep> toTraceIdQuery(DSLContext context, QueryRequest request) { - long endTs = (request.endTs > 0 && request.endTs != Long.MAX_VALUE) ? request.endTs - : System.currentTimeMillis() / 1000; + long endTs = (request.endTs > 0 && request.endTs != Long.MAX_VALUE) ? request.endTs * 1000 + : System.currentTimeMillis() * 1000; - Table> a1 = context.selectDistinct(ZIPKIN_ANNOTATIONS.TRACE_ID) - .from(ZIPKIN_ANNOTATIONS) - .where(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.eq(request.serviceName)) - .and(ZIPKIN_ANNOTATIONS.A_TYPE.eq(-1)) - .groupBy(ZIPKIN_ANNOTATIONS.TRACE_ID).asTable(); - - Table table = ZIPKIN_SPANS.join(a1) - .on(ZIPKIN_SPANS.TRACE_ID.eq(a1.field(ZIPKIN_ANNOTATIONS.TRACE_ID))); + Table table = ZIPKIN_SPANS.join(ZIPKIN_ANNOTATIONS) + .on(ZIPKIN_SPANS.TRACE_ID.eq(ZIPKIN_ANNOTATIONS.TRACE_ID).and( + ZIPKIN_SPANS.ID.eq(ZIPKIN_ANNOTATIONS.SPAN_ID))); Map keyToTables = new LinkedHashMap<>(); int i = 0; @@ -361,16 +370,23 @@ static SelectOffsetStep> toTraceIdQuery(DSLContext context, QueryR SelectConditionStep> dsl = context.selectDistinct(ZIPKIN_SPANS.TRACE_ID) .from(table) - .where(ZIPKIN_SPANS.START_TS.le(endTs)); + .where(ZIPKIN_ANNOTATIONS.ENDPOINT_SERVICE_NAME.eq(request.serviceName)) + .and(ZIPKIN_SPANS.START_TS.between(endTs - request.lookback * 1000, endTs)); if (request.spanName != null) { dsl.and(ZIPKIN_SPANS.NAME.eq(request.spanName)); } + if (request.minDuration != null && request.maxDuration != null) { + dsl.and(ZIPKIN_SPANS.DURATION.between(request.minDuration, request.maxDuration)); + } else if (request.minDuration != null){ + dsl.and(ZIPKIN_SPANS.DURATION.greaterOrEqual(request.minDuration)); + } + for (Map.Entry entry : request.binaryAnnotations.entrySet()) { dsl.and(keyToTables.get(entry.getKey()).A_VALUE.eq(entry.getValue().getBytes(UTF_8))); } - return dsl.limit(request.limit); + return dsl.orderBy(ZIPKIN_SPANS.START_TS.desc()).limit(request.limit); } private static Table join(Table table, ZipkinAnnotations joinTable, String key, int type) { diff --git a/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/internal/generated/tables/ZipkinSpans.java b/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/internal/generated/tables/ZipkinSpans.java index a83e8d0994e..c950bc83272 100644 --- a/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/internal/generated/tables/ZipkinSpans.java +++ b/zipkin-java-jdbc/src/main/java/io/zipkin/jdbc/internal/generated/tables/ZipkinSpans.java @@ -41,7 +41,7 @@ @SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class ZipkinSpans extends TableImpl { - private static final long serialVersionUID = 107308884; + private static final long serialVersionUID = 851868507; /** * The reference instance of zipkin.zipkin_spans @@ -82,9 +82,14 @@ public Class getRecordType() { public final TableField DEBUG = createField("debug", org.jooq.impl.SQLDataType.BIT, this, ""); /** - * The column zipkin.zipkin_spans.start_ts. Used to implement TTL; First Annotation.timestamp() or null + * The column zipkin.zipkin_spans.start_ts. Span.timestamp(): epoch micros used for endTs query and to implement TTL */ - public final TableField START_TS = createField("start_ts", org.jooq.impl.SQLDataType.BIGINT, this, "Used to implement TTL; First Annotation.timestamp() or null"); + public final TableField START_TS = createField("start_ts", org.jooq.impl.SQLDataType.BIGINT, this, "Span.timestamp(): epoch micros used for endTs query and to implement TTL"); + + /** + * The column zipkin.zipkin_spans.duration. Span.duration(): micros used for minDuration and maxDuration query + */ + public final TableField DURATION = createField("duration", org.jooq.impl.SQLDataType.BIGINT, this, "Span.duration(): micros used for minDuration and maxDuration query"); /** * Create a zipkin.zipkin_spans table reference diff --git a/zipkin-java-jdbc/src/main/resources/mysql.sql b/zipkin-java-jdbc/src/main/resources/mysql.sql index 57c2c4cb51b..7bdfe123019 100644 --- a/zipkin-java-jdbc/src/main/resources/mysql.sql +++ b/zipkin-java-jdbc/src/main/resources/mysql.sql @@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS zipkin_spans ( `name` VARCHAR(255) NOT NULL, `parent_id` BIGINT, `debug` BIT(1), - `start_ts` BIGINT COMMENT 'Used to implement TTL; First Annotation.timestamp() or null' + `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', + `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED; ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id`, `id`) COMMENT 'ignore insert on duplicate'; @@ -32,4 +33,3 @@ ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for g ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces'; ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces'; ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_ipv4`) COMMENT 'for getTraces ordering'; - diff --git a/zipkin-java-server/Dockerfile b/zipkin-java-server/Dockerfile index 533a3b3247f..2bc86f31817 100644 --- a/zipkin-java-server/Dockerfile +++ b/zipkin-java-server/Dockerfile @@ -12,7 +12,7 @@ # the License. # -FROM openzipkin/zipkin-base:base-1.21.1 +FROM openzipkin/zipkin-base:base-1.25.0 MAINTAINER OpenZipkin "http://zipkin.io/" diff --git a/zipkin-java-server/pom.xml b/zipkin-java-server/pom.xml index 0a1cd53381a..4e5a2eace98 100644 --- a/zipkin-java-server/pom.xml +++ b/zipkin-java-server/pom.xml @@ -31,7 +31,7 @@ ${project.basedir}/.. - 3.0.0 + 3.1.0 io.zipkin.server.ZipkinServer @@ -63,7 +63,7 @@ com.github.kristofa - brave-zipkin-spancollector + brave-spancollector-scribe ${brave.version} diff --git a/zipkin-java-server/src/main/java/io/zipkin/server/InMemorySpanStore.java b/zipkin-java-server/src/main/java/io/zipkin/server/InMemorySpanStore.java index 813cb33ec64..e4c1ed09109 100644 --- a/zipkin-java-server/src/main/java/io/zipkin/server/InMemorySpanStore.java +++ b/zipkin-java-server/src/main/java/io/zipkin/server/InMemorySpanStore.java @@ -18,6 +18,7 @@ import io.zipkin.QueryRequest; import io.zipkin.Span; import io.zipkin.SpanStore; +import io.zipkin.internal.CorrectForClockSkew; import io.zipkin.internal.Nullable; import java.nio.charset.Charset; import java.util.Collection; @@ -49,8 +50,11 @@ public synchronized void accept(List spans) { spans.forEach(span -> { long traceId = span.traceId; traceIdToSpans.put(span.traceId, span); - span.annotations.stream().filter(a -> a.endpoint != null) - .map(annotation -> annotation.endpoint.serviceName) + Stream.concat(span.annotations.stream().map(a -> a.endpoint), + span.binaryAnnotations.stream().map(a -> a.endpoint)) + .filter(e -> e != null) + .map(e -> e.serviceName) + .distinct() .forEach(serviceName -> { serviceToTraceIds.put(serviceName, traceId); serviceToSpanNames.put(serviceName, span.name); @@ -68,13 +72,9 @@ public synchronized List> getTraces(QueryRequest request) { Collection traceIds = serviceToTraceIds.get(request.serviceName); if (traceIds == null || traceIds.isEmpty()) return Collections.emptyList(); - long endTs = (request.endTs > 0 && request.endTs != Long.MAX_VALUE) ? request.endTs - : System.currentTimeMillis() / 1000; - return toSortedTraces(traceIds.stream().map(traceIdToSpans::get)).stream() - .filter(t -> t.stream().allMatch(s -> s.timestamp() <= endTs)) - .filter(spansPredicate(request)) - .limit(request.limit).collect(Collectors.toList()); + .filter(spansPredicate(request)) + .limit(request.limit).collect(Collectors.toList()); } @Override @@ -96,7 +96,7 @@ public synchronized List getSpanNames(String service) { } @Override - public List getDependencies(@Nullable Long startTs, @Nullable Long endTs) { + public List getDependencies(long endTs, @Nullable Long lookback) { return Collections.emptyList(); } @@ -106,16 +106,32 @@ public void close() { static Predicate> spansPredicate(QueryRequest request) { return spans -> { + Long timestamp = spans.get(0).timestamp; + if (timestamp == null || + timestamp < (request.endTs - request.lookback) * 1000 || + timestamp > request.endTs * 1000) { + return false; + } Set serviceNames = new LinkedHashSet<>(); + Predicate durationPredicate = null; + if (request.minDuration != null && request.maxDuration != null) { + durationPredicate = d -> d >= request.minDuration && d <= request.maxDuration; + } else if (request.minDuration != null){ + durationPredicate = d -> d >= request.minDuration; + } String spanName = request.spanName; Set annotations = new LinkedHashSet<>(request.annotations); Map binaryAnnotations = new LinkedHashMap<>(request.binaryAnnotations); + Set currentServiceNames = new LinkedHashSet<>(); for (Span span : spans) { + currentServiceNames.clear(); + span.annotations.forEach(a -> { annotations.remove(a.value); if (a.endpoint != null) { serviceNames.add(a.endpoint.serviceName); + currentServiceNames.add(a.endpoint.serviceName); } }); @@ -125,21 +141,33 @@ static Predicate> spansPredicate(QueryRequest request) { } if (b.endpoint != null) { serviceNames.add(b.endpoint.serviceName); + currentServiceNames.add(b.endpoint.serviceName); } }); + if (currentServiceNames.contains(request.serviceName) && durationPredicate != null) { + if (durationPredicate.test(span.duration)) { + durationPredicate = null; + } + } + if (span.name.equals(spanName)) { spanName = null; } } - return serviceNames.contains(request.serviceName) && spanName == null && annotations.isEmpty() && binaryAnnotations.isEmpty(); + return serviceNames.contains(request.serviceName) + && spanName == null + && annotations.isEmpty() + && binaryAnnotations.isEmpty() + && durationPredicate == null; }; } static List> toSortedTraces(Stream> unfiltered) { return unfiltered.filter(spans -> spans != null && !spans.isEmpty()) .map(spans -> merge(spans)) - .sorted((left, right) -> left.get(0).compareTo(right.get(0))) + .map(CorrectForClockSkew.INSTANCE) + .sorted((left, right) -> right.get(0).compareTo(left.get(0))) .collect(Collectors.toList()); } diff --git a/zipkin-java-server/src/main/java/io/zipkin/server/ZipkinQueryApiV1.java b/zipkin-java-server/src/main/java/io/zipkin/server/ZipkinQueryApiV1.java index 1724ff84a27..ed87c8a1fee 100644 --- a/zipkin-java-server/src/main/java/io/zipkin/server/ZipkinQueryApiV1.java +++ b/zipkin-java-server/src/main/java/io/zipkin/server/ZipkinQueryApiV1.java @@ -51,6 +51,7 @@ public class ZipkinQueryApiV1 { private SpanStore spanStore; private ZipkinSpanWriter spanWriter; + private final static String DEFAULT_LOOKBACK = "86400000000"; // 7 days @Autowired public ZipkinQueryApiV1(SpanStore spanStore, ZipkinSpanWriter spanWriter) { @@ -59,10 +60,9 @@ public ZipkinQueryApiV1(SpanStore spanStore, ZipkinSpanWriter spanWriter) { } @RequestMapping(value = "/dependencies", method = RequestMethod.GET, produces = APPLICATION_JSON_VALUE) - public byte[] getDependencies( - @RequestParam(value = "startTs", required = false, defaultValue = "0") long startTs, - @RequestParam(value = "endTs", required = true) long endTs) { - return DEPENDENCY_LINKS_TO_JSON.apply(this.spanStore.getDependencies(startTs != 0 ? startTs : null, endTs)); + public byte[] getDependencies(@RequestParam(value = "endTs", required = true) long endTs, + @RequestParam(value = "lookback", required = false, defaultValue = DEFAULT_LOOKBACK) long lookback) { + return DEPENDENCY_LINKS_TO_JSON.apply(this.spanStore.getDependencies(endTs, lookback)); } @RequestMapping(value = "/services", method = RequestMethod.GET) @@ -97,10 +97,20 @@ public byte[] getTraces( @RequestParam(value = "serviceName", required = true) String serviceName, @RequestParam(value = "spanName", defaultValue = "all") String spanName, @RequestParam(value = "annotationQuery", required = false) String annotationQuery, + @RequestParam(value = "minDuration", required = false) Long minDuration, + @RequestParam(value = "maxDuration", required = false) Long maxDuration, @RequestParam(value = "endTs", required = false) Long endTs, + @RequestParam(value = "lookback", required = false, defaultValue = DEFAULT_LOOKBACK) long lookback, @RequestParam(value = "limit", required = false) Integer limit) { - QueryRequest.Builder builder = new QueryRequest.Builder().serviceName(serviceName) - .spanName(spanName.equals("all") ? null : spanName).endTs(endTs).limit(limit); + QueryRequest.Builder builder = new QueryRequest.Builder() + .serviceName(serviceName) + .spanName(spanName.equals("all") ? null : spanName) + .minDuration(minDuration) + .maxDuration(maxDuration) + .endTs(endTs) + .lookback(lookback) + .limit(limit); + if (annotationQuery != null && !annotationQuery.isEmpty()) { for (String ann : annotationQuery.split(" and ")) { if (ann.indexOf('=') == -1) { diff --git a/zipkin-java-server/src/main/java/io/zipkin/server/brave/JDBCTracerConfiguration.java b/zipkin-java-server/src/main/java/io/zipkin/server/brave/JDBCTracerConfiguration.java index 2af6724111f..71581fae76a 100644 --- a/zipkin-java-server/src/main/java/io/zipkin/server/brave/JDBCTracerConfiguration.java +++ b/zipkin-java-server/src/main/java/io/zipkin/server/brave/JDBCTracerConfiguration.java @@ -15,11 +15,7 @@ package io.zipkin.server.brave; import com.github.kristofa.brave.Brave; -import org.mariadb.jdbc.Driver; -import com.twitter.zipkin.gen.AnnotationType; -import com.twitter.zipkin.gen.BinaryAnnotation; -import com.twitter.zipkin.gen.Endpoint; -import com.twitter.zipkin.gen.Span; +import io.zipkin.Endpoint; import java.net.InetAddress; import java.net.URI; import java.net.UnknownHostException; @@ -30,6 +26,7 @@ import org.jooq.impl.DefaultExecuteListener; import org.jooq.impl.DefaultExecuteListenerProvider; import org.jooq.tools.StringUtils; +import org.mariadb.jdbc.Driver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -48,41 +45,29 @@ ExecuteListenerProvider jdbcTraceListenerProvider() { /** Attach the IP of the remote datasource, knowing that DNS may invalidate this */ @Bean - BinaryAnnotation jdbcServerAddr(@Value("${spring.datasource.url}") String jdbcUrl) throws UnknownHostException { + Endpoint jdbcServerAddr(@Value("${spring.datasource.url}") String jdbcUrl) throws UnknownHostException { URI url = URI.create(jdbcUrl.substring(5)); // strip "jdbc:" int ipv4 = ByteBuffer.wrap(InetAddress.getByName(url.getHost()).getAddress()).getInt(); - Endpoint endpoint = new Endpoint(ipv4, (short) url.getPort(), "zipkin-jdbc"); - BinaryAnnotation ba = new BinaryAnnotation(); - ba.setKey("sa"); - ba.setValue(new byte[]{1}); - ba.setAnnotation_type(AnnotationType.BOOL); - ba.setHost(endpoint); - return ba; + return Endpoint.create("zipkin-jdbc", ipv4, url.getPort()); } @Autowired Brave brave; @Autowired - BinaryAnnotation jdbcServerAddr; + Endpoint jdbcEndpoint; @Override public void renderEnd(ExecuteContext ctx) { if (ctx.type() == ExecuteType.READ) { // Don't log writes (so as to not loop on collector) this.brave.clientTracer().startNewSpan("query"); this.brave.clientTracer().setCurrentClientServiceName("zipkin-jdbc"); - - // Temporary until https://github.com/openzipkin/brave/issues/104 - Span span = this.brave.clientSpanThreadBinder().getCurrentClientSpan(); - synchronized (span) { - span.addToBinary_annotations(jdbcServerAddr); - } String[] batchSQL = ctx.batchSQL(); if (!StringUtils.isBlank(ctx.sql())) { this.brave.clientTracer().submitBinaryAnnotation("jdbc.query", ctx.sql()); } else if (batchSQL.length > 0 && batchSQL[batchSQL.length - 1] != null) { this.brave.clientTracer().submitBinaryAnnotation("jdbc.query", StringUtils.join(batchSQL, '\n')); } - this.brave.clientTracer().setClientSent(); + this.brave.clientTracer().setClientSent(jdbcEndpoint.ipv4, jdbcEndpoint.port, jdbcEndpoint.serviceName); } } diff --git a/zipkin-java-server/src/main/resources/zipkin-server.yml b/zipkin-java-server/src/main/resources/zipkin-server.yml index e3d9051966c..16a7078969e 100644 --- a/zipkin-java-server/src/main/resources/zipkin-server.yml +++ b/zipkin-java-server/src/main/resources/zipkin-server.yml @@ -9,6 +9,8 @@ zipkin: type: ${STORAGE_TYPE:mem} server: port: ${QUERY_PORT:9411} + compression: + enabled: true spring: datasource: driver-class-name: org.mariadb.jdbc.Driver diff --git a/zipkin-java-server/src/test/java/io/zipkin/server/brave/SpanStoreSpanCollectorTest.java b/zipkin-java-server/src/test/java/io/zipkin/server/brave/SpanStoreSpanCollectorTest.java index 8cb445f7794..63808c90763 100644 --- a/zipkin-java-server/src/test/java/io/zipkin/server/brave/SpanStoreSpanCollectorTest.java +++ b/zipkin-java-server/src/test/java/io/zipkin/server/brave/SpanStoreSpanCollectorTest.java @@ -50,6 +50,7 @@ private static Span newSpan(long traceId, long id, String spanName, String value Span span = new Span(); span.setId(id); span.setTrace_id(traceId); + span.setParent_id(traceId); span.setName(spanName); Annotation annotation = new Annotation(); annotation.setHost(new Endpoint(0, (short) 80, service));