/**
* 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;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.io.StreamCorruptedException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import zipkin.internal.Nullable;
import zipkin.storage.StorageComponent;
import static zipkin.internal.Util.UTF_8;
import static zipkin.internal.Util.checkNotNull;
import static zipkin.internal.Util.equal;
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.
*
* <p>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.
*
* <p>The root span is where {@link #parentId} is null; it usually has the longest {@link #duration} in the
* trace.
*
* <p>Span identifiers are packed into longs, but should be treated opaquely. ID encoding is
* 16 or 32 character lower-hex, to avoid signed interpretation.
*/
public final class Span implements Comparable<Span>, Serializable { // for Spark jobs
private static final long serialVersionUID = 0L;
/**
* When non-zero, the trace containing this span uses 128-bit trace identifiers.
*
* <p>{@code traceIdHigh} corresponds to the high bits in big-endian format and {@link #traceId}
* corresponds to the low bits.
*
* <p>Ex. to convert the two fields to a 128bit opaque id array, you'd use code like below.
* <pre>{@code
* ByteBuffer traceId128 = ByteBuffer.allocate(16);
* traceId128.putLong(span.traceIdHigh);
* traceId128.putLong(span.traceId);
* traceBytes = traceId128.array();
* }</pre>
*
* @see StorageComponent.Builder#strictTraceId(boolean)
*/
public final long traceIdHigh;
/**
* Unique 8-byte identifier for a trace, set on all spans within it.
*
* @see #traceIdHigh for notes about 128-bit trace identifiers
*/
public final long traceId;
/**
* Span name in lowercase, rpc method for example.
*
* <p>Conventionally, when the span name isn't known, name = "unknown".
*/
public final String name;
/**
* Unique 8-byte identifier of this span within a trace.
*
* <p>A span is uniquely identified in storage by ({@linkplain #traceId}, {@code #id}).
*/
public final long id;
/**
* The parent's {@link #id} or null if this the root span in a trace.
*/
@Nullable
public final Long parentId;
/**
* Epoch microseconds of the start of this span, possibly absent if this an incomplete span.
*
* <p>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.
*
* <p>For compatibility with instrumentation that precede this field, collectors or span stores
* can derive this via Annotation.timestamp. For example, {@link Constants#SERVER_RECV}.timestamp
* or {@link Constants#CLIENT_SEND}.timestamp.
*
* <p>Timestamp is nullable for input only. Spans without a timestamp cannot be presented in a
* timeline: Span stores should not output spans missing a timestamp.
*
* <p>There are two known edge-cases where this could be absent: both cases exist when a
* collector receives a span in parts and a binary annotation precedes a timestamp. This is
* possible when..
* <ul>
* <li>The span is in-flight (ex not yet received a timestamp)</li>
* <li>The span's start event was lost</li>
* </ul>
*/
@Nullable
public final Long timestamp;
/**
* Measurement in microseconds of the critical path, if known. Durations of less than one
* microsecond must be rounded up to 1 microsecond.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>This field is i64 vs i32 to support spans longer than 35 minutes.
*/
@Nullable
public final Long duration;
/**
* Associates events that explain latency with a timestamp.
*
* <p>Unlike log statements, annotations are often codes: for example {@link
* Constants#SERVER_RECV}. Annotations are sorted ascending by timestamp.
*/
public final List<Annotation> annotations;
/**
* Tags a span with context, usually to support query or aggregation.
*
* <p>example, a binary annotation key could be {@link TraceKeys#HTTP_PATH "http.path"}.
*/
public final List<BinaryAnnotation> binaryAnnotations;
/**
* True is a request to store this span even if it overrides sampling policy.
*/
@Nullable
public final Boolean debug;
Span(Builder builder) {
this.traceId = builder.traceId;
this.traceIdHigh = builder.traceIdHigh != null ? builder.traceIdHigh : 0L;
this.name = checkNotNull(builder.name, "name").isEmpty() ? ""
: builder.name.toLowerCase(Locale.ROOT);
this.id = builder.id;
this.parentId = builder.parentId;
this.timestamp = builder.timestamp;
this.duration = builder.duration;
this.annotations = sortedList(builder.annotations);
this.binaryAnnotations = sortedList(builder.binaryAnnotations);
this.debug = builder.debug;
}
public Builder toBuilder() {
return new Builder(this);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
Long traceId;
Long traceIdHigh;
String name;
Long id;
Long parentId;
Long timestamp;
Long duration;
// Not LinkedHashSet, as the constructor makes a sorted copy
HashSet<Annotation> annotations;
HashSet<BinaryAnnotation> binaryAnnotations;
Boolean debug;
boolean isClientSpan; // internal
Builder() {
}
public Builder clear() {
traceId = null;
traceIdHigh = null;
name = null;
id = null;
parentId = null;
timestamp = null;
if (annotations != null) annotations.clear();
if (binaryAnnotations != null) binaryAnnotations.clear();
debug = null;
isClientSpan = false;
return this;
}
Builder(Span source) {
this.traceId = source.traceId;
this.traceIdHigh = source.traceIdHigh;
this.name = source.name;
this.id = source.id;
this.parentId = source.parentId;
this.timestamp = source.timestamp;
this.duration = source.duration;
if (!source.annotations.isEmpty()) {
this.annotations(source.annotations);
}
if (!source.binaryAnnotations.isEmpty()) {
this.binaryAnnotations(source.binaryAnnotations);
}
this.debug = source.debug;
}
public Builder merge(Span that) {
if (this.traceId == null) {
this.traceId = that.traceId;
}
if (this.traceIdHigh == null || this.traceIdHigh == 0) {
this.traceIdHigh = that.traceIdHigh;
}
if (this.name == null || this.name.length() == 0 || this.name.equals("unknown")) {
this.name = that.name;
}
if (this.id == null) {
this.id = that.id;
}
if (this.parentId == null) {
this.parentId = that.parentId;
}
// When we move to span model 2, remove this code in favor of using Span.kind == CLIENT
boolean thisIsClientSpan = this.isClientSpan;
boolean thatIsClientSpan = false;
for (Annotation a : that.annotations) {
if (a.value.equals(Constants.CLIENT_SEND)) thatIsClientSpan = true;
addAnnotation(a);
}
for (BinaryAnnotation a : that.binaryAnnotations) {
addBinaryAnnotation(a);
}
// Single timestamp makes duration easy: just choose max
if (this.timestamp == null || that.timestamp == null || this.timestamp.equals(
that.timestamp)) {
this.timestamp = this.timestamp != null ? this.timestamp : that.timestamp;
if (this.duration == null) {
this.duration = that.duration;
} else if (that.duration != null) {
this.duration = Math.max(this.duration, that.duration);
}
} else {
// We have 2 different timestamps. If we have client data in either one of them, use that,
// else set timestamp and duration to null
if (thatIsClientSpan) {
this.timestamp = that.timestamp;
this.duration = that.duration;
} else if (!thisIsClientSpan) {
this.timestamp = null;
this.duration = null;
}
}
if (this.debug == null) {
this.debug = that.debug;
}
return this;
}
/** @see Span#name */
public Builder name(String name) {
this.name = name;
return this;
}
/** @see Span#traceId */
public Builder traceId(long traceId) {
this.traceId = traceId;
return this;
}
/** @see Span#traceIdHigh */
public Builder traceIdHigh(long traceIdHigh) {
this.traceIdHigh = traceIdHigh;
return this;
}
/** @see Span#id */
public Builder id(long id) {
this.id = id;
return this;
}
/** @see Span#parentId */
public Builder parentId(@Nullable Long parentId) {
this.parentId = parentId;
return this;
}
/** @see Span#timestamp */
public Builder timestamp(@Nullable Long timestamp) {
this.timestamp = timestamp != null && timestamp == 0L ? null : timestamp;
return this;
}
/** @see Span#duration */
public Builder duration(@Nullable Long duration) {
this.duration = duration != null && duration == 0L ? null : duration;
return this;
}
/**
* Replaces currently collected annotations.
*
* @see Span#annotations
*/
public Builder annotations(Collection<Annotation> annotations) {
if (this.annotations != null) this.annotations.clear();
for (Annotation a : annotations) addAnnotation(a);
return this;
}
/** @see Span#annotations */
public Builder addAnnotation(Annotation annotation) {
if (annotations == null) annotations = new HashSet<>();
if (annotation.value.equals(Constants.CLIENT_SEND)) isClientSpan = true;
annotations.add(annotation);
return this;
}
/**
* Replaces currently collected binary annotations.
*
* @see Span#binaryAnnotations
*/
public Builder binaryAnnotations(Collection<BinaryAnnotation> binaryAnnotations) {
if (this.binaryAnnotations != null) this.binaryAnnotations.clear();
for (BinaryAnnotation b : binaryAnnotations) addBinaryAnnotation(b);
return this;
}
/** @see Span#binaryAnnotations */
public Builder addBinaryAnnotation(BinaryAnnotation binaryAnnotation) {
if (binaryAnnotations == null) binaryAnnotations = new HashSet<>();
binaryAnnotations.add(binaryAnnotation);
return this;
}
/** @see Span#debug */
public Builder debug(@Nullable Boolean debug) {
this.debug = debug;
return this;
}
public Span build() {
return new Span(this);
}
}
@Override
public String toString() {
return new String(Codec.JSON.writeSpan(this), UTF_8);
}
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof Span)) return false;
Span that = (Span) o;
return (this.traceIdHigh == that.traceIdHigh)
&& (this.traceId == that.traceId)
&& (this.name.equals(that.name))
&& (this.id == that.id)
&& equal(this.parentId, that.parentId)
&& equal(this.timestamp, that.timestamp)
&& equal(this.duration, that.duration)
&& (this.annotations.equals(that.annotations))
&& (this.binaryAnnotations.equals(that.binaryAnnotations))
&& equal(this.debug, that.debug);
}
@Override
public int hashCode() {
int h = 1;
h *= 1000003;
h ^= (traceIdHigh >>> 32) ^ traceIdHigh;
h *= 1000003;
h ^= (traceId >>> 32) ^ traceId;
h *= 1000003;
h ^= name.hashCode();
h *= 1000003;
h ^= (id >>> 32) ^ id;
h *= 1000003;
h ^= (parentId == null) ? 0 : parentId.hashCode();
h *= 1000003;
h ^= (timestamp == null) ? 0 : timestamp.hashCode();
h *= 1000003;
h ^= (duration == null) ? 0 : duration.hashCode();
h *= 1000003;
h ^= annotations.hashCode();
h *= 1000003;
h ^= binaryAnnotations.hashCode();
h *= 1000003;
h ^= (debug == null) ? 0 : debug.hashCode();
return h;
}
/** Compares by {@link #timestamp}, then {@link #name}. */
@Override
public int compareTo(Span that) {
if (this == that) return 0;
long x = this.timestamp == null ? Long.MIN_VALUE : this.timestamp;
long y = that.timestamp == null ? Long.MIN_VALUE : that.timestamp;
int byTimestamp = x < y ? -1 : x == y ? 0 : 1;
if (byTimestamp != 0) return byTimestamp;
return this.name.compareTo(that.name);
}
/** Returns {@code $traceId.$spanId<:$parentId or $spanId} */
public String idString() {
int resultLength = (3 * 16) + 3; // 3 ids and the constant delimiters
if (traceIdHigh != 0) resultLength += 16;
char[] result = new char[resultLength];
int pos = 0;
if (traceIdHigh != 0) {
writeHexLong(result, pos, traceIdHigh);
pos += 16;
}
writeHexLong(result, pos, traceId);
pos += 16;
result[pos++] = '.';
writeHexLong(result, pos, id);
pos += 16;
result[pos++] = '<';
result[pos++] = ':';
writeHexLong(result, pos, parentId != null ? parentId : id);
return new String(result);
}
/** Returns the distinct {@link Endpoint#serviceName service names} that logged to this span. */
public Set<String> serviceNames() {
Set<String> result = new HashSet<>();
for (Annotation a : annotations) {
if (a.endpoint == null) continue;
if (a.endpoint.serviceName.isEmpty()) continue;
result.add(a.endpoint.serviceName);
}
for (BinaryAnnotation a : binaryAnnotations) {
if (a.endpoint == null) continue;
if (a.endpoint.serviceName.isEmpty()) continue;
result.add(a.endpoint.serviceName);
}
return result;
}
// Since this is an immutable object, and we have thrift handy, defer to a serialization proxy.
final Object writeReplace() throws ObjectStreamException {
return new SerializedForm(Codec.THRIFT.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 Codec.THRIFT.readSpan(bytes);
} catch (IllegalArgumentException e) {
throw new StreamCorruptedException(e.getMessage());
}
}
}
}