From ae8ae764b14aee8938043032babcde54bf43c444 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Fri, 28 Jul 2017 10:21:41 +0800 Subject: [PATCH] Adds zipkin.internal.Span2Codec.JSON (#1671) This adds an internal copy of a span json codec issue #1499. This starts internal to ease review and allow incremental progress. The first consumer will be Elasticsearch, as this format removes nested queries. Note: this change also introduces json serialization of Span2, which allows future use in Spark. --- .../zipkin/benchmarks/CodecBenchmarks.java | 29 +- .../src/main/resources/span-client.json | 18 +- benchmarks/src/main/resources/span2.json | 32 ++ .../src/main/java/zipkin/internal/Span2.java | 36 ++- .../main/java/zipkin/internal/Span2Codec.java | 33 ++ .../java/zipkin/internal/Span2JsonCodec.java | 247 +++++++++++++++ .../zipkin/internal/Span2JsonCodecTest.java | 294 ++++++++++++++++++ .../test/java/zipkin/internal/Span2Test.java | 26 +- 8 files changed, 696 insertions(+), 19 deletions(-) create mode 100644 benchmarks/src/main/resources/span2.json create mode 100644 zipkin/src/main/java/zipkin/internal/Span2Codec.java create mode 100644 zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java create mode 100644 zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java diff --git a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java index b7214e3c627..7ec53da770a 100644 --- a/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java +++ b/benchmarks/src/main/java/zipkin/benchmarks/CodecBenchmarks.java @@ -41,6 +41,8 @@ import zipkin.Codec; import zipkin.Endpoint; import zipkin.Span; +import zipkin.internal.Span2; +import zipkin.internal.Span2Codec; /** * This compares the speed of the bundled java codec with the approach used in the scala @@ -154,6 +156,31 @@ public byte[] writeClientSpan_thrift_libthrift() throws TException { return serialize(clientSpanLibThrift); } + static final byte[] span2Json = read("/span2.json"); + static final Span2 span2 = Span2Codec.JSON.readSpan(span2Json); + static final List tenClientSpan2s = Collections.nCopies(10, span2); + static final byte[] tenClientSpan2sJson = Span2Codec.JSON.writeSpans(tenClientSpan2s); + + @Benchmark + public Span2 readClientSpan_json_span2() { + return Span2Codec.JSON.readSpan(span2Json); + } + + @Benchmark + public List readTenClientSpans_json_span2() { + return Span2Codec.JSON.readSpans(tenClientSpan2sJson); + } + + @Benchmark + public byte[] writeClientSpan_json_span2() { + return Span2Codec.JSON.writeSpan(span2); + } + + @Benchmark + public byte[] writeTenClientSpans_json_span2() { + return Span2Codec.JSON.writeSpans(tenClientSpan2s); + } + static final byte[] rpcSpanJson = read("/span-rpc.json"); static final Span rpcSpan = Codec.JSON.readSpan(rpcSpanJson); static final byte[] rpcSpanThrift = Codec.THRIFT.writeSpan(rpcSpan); @@ -227,7 +254,7 @@ public byte[] writeRpcV6Span_thrift_libthrift() throws TException { // Convenience main entry-point public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() - .include(".*" + CodecBenchmarks.class.getSimpleName() + ".*lientSpan.*") + .include(".*" + CodecBenchmarks.class.getSimpleName()) .build(); new Runner(opt).run(); diff --git a/benchmarks/src/main/resources/span-client.json b/benchmarks/src/main/resources/span-client.json index 701e7b185e7..466eb2a29ae 100644 --- a/benchmarks/src/main/resources/span-client.json +++ b/benchmarks/src/main/resources/span-client.json @@ -40,25 +40,16 @@ } ], "binaryAnnotations": [ - { - "key": "ca", - "value": true, - "endpoint": { - "serviceName": "frontend", - "ipv4": "127.0.0.1", - "port": 49504 - } - }, { "key": "clnt/finagle.version", - "value": "6.36.0", + "value": "6.45.0", "endpoint": { "serviceName": "frontend", "ipv4": "127.0.0.1" } }, { - "key": "http.uri", + "key": "http.path", "value": "/api", "endpoint": { "serviceName": "frontend", @@ -70,10 +61,9 @@ "value": true, "endpoint": { "serviceName": "backend", - "ipv4": "127.0.0.1", + "ipv4": "192.168.99.101", "port": 9000 } } - ], - "debug": false + ] } diff --git a/benchmarks/src/main/resources/span2.json b/benchmarks/src/main/resources/span2.json new file mode 100644 index 00000000000..e210b93413d --- /dev/null +++ b/benchmarks/src/main/resources/span2.json @@ -0,0 +1,32 @@ +{ + "traceId": "86154a4ba6e91385", + "parentId": "86154a4ba6e91385", + "id": "4d1e00c0db9010db", + "kind": "CLIENT", + "name": "get", + "timestamp": 1472470996199000, + "duration": 207000, + "localEndpoint": { + "serviceName": "frontend", + "ipv4": "127.0.0.1" + }, + "remoteEndpoint": { + "serviceName": "backend", + "ipv4": "192.168.99.101", + "port": 9000 + }, + "annotations": [ + { + "timestamp": 1472470996238000, + "value": "ws" + }, + { + "timestamp": 1472470996403000, + "value": "wr" + } + ], + "tags": { + "http.path": "/api", + "clnt/finagle.version": "6.45.0" + } +} diff --git a/zipkin/src/main/java/zipkin/internal/Span2.java b/zipkin/src/main/java/zipkin/internal/Span2.java index 94de3f57c74..cb1e6bef255 100644 --- a/zipkin/src/main/java/zipkin/internal/Span2.java +++ b/zipkin/src/main/java/zipkin/internal/Span2.java @@ -14,6 +14,9 @@ package zipkin.internal; import com.google.auto.value.AutoValue; +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.io.StreamCorruptedException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -27,6 +30,7 @@ import zipkin.Span; import zipkin.TraceKeys; +import static zipkin.internal.Util.UTF_8; import static zipkin.internal.Util.checkNotNull; import static zipkin.internal.Util.lowerHexToUnsignedLong; import static zipkin.internal.Util.sortedList; @@ -54,7 +58,8 @@ * and smaller data. */ @AutoValue -public abstract class Span2 { // TODO: make serializable when needed between stages in Spark jobs +public abstract class Span2 implements Serializable { // for Spark jobs + private static final long serialVersionUID = 0L; /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */ public abstract long traceIdHigh(); @@ -409,4 +414,33 @@ public Span2 build() { ); } } + + @Override + public String toString() { + return new String(Span2Codec.JSON.writeSpan(this), UTF_8); + } + + // Since this is an immutable object, and we have json handy, defer to a serialization proxy. + final Object writeReplace() throws ObjectStreamException { + return new SerializedForm(Span2Codec.JSON.writeSpan(this)); + } + + static final class SerializedForm implements Serializable { + private static final long serialVersionUID = 0L; + + private final byte[] bytes; + + SerializedForm(byte[] bytes) { + this.bytes = bytes; + } + + Object readResolve() throws ObjectStreamException { + try { + return Span2Codec.JSON.readSpan(bytes); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new StreamCorruptedException(e.getMessage()); + } + } + } } diff --git a/zipkin/src/main/java/zipkin/internal/Span2Codec.java b/zipkin/src/main/java/zipkin/internal/Span2Codec.java new file mode 100644 index 00000000000..083f5a915b0 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2Codec.java @@ -0,0 +1,33 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import java.util.List; + +/** Utilities for working with {@link Span2} */ +public interface Span2Codec { + Span2Codec JSON = new Span2JsonCodec(); + + /** Serialize a span recorded from instrumentation into its binary form. */ + byte[] writeSpan(Span2 span); + + /** Serialize a list of spans recorded from instrumentation into their binary form. */ + byte[] writeSpans(List spans); + + /** throws {@linkplain IllegalArgumentException} if a span couldn't be decoded */ + Span2 readSpan(byte[] bytes); + + /** throws {@linkplain IllegalArgumentException} if the spans couldn't be decoded */ + List readSpans(byte[] bytes); +} diff --git a/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java b/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java new file mode 100644 index 00000000000..8d3152144e4 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2JsonCodec.java @@ -0,0 +1,247 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.MalformedJsonException; +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import zipkin.internal.JsonCodec.JsonReaderAdapter; + +import static zipkin.internal.Buffer.asciiSizeInBytes; +import static zipkin.internal.Buffer.jsonEscapedSizeInBytes; +import static zipkin.internal.JsonCodec.ANNOTATION_WRITER; +import static zipkin.internal.JsonCodec.ENDPOINT_READER; +import static zipkin.internal.JsonCodec.ENDPOINT_WRITER; +import static zipkin.internal.JsonCodec.writeList; + +/** + * Internal type supporting codec operations in {@link Span2}. Design rationale is the same as + * {@link JsonCodec}. + */ +public final class Span2JsonCodec implements Span2Codec { + + @Override public Span2 readSpan(byte[] bytes) { + return JsonCodec.read(new SimpleSpanReader(), bytes); + } + + /** Serialize a span recorded from instrumentation into its binary form. */ + @Override public byte[] writeSpan(Span2 span) { + return JsonCodec.write(SPAN_WRITER, span); + } + + @Override public List readSpans(byte[] bytes) { + return JsonCodec.readList(new SimpleSpanReader(), bytes); + } + + @Override public byte[] writeSpans(List value) { + return writeList(SPAN_WRITER, value); + } + + static final class SimpleSpanReader implements JsonReaderAdapter { + Span2.Builder builder; + + @Override public Span2 fromJson(JsonReader reader) throws IOException { + if (builder == null) { + builder = Span2.builder(); + } else { + builder.clear(); + } + reader.beginObject(); + while (reader.hasNext()) { + String nextName = reader.nextName(); + if (nextName.equals("traceId")) { + builder.traceId(reader.nextString()); + } else if (nextName.equals("parentId") && reader.peek() != JsonToken.NULL) { + builder.parentId(reader.nextString()); + } else if (nextName.equals("id")) { + builder.id(reader.nextString()); + } else if (nextName.equals("kind")) { + builder.kind(Span2.Kind.valueOf(reader.nextString())); + } else if (nextName.equals("name") && reader.peek() != JsonToken.NULL) { + builder.name(reader.nextString()); + } else if (nextName.equals("timestamp") && reader.peek() != JsonToken.NULL) { + builder.timestamp(reader.nextLong()); + } else if (nextName.equals("duration") && reader.peek() != JsonToken.NULL) { + builder.duration(reader.nextLong()); + } else if (nextName.equals("localEndpoint") && reader.peek() != JsonToken.NULL) { + builder.localEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("remoteEndpoint") && reader.peek() != JsonToken.NULL) { + builder.remoteEndpoint(ENDPOINT_READER.fromJson(reader)); + } else if (nextName.equals("annotations")) { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + Long timestamp = null; + String value = null; + while (reader.hasNext()) { + nextName = reader.nextName(); + if (nextName.equals("timestamp")) { + timestamp = reader.nextLong(); + } else if (nextName.equals("value")) { + value = reader.nextString(); + } else { + reader.skipValue(); + } + } + reader.endObject(); + if (timestamp != null && value != null) builder.addAnnotation(timestamp, value); + } + reader.endArray(); + } else if (nextName.equals("tags")) { + reader.beginObject(); + while (reader.hasNext()) { + String key = reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + throw new MalformedJsonException("No value at " + reader.getPath()); + } + builder.putTag(key, reader.nextString()); + } + reader.endObject(); + } else if (nextName.equals("debug") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.debug(true); + } else if (nextName.equals("shared") && reader.peek() != JsonToken.NULL) { + if (reader.nextBoolean()) builder.shared(true); + } else { + reader.skipValue(); + } + } + reader.endObject(); + return builder.build(); + } + + @Override public String toString() { + return "Span2"; + } + } + + static final Buffer.Writer SPAN_WRITER = new Buffer.Writer() { + @Override public int sizeInBytes(Span2 value) { + int sizeInBytes = 0; + if (value.traceIdHigh() != 0) sizeInBytes += 16; + sizeInBytes += asciiSizeInBytes("{\"traceId\":\"") + 16 + 1; + if (value.parentId() != null) { + sizeInBytes += asciiSizeInBytes(",\"parentId\":\"") + 16 + 1; + } + sizeInBytes += asciiSizeInBytes(",\"id\":\"") + 16 + 1; + if (value.kind() != null) { + sizeInBytes += asciiSizeInBytes(",\"kind\":\""); + sizeInBytes += asciiSizeInBytes(value.kind().toString()) + 1; + } + if (value.name() != null) { + sizeInBytes += asciiSizeInBytes(",\"name\":\""); + sizeInBytes += jsonEscapedSizeInBytes(value.name()) + 1; + } + if (value.timestamp() != null) { + sizeInBytes += asciiSizeInBytes(",\"timestamp\":"); + sizeInBytes += asciiSizeInBytes(value.timestamp()); + } + if (value.duration() != null) { + sizeInBytes += asciiSizeInBytes(",\"duration\":"); + sizeInBytes += asciiSizeInBytes(value.duration()); + } + if (value.localEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"localEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.localEndpoint()); + } + if (value.remoteEndpoint() != null) { + sizeInBytes += asciiSizeInBytes(",\"remoteEndpoint\":"); + sizeInBytes += ENDPOINT_WRITER.sizeInBytes(value.remoteEndpoint()); + } + if (!value.annotations().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"annotations\":"); + sizeInBytes += JsonCodec.sizeInBytes(ANNOTATION_WRITER, value.annotations()); + } + if (!value.tags().isEmpty()) { + sizeInBytes += asciiSizeInBytes(",\"tags\":"); + sizeInBytes += 2; // curly braces + int tagCount = value.tags().size(); + if (tagCount > 1) sizeInBytes += tagCount - 1; // comma to join elements + for (Map.Entry entry : value.tags().entrySet()) { + sizeInBytes += 5; // 4 quotes and a colon + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getKey()); + sizeInBytes += Buffer.jsonEscapedSizeInBytes(entry.getValue()); + } + } + if (Boolean.TRUE.equals(value.debug())) { + sizeInBytes += asciiSizeInBytes(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + sizeInBytes += asciiSizeInBytes(",\"shared\":true"); + } + return ++sizeInBytes;// end curly-brace + } + + @Override public void write(Span2 value, Buffer b) { + b.writeAscii("{\"traceId\":\""); + if (value.traceIdHigh() != 0) { + b.writeLowerHex(value.traceIdHigh()); + } + b.writeLowerHex(value.traceId()).writeByte('"'); + if (value.parentId() != null) { + b.writeAscii(",\"parentId\":\"").writeLowerHex(value.parentId()).writeByte('"'); + } + b.writeAscii(",\"id\":\"").writeLowerHex(value.id()).writeByte('"'); + if (value.kind() != null) { + b.writeAscii(",\"kind\":\"").writeJsonEscaped(value.kind().toString()).writeByte('"'); + } + if (value.name() != null) { + b.writeAscii(",\"name\":\"").writeJsonEscaped(value.name()).writeByte('"'); + } + if (value.timestamp() != null) { + b.writeAscii(",\"timestamp\":").writeAscii(value.timestamp()); + } + if (value.duration() != null) { + b.writeAscii(",\"duration\":").writeAscii(value.duration()); + } + if (value.localEndpoint() != null) { + b.writeAscii(",\"localEndpoint\":"); + ENDPOINT_WRITER.write(value.localEndpoint(), b); + } + if (value.remoteEndpoint() != null) { + b.writeAscii(",\"remoteEndpoint\":"); + ENDPOINT_WRITER.write(value.remoteEndpoint(), b); + } + if (!value.annotations().isEmpty()) { + b.writeAscii(",\"annotations\":"); + writeList(ANNOTATION_WRITER, value.annotations(), b); + } + if (!value.tags().isEmpty()) { + b.writeAscii(",\"tags\":{"); + Iterator> i = value.tags().entrySet().iterator(); + while (i.hasNext()) { + Map.Entry entry = i.next(); + b.writeByte('"').writeJsonEscaped(entry.getKey()).writeAscii("\":\""); + b.writeJsonEscaped(entry.getValue()).writeByte('"'); + if (i.hasNext()) b.writeByte(','); + } + b.writeByte('}'); + } + if (Boolean.TRUE.equals(value.debug())) { + b.writeAscii(",\"debug\":true"); + } + if (Boolean.TRUE.equals(value.shared())) { + b.writeAscii(",\"shared\":true"); + } + b.writeByte('}'); + } + + @Override public String toString() { + return "Span2"; + } + }; +} diff --git a/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java b/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java new file mode 100644 index 00000000000..2fb11e91473 --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/Span2JsonCodecTest.java @@ -0,0 +1,294 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import zipkin.Constants; +import zipkin.Endpoint; +import zipkin.TraceKeys; + +import static org.assertj.core.api.Assertions.assertThat; +import static zipkin.internal.Util.UTF_8; + +public class Span2JsonCodecTest { + Span2JsonCodec codec = new Span2JsonCodec(); + + Endpoint frontend = Endpoint.create("frontend", 127 << 24 | 1); + Endpoint backend = Endpoint.builder() + .serviceName("backend") + .ipv4(192 << 24 | 168 << 16 | 99 << 8 | 101) + .port(9000) + .build(); + + Span2 span = Span2.builder() + .traceId("7180c278b62e8f6a216a2aea45d08fc9") + .parentId("6b221d5bc9e6496c") + .id("5b4185666d50f68b") + .name("get") + .kind(Span2.Kind.CLIENT) + .localEndpoint(frontend) + .remoteEndpoint(backend) + .timestamp(1472470996199000L) + .duration(207000L) + .addAnnotation(1472470996238000L, Constants.WIRE_SEND) + .addAnnotation(1472470996403000L, Constants.WIRE_RECV) + .putTag(TraceKeys.HTTP_PATH, "/api") + .putTag("clnt/finagle.version", "6.45.0") + .build(); + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Test public void spanRoundTrip() throws IOException { + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes() throws IOException { + assertThat(Span2JsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + @Test public void spanRoundTrip_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void spanRoundTrip_shared() throws IOException { + span = span.toBuilder().shared(true).build(); + byte[] bytes = codec.writeSpan(span); + assertThat(codec.readSpan(bytes)) + .isEqualTo(span); + } + + @Test public void sizeInBytes_64bitTraceId() throws IOException { + span = span.toBuilder().traceIdHigh(0L).build(); + assertThat(Span2JsonCodec.SPAN_WRITER.sizeInBytes(span)) + .isEqualTo(codec.writeSpan(span).length); + } + + /** + * This isn't a test of what we "should" accept as a span, rather that characters that trip-up + * json don't fail in codec. + */ + @Test public void specialCharsInJson() throws IOException { + // service name is surrounded by control characters + Span2 worstSpanInTheWorld = Span2.builder().traceId(1L).id(1L) + // name is terrible + .name(new String(new char[] {'"', '\\', '\t', '\b', '\n', '\r', '\f'})) + .localEndpoint(Endpoint.create(new String(new char[] {0, 'a', 1}), 0)) + // annotation value includes some json newline characters + .addAnnotation(1L, "\u2028 and \u2029") + // tag key includes a quote and value newlines + .putTag("\"foo", "Database error: ORA-00942:\u2028 and \u2029 table or view does not exist\n") + .build(); + + byte[] bytes = codec.writeSpan(worstSpanInTheWorld); + assertThat(codec.readSpan(bytes)) + .isEqualTo(worstSpanInTheWorld); + } + + @Test public void decentErrorMessageOnEmptyInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading Span2"); + + codec.readSpan(new byte[0]); + } + + @Test public void decentErrorMessageOnEmptyInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Empty input reading List"); + + codec.readSpans(new byte[0]); + } + + @Test public void decentErrorMessageOnMalformedInput_span() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading Span2 from "); + + codec.readSpan(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + /** + * Particulary, thrift can mistake malformed content as a huge list. Let's not blow up. + */ + @Test public void decentErrorMessageOnMalformedInput_spans() throws IOException { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Malformed reading List from "); + + codec.readSpans(new byte[] {'h', 'e', 'l', 'l', 'o'}); + } + + @Test public void spansRoundTrip() throws IOException { + List tenClientSpans = Collections.nCopies(10, span); + + byte[] bytes = codec.writeSpans(tenClientSpans); + assertThat(codec.readSpans(bytes)) + .isEqualTo(tenClientSpans); + } + + @Test public void writesTraceIdHighIntoTraceIdField() { + Span2 with128BitTraceId = Span2.builder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")) + .traceId(Util.lowerHexToUnsignedLong("6b221d5bc9e6496c")) + .localEndpoint(frontend) + .id(1).name("").build(); + + assertThat(new String(codec.writeSpan(with128BitTraceId), Util.UTF_8)) + .startsWith("{\"traceId\":\"48485a3953bb61246b221d5bc9e6496c\""); + } + + @Test public void readsTraceIdHighFromTraceIdField() { + byte[] with128BitTraceId = ("{\n" + + " \"traceId\": \"48485a3953bb61246b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + byte[] withLower64bitsTraceId = ("{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}").getBytes(UTF_8); + + assertThat(codec.readSpan(with128BitTraceId)) + .isEqualTo(codec.readSpan(withLower64bitsTraceId).toBuilder() + .traceIdHigh(Util.lowerHexToUnsignedLong("48485a3953bb6124")).build()); + } + + @Test public void ignoreNull_parentId() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"parentId\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_timestamp() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"timestamp\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_duration() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"duration\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_debug() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"debug\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_shared() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"shared\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_localEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"localEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void ignoreNull_remoteEndpoint() { + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"remoteEndpoint\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_traceId() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": null,\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\"\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void niceErrorOnNull_id() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Expected a string but was NULL"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": null\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } + + @Test public void missingValue() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("No value at $.tags.foo"); + + String json = "{\n" + + " \"traceId\": \"6b221d5bc9e6496c\",\n" + + " \"name\": \"get-traces\",\n" + + " \"id\": \"6b221d5bc9e6496c\",\n" + + " \"tags\": {\n" + + " \"foo\": NULL\n" + + " }\n" + + "}"; + + codec.readSpan(json.getBytes(UTF_8)); + } +} diff --git a/zipkin/src/test/java/zipkin/internal/Span2Test.java b/zipkin/src/test/java/zipkin/internal/Span2Test.java index 0deae2ca19c..05d0ce5b19c 100644 --- a/zipkin/src/test/java/zipkin/internal/Span2Test.java +++ b/zipkin/src/test/java/zipkin/internal/Span2Test.java @@ -13,6 +13,10 @@ */ package zipkin.internal; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import okio.Buffer; +import okio.ByteString; import org.junit.Test; import zipkin.Annotation; @@ -109,9 +113,25 @@ public class Span2Test { .isNull(); } - // TODO: toString_isJson + @Test public void toString_isJson() { + assertThat(base.toString()).hasToString( + "{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"localEndpoint\":{\"serviceName\":\"app\",\"ipv4\":\"172.17.0.2\",\"port\":8080}}" + ); + } + + @Test public void serialization() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); - // TODO: serialization + assertThat(new ObjectInputStream(buffer.inputStream()).readObject()) + .isEqualTo(base); + } + + @Test public void serializationUsesJson() throws Exception { + Buffer buffer = new Buffer(); + new ObjectOutputStream(buffer.outputStream()).writeObject(base); - // TODO: serializationUsesJson + assertThat(buffer.indexOf(ByteString.encodeUtf8(base.toString()))) + .isPositive(); + } }