com.nike.wingtips.Span Maven / Gradle / Ivy
Show all versions of wingtips-core Show documentation
package com.nike.wingtips;
import com.nike.wingtips.http.HttpRequestTracingUtils;
import com.nike.wingtips.util.TracerManagedSpanStatus;
import com.nike.wingtips.util.parser.SpanParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents some logical "unit of work" that is part of the larger distributed trace. A given request's trace tree is made up of all the spans with the same {@link #traceId}
* and the spans are connected via parent-child relationship. The first span in the trace tree covers the entire distributed trace - it is often referred to as the root span.
*
* For any given service call in a distributed trace there is usually one span covering the full call; that service-level span is started when a request first hits the service
* and {@link #complete()} is called when the response is sent to the caller, and at that point the resulting completed span is logged or otherwise sent to a span collector
* of some sort so the span's data can be used later for investigations/aggregations/statistics.
*
*
Extra "sub-spans" can be generated by a service if it wants additional tracing around specific functions inside the service. One sub-span example might be to create a sub-span
* in service A when it makes a downstream call to service B. You could then subtract {@link #getDurationNanos()} for service B's service-level span from the time spent in
* service A's downstream call span to determine what the network lag was for that call.
*
*
Spans support Java try-with-resources statements to help guarantee proper usage in blocking/non-async scenarios
* (for asynchronous scenarios please refer to the
* asynchronous usage section of the Wingtips readme). Here are some examples.
*
*
Overall request span using try-with-resources:
*
* try(Span requestSpan = Tracer.getInstance().startRequestWith*(...)) {
* // Traced blocking code for overall request (not asynchronous) goes here ...
* }
* // No finally block needed to properly complete the overall request span
*
*
* Subspan using try-with-resources:
*
* try (Span subspan = Tracer.getInstance().startSubSpan(...)) {
* // Traced blocking code for subspan (not asynchronous) goes here ...
* }
* // No finally block needed to properly complete the subspan
*
*
* @author Nic Munroe
*/
@SuppressWarnings("WeakerAccess")
public class Span implements Closeable, Serializable {
private final String traceId;
private final String spanId;
private final String parentSpanId;
private String spanName;
private final boolean sampleable;
private final String userId;
private final SpanPurpose spanPurpose;
private final long spanStartTimeEpochMicros;
private final long spanStartTimeNanos;
// The default initial capacity (16) and load factor (0.75) is enough space to handle the ZipkinHttpTagStrategy
// tags plus a few extra without any internal rehashing of the map as tags are added.
// This seems about right for most use cases, so we'll leave the default settings.
private final Map tags = new LinkedHashMap<>();
private final Map unmodifiableTags = Collections.unmodifiableMap(tags);
// The default initial capacity (10) seems ok for the annotations list.
private final List annotations = new ArrayList<>();
private final List unmodifiableAnnotations = Collections.unmodifiableList(annotations);
private Long durationNanos;
// Used to prevent two threads from trying to close the span at the same time.
private final AtomicBoolean completedFlag = new AtomicBoolean(false);
private String cachedJsonRepresentation;
private String cachedKeyValueRepresentation;
/**
* Represents a span's intended purpose in the distributed trace. This is not strictly necessary for distributed tracing to work, but it
* does help give visualizers a hint on the exact relationships between spans and can help sharpen trace analysis.
*/
public enum SpanPurpose {
/**
* Intended for spans that are generated by a server in response to receiving an inbound request from another process. This and {@code CLIENT} are
* mutually exclusive - if a server request also needs to make an outbound call to a different system then a sub-span should be created specifically
* to surround the client call (and only the client call) with a {@code CLIENT} span. {@code SERVER} spans are created automatically when you call
* any of the {@code Tracer.startRequestWith*(...)} methods.
*/
SERVER,
/**
* Intended for spans that represent an outbound/downstream call that crosses network/system/process boundaries. This and {@code SERVER} are mutually
* exclusive - {@code CLIENT} spans should be used to surround the outbound client call *ONLY*, therefore if you are in a {@code SERVER} span
* that needs to make an outbound {@code CLIENT} call you should surround the {@code CLIENT} call (and only the client call) with a sub-span.
* {@code CLIENT} sub-spans can be easily created by calling {@code Tracer.startSubSpan(spanName, SpanPurpose.CLIENT)}
*/
CLIENT,
/**
* Intended for spans that are not related to any outside-the-process behavior - these spans do not receive inbound requests or make outbound requests.
* They are sub-spans that are 100% contained in the existing process.
*/
LOCAL_ONLY,
/**
* If all else fails then a span will be marked with this. It means the span purpose was unspecified.
*/
UNKNOWN
}
/**
*
* The full constructor allowing you to set all fields representing a span's state. The traceId, spanId, and spanName fields cannot be null (an
* {@link IllegalArgumentException} will be thrown if any are null). This method is public to allow full flexibility, however in practice you should use
* one of the provided {@link #generateRootSpanForNewTrace(String, SpanPurpose)} or {@link #generateChildSpan(String, SpanPurpose)} helper methods, or the
* {@link Builder} if you need more flexibility than the helper methods. If you pass in null for {@code spanPurpose} then {@link SpanPurpose#UNKNOWN}
* will be used.
*
* WARNING: When deserializing a span that was initially started on a different JVM, you *must* pass in null for {@code spanStartTimeNanos}. Otherwise
* timing will be completely broken since {@link System#nanoTime()} is not comparable across JVMs.
*/
public Span(String traceId, String parentSpanId, String spanId, String spanName, boolean sampleable, String userId,
SpanPurpose spanPurpose, long spanStartTimeEpochMicros, Long spanStartTimeNanos, Long durationNanos,
Map tags, List annotations
) {
if (traceId == null)
throw new IllegalArgumentException("traceId cannot be null");
if (spanId == null)
throw new IllegalArgumentException("spanId cannot be null");
if (spanName == null)
throw new IllegalArgumentException("spanName cannot be null");
this.traceId = traceId;
this.spanId = spanId;
this.parentSpanId = parentSpanId;
this.spanName = spanName;
this.sampleable = sampleable;
this.userId = userId;
this.spanStartTimeEpochMicros = spanStartTimeEpochMicros;
if (spanStartTimeNanos == null) {
// No start time nanos was sent. Calculate it as best we can based on spanStartTimeEpochMicros, the current epoch time, and current nano time.
long currentTimeEpochMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
long currentDurationMicros = currentTimeEpochMicros - spanStartTimeEpochMicros;
long nanoStartTimeOffset = TimeUnit.MICROSECONDS.toNanos(currentDurationMicros);
spanStartTimeNanos = System.nanoTime() - nanoStartTimeOffset;
}
this.spanStartTimeNanos = spanStartTimeNanos;
this.durationNanos = durationNanos;
if (spanPurpose == null)
spanPurpose = SpanPurpose.UNKNOWN;
this.spanPurpose = spanPurpose;
if(tags != null) {
this.tags.putAll(tags);
}
if (annotations != null) {
this.annotations.addAll(annotations);
}
}
// For deserialization only - this will create an invalid span object and is only here to support deserializers that need a default constructor but set the fields directly (e.g. Jackson)
protected Span() {
this(
"PLACEHOLDER", null, "PLACEHOLDER", "PLACEHOLDER", false, null, SpanPurpose.UNKNOWN, -1, -1L, -1L, null,
null
);
}
/**
* @param spanName The {@link Span#getSpanName()} to initialize the builder with.
* @param spanPurpose The {@link SpanPurpose} to initialize the builder with. See the javadocs for {@link SpanPurpose} for full details on what each enum option
* means. If you pass in null for this then {@link SpanPurpose#UNKNOWN} will be used.
* @return A builder setup to generate an uncompleted root span for a new trace. Since it is a root span it will have a null {@link #getParentSpanId()}.
* If no further changes are made to the returned builder then when {@link Builder#build()} is called the resulting span's {@link #getTraceId()}
* and {@link #getSpanId()} will be randomly generated, and {@link #getSpanStartTimeEpochMicros()} and {@link #getSpanStartTimeNanos()} will be set to
* the appropriate values based on when {@link Builder#build()} is called. If further tweaks to the span are necessary you can adjust the builder
* before calling {@link Builder#build()} (e.g. setting a user ID via {@link Builder#withUserId(String)}.
*/
public static Builder generateRootSpanForNewTrace(String spanName, SpanPurpose spanPurpose) {
return Span.newBuilder(spanName, spanPurpose);
}
/**
* @param spanName The {@link Span#getSpanName()} to use for the new child sub-span.
* @param spanPurpose The {@link SpanPurpose} for the new child span. See the javadocs for {@link SpanPurpose} for
* full details on what each enum option means. If you pass in null for this then {@link SpanPurpose#UNKNOWN} will
* be used.
* @return A new uncompleted span representing a child of this instance. The returned instance's {@link
* #getParentSpanId()} will be this instance's {@link #getSpanId()}, its {@link #getSpanName()} will be the given
* value, its {@link #getSpanId()} will be randomly generated, and its {@link #getSpanStartTimeEpochMicros()} and
* {@link #getSpanStartTimeNanos()} values will be set to the appropriate values based on when this method is
* called. It will share this instance's {@link #getTraceId()}, {@link #isSampleable()}, and {@link #getUserId()}
* values.
*/
public Span generateChildSpan(String spanName, SpanPurpose spanPurpose) {
// Rather than losing precision for the child span's start time by using System.currentTimeMillis(), we
// calculate the child's start time based on this instance's (the parent's) start time epoch micros,
// plus a micro offset calculated from the current nano time minus the parent's nano start time.
// NOTE: This could cause a little drift from System.currentTimeMillis() if the parent span has been running
// for several minutes before the child span is created, but it's still probably the right tradeoff and
// will prevent surprising results when visualizing things (e.g. a child annotation appearing "after" a
// parent annotation, even though the child event must have strictly happened before the parent event in
// reality). Between systems, those kinds of oddities are easily explained by clock drift. But on the
// same system, it can be a head-scratcher.
long currentNanoTime = System.nanoTime();
long nanosSinceParentStart = currentNanoTime - this.spanStartTimeNanos;
long childStartTimeEpochMicros =
this.spanStartTimeEpochMicros + TimeUnit.NANOSECONDS.toMicros(nanosSinceParentStart);
String parentSpanIdForChild = this.getSpanId();
boolean addBadParentIdIndicatorTag = false;
if (HttpRequestTracingUtils.hasInvalidSpanIdBecauseCallerDidNotSendOne(this)) {
parentSpanIdForChild = null;
addBadParentIdIndicatorTag = true;
}
Builder childBuilder = Span.newBuilder(spanName, spanPurpose)
.withTraceId(this.getTraceId())
.withSampleable(this.isSampleable())
.withUserId(this.getUserId())
.withParentSpanId(parentSpanIdForChild)
.withSpanId(TraceAndSpanIdGenerator.generateId())
.withSpanStartTimeEpochMicros(childStartTimeEpochMicros)
.withSpanStartTimeNanos(currentNanoTime)
.withDurationNanos(null);
if (addBadParentIdIndicatorTag) {
childBuilder.withTag(
HttpRequestTracingUtils.CHILD_OF_SPAN_FROM_HEADERS_WHERE_CALLER_DID_NOT_SEND_SPAN_ID_TAG_KEY,
"true"
);
}
return childBuilder.build();
}
/**
* @param spanName The {@link Span#getSpanName()} to initialize the builder with.
* @param spanPurpose The {@link SpanPurpose} to initialize the builder with. See the javadocs for {@link SpanPurpose} for full details on what each enum
* option means. If you pass in null for this then {@link SpanPurpose#UNKNOWN} will be used.
* @return A new span builder with nothing populated beyond the given span name. Based on the behavior of {@link Builder#build()}, if you were to build it
* immediately you'd end up with an uncompleted root span for a new trace (same behavior as {@link #generateRootSpanForNewTrace(String, SpanPurpose)}.
* You are free to adjust any values before building however, so you can setup the builder to represent any span you want.
*/
public static Builder newBuilder(String spanName, SpanPurpose spanPurpose) {
return new Builder(spanName, spanPurpose);
}
/**
* @return A new span builder that has all its fields copied from the given {@link Span}. If you call {@link Builder#build()} immediately on the returned builder you'd
* end up with an exact duplicate of the given span.
*/
public static Builder newBuilder(Span copy) {
Builder builder = new Builder(copy.spanName, copy.spanPurpose);
builder.traceId = copy.traceId;
builder.spanId = copy.spanId;
builder.parentSpanId = copy.parentSpanId;
builder.sampleable = copy.sampleable;
builder.userId = copy.userId;
builder.spanStartTimeEpochMicros = copy.spanStartTimeEpochMicros;
builder.spanStartTimeNanos = copy.spanStartTimeNanos;
builder.durationNanos = copy.durationNanos;
builder.tags = new LinkedHashMap<>(copy.tags);
builder.annotations = new ArrayList<>(copy.annotations);
return builder;
}
/**
* @return The ID associated with the overall distributed trace - a.k.a. the trace tree ID. All spans in a distributed trace will share the same trace ID.
* Don't confuse this with {@link #getSpanId()}, which is the ID for an individual span of work as part of the larger distributed trace. This will never
* be null. NOTE: By convention this will likely be a 16 character lowercase hex-encoded 64-bit long-integer value
* (see {@link TraceAndSpanIdGenerator#generateId()} for details).
*/
public String getTraceId() {
return traceId;
}
/**
* @return The ID for this span of work in the distributed trace. Don't confuse this with the {@link #getTraceId()}, which is the ID associated with the overall
* distributed trace and is the same for all spans in a trace. Also don't confuse this with {@link #getParentSpanId()}, which is the ID of the span that spawned
* this span instance (the logical "parent" of this span). This will never be null. NOTE: By convention this will likely be a 16 character
* lowercase hex-encoded 64-bit long-integer value (see {@link TraceAndSpanIdGenerator#generateId()} for details).
*/
public String getSpanId() {
return spanId;
}
/**
* @return The ID of the span that spawned this span instance (the logical "parent" of this span), or null if no such parent exists. If this returns null then this
* span is the "root span" for the distributed trace - the ultimate ancestor of all other spans in the trace tree.
* NOTE: By convention this will likely be a 16 character lowercase hex-encoded 64-bit long-integer value
* (see {@link TraceAndSpanIdGenerator#generateId()} for details).
*/
public String getParentSpanId() {
return parentSpanId;
}
/**
* @return The human-readable name for this span. This will never be null.
*/
public String getSpanName() {
return spanName;
}
/**
* Sets the {@link #spanName} to the given value. Intentionally package-scoped to force all span name changes to go
* through {@link SpanMutator} - this is not a method that should normally be called or that users should normally
* have to see and worry about, so this package scoping reduces accidental and unwise usage.
*
* The main reason this method exists at all is to facilitate situations where full context for the correct
* span name isn't known at the time of span creation. For example, for HTTP spans we'd like the span name to
* include the HTTP route (URL path template), but this information usually isn't known until after the request
* has been processed. So this method allows for the span name to be set to something initially, and then changed
* later when the data for the final span name is available.
*
* @param newName The new span name that should be set on {@link #spanName} - cannot be null.
*/
/*package*/ void setSpanName(String newName) {
if (newName == null) {
throw new IllegalArgumentException("The new span name cannot be null");
}
this.spanName = newName;
// This span's state changed, so clear the cached serialized representations.
clearCachedDataDueToStateChange();
}
/**
* @return True if this span is sampleable and should be output to the logging/span collection system, false otherwise.
*/
public boolean isSampleable() {
return sampleable;
}
/**
* @return The ID of the user logically associated with this span, or null if no such user ID exists or is appropriate.
*/
public String getUserId() {
return userId;
}
/**
* @return The start timestamp of the span in epoch microseconds (*not* milliseconds!). NOTE: Due to java limitations this currently only has millisecond
* resolution (i.e. this was generated by converting {@link System#currentTimeMillis()} to microseconds), however other timestamps generated
* for this span will be generated relative to this starting timestamp using microsecond resolution by utilizing {@link System#nanoTime()} to
* calculate differences.
*/
public long getSpanStartTimeEpochMicros() {
return spanStartTimeEpochMicros;
}
/**
* @return The start time of the span - calculated by calling {@link System#nanoTime()} when the constructor for this instance was called. WARNING: As per the
* javadocs for {@link System#nanoTime()} this value is *NOT* an epoch value like {@link System#currentTimeMillis()} - it is only usable when
* comparing against other {@link System#nanoTime()} calls performed on the *same JVM*.
*/
public long getSpanStartTimeNanos() {
return spanStartTimeNanos;
}
/**
* @return The purpose of this span. This will never be null - if the purpose is unknown then {@link SpanPurpose#UNKNOWN} will be returned. See the javadocs
* for {@link SpanPurpose} for information on what each enum option means.
*/
public SpanPurpose getSpanPurpose() {
return spanPurpose;
}
/**
* Causes this {@link Span} to be completed/finished/finalized by setting {@link #getDurationNanos()} to
* {@link System#nanoTime()} minus {@link #getSpanStartTimeNanos()}. After this is called then
* {@link #isCompleted()} will return true and {@link #getDurationNanos()} will return the value calculated here.
* A return value of true means this span wasn't previously completed, and this method call was the one that
* caused it to be completed. This is done atomically to guarantee only one caller can ever return true for this
* method for a given span. If this span has already been completed, then this method will return false and won't
* do anything.
*
*
This atomic completion behavior is necessary to support some asynchronous use cases, where multiple threads
* may try to complete the span at the same time (most often in the case of a problem that can trigger multiple
* simultaneous errors).
*
*
NOTE: This is intentionally package scoped to make sure completions and logging/span output logic happens
* centrally through {@link Tracer}.
*
* @return true if this span was *not* previously completed and the call to this method caused it to become
* completed, false if this span was previously completed (and therefore the call to this method did nothing).
*/
/*package*/ boolean complete() {
boolean allowedToComplete = completedFlag.compareAndSet(false, true);
if (!allowedToComplete) {
// This span was completed previously (or simultaneously by another thread, and that other thread won).
// So we're not allowed to complete the span since someone else already did it. Return false
// to let the caller know this.
return false;
}
// This span wasn't already completed, so do it now.
this.durationNanos = System.nanoTime() - spanStartTimeNanos;
// This span's state changed, so clear the cached serialized representations.
clearCachedDataDueToStateChange();
return true;
}
/**
* @return true if {@link #complete()} has been called on this instance (representing a completed/finished/finalized span), false otherwise.
*/
public boolean isCompleted() {
return durationNanos != null;
}
/**
* @return The amount of time in nanoseconds between when this span was started and when it was completed - this will return null if {@link #isCompleted()} is false.
* You must call {@link #complete()} to complete the span and populate the field this method returns.
*/
public Long getDurationNanos() {
return durationNanos;
}
/**
* @return this span's *current* status relative to {@link Tracer} on the current thread at the time this method is
* called. This status is recalculated every time this method is called and is only relevant/correct until {@link
* Tracer}'s state is modified (i.e. by starting a subspan, completing a span, using any of the asynchronous helper
* methods to modify the span stack in any way, etc), so it should only be considered relevant for the moment the
* call is made.
*
*
NOTE: Most app-level developers should not need to worry about this at all.
*/
public TracerManagedSpanStatus getCurrentTracerManagedSpanStatus() {
return Tracer.getInstance().getCurrentManagedStatusForSpan(this);
}
/**
* @return An unmodifiable read-only view of this Span's collection of key/value tags - will never be null.
* Any attempt to modify the returned map will result in a {@link UnsupportedOperationException}.
*/
public Map getTags() {
return unmodifiableTags;
}
/**
* Tags are expressed as key/value pairs. A call to this method will add the key/value pair if it exists
* or replaces the current {@code value} if one exists for the respective {@code key}.
*
* NOTE: If you're integrating with a system that understands Zipkin tags, see {@link
* com.nike.wingtips.tags.KnownZipkinTags} for common tag key/value pairs that have special meaning that you might
* want to take advantage of. Similarly, if you're integrating with a system that understands OpenTracing tags,
* see {@link com.nike.wingtips.tags.KnownOpenTracingTags}.
*
* @param key The tag {@code key}.
* @param value The tag {@code value} to be set.
*/
public void putTag(String key, String value) {
tags.put(key, value);
// This span's state changed, so clear the cached serialized representations.
clearCachedDataDueToStateChange();
}
/**
* Removes the tag from this span with the given tag key. If this span does not have the given tag, then nothing
* will be done.
*
* @param key The tag {@code key} to remove.
*/
public void removeTag(String key) {
tags.remove(key);
// This span's state changed, so clear the cached serialized representations.
clearCachedDataDueToStateChange();
}
/**
* @return An unmodifiable read-only view of this Span's list of {@link TimestampedAnnotation}s - will
* never be null. Any attempt to modify the returned list will result in a {@link UnsupportedOperationException}.
*/
public List getTimestampedAnnotations() {
return unmodifiableAnnotations;
}
/**
* Adds a {@link TimestampedAnnotation} to this Span's {@link #getTimestampedAnnotations()} list, with the current
* time in epoch microseconds as the timestamp and the given value.
*
* NOTE: Since the span keeps track of its duration in nanoseconds, this method results in a
* {@link TimestampedAnnotation} with a {@link TimestampedAnnotation#getTimestampEpochMicros()} that is more
* accurate than if you created the {@link TimestampedAnnotation} directly (i.e. this method uses {@link
* TimestampedAnnotation#forEpochMicrosWithNanoOffset(long, long, String)}). This method should therefore be
* used anytime you want to add an annotation to a Span with a timestamp of "right now".
*
* @param value The desired {@link TimestampedAnnotation#getValue()} for the new annotation.
*/
public void addTimestampedAnnotationForCurrentTime(String value) {
addTimestampedAnnotation(
TimestampedAnnotation.forEpochMicrosWithNanoOffset(
spanStartTimeEpochMicros,
System.nanoTime() - spanStartTimeNanos,
value
)
);
}
/**
* Adds the given {@link TimestampedAnnotation} to this Span's {@link #getTimestampedAnnotations()} list.
*
* @param timestampedAnnotation The annotation to add to this Span.
*/
public void addTimestampedAnnotation(TimestampedAnnotation timestampedAnnotation) {
this.annotations.add(timestampedAnnotation);
// This span's state changed, so clear the cached serialized representations.
clearCachedDataDueToStateChange();
}
/**
* @return The JSON representation of this span. See {@link #toJSON()}.
*/
@Override
public String toString() {
return toJSON();
}
/**
* @return A comma-delimited {@code key="value"} string based on this {@link Span} instance.
*
*
NOTE: The {@link SpanParser#DURATION_NANOS_FIELD} field will be added only if {@link #isCompleted()} is true.
* This lets you call this method at any time and only the relevant data will be output in the returned String
* (e.g. in case you want to log info about the span before it has been completed).
*
*
ALSO NOTE: This delegates the logic to {@link SpanParser#convertSpanToKeyValueFormat(Span)}, however this
* method will cache the result so it won't be recalculated on repeat method calls, and for that reason this method
* should always be preferred over calling {@link SpanParser#convertSpanToKeyValueFormat(Span)}. If something
* changes in the span that would cause the cached value to be stale, then the cache will be thrown away and
* recalculated.
*/
public String toKeyValueString() {
// Profiling shows this serialization to generate a lot of garbage in certain situations,
// so we should cache the result.
if (cachedKeyValueRepresentation == null) {
cachedKeyValueRepresentation = SpanParser.convertSpanToKeyValueFormat(this);
}
return cachedKeyValueRepresentation;
}
/**
* @return A JSON string based on this {@link Span} instance.
*
*
NOTE: The {@link SpanParser#DURATION_NANOS_FIELD} field will be added only if {@link #isCompleted()} is true.
* This lets you call this method at any time and only the relevant data will be output in the returned JSON
* (e.g. in case you want to log info about the span before it has been completed).
*
*
ALSO NOTE: This delegates the logic to {@link SpanParser#convertSpanToJSON(Span)}, however this
* method will cache the result so it won't be recalculated on repeat method calls, and for that reason this method
* should always be preferred over calling {@link SpanParser#convertSpanToJSON(Span)}. If something
* changes in the span that would cause the cached value to be stale, then the cache will be thrown away and
* recalculated.
*/
public String toJSON() {
// Profiling shows this serialization to generate a lot of garbage in certain situations,
// so we should cache the result.
if (cachedJsonRepresentation == null) {
cachedJsonRepresentation = SpanParser.convertSpanToJSON(this);
}
return cachedJsonRepresentation;
}
/**
* Handles the implementation of {@link Closeable#close()} for spans to allow them to be used in
* try-with-resources statements or other libraries that work with {@link Closeable} objects.
*
* -
* If this span is already completed ({@link #isCompleted()} returns true) then an error will be logged
* and nothing will be done.
*
* -
* If this span is the current span ({@link Tracer#getCurrentSpan()} equals the given span), then {@link
* Tracer#completeRequestSpan()} or {@link Tracer#completeSubSpan()} will be called, whichever is
* appropriate.
*
* -
* If this span is *not* the current span ({@link Tracer#getCurrentSpan()} does not equal this span), then
* this may or may not be an error depending on whether this span is managed by {@link Tracer} or not.
*
* -
* If this span is managed by {@link Tracer} (i.e. it is contained in the span stack somewhere even
* though it's not the current span) then this is a wingtips usage error - this span should not be
* completed yet - and an error will be logged and this span will be completed and logged to the
* "invalid span logger".
*
* -
* Otherwise this span is not managed by us, and since there may be valid use cases for manually
* managing spans we must assume the call was intentional. No error will be logged, and this span
* will be completed and logged to the "valid span logger".
*
* -
* In either case, {@link Tracer}'s current span stack and MDC info will be left untouched if
* this span is not the current span.
*
*
*
*
*/
@Override
public void close() {
Tracer.getInstance().handleSpanCloseMethod(this);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Span)) {
return false;
}
Span span = (Span) o;
return sampleable == span.sampleable &&
spanStartTimeEpochMicros == span.spanStartTimeEpochMicros &&
spanPurpose == span.spanPurpose &&
Objects.equals(traceId, span.traceId) &&
Objects.equals(spanId, span.spanId) &&
Objects.equals(parentSpanId, span.parentSpanId) &&
Objects.equals(spanName, span.spanName) &&
Objects.equals(userId, span.userId) &&
Objects.equals(durationNanos, span.durationNanos) &&
Objects.equals(tags, span.tags) &&
Objects.equals(annotations, span.annotations);
}
@Override
public int hashCode() {
return Objects.hash(
traceId, spanId, parentSpanId, spanName, sampleable, userId, spanPurpose, spanStartTimeEpochMicros,
durationNanos, tags, annotations
);
}
/**
* Sets {@link #cachedJsonRepresentation} and {@link #cachedKeyValueRepresentation} to null so that they will
* be recalculated the next time {@link #toJSON()}, {@link #toKeyValueString()}, or {@link #toString()} is called.
*
* This method should be called any time this span's state is changed.
*/
private void clearCachedDataDueToStateChange() {
// By setting a cached value to null it will be regenerated the next time it is requested.
cachedJsonRepresentation = null;
cachedKeyValueRepresentation = null;
}
/**
* @deprecated Switch to referencing {@link SpanParser#fromKeyValueString(String)} directly.
*/
@Deprecated
public static Span fromKeyValueString(String keyValueStr) {
return SpanParser.fromKeyValueString(keyValueStr);
}
/**
* @deprecated Switch to referencing {@link SpanParser#fromJSON(String)} directly.
*/
@Deprecated
public static Span fromJSON(String json) {
return SpanParser.fromJSON(json);
}
/** @deprecated Reference {@link SpanParser#TRACE_ID_FIELD} directly instead. */
@Deprecated
public static final String TRACE_ID_FIELD = SpanParser.TRACE_ID_FIELD;
/** @deprecated Reference {@link SpanParser#PARENT_SPAN_ID_FIELD} directly instead. */
@Deprecated
public static final String PARENT_SPAN_ID_FIELD = SpanParser.PARENT_SPAN_ID_FIELD;
/** @deprecated Reference {@link SpanParser#SPAN_ID_FIELD} directly instead. */
@Deprecated
public static final String SPAN_ID_FIELD = SpanParser.SPAN_ID_FIELD;
/** @deprecated Reference {@link SpanParser#SPAN_NAME_FIELD} directly instead. */
@Deprecated
public static final String SPAN_NAME_FIELD = SpanParser.SPAN_NAME_FIELD;
/** @deprecated Reference {@link SpanParser#SAMPLEABLE_FIELD} directly instead. */
@Deprecated
public static final String SAMPLEABLE_FIELD = SpanParser.SAMPLEABLE_FIELD;
/** @deprecated Reference {@link SpanParser#USER_ID_FIELD} directly instead. */
@Deprecated
public static final String USER_ID_FIELD = SpanParser.USER_ID_FIELD;
/** @deprecated Reference {@link SpanParser#SPAN_PURPOSE_FIELD} directly instead. */
@Deprecated
public static final String SPAN_PURPOSE_FIELD = SpanParser.SPAN_PURPOSE_FIELD;
/** @deprecated Reference {@link SpanParser#START_TIME_EPOCH_MICROS_FIELD} directly instead. */
@Deprecated
public static final String START_TIME_EPOCH_MICROS_FIELD = SpanParser.START_TIME_EPOCH_MICROS_FIELD;
/** @deprecated Reference {@link SpanParser#DURATION_NANOS_FIELD} directly instead. */
@Deprecated
public static final String DURATION_NANOS_FIELD = SpanParser.DURATION_NANOS_FIELD;
/**
* Represents a timestamped annotation for a {@link Span}. In other words, this class represents an "event" of
* some sort that is related to a {@link Span} and keeps track of the time that event occurred (in epoch microseconds).
*
*
The timestamp is retrieved via {@link #getTimestampEpochMicros()}, and the value associated with that
* timestamp is retrieved via {@link #getValue()}.
*/
public static class TimestampedAnnotation implements Serializable {
private final long timestampEpochMicros;
private final String value;
/**
* Creates a new instance with the given arguments.
*
*
NOTE: There are convenience factory methods for creating instances of this class for various use cases:
*
* - {@link #forEpochMicros(long, String)}
* - {@link #forEpochMillis(long, String)}
* - {@link #forCurrentTime(String)}
* - {@link #forEpochMicrosWithNanoOffset(long, long, String)}
*
* See the javadocs on those methods for details.
*
* @param timestampEpochMicros The timestamp in epoch microseconds (not milliseconds).
* @param value The value to associate with the given timestamp.
*/
public TimestampedAnnotation(long timestampEpochMicros, String value) {
this.timestampEpochMicros = timestampEpochMicros;
this.value = value;
}
/**
* A convenience static factory method that serves the same purpose as the
* {@link TimestampedAnnotation#TimestampedAnnotation(long, String)} constructor.
*
* @param timestampEpochMicros The timestamp in epoch microseconds (not milliseconds).
* @param value The value to associate with the given timestamp.
* @return A new {@link TimestampedAnnotation} with the given timestamp and value.
*/
public static TimestampedAnnotation forEpochMicros(long timestampEpochMicros, String value) {
return new TimestampedAnnotation(timestampEpochMicros, value);
}
/**
* A convenience static factory method that lets you generate an instance of {@link TimestampedAnnotation}
* based on a timestamp in epoch milliseconds rather than microseconds. For example you could use this method
* when a timestamp was recorded with {@link System#currentTimeMillis()}, and you wanted to create a
* {@link TimestampedAnnotation} using that timestamp.
*
* @param timestampEpochMillis The timestamp in epoch milliseconds (not microseconds).
* @param value The value to associate with the given timestamp.
* @return A new {@link TimestampedAnnotation} with the given timestamp (converted to microseconds), and the
* given value.
*/
public static TimestampedAnnotation forEpochMillis(long timestampEpochMillis, String value) {
return forEpochMicros(TimeUnit.MILLISECONDS.toMicros(timestampEpochMillis), value);
}
/**
* A convenience static factory method that lets you generate an instance of {@link TimestampedAnnotation}
* with a timestamp of "right now", however unfortunately that "right now" timestamp will have a resolution of
* only epoch milliseconds. This epoch millisecond resolution is the best we can do, because without more info
* we don't have anything more accurate than {@link System#currentTimeMillis()}.
*
* NOTE: {@link Span}s keep track of nanosecond duration, so {@link
* Span#addTimestampedAnnotation(TimestampedAnnotation)} is able to give a more accurate timestamp than this
* method by calling {@link #forEpochMicrosWithNanoOffset(long, long, String)} with the nanosecond offset.
* Since {@link Span#addTimestampedAnnotation(TimestampedAnnotation)} is more accurate, you should use that
* method directly on the {@link Span} rather than this method whenever possible.
*
* @param value The value to associate with the "right now" timestamp.
* @return A new {@link TimestampedAnnotation} with a timestamp of {@link System#currentTimeMillis()}
* (converted to microseconds), and the given value.
*/
public static TimestampedAnnotation forCurrentTime(String value) {
return forEpochMillis(System.currentTimeMillis(), value);
}
/**
* A convenience static factory method that lets you generate an instance of {@link TimestampedAnnotation}
* with a timestamp based on the given timestamp, but offset by the given nanosecond offset value. This
* method can be used to create instances of {@link TimestampedAnnotation} that have a more accurate
* timestamp than simply {@link System#currentTimeMillis()} converted to microseconds.
*
* @param timestampEpochMicros The timestamp in epoch microseconds (not milliseconds).
* @param offsetNanos The offset (in nanoseconds) to apply to the given timestamp in order to come up with
* the desired final timestamp.
* @param value The value to associate with the resulting timestamp.
* @return A new {@link TimestampedAnnotation} with a final timestamp calculated by adding the given timestamp
* and offset, and with the given value.
*/
public static TimestampedAnnotation forEpochMicrosWithNanoOffset(
long timestampEpochMicros, long offsetNanos, String value
) {
long offsetMicros = TimeUnit.NANOSECONDS.toMicros(offsetNanos);
return forEpochMicros(timestampEpochMicros + offsetMicros, value);
}
/**
* @return The timestamp of this instance in epoch microseconds (not milliseconds).
*/
public long getTimestampEpochMicros() {
return timestampEpochMicros;
}
/**
* @return The value of this annotation.
*/
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof TimestampedAnnotation)) {
return false;
}
TimestampedAnnotation that = (TimestampedAnnotation) o;
return getTimestampEpochMicros() == that.getTimestampEpochMicros() &&
Objects.equals(getValue(), that.getValue());
}
@Override
public int hashCode() {
return Objects.hash(getTimestampEpochMicros(), getValue());
}
}
/**
* Builder for creating {@link Span} objects.
*
* IMPORTANT NOTE: Calling {@link #build()} will choose sensible defaults for {@code traceId} and {@code spanId} if they are null when {@link #build()} is called by
* calling {@link TraceAndSpanIdGenerator#generateId()} for each, and will default {@code spanStartTimeNanos} to {@link System#nanoTime()}.
* This allows you to generate a fresh "root" span by creating a new instance of this builder and immediately calling {@link #build()}
* without any further parameters being set. The simplest way to generate a child span is to get your hands on the parent span and call
* {@link Span#generateChildSpan(String, SpanPurpose)} rather than use this builder directly. For any other use cases you can use this
* builder to set any {@link Span} fields you want manually.
*/
public static final class Builder {
private static final Logger builderLogger = LoggerFactory.getLogger(Builder.class);
private String traceId;
private String spanId;
private String parentSpanId;
private String spanName;
private boolean sampleable = true;
private String userId;
private Long spanStartTimeEpochMicros;
private Long spanStartTimeNanos;
private Long durationNanos;
private SpanPurpose spanPurpose;
private Map tags;
private List annotations;
private Builder(String spanName, SpanPurpose spanPurpose) {
this.spanName = spanName;
this.spanPurpose = spanPurpose;
}
/**
* Sets the The ID associated with the overall distributed trace - a.k.a. the trace tree ID. All spans in a distributed trace will share the same trace ID.
* Don't confuse this with {@link #withSpanId(String)}, which sets the ID for an individual span of work as part of the larger distributed trace.
*
* IMPORTANT NOTE: {@link Span}s are not allowed to have a null trace ID, so if this is null when {@link #build()} is called then the {@link Span} returned by
* {@link #build()} will have its trace ID defaulted to a new random ID using {@link TraceAndSpanIdGenerator#generateId()}.
*
* @param traceId the {@code traceId} to set
* @return a reference to this Builder
*/
public Builder withTraceId(String traceId) {
this.traceId = traceId;
return this;
}
/**
* Sets the ID for this span of work in the distributed trace. Don't confuse this with {@link #withTraceId(String)}, which sets the ID associated with the overall
* distributed trace and is the same for all spans in a trace. Also don't confuse this with {@link #getParentSpanId()}, which is the ID of the span that spawned
* this span instance (the logical "parent" of this span).
*
* IMPORTANT NOTE: {@link Span}s are not allowed to have a null span ID, so if this is null when {@link #build()} is called then the {@link Span} returned by
* {@link #build()} will have its span ID defaulted to a new random ID using {@link TraceAndSpanIdGenerator#generateId()}.
*
* @param spanId the {@code spanId} to set
* @return a reference to this Builder
*/
public Builder withSpanId(String spanId) {
this.spanId = spanId;
return this;
}
/**
* Sets the ID of the span that spawned this span instance (the logical "parent" of this span), or pass in null if no such parent exists. If you pass in null then
* this instance will be a root span for the distributed trace - the ultimate ancestor of all other spans in the trace tree.
*
* @param parentSpanId the {@code parentSpanId} to set
* @return a reference to this Builder
*/
public Builder withParentSpanId(String parentSpanId) {
this.parentSpanId = parentSpanId;
return this;
}
/**
* Sets the human-readable name for this span. This should never be null - if you set this to null and call {@link #build()} then an {@link IllegalArgumentException} will
* be thrown.
*
* @param spanName the {@code spanName} to set
* @return a reference to this Builder
*/
public Builder withSpanName(String spanName) {
this.spanName = spanName;
return this;
}
/**
* Set this to true if this span is sampleable and should be output to the logging/span collection system, false otherwise. This defaults to true.
*
* @param sampleable the {@code sampleable} value to set
* @return a reference to this Builder
*/
public Builder withSampleable(boolean sampleable) {
this.sampleable = sampleable;
return this;
}
/**
* Sets the ID of the user logically associated with this span, or pass in null if no such user ID exists or is appropriate.
*
* @param userId the {@code userId} to set
* @return a reference to this Builder
*/
public Builder withUserId(String userId) {
this.userId = userId;
return this;
}
/**
*
* Sets the start timestamp in microseconds since the epoch for this span (*not* milliseconds), or pass in null if this is a new span and
* you want this value calculated automatically when {@link #build()} is called.
*
* NOTE: Since Java 7 does not have a reliable way to calculate this value accurately we generally just get as close as we can by turning
* {@link System#currentTimeMillis()} into microseconds. That means the start timestamp only has millisecond resolution.
*
* @param spanStartTimeEpochMicros the {@code spanStartTimeEpochMicros} to set
* @return a reference to this Builder
*/
public Builder withSpanStartTimeEpochMicros(Long spanStartTimeEpochMicros) {
this.spanStartTimeEpochMicros = spanStartTimeEpochMicros;
return this;
}
/**
*
* TLDR; Passing in null (or not calling this method at all) is always a safe option - when in doubt leave this null!
*
*
* Sets the start time of the span in nanoseconds. This value is used when calculating {@link #getDurationNanos()}. If this is left null then one of
* two things will be done to generate it depending on the situation:
*
* -
* If {@link #withSpanStartTimeEpochMicros(Long)} is null - this indicates a brand new span is being created on this JVM. This value will be
* set to {@link System#nanoTime()} when {@link #build()} is called. This leads to {@link #getDurationNanos()} being accurate to the
* nanosecond, however it is only possible to do this when you start and complete a given span on the same JVM.
*
* -
* If {@link #withSpanStartTimeEpochMicros(Long)} is *not* null - this indicates you're deserializing a span that was created on a different
* JVM. This nano start time value will then be inferred based on the span's starting timestamp ({@code spanStartTimeEpochMicros}),
* {@link System#currentTimeMillis()}, and {@link System#nanoTime()}. You will lose a little bit of duration resolution compared with starting
* and completing a span on the same JVM, but it will be reasonable (guaranteed less than 1 millisecond of error).
*
*
* This means you should only pass in a non-null value to this method when you are recreating/deserializing
* a span that was originally created on this exact same JVM instance. Since this is an unusual use case, you will almost always want to pass in null
* (or not call this method at all).
*
*
* @param spanStartTimeNanos the {@code spanStartTimeNanos} to set
* @return a reference to this Builder
*/
public Builder withSpanStartTimeNanos(Long spanStartTimeNanos) {
this.spanStartTimeNanos = spanStartTimeNanos;
return this;
}
/**
*
* Sets the duration of the span in nanoseconds, or null if this {@link Span} should not be considered {@link Span#isCompleted()} yet.
*
*
* NOTE: Under most circumstances you will want this to be null (which is the default) since there are not many use cases where you need to create an
* already-completed {@link Span}.
*
*
* @param durationNanos the {@code durationNanos} to set
* @return a reference to this Builder
*/
public Builder withDurationNanos(Long durationNanos) {
this.durationNanos = durationNanos;
return this;
}
/**
* Sets the {@link SpanPurpose} for this span. See the javadocs on that class for details on what each enum option means. Passing in null is equivalent
* to {@link SpanPurpose#UNKNOWN}.
*
* @param spanPurpose the {@code spanPurpose} to set
* @return a reference to this Builder
*/
public Builder withSpanPurpose(SpanPurpose spanPurpose) {
this.spanPurpose = spanPurpose;
return this;
}
/**
* Sets the value of a tag for the respective key. This will replace an existing tag value for the respective key.
*
* @param key the {@code key} of the tag
* @param value the {@code value} of the tag
* @return a reference to this Builder
*/
public Builder withTag(String key, String value) {
if (this.tags == null) {
this.tags = new LinkedHashMap<>();
}
this.tags.put(key, value);
return this;
}
/**
* Puts all the tags from the supplied {@code Map} to the existing set of tags.
*
* @param tags The tags to be put
* @return a reference to this Builder
* @throws NullPointerException if the specified map is null, or if
* this map does not permit null keys or values, and the
* specified map contains null keys or values
*/
public Builder withTags(Map tags) {
if (tags == null) {
return this;
}
if (this.tags == null) {
this.tags = new LinkedHashMap<>(tags.size());
}
this.tags.putAll(tags);
return this;
}
/**
* Adds the given {@link TimestampedAnnotation}.
*
* @param annotation the {@link TimestampedAnnotation} to add.
* @return a reference to this Builder
*/
public Builder withTimestampedAnnotation(TimestampedAnnotation annotation) {
if (this.annotations == null) {
this.annotations = new ArrayList<>();
}
this.annotations.add(annotation);
return this;
}
/**
* Adds the given {@link TimestampedAnnotation}s.
*
* @param annotations the collection of {@link TimestampedAnnotation}s to add.
* @return a reference to this Builder
*/
public Builder withTimestampedAnnotations(Collection extends TimestampedAnnotation> annotations) {
if (annotations == null) {
return this;
}
if (this.annotations == null) {
this.annotations = new ArrayList<>(annotations.size());
}
this.annotations.addAll(annotations);
return this;
}
/**
* Returns a {@link Span} built from the parameters set via the various {@code with*(...)} methods on this
* builder instance.
*
* IMPORTANT NOTE: {@link Span} objects are not allowed to have null {@link Span#getTraceId()}, {@link
* Span#getSpanId()}, or {@link Span#getSpanStartTimeEpochMicros()}, and there are sensible defaults we can set
* for those values when creating a new span, so if any of them are null when this method is called it is
* assumed you are creating a new span and they will be set to the following:
*
* - {@code traceId} is defaulted to {@link TraceAndSpanIdGenerator#generateId()}.
* - {@code spanId} is defaulted to {@link TraceAndSpanIdGenerator#generateId()}.
* -
* {@code spanStartTimeEpochMicros} is defaulted to {@link System#currentTimeMillis()} converted to
* microseconds.
*
*
* -
* Side note - {@code spanStartTimeNanos} is calculated based on the rules described in
* {@link #withSpanStartTimeNanos(Long)}.
*
*
*
*
* Span name is also not allowed to be null, but since there is no sensible default for that value an
* {@link IllegalArgumentException} will be thrown if span name is null when this method is called.
*
* @return a {@link Span} built with parameters of this {@code Span.Builder}
*/
public Span build() {
if (traceId == null) {
traceId = TraceAndSpanIdGenerator.generateId();
}
if (spanId == null) {
spanId = TraceAndSpanIdGenerator.generateId();
}
if (spanStartTimeEpochMicros == null) {
spanStartTimeEpochMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
if (spanStartTimeNanos != null) {
// The nano start time was set but the start time in epoch microseconds was *not*.
// This makes no sense, so we'll null out the nano start and log a warning.
builderLogger.warn(
"The builder was setup with a null spanStartTimeEpochMicros and non-null spanStartTimeNanos. "
+ "This makes no sense (if you have a nano start time then you should also have the epoch "
+ "micros start time), so the nano start time passed into this builder will be ignored and "
+ "calculated fresh along with the epoch micros start timestamp."
);
spanStartTimeNanos = null;
}
}
if (spanStartTimeNanos == null) {
spanStartTimeNanos = System.nanoTime();
}
return new Span(
traceId, parentSpanId, spanId, spanName, sampleable, userId, spanPurpose, spanStartTimeEpochMicros,
spanStartTimeNanos, durationNanos, tags, annotations
);
}
}
}