diff --git a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java index b7214e3c627..bf05798b818 100644 --- a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java +++ b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java @@ -41,6 +41,8 @@ import zipkin.Codec; import zipkin.Endpoint; import zipkin.Span; +import zipkin.simplespan.SimpleSpan; +import zipkin.simplespan.SimpleSpanCodec; /** * This compares the speed of the bundled java codec with the approach used in the scala @@ -154,6 +156,31 @@ public byte[] writeClientSpan_thrift_libthrift() throws TException { return serialize(clientSpanLibThrift); } + static final byte[] simpleSpanJson = read("/span-simple.json"); + static final SimpleSpan simpleSpan = SimpleSpanCodec.JSON.readSpan(simpleSpanJson); + static final List tenSimpleSpans = Collections.nCopies(10, simpleSpan); + static final byte[] tenSimpleSpansJson = SimpleSpanCodec.JSON.writeSpans(tenSimpleSpans); + + @Benchmark + public SimpleSpan readClientSpan_json_zipkin_simple() { + return SimpleSpanCodec.JSON.readSpan(simpleSpanJson); + } + + @Benchmark + public List readTenClientSpans_json_zipkin_simple() { + return SimpleSpanCodec.JSON.readSpans(tenSimpleSpansJson); + } + + @Benchmark + public byte[] writeClientSpan_json_zipkin_simple() { + return SimpleSpanCodec.JSON.writeSpan(simpleSpan); + } + + @Benchmark + public byte[] writeTenClientSpans_json_zipkin_simple() { + return SimpleSpanCodec.JSON.writeSpans(tenSimpleSpans); + } + static final byte[] rpcSpanJson = read("/span-rpc.json"); static final Span rpcSpan = Codec.JSON.readSpan(rpcSpanJson); static final byte[] rpcSpanThrift = Codec.THRIFT.writeSpan(rpcSpan); diff --git a/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java index e8a46b0f329..032e3f2584d 100644 --- a/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java +++ b/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java @@ -32,6 +32,7 @@ import zipkin.BinaryAnnotation; import zipkin.Constants; import zipkin.Endpoint; +import zipkin.simplespan.SimpleSpan; import zipkin.Span; import zipkin.TraceKeys; import zipkin.internal.Util; @@ -50,9 +51,11 @@ public class SpanBenchmarks { Endpoint.builder().serviceName("app").ipv4(172 << 24 | 17 << 16 | 2).port(8080).build(); final Span.Builder sharedBuilder; + final SimpleSpan.Builder sharedSimpleSpanBuilder; public SpanBenchmarks() { sharedBuilder = buildClientOnlySpan(Span.builder()).toBuilder(); + sharedSimpleSpanBuilder = buildClientSimpleSpan().toBuilder(); } @Benchmark @@ -104,6 +107,39 @@ public Span buildClientOnlySpan_clear() { return buildClientOnlySpan(sharedBuilder.clear()); } + @Benchmark + public SimpleSpan buildClientSimpleSpan() { + return buildClientSimpleSpan(SimpleSpan.builder()); + } + + static SimpleSpan buildClientSimpleSpan(SimpleSpan.Builder builder) { + return builder + .traceId(traceId) + .parentId(traceId) + .id(spanId) + .name("get") + .kind(SimpleSpan.Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + } + + @Benchmark + public SimpleSpan buildClientSimpleSpan_clear() { + return buildClientSimpleSpan(sharedSimpleSpanBuilder.clear()); + } + + @Benchmark + public SimpleSpan buildClientSimpleSpan_clone() { + return sharedSimpleSpanBuilder.clone().build(); + } + @Benchmark public Span buildRpcSpan() { return Span.builder() // web calls app diff --git a/benchmarks/src/main/resources/span-client.json b/benchmarks/src/main/resources/span-client.json index 701e7b185e7..466eb2a29ae 100644 --- a/benchmarks/src/main/resources/span-client.json +++ b/benchmarks/src/main/resources/span-client.json @@ -40,25 +40,16 @@ } ], "binaryAnnotations": [ - { - "key": "ca", - "value": true, - "endpoint": { - "serviceName": "frontend", - "ipv4": "127.0.0.1", - "port": 49504 - } - }, { "key": "clnt/finagle.version", - "value": "6.36.0", + "value": "6.45.0", "endpoint": { "serviceName": "frontend", "ipv4": "127.0.0.1" } }, { - "key": "http.uri", + "key": "http.path", "value": "/api", "endpoint": { "serviceName": "frontend", @@ -70,10 +61,9 @@ "value": true, "endpoint": { "serviceName": "backend", - "ipv4": "127.0.0.1", + "ipv4": "192.168.99.101", "port": 9000 } } - ], - "debug": false + ] } diff --git a/benchmarks/src/main/resources/span-simple.json b/benchmarks/src/main/resources/span-simple.json new file mode 100644 index 00000000000..9286ebccb23 --- /dev/null +++ b/benchmarks/src/main/resources/span-simple.json @@ -0,0 +1,32 @@ +{ + "traceId": "86154a4ba6e91385", + "parentId": "86154a4ba6e91385", + "id": "4d1e00c0db9010db", + "kind": "CLIENT", + "name": "get", + "startTimestamp": 1472470996199000, + "finishTimestamp": 1472470996406000, + "localEndpoint": { + "serviceName": "frontend", + "ipv4": "127.0.0.1" + }, + "remoteEndpoint": { + "serviceName": "backend", + "ipv4": "192.168.99.101", + "port": 9000 + }, + "annotations": [ + { + "timestamp": 1472470996238000, + "value": "ws" + }, + { + "timestamp": 1472470996403000, + "value": "wr" + } + ], + "tags": { + "http.path": "/api", + "clnt/finagle.version": "6.45.0" + } +} diff --git a/zipkin/bnd.bnd b/zipkin/bnd.bnd index 999daeabc07..c6051619351 100644 --- a/zipkin/bnd.bnd +++ b/zipkin/bnd.bnd @@ -5,4 +5,5 @@ Export-Package: \ zipkin,\ zipkin.collector,\ zipkin.internal,\ - zipkin.storage \ No newline at end of file + zipkin.simplespan,\ + zipkin.storage diff --git a/zipkin/pom.xml b/zipkin/pom.xml index d00ec557c80..85b13c68738 100644 --- a/zipkin/pom.xml +++ b/zipkin/pom.xml @@ -33,6 +33,12 @@ + + com.google.auto.value + auto-value + provided + + com.google.code.gson gson diff --git a/zipkin/src/main/java/zipkin/internal/SimpleSpanJsonCodec.java b/zipkin/src/main/java/zipkin/internal/SimpleSpanJsonCodec.java new file mode 100644 index 00000000000..3236c5bb9cf --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/SimpleSpanJsonCodec.java @@ -0,0 +1,249 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.MalformedJsonException; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import zipkin.internal.JsonCodec.JsonReaderAdapter; +import zipkin.simplespan.SimpleSpan; +import zipkin.simplespan.SimpleSpanCodec; + +import static zipkin.internal.Buffer.asciiSizeInBytes; +import static zipkin.internal.Buffer.jsonEscapedSizeInBytes; +import static zipkin.internal.JsonCodec.ANNOTATION_WRITER; +import static zipkin.internal.JsonCodec.ENDPOINT_READER; +import static zipkin.internal.JsonCodec.ENDPOINT_WRITER; +import static zipkin.internal.JsonCodec.writeList; + +/** + * Internal type supporting codec operations in {@link SimpleSpan}. Design rationale is the same as + * {@link JsonCodec}. + */ +public final class SimpleSpanJsonCodec implements SimpleSpanCodec { + + @Override public SimpleSpan readSpan(byte[] bytes) { + return JsonCodec.read(new SimpleSpanReader(), bytes); + } + + /** Serialize a span recorded from instrumentation into its binary form. */ + @Override public byte[] writeSpan(SimpleSpan span) { + return JsonCodec.write(SPAN_WRITER, span); + } + + @Override public List readSpans(byte[] bytes) { + return JsonCodec.readList(new SimpleSpanReader(), bytes); + } + + @Override public byte[] writeSpans(List value) { + return writeList(SPAN_WRITER, value); + } + + static final class SimpleSpanReader implements JsonReaderAdapter { + SimpleSpan.Builder builder; + + @Override public SimpleSpan fromJson(JsonReader reader) throws IOException { + if (builder == null) { + builder = SimpleSpan.builder(); + } else { + builder.clear(); + } + reader.beginObject(); + while (reader.hasNext()) { + String nextName = reader.nextName(); + if (nextName.equals("traceId")) { + builder.traceId(reader.nextString()); + } else if (nextName.equals("parentId") && reader.peek() != JsonToken.NULL) { + builder.parentId(reader.nextString()); + } else if (nextName.equals("id")) { + builder.id(reader.nextString()); + } else if (nextName.equals("kind")) { + builder.kind(SimpleSpan.Kind.valueOf(reader.nextString())); + } else if (nextName.equals("name") && reader.peek() != JsonToken.NULL) { + builder.name(reader.nextString()); + } else if (nextName.equals("startTimestamp") && reader.peek() != JsonToken.NULL) { + builder.startTimestamp(reader.nextLong()); + } else if (nextName.equals("finishTimestamp") && reader.peek() != JsonToken.NULL) { + builder.finishTimestamp(reader.nextLong()); + } else if (nextName.equals("localEndpoint") && reader.peek() != JsonToken.NULL) { + builder.localEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("remoteEndpoint") && reader.peek() != JsonToken.NULL) { + builder.remoteEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("annotations")) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + Long timestamp = null; + String value = null; + while (reader.hasNext()) { + nextName = reader.nextName(); + if (nextName.equals("timestamp")) { + timestamp = reader.nextLong(); + } else if (nextName.equals("value")) { + value = reader.nextString(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + if (timestamp != null && value != null) builder.addAnnotation(timestamp, value); + } + reader.endArray(); + } else if (nextName.equals("tags")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + throw new MalformedJsonException("No value at " + reader.getPath()); + } + builder.putTag(key, reader.nextString()); + } + reader.endObject(); + } else if (nextName.equals("debug") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.debug(true); + } else if (nextName.equals("shared") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.shared(true); + } else { + reader.skipValue(); + } + } + reader.endObject(); + return builder.build(); + } + + @Override public String toString() { + return "SimpleSpan"; + } + } + + static final Buffer.Writer SPAN_WRITER = new Buffer.Writer() { + @Override public int sizeInBytes(SimpleSpan value) { + int sizeInBytes = 0; + if (value.traceIdHigh() != 0) sizeInBytes += 16; + sizeInBytes += asciiSizeInBytes("{\"traceId\":\"") + 16 + 1; + if (value.parentId() != null) { + sizeInBytes += asciiSizeInBytes(",\"parentId\":\"") + 16 + 1; + } + sizeInBytes += asciiSizeInBytes(",\"id\":\"") + 16 + 1; + if (value.kind() != null) { + sizeInBytes += asciiSizeInBytes(",\"kind\":\""); + sizeInBytes += asciiSizeInBytes(value.kind().toString()) + 1; + } + if (value.name() != null) { + sizeInBytes += asciiSizeInBytes(",\"name\":\""); + sizeInBytes += jsonEscapedSizeInBytes(value.name()) + 1; + } + if (value.startTimestamp() != null) { + sizeInBytes += asciiSizeInBytes(",\"startTimestamp\":"); + sizeInBytes += asciiSizeInBytes(value.startTimestamp()); + } + if (value.finishTimestamp() != null) { + sizeInBytes += asciiSizeInBytes(",\"finishTimestamp\":"); + sizeInBytes += asciiSizeInBytes(value.finishTimestamp()); + } + if (value.localEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"localEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.localEndpoint()); + } + if (value.remoteEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"remoteEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.remoteEndpoint()); + } + if (!value.annotations().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"annotations\":"); + sizeInBytes += JsonCodec.sizeInBytes(ANNOTATION_WRITER, value.annotations()); + } + if (!value.tags().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"tags\":"); + sizeInBytes += 2; // curly braces + int tagCount = value.tags().size(); + if (tagCount > 1) sizeInBytes += tagCount - 1; // comma to join elements + for (Map.Entry entry : value.tags().entrySet()) { + sizeInBytes += 5; // 4 quotes and a colon + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getKey()); + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getValue()); + } + } + if (Boolean.TRUE.equals(value.debug())) { + sizeInBytes += asciiSizeInBytes(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + sizeInBytes += asciiSizeInBytes(",\"shared\":true"); + } + return ++sizeInBytes;// end curly-brace + } + + @Override public void write(SimpleSpan value, Buffer b) { + b.writeAscii("{\"traceId\":\""); + if (value.traceIdHigh() != 0) { + b.writeLowerHex(value.traceIdHigh()); + } + b.writeLowerHex(value.traceId()).writeByte('"'); + if (value.parentId() != null) { + b.writeAscii(",\"parentId\":\"").writeLowerHex(value.parentId()).writeByte('"'); + } + b.writeAscii(",\"id\":\"").writeLowerHex(value.id()).writeByte('"'); + if (value.kind() != null) { + b.writeAscii(",\"kind\":\"").writeJsonEscaped(value.kind().toString()).writeByte('"'); + } + if (value.name() != null) { + b.writeAscii(",\"name\":\"").writeJsonEscaped(value.name()).writeByte('"'); + } + if (value.startTimestamp() != null) { + b.writeAscii(",\"startTimestamp\":").writeAscii(value.startTimestamp()); + } + if (value.finishTimestamp() != null) { + b.writeAscii(",\"finishTimestamp\":").writeAscii(value.finishTimestamp()); + } + if (value.localEndpoint() != null) { + b.writeAscii(",\"localEndpoint\":"); + ENDPOINT_WRITER.write(value.localEndpoint(), b); + } + if (value.remoteEndpoint() != null) { + b.writeAscii(",\"remoteEndpoint\":"); + ENDPOINT_WRITER.write(value.remoteEndpoint(), b); + } + if (!value.annotations().isEmpty()) { + b.writeAscii(",\"annotations\":"); + writeList(ANNOTATION_WRITER, value.annotations(), b); + } + if (!value.tags().isEmpty()) { + b.writeAscii(",\"tags\":{"); + Iterator> i = value.tags().entrySet().iterator(); + while (i.hasNext()) { + Map.Entry entry = i.next(); + b.writeByte('"').writeJsonEscaped(entry.getKey()).writeAscii("\":\""); + b.writeJsonEscaped(entry.getValue()).writeByte('"'); + if (i.hasNext()) b.writeByte(','); + } + b.writeByte('}'); + } + if (Boolean.TRUE.equals(value.debug())) { + b.writeAscii(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + b.writeAscii(",\"shared\":true"); + } + b.writeByte('}'); + } + + @Override public String toString() { + return "SimpleSpan"; + } + }; +} diff --git a/zipkin/src/main/java/zipkin/simplespan/SimpleSpan.java b/zipkin/src/main/java/zipkin/simplespan/SimpleSpan.java new file mode 100644 index 00000000000..b1f58687866 --- /dev/null +++ b/zipkin/src/main/java/zipkin/simplespan/SimpleSpan.java @@ -0,0 +1,413 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.simplespan; + +import com.google.auto.value.AutoValue; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.io.StreamCorruptedException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import zipkin.Annotation; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.TraceKeys; +import zipkin.internal.Nullable; + +import static zipkin.internal.Util.UTF_8; +import static zipkin.internal.Util.checkNotNull; +import static zipkin.internal.Util.lowerHexToUnsignedLong; +import static zipkin.internal.Util.sortedList; + +/** + * This is a single-host view of a {@link Span}: the primary way tracers record data. + * + *

