From d81aedeacec39cc4e71738e94160ee2f655368ac Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Thu, 27 Jul 2017 14:43:01 +0800 Subject: [PATCH] Adds zipkin.internal.Span2 This adds an internal copy of the new span data structure defined in issue 1499. This is starting internal to ease review and allow incremental progress. The first consumer will be dependency linking. --- .../zipkin/benchmarks/SpanBenchmarks.java | 36 ++ zipkin/pom.xml | 6 + .../src/main/java/zipkin/internal/Span2.java | 412 ++++++++++++++++++ .../test/java/zipkin/internal/Span2Test.java | 117 +++++ 4 files changed, 571 insertions(+) create mode 100644 zipkin/src/main/java/zipkin/internal/Span2.java create mode 100644 zipkin/src/test/java/zipkin/internal/Span2Test.java diff --git a/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java b/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java index e8a46b0f329..a50b552f4cd 100644 --- a/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java +++ b/benchmarks/src/main/java/zipkin/benchmarks/SpanBenchmarks.java @@ -34,6 +34,7 @@ import zipkin.Endpoint; import zipkin.Span; import zipkin.TraceKeys; +import zipkin.internal.Span2; import zipkin.internal.Util; @Measurement(iterations = 5, time = 1) @@ -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 Span2.Builder shared2Builder; public SpanBenchmarks() { sharedBuilder = buildClientOnlySpan(Span.builder()).toBuilder(); + shared2Builder = buildClientOnlySpan2().toBuilder(); } @Benchmark @@ -104,6 +107,39 @@ public Span buildClientOnlySpan_clear() { return buildClientOnlySpan(sharedBuilder.clear()); } + @Benchmark + public Span2 buildClientOnlySpan2() { + return buildClientOnlySpan2(Span2.builder()); + } + + static Span2 buildClientOnlySpan2(Span2.Builder builder) { + return builder + .traceId(traceId) + .parentId(traceId) + .id(spanId) + .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(); + } + + @Benchmark + public Span2 buildClientOnlySpan2_clear() { + return buildClientOnlySpan2(shared2Builder.clear()); + } + + @Benchmark + public Span2 buildClientOnlySpan2_clone() { + return shared2Builder.clone().build(); + } + @Benchmark public Span buildRpcSpan() { return Span.builder() // web calls app 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/Span2.java b/zipkin/src/main/java/zipkin/internal/Span2.java new file mode 100644 index 00000000000..94de3f57c74 --- /dev/null +++ b/zipkin/src/main/java/zipkin/internal/Span2.java @@ -0,0 +1,412 @@ +/** + * 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.auto.value.AutoValue; +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.Constants; +import zipkin.Endpoint; +import zipkin.Span; +import zipkin.TraceKeys; + +import static zipkin.internal.Util.checkNotNull; +import static zipkin.internal.Util.lowerHexToUnsignedLong; +import static zipkin.internal.Util.sortedList; +import static zipkin.internal.Util.writeHexLong; + +/** + * + * A trace is a series of spans (often RPC calls) which form a latency tree. + * + *

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

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

Span identifiers are packed into longs, but should be treated opaquely. ID encoding is + * 16 or 32 character lower-hex, to avoid signed interpretation. * This is a single-host view of a {@link Span}: the primary way tracers record data. + * + *

Relationship to {@link zipkin.Span}

+ *

This type is intended to replace use of {@link zipkin.Span}. Particularly, tracers represent 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 zipkin.Span span} does. This results in simpler + * and smaller data. + */ +@AutoValue +public abstract class Span2 { // TODO: make serializable when needed between stages in Spark jobs + + /** 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 #duration() + */ + @Nullable public abstract Long timestamp(); + + /** + * Measurement in microseconds of the critical path, if known. Durations of less than one + * microsecond must be rounded up to 1 microsecond. + * + *

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

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

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

    This field is i64 vs i32 to support spans longer than 35 minutes. + */ + @Nullable public abstract Long duration(); + + /** + * 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(); + + /** Returns the hex representation of the span's trace ID */ + public String traceIdString() { + if (traceIdHigh() != 0) { + char[] result = new char[32]; + writeHexLong(result, 0, traceIdHigh()); + writeHexLong(result, 16, traceId()); + return new String(result); + } + char[] result = new char[16]; + writeHexLong(result, 0, traceId()); + return new String(result); + } + + 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 timestamp; + Long duration; + 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; + timestamp = null; + duration = 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.timestamp = timestamp; + result.duration = duration; + 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(Span2 source) { + traceId = source.traceId(); + parentId = source.parentId(); + id = source.id(); + kind = source.kind(); + name = source.name(); + timestamp = source.timestamp(); + duration = source.duration(); + 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 Span2#traceIdHigh */ + public Builder traceIdHigh(long traceIdHigh) { + this.traceIdHigh = traceIdHigh; + return this; + } + + /** @see Span2#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 Span2#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 Span2#id */ + public Builder id(long id) { + this.id = id; + return this; + } + + /** @see Span2#kind */ + public Builder kind(@Nullable Kind kind) { + this.kind = kind; + return this; + } + + /** @see Span2#name */ + public Builder name(@Nullable String name) { + this.name = name == null || name.isEmpty() ? null : name.toLowerCase(Locale.ROOT); + return this; + } + + /** @see Span2#timestamp */ + public Builder timestamp(@Nullable Long timestamp) { + if (timestamp != null && timestamp == 0L) timestamp = null; + this.timestamp = timestamp; + return this; + } + + /** @see Span2#duration */ + public Builder duration(@Nullable Long duration) { + if (duration != null && duration == 0L) duration = null; + this.duration = duration; + return this; + } + + /** @see Span2#localEndpoint */ + public Builder localEndpoint(@Nullable Endpoint localEndpoint) { + this.localEndpoint = localEndpoint; + return this; + } + + /** @see Span2#remoteEndpoint */ + public Builder remoteEndpoint(@Nullable Endpoint remoteEndpoint) { + this.remoteEndpoint = remoteEndpoint; + return this; + } + + /** @see Span2#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 Span2#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 Span2#debug */ + public Builder debug(@Nullable Boolean debug) { + this.debug = debug; + return this; + } + + /** @see Span2#shared */ + public Builder shared(@Nullable Boolean shared) { + this.shared = shared; + return this; + } + + public Span2 build() { + return new AutoValue_Span2( + traceIdHigh, + traceId, + parentId, + id, + kind, + name, + timestamp, + duration, + localEndpoint, + remoteEndpoint, + sortedList(annotations), + tags == null ? Collections.emptyMap() : new LinkedHashMap<>(tags), + debug, + shared + ); + } + } +} diff --git a/zipkin/src/test/java/zipkin/internal/Span2Test.java b/zipkin/src/test/java/zipkin/internal/Span2Test.java new file mode 100644 index 00000000000..0deae2ca19c --- /dev/null +++ b/zipkin/src/test/java/zipkin/internal/Span2Test.java @@ -0,0 +1,117 @@ +/** + * Copyright 2015-2017 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package zipkin.internal; + +import org.junit.Test; +import zipkin.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 Span2Test { + Span2 base = Span2.builder().traceId(1L).id(1L).localEndpoint(APP_ENDPOINT).build(); + + @Test public void traceIdString() { + Span2 with128BitId = Span2.builder() + .traceId(Util.lowerHexToUnsignedLong("48485a3953bb6124")) + .id(1) + .name("foo").build(); + + assertThat(with128BitId.traceIdString()) + .isEqualTo("48485a3953bb6124"); + } + + @Test public void traceIdString_high() { + Span2 with128BitId = Span2.builder() + .traceId(Util.lowerHexToUnsignedLong("48485a3953bb6124")) + .traceIdHigh(Util.lowerHexToUnsignedLong("463ac35c9f6413ad")) + .id(1) + .name("foo").build(); + + assertThat(with128BitId.traceIdString()) + .isEqualTo("463ac35c9f6413ad48485a3953bb6124"); + } + + @Test public void spanNamesLowercase() { + assertThat(base.toBuilder().name("GET").build().name()) + .isEqualTo("get"); + } + + @Test public void annotationsSortByTimestamp() { + Span2 span = base.toBuilder() + .addAnnotation(2L, "foo") + .addAnnotation(1L, "foo") + .build(); + + // note: annotations don't also have endpoints, as it is implicit to Span2.localEndpoint + assertThat(span.annotations()).containsExactly( + Annotation.create(1L, "foo", null), + Annotation.create(2L, "foo", null) + ); + } + + @Test public void putTagOverwritesValue() { + Span2 span = base.toBuilder() + .putTag("foo", "bar") + .putTag("foo", "qux") + .build(); + + assertThat(span.tags()).containsExactly( + entry("foo", "qux") + ); + } + + @Test public void clone_differentCollections() { + Span2.Builder builder = base.toBuilder() + .addAnnotation(1L, "foo") + .putTag("foo", "qux"); + + Span2.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() + ); + } + + /** Catches common error when zero is passed instead of null for a timestamp */ + @Test public void coercesZeroTimestampsToNull() { + Span2 span = base.toBuilder() + .timestamp(0L) + .duration(0L) + .build(); + + assertThat(span.timestamp()) + .isNull(); + assertThat(span.duration()) + .isNull(); + } + + // TODO: toString_isJson + + // TODO: serialization + + // TODO: serializationUsesJson +}