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.32.112.6
- 2.5.2
- 2.4.1
+ 2.5.3
+ 2.4.21.6.61.61.0.01.6.0
- 1.2.7.RELEASE
+ 1.3.0.RELEASE0.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.02.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