opentelemetry-java-instrume.../dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java

855 lines
26 KiB
Java

package datadog.opentracing;
import datadog.opentracing.decorators.AbstractDecorator;
import datadog.opentracing.decorators.DDDecoratorsFactory;
import datadog.opentracing.propagation.ExtractedContext;
import datadog.opentracing.propagation.HttpCodec;
import datadog.opentracing.propagation.TagContext;
import datadog.opentracing.scopemanager.ContextualScopeManager;
import datadog.opentracing.scopemanager.ScopeContext;
import datadog.trace.api.Config;
import datadog.trace.api.interceptor.MutableSpan;
import datadog.trace.api.interceptor.TraceInterceptor;
import datadog.trace.api.sampling.PrioritySampling;
import datadog.trace.common.sampling.PrioritySampler;
import datadog.trace.common.sampling.Sampler;
import datadog.trace.common.writer.DDAgentWriter;
import datadog.trace.common.writer.Writer;
import datadog.trace.common.writer.ddagent.DDAgentResponseListener;
import datadog.trace.context.ScopeListener;
import io.opentracing.References;
import io.opentracing.Scope;
import io.opentracing.ScopeManager;
import io.opentracing.Span;
import io.opentracing.SpanContext;
import io.opentracing.propagation.Format;
import io.opentracing.propagation.TextMapExtract;
import io.opentracing.propagation.TextMapInject;
import io.opentracing.tag.Tag;
import java.io.Closeable;
import java.lang.ref.WeakReference;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ThreadLocalRandom;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/** DDTracer makes it easy to send traces and span to DD using the OpenTracing API. */
@Slf4j
public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace.api.Tracer {
// UINT64 max value
public static final BigInteger TRACE_ID_MAX =
BigInteger.valueOf(2).pow(64).subtract(BigInteger.ONE);
public static final BigInteger TRACE_ID_MIN = BigInteger.ZERO;
/** Default service name if none provided on the trace or span */
final String serviceName;
/** Writer is an charge of reporting traces and spans to the desired endpoint */
final Writer writer;
/** Sampler defines the sampling policy in order to reduce the number of traces for instance */
final Sampler sampler;
/** Scope manager is in charge of managing the scopes from which spans are created */
final ScopeManager scopeManager;
/** A set of tags that are added only to the application's root span */
private final Map<String, String> localRootSpanTags;
/** A set of tags that are added to every span */
private final Map<String, String> defaultSpanTags;
/** A configured mapping of service names to update with new values */
private final Map<String, String> serviceNameMappings;
/** number of spans in a pending trace before they get flushed */
@Getter private final int partialFlushMinSpans;
/**
* JVM shutdown callback, keeping a reference to it to remove this if DDTracer gets destroyed
* earlier
*/
private final Thread shutdownCallback;
/** Span context decorators */
private final Map<String, List<AbstractDecorator>> spanContextDecorators =
new ConcurrentHashMap<>();
private final SortedSet<TraceInterceptor> interceptors =
new ConcurrentSkipListSet<>(
new Comparator<TraceInterceptor>() {
@Override
public int compare(final TraceInterceptor o1, final TraceInterceptor o2) {
return Integer.compare(o1.priority(), o2.priority());
}
});
private final HttpCodec.Injector injector;
private final HttpCodec.Extractor extractor;
public static class DDTracerBuilder {
public DDTracerBuilder() {
// Apply the default values from config.
config(Config.get());
}
public DDTracerBuilder withProperties(final Properties properties) {
return config(Config.get(properties));
}
public DDTracerBuilder config(final Config config) {
this.config = config;
serviceName(config.getServiceName());
// Explicitly skip setting writer to avoid allocating resources prematurely.
sampler(Sampler.Builder.forConfig(config));
injector(HttpCodec.createInjector(config));
extractor(HttpCodec.createExtractor(config, config.getHeaderTags()));
scopeManager(new ContextualScopeManager(config.getScopeDepthLimit()));
localRootSpanTags(config.getLocalRootSpanTags());
defaultSpanTags(config.getMergedSpanTags());
serviceNameMappings(config.getServiceMapping());
taggedHeaders(config.getHeaderTags());
partialFlushMinSpans(config.getPartialFlushMinSpans());
return this;
}
}
/** By default, report to local agent and collect all traces. */
@Deprecated
public DDTracer() {
this(Config.get());
}
@Deprecated
public DDTracer(final String serviceName) {
this(serviceName, Config.get());
}
@Deprecated
public DDTracer(final Properties config) {
this(Config.get(config));
}
@Deprecated
public DDTracer(final Config config) {
this(config.getServiceName(), config);
}
// This constructor is already used in the wild, so we have to keep it inside this API for now.
@Deprecated
public DDTracer(final String serviceName, final Writer writer, final Sampler sampler) {
this(serviceName, writer, sampler, Config.get().getLocalRootSpanTags());
}
@Deprecated
private DDTracer(final String serviceName, final Config config) {
this(
serviceName,
Writer.Builder.forConfig(config),
Sampler.Builder.forConfig(config),
config.getLocalRootSpanTags(),
config.getMergedSpanTags(),
config.getServiceMapping(),
config.getHeaderTags(),
config.getPartialFlushMinSpans());
log.debug("Using config: {}", config);
}
/** Visible for testing */
@Deprecated
DDTracer(
final String serviceName,
final Writer writer,
final Sampler sampler,
final Map<String, String> runtimeTags) {
this(
serviceName,
writer,
sampler,
runtimeTags,
Collections.<String, String>emptyMap(),
Collections.<String, String>emptyMap(),
Collections.<String, String>emptyMap(),
0);
}
@Deprecated
public DDTracer(final Writer writer) {
this(Config.get(), writer);
}
@Deprecated
public DDTracer(final Config config, final Writer writer) {
this(
config.getServiceName(),
writer,
Sampler.Builder.forConfig(config),
config.getLocalRootSpanTags(),
config.getMergedSpanTags(),
config.getServiceMapping(),
config.getHeaderTags(),
config.getPartialFlushMinSpans());
}
@Deprecated
public DDTracer(
final String serviceName,
final Writer writer,
final Sampler sampler,
final String runtimeId,
final Map<String, String> localRootSpanTags,
final Map<String, String> defaultSpanTags,
final Map<String, String> serviceNameMappings,
final Map<String, String> taggedHeaders) {
this(
serviceName,
writer,
sampler,
customRuntimeTags(runtimeId, localRootSpanTags),
defaultSpanTags,
serviceNameMappings,
taggedHeaders,
Config.get().getPartialFlushMinSpans());
}
@Deprecated
public DDTracer(
final String serviceName,
final Writer writer,
final Sampler sampler,
final Map<String, String> localRootSpanTags,
final Map<String, String> defaultSpanTags,
final Map<String, String> serviceNameMappings,
final Map<String, String> taggedHeaders) {
this(
serviceName,
writer,
sampler,
localRootSpanTags,
defaultSpanTags,
serviceNameMappings,
taggedHeaders,
Config.get().getPartialFlushMinSpans());
}
@Deprecated
public DDTracer(
final String serviceName,
final Writer writer,
final Sampler sampler,
final Map<String, String> localRootSpanTags,
final Map<String, String> defaultSpanTags,
final Map<String, String> serviceNameMappings,
final Map<String, String> taggedHeaders,
final int partialFlushMinSpans) {
this(
Config.get(),
serviceName,
writer,
sampler,
HttpCodec.createInjector(Config.get()),
HttpCodec.createExtractor(Config.get(), taggedHeaders),
new ContextualScopeManager(Config.get().getScopeDepthLimit()),
localRootSpanTags,
defaultSpanTags,
serviceNameMappings,
taggedHeaders,
partialFlushMinSpans);
}
@Builder
// These field names must be stable to ensure the builder api is stable.
private DDTracer(
final Config config,
final String serviceName,
final Writer writer,
final Sampler sampler,
final HttpCodec.Injector injector,
final HttpCodec.Extractor extractor,
final ScopeManager scopeManager,
final Map<String, String> localRootSpanTags,
final Map<String, String> defaultSpanTags,
final Map<String, String> serviceNameMappings,
final Map<String, String> taggedHeaders,
final int partialFlushMinSpans) {
assert localRootSpanTags != null;
assert defaultSpanTags != null;
assert serviceNameMappings != null;
assert taggedHeaders != null;
this.serviceName = serviceName;
if (writer == null) {
this.writer = Writer.Builder.forConfig(config);
} else {
this.writer = writer;
}
this.sampler = sampler;
this.injector = injector;
this.extractor = extractor;
this.scopeManager = scopeManager;
this.localRootSpanTags = localRootSpanTags;
this.defaultSpanTags = defaultSpanTags;
this.serviceNameMappings = serviceNameMappings;
this.partialFlushMinSpans = partialFlushMinSpans;
this.writer.start();
shutdownCallback = new ShutdownHook(this);
try {
Runtime.getRuntime().addShutdownHook(shutdownCallback);
} catch (final IllegalStateException ex) {
// The JVM is already shutting down.
}
if (this.writer instanceof DDAgentWriter && sampler instanceof DDAgentResponseListener) {
((DDAgentWriter) this.writer).addResponseListener((DDAgentResponseListener) this.sampler);
}
log.info("New instance: {}", this);
final List<AbstractDecorator> decorators = DDDecoratorsFactory.createBuiltinDecorators();
for (final AbstractDecorator decorator : decorators) {
addDecorator(decorator);
}
registerClassLoader(ClassLoader.getSystemClassLoader());
// Ensure that PendingTrace.SPAN_CLEANER is initialized in this thread:
// FIXME: add test to verify the span cleaner thread is started with this call.
PendingTrace.initialize();
}
@Override
public void finalize() {
try {
Runtime.getRuntime().removeShutdownHook(shutdownCallback);
shutdownCallback.run();
} catch (final Exception e) {
log.error("Error while finalizing DDTracer.", e);
}
}
/**
* Returns the list of span context decorators
*
* @return the list of span context decorators
*/
public List<AbstractDecorator> getSpanContextDecorators(final String tag) {
return spanContextDecorators.get(tag);
}
/**
* Add a new decorator in the list ({@link AbstractDecorator})
*
* @param decorator The decorator in the list
*/
public void addDecorator(final AbstractDecorator decorator) {
List<AbstractDecorator> list = spanContextDecorators.get(decorator.getMatchingTag());
if (list == null) {
list = new ArrayList<>();
}
list.add(decorator);
spanContextDecorators.put(decorator.getMatchingTag(), list);
log.debug(
"Decorator added: '{}' -> {}", decorator.getMatchingTag(), decorator.getClass().getName());
}
@Deprecated
public void addScopeContext(final ScopeContext context) {
if (scopeManager instanceof ContextualScopeManager) {
((ContextualScopeManager) scopeManager).addScopeContext(context);
}
}
/**
* If an application is using a non-system classloader, that classloader should be registered
* here. Due to the way Spring Boot structures its' executable jar, this might log some warnings.
*
* @param classLoader to register.
*/
public void registerClassLoader(final ClassLoader classLoader) {
try {
for (final TraceInterceptor interceptor :
ServiceLoader.load(TraceInterceptor.class, classLoader)) {
addTraceInterceptor(interceptor);
}
} catch (final ServiceConfigurationError e) {
log.warn("Problem loading TraceInterceptor for classLoader: " + classLoader, e);
}
}
@Override
public ScopeManager scopeManager() {
return scopeManager;
}
@Override
public Span activeSpan() {
return scopeManager.activeSpan();
}
@Override
public Scope activateSpan(final Span span) {
return scopeManager.activate(span);
}
@Override
public DDSpanBuilder buildSpan(final String operationName) {
return new DDSpanBuilder(operationName, scopeManager);
}
@Override
public <T> void inject(final SpanContext spanContext, final Format<T> format, final T carrier) {
if (carrier instanceof TextMapInject) {
final DDSpanContext ddSpanContext = (DDSpanContext) spanContext;
final DDSpan rootSpan = ddSpanContext.getTrace().getRootSpan();
setSamplingPriorityIfNecessary(rootSpan);
injector.inject(ddSpanContext, (TextMapInject) carrier);
} else {
log.debug("Unsupported format for propagation - {}", format.getClass().getName());
}
}
@Override
public <T> SpanContext extract(final Format<T> format, final T carrier) {
if (carrier instanceof TextMapExtract) {
return extractor.extract((TextMapExtract) carrier);
} else {
log.debug("Unsupported format for propagation - {}", format.getClass().getName());
return null;
}
}
/**
* We use the sampler to know if the trace has to be reported/written. The sampler is called on
* the first span (root span) of the trace. If the trace is marked as a sample, we report it.
*
* @param trace a list of the spans related to the same trace
*/
void write(final Collection<DDSpan> trace) {
if (trace.isEmpty()) {
return;
}
final ArrayList<DDSpan> writtenTrace;
if (interceptors.isEmpty()) {
writtenTrace = new ArrayList<>(trace);
} else {
Collection<? extends MutableSpan> interceptedTrace = new ArrayList<>(trace);
for (final TraceInterceptor interceptor : interceptors) {
interceptedTrace = interceptor.onTraceComplete(interceptedTrace);
}
writtenTrace = new ArrayList<>(interceptedTrace.size());
for (final MutableSpan span : interceptedTrace) {
if (span instanceof DDSpan) {
writtenTrace.add((DDSpan) span);
}
}
}
incrementTraceCount();
if (!writtenTrace.isEmpty()) {
final DDSpan rootSpan = (DDSpan) writtenTrace.get(0).getLocalRootSpan();
setSamplingPriorityIfNecessary(rootSpan);
final DDSpan spanToSample = rootSpan == null ? writtenTrace.get(0) : rootSpan;
if (sampler.sample(spanToSample)) {
writer.write(writtenTrace);
}
}
}
void setSamplingPriorityIfNecessary(final DDSpan rootSpan) {
// There's a race where multiple threads can see PrioritySampling.UNSET here
// This check skips potential complex sampling priority logic when we know its redundant
// Locks inside DDSpanContext ensure the correct behavior in the race case
if (sampler instanceof PrioritySampler
&& rootSpan != null
&& rootSpan.context().getSamplingPriority() == PrioritySampling.UNSET) {
((PrioritySampler) sampler).setSamplingPriority(rootSpan);
}
}
/** Increment the reported trace count, but do not write a trace. */
void incrementTraceCount() {
writer.incrementTraceCount();
}
@Override
public String getTraceId() {
final Span activeSpan = activeSpan();
if (activeSpan instanceof DDSpan) {
return ((DDSpan) activeSpan).getTraceId().toString();
}
return "0";
}
@Override
public String getSpanId() {
final Span activeSpan = activeSpan();
if (activeSpan instanceof DDSpan) {
return ((DDSpan) activeSpan).getSpanId().toString();
}
return "0";
}
@Override
public boolean addTraceInterceptor(final TraceInterceptor interceptor) {
return interceptors.add(interceptor);
}
@Override
public void addScopeListener(final ScopeListener listener) {
if (scopeManager instanceof ContextualScopeManager) {
((ContextualScopeManager) scopeManager).addScopeListener(listener);
}
}
@Override
public void close() {
PendingTrace.close();
writer.close();
}
@Override
public String toString() {
return "DDTracer-"
+ Integer.toHexString(hashCode())
+ "{ serviceName="
+ serviceName
+ ", writer="
+ writer
+ ", sampler="
+ sampler
+ ", defaultSpanTags="
+ defaultSpanTags
+ '}';
}
@Deprecated
private static Map<String, String> customRuntimeTags(
final String runtimeId, final Map<String, String> applicationRootSpanTags) {
final Map<String, String> runtimeTags = new HashMap<>(applicationRootSpanTags);
runtimeTags.put(Config.RUNTIME_ID_TAG, runtimeId);
return Collections.unmodifiableMap(runtimeTags);
}
/** Spans are built using this builder */
public class DDSpanBuilder implements SpanBuilder {
private final ScopeManager scopeManager;
/** Each span must have an operationName according to the opentracing specification */
private final String operationName;
// Builder attributes
private final Map<String, Object> tags = new LinkedHashMap<String, Object>(defaultSpanTags);
private long timestampMicro;
private SpanContext parent;
private String serviceName;
private String resourceName;
private boolean errorFlag;
private String spanType;
private boolean ignoreScope = false;
public DDSpanBuilder(final String operationName, final ScopeManager scopeManager) {
this.operationName = operationName;
this.scopeManager = scopeManager;
}
@Override
public SpanBuilder ignoreActiveSpan() {
ignoreScope = true;
return this;
}
private DDSpan startSpan() {
return new DDSpan(timestampMicro, buildSpanContext());
}
@Override
public Scope startActive(final boolean finishSpanOnClose) {
final DDSpan span = startSpan();
final Scope scope = scopeManager.activate(span, finishSpanOnClose);
log.debug("Starting a new active span: {}", span);
return scope;
}
@Override
@Deprecated
public DDSpan startManual() {
return start();
}
@Override
public DDSpan start() {
final DDSpan span = startSpan();
log.debug("Starting a new span: {}", span);
return span;
}
@Override
public DDSpanBuilder withTag(final String tag, final Number number) {
return withTag(tag, (Object) number);
}
@Override
public DDSpanBuilder withTag(final String tag, final String string) {
return withTag(tag, (Object) string);
}
@Override
public DDSpanBuilder withTag(final String tag, final boolean bool) {
return withTag(tag, (Object) bool);
}
@Override
public <T> SpanBuilder withTag(final Tag<T> tag, final T value) {
return withTag(tag.getKey(), value);
}
@Override
public DDSpanBuilder withStartTimestamp(final long timestampMicroseconds) {
timestampMicro = timestampMicroseconds;
return this;
}
public DDSpanBuilder withServiceName(final String serviceName) {
this.serviceName = serviceName;
return this;
}
public DDSpanBuilder withResourceName(final String resourceName) {
this.resourceName = resourceName;
return this;
}
public DDSpanBuilder withErrorFlag() {
errorFlag = true;
return this;
}
public DDSpanBuilder withSpanType(final String spanType) {
this.spanType = spanType;
return this;
}
public Iterable<Map.Entry<String, String>> baggageItems() {
if (parent == null) {
return Collections.emptyList();
}
return parent.baggageItems();
}
@Override
public DDSpanBuilder asChildOf(final Span span) {
return asChildOf(span == null ? null : span.context());
}
@Override
public DDSpanBuilder asChildOf(final SpanContext spanContext) {
parent = spanContext;
return this;
}
@Override
public DDSpanBuilder addReference(final String referenceType, final SpanContext spanContext) {
if (spanContext == null) {
return this;
}
if (!(spanContext instanceof ExtractedContext) && !(spanContext instanceof DDSpanContext)) {
log.debug(
"Expected to have a DDSpanContext or ExtractedContext but got "
+ spanContext.getClass().getName());
return this;
}
if (References.CHILD_OF.equals(referenceType)
|| References.FOLLOWS_FROM.equals(referenceType)) {
return asChildOf(spanContext);
} else {
log.debug("Only support reference type of CHILD_OF and FOLLOWS_FROM");
}
return this;
}
// Private methods
private DDSpanBuilder withTag(final String tag, final Object value) {
if (value == null || (value instanceof String && ((String) value).isEmpty())) {
tags.remove(tag);
} else {
tags.put(tag, value);
}
return this;
}
private BigInteger generateNewId() {
// It is **extremely** unlikely to generate the value "0" but we still need to handle that
// case
BigInteger value;
do {
value = new BigInteger(63, ThreadLocalRandom.current());
} while (value.signum() == 0);
return value;
}
/**
* Build the SpanContext, if the actual span has a parent, the following attributes must be
* propagated: - ServiceName - Baggage - Trace (a list of all spans related) - SpanType
*
* @return the context
*/
private DDSpanContext buildSpanContext() {
final BigInteger traceId;
final BigInteger spanId = generateNewId();
final BigInteger parentSpanId;
final Map<String, String> baggage;
final PendingTrace parentTrace;
final int samplingPriority;
final String origin;
final DDSpanContext context;
SpanContext parentContext = parent;
if (parentContext == null && !ignoreScope) {
// use the Scope as parent unless overridden or ignored.
final Span activeSpan = scopeManager.activeSpan();
if (activeSpan != null) {
parentContext = activeSpan.context();
}
}
// Propagate internal trace.
// Note: if we are not in the context of distributed tracing and we are starting the first
// root span, parentContext will be null at this point.
if (parentContext instanceof DDSpanContext) {
final DDSpanContext ddsc = (DDSpanContext) parentContext;
traceId = ddsc.getTraceId();
parentSpanId = ddsc.getSpanId();
baggage = ddsc.getBaggageItems();
parentTrace = ddsc.getTrace();
samplingPriority = PrioritySampling.UNSET;
origin = null;
if (serviceName == null) {
serviceName = ddsc.getServiceName();
}
} else {
if (parentContext instanceof ExtractedContext) {
// Propagate external trace
final ExtractedContext extractedContext = (ExtractedContext) parentContext;
traceId = extractedContext.getTraceId();
parentSpanId = extractedContext.getSpanId();
samplingPriority = extractedContext.getSamplingPriority();
baggage = extractedContext.getBaggage();
} else {
// Start a new trace
traceId = generateNewId();
parentSpanId = BigInteger.ZERO;
samplingPriority = PrioritySampling.UNSET;
baggage = null;
}
// Get header tags and set origin whether propagating or not.
if (parentContext instanceof TagContext) {
tags.putAll(((TagContext) parentContext).getTags());
origin = ((TagContext) parentContext).getOrigin();
} else {
origin = null;
}
tags.putAll(localRootSpanTags);
parentTrace = new PendingTrace(DDTracer.this, traceId, serviceNameMappings);
}
if (serviceName == null) {
serviceName = DDTracer.this.serviceName;
}
final String operationName = this.operationName != null ? this.operationName : resourceName;
// some attributes are inherited from the parent
context =
new DDSpanContext(
traceId,
spanId,
parentSpanId,
serviceName,
operationName,
resourceName,
samplingPriority,
origin,
baggage,
errorFlag,
spanType,
tags,
parentTrace,
DDTracer.this);
// Apply Decorators to handle any tags that may have been set via the builder.
for (final Map.Entry<String, Object> tag : tags.entrySet()) {
if (tag.getValue() == null) {
context.setTag(tag.getKey(), null);
continue;
}
boolean addTag = true;
// Call decorators
final List<AbstractDecorator> decorators = getSpanContextDecorators(tag.getKey());
if (decorators != null) {
for (final AbstractDecorator decorator : decorators) {
try {
addTag &= decorator.shouldSetTag(context, tag.getKey(), tag.getValue());
} catch (final Throwable ex) {
log.debug(
"Could not decorate the span decorator={}: {}",
decorator.getClass().getSimpleName(),
ex.getMessage());
}
}
}
if (!addTag) {
context.setTag(tag.getKey(), null);
}
}
return context;
}
}
private static class ShutdownHook extends Thread {
private final WeakReference<DDTracer> reference;
private ShutdownHook(final DDTracer tracer) {
super("dd-tracer-shutdown-hook");
reference = new WeakReference<>(tracer);
}
@Override
public void run() {
final DDTracer tracer = reference.get();
if (tracer != null) {
tracer.close();
}
}
}
}