This type is intended to replace use of {@link Span} in tracers, as it represents a single- + * host view of an operation. By making one endpoint implicit for all data, this type does not need + * to repeat endpoints on each data like {@link Span span} does. This results in simpler and smaller + * data. + */ +@AutoValue +public abstract class SimpleSpan implements Serializable { // for Spark jobs + private static final long serialVersionUID = 0L; + + /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */ + public abstract long traceIdHigh(); + + /** Unique 8-byte identifier for a trace, set on all spans within it. */ + public abstract long traceId(); + + /** The parent's {@link #id} or null if this the root span in a trace. */ + @Nullable public abstract Long parentId(); + + /** + * Unique 8-byte identifier of this span within a trace. + * + *

A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #id()}). + */ + public abstract long id(); + + /** Indicates the primary span type. */ + public enum Kind { + CLIENT, + SERVER + } + + /** When present, used to interpret {@link #remoteEndpoint} */ + @Nullable public abstract Kind kind(); + + /** + * Span name in lowercase, rpc method for example. + * + *

Conventionally, when the span name isn't known, name = "unknown". + */ + @Nullable public abstract String name(); + + /** + * 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 multiplying {@link System#currentTimeMillis} by 1000. + * + *

There are three known edge-cases where this could be reported absent: + * + *

    + *
  • A span was allocated but never started (ex not yet received a timestamp)
  • + *
  • The span's start event was lost
  • + *
  • Data about a completed span (ex tags) were sent after the fact
  • + *
    + * + * @see #finishTimestamp() + */ + @Nullable public abstract Long startTimestamp(); + + /** + * Epoch microseconds of the completion of this span, possibly absent if this a span is in-flight + * or incomplete. The critical path latency in microseconds of this operation is: {@code + * finishTimestamp - startTimestamp}. + * + *

    Since this type represents a span within a single host, care should be taken to not report + * an inaccurate timestamp. Typically, this is accomplished by using an offset from {@link + * #startTimestamp}, as this avoids problems of clocks, such as skew or NTP updates causing time + * to move backwards. + * + * @see #startTimestamp + */ + @Nullable public abstract Long finishTimestamp(); + + /** + * The host that recorded this span, primarily for query by service name. + * + *

    Instrumentation should always record this and be consistent as possible with the service + * name as it is used in search. This is nullable for legacy reasons. + */ + // Nullable for data conversion especially late arriving data which might not have an annotation + @Nullable public abstract Endpoint localEndpoint(); + + /** When an RPC (or messaging) span, indicates the other side of the connection. */ + @Nullable public abstract Endpoint remoteEndpoint(); + + /** + * Events that explain latency with a timestamp. Unlike log statements, annotations are often + * short or contain codes: for example "brave.flush". Annotations are sorted ascending by + * timestamp. + */ + public abstract List annotations(); + + /** + * Tags a span with context, usually to support query or aggregation. + * + *

    example, a binary annotation key could be {@link TraceKeys#HTTP_PATH "http.path"}. + */ + public abstract Map tags(); + + /** True is a request to store this span even if it overrides sampling policy. */ + @Nullable public abstract Boolean debug(); + + /** + * True if we are contributing to a span started by another tracer (ex on a different host). + * Defaults to null. When set, it is expected for {@link #kind()} to be {@link Kind#SERVER}. + * + *

    When an RPC trace is client-originated, it will be sampled and the same span ID is used for + * the server side. However, the server shouldn't set span.timestamp or duration since it didn't + * start the span. + */ + @Nullable public abstract Boolean shared(); + + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static final class Builder { + Long traceId; + long traceIdHigh; + Long parentId; + Long id; + Kind kind; + String name; + Long startTimestamp; + Long finishTimestamp; + Endpoint localEndpoint; + Endpoint remoteEndpoint; + ArrayList annotations; + TreeMap tags; + Boolean debug; + Boolean shared; + + Builder() { + } + + public Builder clear() { + traceIdHigh = 0L; + traceId = null; + parentId = null; + id = null; + kind = null; + name = null; + startTimestamp = null; + finishTimestamp = null; + localEndpoint = null; + remoteEndpoint = null; + if (annotations != null) annotations.clear(); + if (tags != null) tags.clear(); + debug = null; + shared = null; + return this; + } + + @Override public Builder clone() { + Builder result = new Builder(); + result.traceIdHigh = traceIdHigh; + result.traceId = traceId; + result.parentId = parentId; + result.id = id; + result.kind = kind; + result.name = name; + result.startTimestamp = startTimestamp; + result.finishTimestamp = finishTimestamp; + result.localEndpoint = localEndpoint; + result.remoteEndpoint = remoteEndpoint; + if (annotations != null) { + result.annotations = (ArrayList) annotations.clone(); + } + if (tags != null) { + result.tags = (TreeMap) tags.clone(); + } + result.debug = debug; + result.shared = shared; + return result; + } + + Builder(SimpleSpan source) { + traceId = source.traceId(); + parentId = source.parentId(); + id = source.id(); + kind = source.kind(); + name = source.name(); + startTimestamp = source.startTimestamp(); + finishTimestamp = source.finishTimestamp(); + localEndpoint = source.localEndpoint(); + remoteEndpoint = source.remoteEndpoint(); + if (!source.annotations().isEmpty()) { + annotations = new ArrayList<>(source.annotations().size()); + annotations.addAll(source.annotations()); + } + if (!source.tags().isEmpty()) { + tags = new TreeMap<>(); + tags.putAll(source.tags()); + } + debug = source.debug(); + shared = source.shared(); + } + + /** + * Decodes the trace ID from its lower-hex representation. + * + *

    Use this instead decoding yourself and calling {@link #traceIdHigh(long)} and {@link + * #traceId(long)} + */ + public Builder traceId(String traceId) { + checkNotNull(traceId, "traceId"); + if (traceId.length() == 32) { + traceIdHigh(lowerHexToUnsignedLong(traceId, 0)); + } + return traceId(lowerHexToUnsignedLong(traceId)); + } + + /** @see SimpleSpan#traceIdHigh */ + public Builder traceIdHigh(long traceIdHigh) { + this.traceIdHigh = traceIdHigh; + return this; + } + + /** @see SimpleSpan#traceId */ + public Builder traceId(long traceId) { + this.traceId = traceId; + return this; + } + + /** + * Decodes the parent ID from its lower-hex representation. + * + *

    Use this instead decoding yourself and calling {@link #parentId(Long)} + */ + public Builder parentId(@Nullable String parentId) { + this.parentId = parentId != null ? lowerHexToUnsignedLong(parentId) : null; + return this; + } + + /** @see SimpleSpan#parentId */ + public Builder parentId(@Nullable Long parentId) { + this.parentId = parentId; + return this; + } + + /** + * Decodes the span ID from its lower-hex representation. + * + *

    Use this instead decoding yourself and calling {@link #id(long)} + */ + public Builder id(String id) { + this.id = lowerHexToUnsignedLong(id); + return this; + } + + /** @see SimpleSpan#id */ + public Builder id(long id) { + this.id = id; + return this; + } + + /** @see SimpleSpan#kind */ + public Builder kind(@Nullable Kind kind) { + this.kind = kind; + return this; + } + + /** @see SimpleSpan#name */ + public Builder name(@Nullable String name) { + this.name = name == null || name.isEmpty() ? null : name.toLowerCase(Locale.ROOT); + return this; + } + + /** @see SimpleSpan#startTimestamp */ + public Builder startTimestamp(@Nullable Long startTimestamp) { + if (startTimestamp != null && startTimestamp == 0L) startTimestamp = null; + this.startTimestamp = startTimestamp; + return this; + } + + /** @see SimpleSpan#finishTimestamp */ + public Builder finishTimestamp(@Nullable Long finishTimestamp) { + if (finishTimestamp != null && finishTimestamp == 0L) finishTimestamp = null; + this.finishTimestamp = finishTimestamp; + return this; + } + + /** @see SimpleSpan#localEndpoint */ + public Builder localEndpoint(@Nullable Endpoint localEndpoint) { + this.localEndpoint = localEndpoint; + return this; + } + + /** @see SimpleSpan#remoteEndpoint */ + public Builder remoteEndpoint(@Nullable Endpoint remoteEndpoint) { + this.remoteEndpoint = remoteEndpoint; + return this; + } + + /** @see SimpleSpan#annotations */ + public Builder addAnnotation(long timestamp, String value) { + if (annotations == null) annotations = new ArrayList<>(2); + annotations.add(Annotation.create(timestamp, value, null)); + return this; + } + + /** @see SimpleSpan#tags */ + public Builder putTag(String key, String value) { + if (tags == null) tags = new TreeMap<>(); + this.tags.put(checkNotNull(key, "key"), checkNotNull(value, "value")); + return this; + } + + /** @see SimpleSpan#debug */ + public Builder debug(@Nullable Boolean debug) { + this.debug = debug; + return this; + } + + /** @see SimpleSpan#shared */ + public Builder shared(@Nullable Boolean shared) { + this.shared = shared; + return this; + } + + public SimpleSpan build() { + return new AutoValue_SimpleSpan( + traceIdHigh, + traceId, + parentId, + id, + kind, + name, + startTimestamp, + finishTimestamp, + localEndpoint, + remoteEndpoint, + sortedList(annotations), + tags == null ? Collections.emptyMap() : new LinkedHashMap(tags), + debug, + shared + ); + } + } + + @Override + public String toString() { + return new String(SimpleSpanCodec.JSON.writeSpan(this), UTF_8); + } + + // Since this is an immutable object, and we have thrift handy, defer to a serialization proxy. + final Object writeReplace() throws ObjectStreamException { + return new SerializedForm(SimpleSpanCodec.JSON.writeSpan(this)); + } + + static final class SerializedForm implements Serializable { + private static final long serialVersionUID = 0L; + + private final byte[] bytes; + + SerializedForm(byte[] bytes) { + this.bytes = bytes; + } + + Object readResolve() throws ObjectStreamException { + try { + return SimpleSpanCodec.JSON.readSpan(bytes); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new StreamCorruptedException(e.getMessage()); + } + } + } +} diff --git a/zipkin/src/main/java/zipkin/simplespan/SimpleSpanCodec.java b/zipkin/src/main/java/zipkin/simplespan/SimpleSpanCodec.java new file mode 100644 index 00000000000..4683d5346c6 --- /dev/null +++ b/zipkin/src/main/java/zipkin/simplespan/SimpleSpanCodec.java @@ -0,0 +1,34 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.simplespan; + +import java.util.List; +import zipkin.internal.SimpleSpanJsonCodec; + +/** Utilities for working with {@link SimpleSpan} */ +public interface SimpleSpanCodec { + SimpleSpanCodec JSON = new SimpleSpanJsonCodec(); + + /** Serialize a span recorded from instrumentation into its binary form. */ + byte[] writeSpan(SimpleSpan span); + + /** Serialize a list of spans recorded from instrumentation into their binary form. */ + byte[] writeSpans(List spans); + + /** throws {@linkplain IllegalArgumentException} if a span couldn't be decoded */ + SimpleSpan readSpan(byte[] bytes); + + /** throws {@linkplain IllegalArgumentException} if the spans couldn't be decoded */ + List readSpans(byte[] bytes); +} diff --git a/zipkin/src/main/java/zipkin/simplespan/SimpleSpanConverter.java b/zipkin/src/main/java/zipkin/simplespan/SimpleSpanConverter.java new file mode 100644 index 00000000000..cff223cb436 --- /dev/null +++ b/zipkin/src/main/java/zipkin/simplespan/SimpleSpanConverter.java @@ -0,0 +1,317 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.simplespan; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import zipkin.Annotation; +import zipkin.BinaryAnnotation; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.internal.Nullable; +import zipkin.internal.Util; +import zipkin.simplespan.SimpleSpan.Kind; + +import static zipkin.BinaryAnnotation.Type.BOOL; +import static zipkin.BinaryAnnotation.Type.STRING; +import static zipkin.Constants.CLIENT_ADDR; +import static zipkin.Constants.LOCAL_COMPONENT; +import static zipkin.Constants.SERVER_ADDR; + +public final class SimpleSpanConverter { + + public static List fromSpan(Span source) { + Builders builders = new Builders(source); + // add annotations unless they are "core" + builders.processAnnotations(source); + // convert binary annotations to tags and addresses + builders.processBinaryAnnotations(source); + return builders.build(); + } + + static class Builders { + final List spans = new ArrayList<>(); + Annotation cs = null, sr = null, ss = null, cr = null; + + Builders(Span source) { + this.spans.add(newBuilder(source)); + } + + void processAnnotations(Span source) { + for (int i = 0, length = source.annotations.size(); i < length; i++) { + Annotation a = source.annotations.get(i); + SimpleSpan.Builder currentSpan = forEndpoint(source, a.endpoint); + // core annotations require an endpoint. Don't give special treatment when that's missing + if (a.value.length() == 2 && a.endpoint != null) { + if (a.value.equals(Constants.CLIENT_SEND)) { + currentSpan.kind(Kind.CLIENT); + cs = a; + } else if (a.value.equals(Constants.SERVER_RECV)) { + currentSpan.kind(Kind.SERVER); + sr = a; + } else if (a.value.equals(Constants.SERVER_SEND)) { + currentSpan.kind(Kind.SERVER); + ss = a; + } else if (a.value.equals(Constants.CLIENT_RECV)) { + currentSpan.kind(Kind.CLIENT); + cr = a; + } else { + currentSpan.addAnnotation(a.timestamp, a.value); + } + } else { + currentSpan.addAnnotation(a.timestamp, a.value); + } + } + + if (cs != null && cr != null && sr != null && ss != null) { + // in a shared span, the client side owns span duration by annotations or explicit timestamp + maybeTimestampDuration(source, cs, cr); + + // special-case loopback: We need to make sure on loopback there are two simple spans + SimpleSpan.Builder server; + if (closeEnough(cs.endpoint, sr.endpoint)) { + forEndpoint(source, cs.endpoint).kind(Kind.CLIENT); + // fork a new span for the server side + server = newSpanBuilder(source, sr.endpoint).kind(Kind.SERVER); + } else { + server = forEndpoint(source, sr.endpoint); + } + + // the server side is smaller than that, we have to read annotations to find out + server.shared(true).startTimestamp(sr.timestamp).finishTimestamp(ss.timestamp); + } else if (cs != null && cr != null) { + maybeTimestampDuration(source, cs, cr); + } else if (sr != null && ss != null) { + maybeTimestampDuration(source, sr, ss); + } else { // otherwise, the span is incomplete. revert special-casing + revertCoreAnnotation(source, cs); + revertCoreAnnotation(source, sr); + revertCoreAnnotation(source, ss); + revertCoreAnnotation(source, cr); + + if (source.timestamp != null) { + SimpleSpan.Builder first = spans.get(0).startTimestamp(source.timestamp); + if (source.duration != null) first.finishTimestamp(source.timestamp + source.duration); + } + } + } + + void revertCoreAnnotation(Span source, Annotation a) { + if (a == null) return; + forEndpoint(source, a.endpoint).kind(null).addAnnotation(a.timestamp, a.value); + } + + void maybeTimestampDuration(Span source, Annotation begin, Annotation end) { + SimpleSpan.Builder simple = forEndpoint(source, begin.endpoint); + if (source.timestamp != null && source.duration != null) { + simple.startTimestamp(source.timestamp).finishTimestamp(source.timestamp + source.duration); + } else { + simple.startTimestamp(begin.timestamp).finishTimestamp(end.timestamp); + } + } + + void processBinaryAnnotations(Span source) { + Endpoint ca = null, sa = null; + for (int i = 0, length = source.binaryAnnotations.size(); i < length; i++) { + BinaryAnnotation b = source.binaryAnnotations.get(i); + if (b.type == BOOL) { + if (Constants.CLIENT_ADDR.equals(b.key)) { + ca = b.endpoint; + } else if (Constants.SERVER_ADDR.equals(b.key)) { + sa = b.endpoint; + } + continue; + } + SimpleSpan.Builder currentSpan = forEndpoint(source, b.endpoint); + if (b.type == STRING) { + // don't add marker "lc" tags + if (Constants.LOCAL_COMPONENT.equals(b.key) && b.value.length == 0) continue; + currentSpan.putTag(b.key, new String(b.value, Util.UTF_8)); + } + } + + if (cs != null && sa != null && !closeEnough(sa, cs.endpoint)) { + forEndpoint(source, cs.endpoint).remoteEndpoint(sa); + } + + if (sr != null && ca != null && !closeEnough(ca, sr.endpoint)) { + forEndpoint(source, sr.endpoint).remoteEndpoint(ca); + } + + // special-case when we are missing core annotations, but we have both address annotations + if ((cs == null && sr == null) && (ca != null && sa != null)) { + forEndpoint(source, ca).remoteEndpoint(sa); + } + } + + SimpleSpan.Builder forEndpoint(Span source, @Nullable Endpoint e) { + if (e == null) return spans.get(0); // allocate missing endpoint data to first span + for (int i = 0, length = spans.size(); i < length; i++) { + SimpleSpan.Builder next = spans.get(i); + if (next.localEndpoint == null) { + next.localEndpoint = e; + return next; + } else if (closeEnough(next.localEndpoint, e)) { + return next; + } + } + return newSpanBuilder(source, e); + } + + SimpleSpan.Builder newSpanBuilder(Span source, Endpoint e) { + SimpleSpan.Builder result = newBuilder(source).localEndpoint(e); + spans.add(result); + return result; + } + + List build() { + int length = spans.size(); + if (length == 1) return Collections.singletonList(spans.get(0).build()); + List result = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + result.add(spans.get(i).build()); + } + return result; + } + } + + static boolean closeEnough(Endpoint left, Endpoint right) { + return left.serviceName.equals(right.serviceName); + } + + private static SimpleSpan.Builder newBuilder(Span source) { + return SimpleSpan.builder() + .traceIdHigh(source.traceIdHigh) + .traceId(source.traceId) + .parentId(source.parentId) + .id(source.id) + .name(source.name) + .debug(source.debug); + } + + public static Span toSpan(SimpleSpan in) { + Span.Builder result = Span.builder() + .traceIdHigh(in.traceIdHigh()) + .traceId(in.traceId()) + .parentId(in.parentId()) + .id(in.id()) + .debug(in.debug()) + .name(in.name() == null ? "" : in.name()); // avoid a NPE + + long startTimestamp = in.startTimestamp() == null ? 0L : in.startTimestamp(); + long finishTimestamp = in.finishTimestamp() == null ? 0L : in.finishTimestamp(); + if (startTimestamp != 0L) { + result.timestamp(startTimestamp); + if (finishTimestamp != 0L && startTimestamp != finishTimestamp) { + result.duration(finishTimestamp - startTimestamp); + } + } + + Annotation cs = null, sr = null, ss = null, cr = null; + String remoteEndpointType = null; + + if (in.kind() != null) { + switch (in.kind()) { + case CLIENT: + remoteEndpointType = Constants.SERVER_ADDR; + if (startTimestamp != 0L) { + cs = Annotation.create(startTimestamp, Constants.CLIENT_SEND, in.localEndpoint()); + } + if (finishTimestamp != 0L) { + cr = Annotation.create(finishTimestamp, Constants.CLIENT_RECV, in.localEndpoint()); + } + break; + case SERVER: + remoteEndpointType = Constants.CLIENT_ADDR; + if (startTimestamp != 0L) { + sr = Annotation.create(startTimestamp, Constants.SERVER_RECV, in.localEndpoint()); + } + if (finishTimestamp != 0L) { + ss = Annotation.create(finishTimestamp, Constants.SERVER_SEND, in.localEndpoint()); + } + break; + default: + throw new AssertionError("update kind mapping"); + } + } + + boolean wroteEndpoint = false; + + for (int i = 0, length = in.annotations().size(); i < length; i++) { + Annotation a = in.annotations().get(i); + if (in.localEndpoint() != null) { + a = a.toBuilder().endpoint(in.localEndpoint()).build(); + } + if (a.value.length() == 2) { + if (a.value.equals(Constants.CLIENT_SEND)) { + cs = a; + remoteEndpointType = SERVER_ADDR; + } else if (a.value.equals(Constants.SERVER_RECV)) { + sr = a; + remoteEndpointType = CLIENT_ADDR; + } else if (a.value.equals(Constants.SERVER_SEND)) { + ss = a; + } else if (a.value.equals(Constants.CLIENT_RECV)) { + cr = a; + } else { + wroteEndpoint = true; + result.addAnnotation(a); + } + } else { + wroteEndpoint = true; + result.addAnnotation(a); + } + } + + for (Map.Entry tag : in.tags().entrySet()) { + wroteEndpoint = true; + result.addBinaryAnnotation( + BinaryAnnotation.create(tag.getKey(), tag.getValue(), in.localEndpoint())); + } + + if (cs != null || sr != null || ss != null || cr != null) { + if (cs != null) result.addAnnotation(cs); + if (sr != null) result.addAnnotation(sr); + if (ss != null) result.addAnnotation(ss); + if (cr != null) result.addAnnotation(cr); + wroteEndpoint = true; + } else if (in.localEndpoint() != null && in.remoteEndpoint() != null) { + // special-case when we are missing core annotations, but we have both address annotations + result.addBinaryAnnotation(BinaryAnnotation.address(CLIENT_ADDR, in.localEndpoint())); + wroteEndpoint = true; + remoteEndpointType = SERVER_ADDR; + } + + if (remoteEndpointType != null && in.remoteEndpoint() != null) { + result.addBinaryAnnotation(BinaryAnnotation.address(remoteEndpointType, in.remoteEndpoint())); + } + + // don't report server-side timestamp on shared or incomplete spans + if (Boolean.TRUE.equals(in.shared()) && sr != null) { + result.timestamp(null).duration(null); + } + // don't report client span.timestamp if unfinished. + // This allows one-way to be modeled as span.kind(serverOrClient).start().flush() + if (cs != null && finishTimestamp == 0L) { + result.timestamp(null); + } + if (in.localEndpoint() != null && !wroteEndpoint) { // create a dummy annotation + result.addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "", in.localEndpoint())); + } + return result.build(); + } +} diff --git a/zipkin/src/test/java/zipkin/internal/SimpleSpanJsonCodecTest.java b/zipkin/src/test/java/zipkin/internal/SimpleSpanJsonCodecTest.java new file mode 100644 index 00000000000..daf157044a3 --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/SimpleSpanJsonCodecTest.java @@ -0,0 +1,295 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.TraceKeys; +import zipkin.simplespan.SimpleSpan; + +import static org.assertj.core.api.Assertions.assertThat; +import static zipkin.internal.Util.UTF_8; + +public class SimpleSpanJsonCodecTest { + SimpleSpanJsonCodec codec = new SimpleSpanJsonCodec(); + + Endpoint frontend = Endpoint.create("frontend", 127 << 24 | 1); + Endpoint backend = Endpoint.builder() + .serviceName("backend") + .ipv4(192 << 24 | 168 << 16 | 99 << 8 | 101) + .port(9000) + .build(); + + SimpleSpan span = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(SimpleSpan.Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test public void spanRoundTrip() throws IOException { + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes() throws IOException { + assertThat(SimpleSpanJsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + @Test public void spanRoundTrip_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void spanRoundTrip_shared() throws IOException { + span = span.toBuilder().shared(true).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + assertThat(SimpleSpanJsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + /** + * This isn't a test of what we "should" accept as a span, rather that characters that trip-up + * json don't fail in codec. + */ + @Test public void specialCharsInJson() throws IOException { + // service name is surrounded by control characters + SimpleSpan worstSpanInTheWorld = SimpleSpan.builder().traceId(1L).id(1L) + // name is terrible + .name(new String(new char[] {'"', '\\', '\t', '\b', '\n', '\r', '\f'})) + .localEndpoint(Endpoint.create(new String(new char[] {0, 'a', 1}), 0)) + // annotation value includes some json newline characters + .addAnnotation(1L, "\u2028 and \u2029") + // tag key includes a quote and value newlines + .putTag("\"foo", "Database error: ORA-00942:\u2028 and \u2029 table or view does not exist\n") + .build(); + + byte[] bytes = codec.writeSpan(worstSpanInTheWorld); + assertThat(codec.readSpan(bytes)) + .isEqualTo(worstSpanInTheWorld); + } + + @Test public void decentErrorMessageOnEmptyInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading SimpleSpan"); + + codec.readSpan(new byte[0]); + } + + @Test public void decentErrorMessageOnEmptyInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading List"); + + codec.readSpans(new byte[0]); + } + + @Test public void decentErrorMessageOnMalformedInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading SimpleSpan from "); + + codec.readSpan(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + /** + * Particulary, thrift can mistake malformed content as a huge list. Let's not blow up. + */ + @Test public void decentErrorMessageOnMalformedInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading List from "); + + codec.readSpans(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + @Test public void spansRoundTrip() throws IOException { + List tenClientSpans = Collections.nCopies(10, span); + + byte[] bytes = codec.writeSpans(tenClientSpans); + assertThat(codec.readSpans(bytes)) + .isEqualTo(tenClientSpans); + } + + @Test public void writesTraceIdHighIntoTraceIdField() { + SimpleSpan with128BitTraceId = SimpleSpan.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")) + .traceId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .localEndpoint(frontend) + .id(1).name("").build(); + + assertThat(new String(codec.writeSpan(with128BitTraceId), Util.UTF_8)) + .startsWith("{\"traceId\":\"48485a3953bb61246b221d5bc9e6496c\""); + } + + @Test public void readsTraceIdHighFromTraceIdField() { + byte[] with128BitTraceId = ("{\n" + + " \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + byte[] withLower64bitsTraceId = ("{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + + assertThat(codec.readSpan(with128BitTraceId)) + .isEqualTo(codec.readSpan(withLower64bitsTraceId).toBuilder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")).build()); + } + + @Test public void ignoreNull_parentId() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"parentId\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_startTimestamp() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"startTimestamp\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_finishTimestamp() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"finishTimestamp\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_debug() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"debug\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_shared() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"shared\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_localEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"localEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_remoteEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"remoteEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_traceId() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": null,\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_id() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void missingValue() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("No value at $.tags.foo"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"tags\": {\n" + + " \"foo\": NULL\n" + + " }\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } +} diff --git a/zipkin/src/test/java/zipkin/simplespan/SimpleSpanConverterTest.java b/zipkin/src/test/java/zipkin/simplespan/SimpleSpanConverterTest.java new file mode 100644 index 00000000000..3d322198a94 --- /dev/null +++ b/zipkin/src/test/java/zipkin/simplespan/SimpleSpanConverterTest.java @@ -0,0 +1,385 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.simplespan; + +import org.junit.Test; +import zipkin.Annotation; +import zipkin.BinaryAnnotation; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.TraceKeys; +import zipkin.internal.Util; +import zipkin.simplespan.SimpleSpan.Kind; + +import static org.assertj.core.api.Assertions.assertThat; +import static zipkin.Constants.LOCAL_COMPONENT; + +public class SimpleSpanConverterTest { + Endpoint frontend = Endpoint.create("frontend", 127 << 24 | 1); + Endpoint backend = Endpoint.builder() + .serviceName("backend") + .ipv4(192 << 24 | 168 << 16 | 99 << 8 | 101) + .port(9000) + .build(); + + @Test public void client() { + SimpleSpan simpleClient = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + Span client = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996403000L, Constants.WIRE_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + assertThat(SimpleSpanConverter.toSpan(simpleClient)) + .isEqualTo(client); + assertThat(SimpleSpanConverter.fromSpan(client)) + .containsExactly(simpleClient); + } + + @Test public void noAnnotationsExceptAddresses() { + SimpleSpan simple = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .localEndpoint(frontend) + .remoteEndpoint(backend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .build(); + + Span span = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + assertThat(SimpleSpanConverter.toSpan(simple)) + .isEqualTo(span); + assertThat(SimpleSpanConverter.fromSpan(span)) + .containsExactly(simple); + } + + @Test public void fromSpan_redundantAddressAnnotations() { + SimpleSpan simple = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .kind(Kind.CLIENT) + .name("get") + .localEndpoint(frontend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .build(); + + Span span = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, frontend)) + .build(); + + assertThat(SimpleSpanConverter.fromSpan(span)) + .containsExactly(simple); + } + + @Test public void server() { + SimpleSpan simpleServer = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .id("216a2aea45d08fc9") + .name("get") + .kind(Kind.SERVER) + .localEndpoint(backend) + .remoteEndpoint(frontend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + Span server = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .id(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.SERVER_SEND, backend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .build(); + + assertThat(SimpleSpanConverter.toSpan(simpleServer)) + .isEqualTo(server); + assertThat(SimpleSpanConverter.fromSpan(server)) + .containsExactly(simpleServer); + } + + /** Buggy instrumentation can send data with missing endpoints. Make sure we can record it. */ + @Test public void missingEndpoints() { + SimpleSpan simple = SimpleSpan.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .build(); + + Span span = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L).duration(207000L) + .build(); + + assertThat(SimpleSpanConverter.toSpan(simple)) + .isEqualTo(span); + assertThat(SimpleSpanConverter.fromSpan(span)) + .containsExactly(simple); + } + + /** No special treatment for invalid core annotations: missing endpoint */ + @Test public void missingEndpoints_coreAnnotation() { + SimpleSpan simple = SimpleSpan.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .addAnnotation(1472470996199000L, "sr") + .build(); + + Span span = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("foo") + .timestamp(1472470996199000L).duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, "sr", null)) + .build(); + + assertThat(SimpleSpanConverter.toSpan(simple)) + .isEqualTo(span); + assertThat(SimpleSpanConverter.fromSpan(span)) + .containsExactly(simple); + } + + @Test public void localSpan_emptyComponent() { + SimpleSpan simpleLocal = SimpleSpan.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("local") + .localEndpoint(frontend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .build(); + + Span local = Span.builder() + .traceId(1L) + .parentId(1L) + .id(2L) + .name("local") + .timestamp(1472470996199000L).duration(207000L) + .addBinaryAnnotation(BinaryAnnotation.create(LOCAL_COMPONENT, "", frontend)).build(); + + assertThat(SimpleSpanConverter.toSpan(simpleLocal)) + .isEqualTo(local); + assertThat(SimpleSpanConverter.fromSpan(local)) + .containsExactly(simpleLocal); + } + + @Test public void clientAndServer() { + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996238000L, Constants.WIRE_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, backend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, backend)) + .addAnnotation(Annotation.create(1472470996403000L, Constants.WIRE_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/api", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create(TraceKeys.HTTP_PATH, "/backend", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("clnt/finagle.version", "6.45.0", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("srv/finagle.version", "6.44.0", backend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.CLIENT_ADDR, frontend)) + .addBinaryAnnotation(BinaryAnnotation.address(Constants.SERVER_ADDR, backend)) + .build(); + + SimpleSpan.Builder builder = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get"); + + // the client side owns timestamp and duration + SimpleSpan client = builder.clone() + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + // notice server tags are different than the client, and the client's annotations aren't here + SimpleSpan server = builder.clone() + .kind(Kind.SERVER) + .shared(true) + .localEndpoint(backend) + .remoteEndpoint(frontend) + .startTimestamp(1472470996250000L) + .finishTimestamp(1472470996350000L) + .putTag(TraceKeys.HTTP_PATH, "/backend") + .putTag("srv/finagle.version", "6.44.0") + .build(); + + assertThat(SimpleSpanConverter.fromSpan(shared)) + .containsExactly(client, server); + } + + @Test public void clientAndServer_loopback() { + Span shared = Span.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("7180c278b62e8f6a")) + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .parentId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("get") + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(Annotation.create(1472470996199000L, Constants.CLIENT_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996250000L, Constants.SERVER_RECV, frontend)) + .addAnnotation(Annotation.create(1472470996350000L, Constants.SERVER_SEND, frontend)) + .addAnnotation(Annotation.create(1472470996406000L, Constants.CLIENT_RECV, frontend)) + .build(); + + SimpleSpan.Builder builder = SimpleSpan.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get"); + + SimpleSpan client = builder.clone() + .kind(Kind.CLIENT) + .localEndpoint(frontend) + .startTimestamp(1472470996199000L) + .finishTimestamp(1472470996199000L + 207000L) + .build(); + + SimpleSpan server = builder.clone() + .kind(Kind.SERVER) + .shared(true) + .localEndpoint(frontend) + .startTimestamp(1472470996250000L) + .finishTimestamp(1472470996350000L) + .build(); + + assertThat(SimpleSpanConverter.fromSpan(shared)) + .containsExactly(client, server); + } + + @Test public void dataMissingEndpointGoesOnFirstSpan() { + Span shared = Span.builder() + .traceId(Util.lowerHexToUnsignedLong("216a2aea45d08fc9")) + .id(Util.lowerHexToUnsignedLong("5b4185666d50f68b")) + .name("missing") + .addAnnotation(Annotation.create(1472470996199000L, "foo", frontend)) + .addAnnotation(Annotation.create(1472470996238000L, "bar", frontend)) + .addAnnotation(Annotation.create(1472470996250000L, "baz", backend)) + .addAnnotation(Annotation.create(1472470996350000L, "qux", backend)) + .addAnnotation(Annotation.create(1472470996403000L, "missing", null)) + .addBinaryAnnotation(BinaryAnnotation.create("foo", "bar", frontend)) + .addBinaryAnnotation(BinaryAnnotation.create("baz", "qux", backend)) + .addBinaryAnnotation(BinaryAnnotation.create("missing", "", null)) + .build(); + + SimpleSpan.Builder builder = SimpleSpan.builder() + .traceId("216a2aea45d08fc9") + .id("5b4185666d50f68b") + .name("missing"); + + SimpleSpan first = builder.clone() + .localEndpoint(frontend) + .addAnnotation(1472470996199000L, "foo") + .addAnnotation(1472470996238000L, "bar") + .addAnnotation(1472470996403000L, "missing") + .putTag("foo", "bar") + .putTag("missing", "") + .build(); + + SimpleSpan second = builder.clone() + .localEndpoint(backend) + .addAnnotation(1472470996250000L, "baz") + .addAnnotation(1472470996350000L, "qux") + .putTag("baz", "qux") + .build(); + + assertThat(SimpleSpanConverter.fromSpan(shared)) + .containsExactly(first, second); + } +} diff --git a/zipkin/src/test/java/zipkin/simplespan/SimpleSpanTest.java b/zipkin/src/test/java/zipkin/simplespan/SimpleSpanTest.java new file mode 100644 index 00000000000..e0f36345ec0 --- /dev/null +++ b/zipkin/src/test/java/zipkin/simplespan/SimpleSpanTest.java @@ -0,0 +1,116 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.simplespan; + +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import okio.Buffer; +import okio.ByteString; +import org.junit.Test; +import zipkin.Annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static zipkin.TestObjects.APP_ENDPOINT; + +public class SimpleSpanTest { + SimpleSpan base = SimpleSpan.builder().traceId(1L).id(1L).localEndpoint(APP_ENDPOINT).build(); + + @Test public void spanNamesLowercase() { + assertThat(base.toBuilder().name("GET").build().name()) + .isEqualTo("get"); + } + + @Test public void annotationsSortByTimestamp() { + SimpleSpan span = base.toBuilder() + .addAnnotation(2L, "foo") + .addAnnotation(1L, "foo") + .build(); + + // note: annotations don't also have endpoints, as it is implicit to SimpleSpan.localEndpoint + assertThat(span.annotations()).containsExactly( + Annotation.create(1L, "foo", null), + Annotation.create(2L, "foo", null) + ); + } + + @Test public void putTagOverwritesValue() { + SimpleSpan span = base.toBuilder() + .putTag("foo", "bar") + .putTag("foo", "qux") + .build(); + + assertThat(span.tags()).containsExactly( + entry("foo", "qux") + ); + } + + @Test public void clone_differentCollections() { + SimpleSpan.Builder builder = base.toBuilder() + .addAnnotation(1L, "foo") + .putTag("foo", "qux"); + + SimpleSpan.Builder builder2 = builder.clone() + .addAnnotation(2L, "foo") + .putTag("foo", "bar"); + + assertThat(builder.build()).isEqualTo(base.toBuilder() + .addAnnotation(1L, "foo") + .putTag("foo", "qux") + .build() + ); + + assertThat(builder2.build()).isEqualTo(base.toBuilder() + .addAnnotation(1L, "foo") + .addAnnotation(2L, "foo") + .putTag("foo", "bar") + .build() + ); + } + + @Test public void toString_isJson() { + assertThat(base.toString()).hasToString( + "{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"localEndpoint\":{\"serviceName\":\"app\",\"ipv4\":\"172.17.0.2\",\"port\":8080}}" + ); + } + + /** Catches common error when zero is passed instead of null for a timestamp */ + @Test public void coercesZeroTimestampsToNull() { + SimpleSpan span = base.toBuilder() + .startTimestamp(0L) + .finishTimestamp(0L) + .build(); + + assertThat(span.startTimestamp()) + .isNull(); + assertThat(span.finishTimestamp()) + .isNull(); + } + + @Test public void serialization() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); + + assertThat(new ObjectInputStream(buffer.inputStream()).readObject()) + .isEqualTo(base); + } + + @Test public void serializationUsesJson() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); + + assertThat(buffer.indexOf(ByteString.encodeUtf8(base.toString()))) + .isPositive(); + } +}