diff --git a/dd-java-agent/instrumentation/grizzly-2/src/test/groovy/GrizzlyTest.groovy b/dd-java-agent/instrumentation/grizzly-2/src/test/groovy/GrizzlyTest.groovy index 38ca9b6fc9f..b348415abb8 100644 --- a/dd-java-agent/instrumentation/grizzly-2/src/test/groovy/GrizzlyTest.groovy +++ b/dd-java-agent/instrumentation/grizzly-2/src/test/groovy/GrizzlyTest.groovy @@ -22,6 +22,7 @@ class GrizzlyTest extends HttpServerTest { static { System.setProperty("dd.integration.grizzly.enabled", "true") + // This is needed by various subclass tests, so we can't clear it. } @Override diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry.gradle b/dd-java-agent/instrumentation/opentelemetry/opentelemetry.gradle new file mode 100644 index 00000000000..dae9cf8f635 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry.gradle @@ -0,0 +1,32 @@ +// Made this a variable so we can easily update to latest releases. +def otelVersion = '0.3.0' + +muzzle { + pass { + module = 'opentelemetry-api' + group = 'io.opentelemetry' + versions = "[$otelVersion,]" + assertInverse = true + skipVersions = ['0.2.2', '0.2.3'] + } +} + +apply from: "$rootDir/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +dependencies { + compileOnly group: 'io.opentelemetry', name: 'opentelemetry-api', version: otelVersion + + compileOnly group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + compileOnly group: 'com.google.auto.value', name: 'auto-value-annotations', version: '1.6.6' + + testCompile group: 'io.opentelemetry', name: 'opentelemetry-api', version: otelVersion + latestDepTestCompile group: 'io.opentelemetry', name: 'opentelemetry-api', version: '+' +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OpenTelemetryInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OpenTelemetryInstrumentation.java new file mode 100644 index 00000000000..6ef715e5d99 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OpenTelemetryInstrumentation.java @@ -0,0 +1,87 @@ +package datadog.trace.instrumentation.opentelemetry; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.trace.TracerProvider; +import java.util.HashMap; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This is experimental instrumentation and should only be enabled for evaluation/testing purposes. + */ +@AutoService(Instrumenter.class) +public class OpenTelemetryInstrumentation extends Instrumenter.Default { + public OpenTelemetryInstrumentation() { + super("opentelemetry-beta"); + } + + @Override + protected boolean defaultEnabled() { + return false; + } + + @Override + public ElementMatcher typeMatcher() { + return named("io.opentelemetry.OpenTelemetry"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".OtelScope", + packageName + ".OtelSpan", + packageName + ".OtelSpan$1", // switch statement + packageName + ".OtelSpanContext", + packageName + ".OtelTracer", + packageName + ".OtelTracer$1", // switch statement + packageName + ".OtelTracerProvider", + packageName + ".OtelTracer$SpanBuilder", + packageName + ".OtelContextPropagators", + packageName + ".OtelContextPropagators$1", // switch statement + packageName + ".OtelContextPropagators$OtelHttpTextFormat", + packageName + ".OtelContextPropagators$OtelSetter", + packageName + ".OtelContextPropagators$OtelGetter", + packageName + ".TypeConverter", + }; + } + + @Override + public Map, String> transformers() { + final Map, String> transformers = new HashMap<>(); + transformers.put( + named("getTracerProvider").and(returns(named("io.opentelemetry.trace.TracerProvider"))), + OpenTelemetryInstrumentation.class.getName() + "$TracerProviderAdvice"); + transformers.put( + named("getPropagators") + .and(returns(named("io.opentelemetry.context.propagation.ContextPropagators"))), + OpenTelemetryInstrumentation.class.getName() + "$ContextPropagatorsAdvice"); + return transformers; + } + + public static class TracerProviderAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void returnProvider(@Advice.Return(readOnly = false) TracerProvider result) { + result = OtelTracerProvider.INSTANCE; + } + } + + public static class ContextPropagatorsAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void returnProvider(@Advice.Return(readOnly = false) ContextPropagators result) { + result = OtelContextPropagators.INSTANCE; + } + + // Muzzle doesn't detect the advice method's argument type, so we have to help it a bit. + public static void muzzleCheck(final ContextPropagators propagators) { + propagators.getHttpTextFormat(); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelContextPropagators.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelContextPropagators.java new file mode 100644 index 00000000000..abe01114ab8 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelContextPropagators.java @@ -0,0 +1,111 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import io.grpc.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.HttpTextFormat; +import io.opentelemetry.trace.DefaultSpan; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.TracingContextUtils; +import java.util.Arrays; +import java.util.List; + +public class OtelContextPropagators implements ContextPropagators { + public static final OtelContextPropagators INSTANCE = new OtelContextPropagators(); + + private OtelContextPropagators() {} + + @Override + public HttpTextFormat getHttpTextFormat() { + return OtelHttpTextFormat.INSTANCE; + } + + private static class OtelHttpTextFormat implements HttpTextFormat { + private static final OtelHttpTextFormat INSTANCE = new OtelHttpTextFormat(); + + private final AgentTracer.TracerAPI tracer = AgentTracer.get(); + private final TypeConverter converter = new TypeConverter(); + + @Override + public List fields() { + return null; + } + + @Override + public void inject(final Context context, final C carrier, final Setter setter) { + final Span span = TracingContextUtils.getSpanWithoutDefault(context); + if (span == null || !span.getContext().isValid()) { + return; + } + tracer.inject(converter.toAgentSpan(span), carrier, new OtelSetter<>(setter)); + } + + @Override + public Context extract(final Context context, final C carrier, final Getter getter) { + final AgentSpan.Context agentContext = tracer.extract(carrier, new OtelGetter<>(getter)); + return TracingContextUtils.withSpan( + DefaultSpan.create(converter.toSpanContext(agentContext)), context); + } + } + + private static class OtelSetter implements AgentPropagation.Setter { + private final HttpTextFormat.Setter setter; + + private OtelSetter(final HttpTextFormat.Setter setter) { + this.setter = setter; + } + + @Override + public void set(final C carrier, final String key, final String value) { + setter.set(carrier, key, value); + } + } + + private static class OtelGetter implements AgentPropagation.Getter { + private static final String DD_TRACE_ID_KEY = "x-datadog-trace-id"; + private static final String DD_SPAN_ID_KEY = "x-datadog-parent-id"; + private static final String DD_SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; + private static final String DD_ORIGIN_KEY = "x-datadog-origin"; + + private static final String B3_TRACE_ID_KEY = "X-B3-TraceId"; + private static final String B3_SPAN_ID_KEY = "X-B3-SpanId"; + private static final String B3_SAMPLING_PRIORITY_KEY = "X-B3-Sampled"; + + private static final String HAYSTACK_TRACE_ID_KEY = "Trace-ID"; + private static final String HAYSTACK_SPAN_ID_KEY = "Span-ID"; + private static final String HAYSTACK_PARENT_ID_KEY = "Parent_ID"; + + private static final List KEYS = + Arrays.asList( + DD_TRACE_ID_KEY, + DD_SPAN_ID_KEY, + DD_SAMPLING_PRIORITY_KEY, + DD_ORIGIN_KEY, + B3_TRACE_ID_KEY, + B3_SPAN_ID_KEY, + B3_SAMPLING_PRIORITY_KEY, + HAYSTACK_TRACE_ID_KEY, + HAYSTACK_SPAN_ID_KEY, + HAYSTACK_PARENT_ID_KEY); + + private final HttpTextFormat.Getter getter; + + private OtelGetter(final HttpTextFormat.Getter getter) { + this.getter = getter; + } + + @Override + public Iterable keys(final C carrier) { + // TODO: Otel doesn't expose the keys, so we have to rely on hard coded keys. + // https://github.com/open-telemetry/opentelemetry-specification/issues/433 + return KEYS; + } + + @Override + public String get(final C carrier, final String key) { + return getter.get(carrier, key); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelScope.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelScope.java new file mode 100644 index 00000000000..38ea828ea72 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelScope.java @@ -0,0 +1,37 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.context.TraceScope; +import io.opentelemetry.context.Scope; + +public class OtelScope implements Scope, TraceScope { + private final AgentScope delegate; + + OtelScope(final AgentScope delegate) { + this.delegate = delegate; + } + + @Override + public Continuation capture() { + return delegate.capture(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public boolean isAsyncPropagating() { + return delegate.isAsyncPropagating(); + } + + @Override + public void setAsyncPropagation(final boolean value) { + delegate.setAsyncPropagation(value); + } + + public AgentScope getDelegate() { + return delegate; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpan.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpan.java new file mode 100644 index 00000000000..f863df0e3fb --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpan.java @@ -0,0 +1,223 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.api.DDTags; +import datadog.trace.api.interceptor.MutableSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import io.opentelemetry.common.AttributeValue; +import io.opentelemetry.trace.EndSpanOptions; +import io.opentelemetry.trace.Event; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.Status; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class OtelSpan implements Span, MutableSpan { + private final AgentSpan delegate; + private final TypeConverter converter; + + OtelSpan(final AgentSpan agentSpan, final TypeConverter typeConverter) { + delegate = agentSpan; + converter = typeConverter; + } + + @Override + public void setAttribute(final String key, final String value) { + delegate.setTag(key, value); + } + + @Override + public void setAttribute(final String key, final long value) { + delegate.setTag(key, value); + } + + @Override + public void setAttribute(final String key, final double value) { + delegate.setTag(key, value); + } + + @Override + public void setAttribute(final String key, final boolean value) { + delegate.setTag(key, value); + } + + @Override + public void setAttribute(final String key, final AttributeValue value) { + switch (value.getType()) { + case LONG: + delegate.setTag(key, value.getLongValue()); + break; + case DOUBLE: + delegate.setTag(key, value.getDoubleValue()); + break; + case STRING: + delegate.setTag(key, value.getStringValue()); + break; + case BOOLEAN: + delegate.setTag(key, value.getBooleanValue()); + break; + default: + // Unsupported.... Ignoring. + } + } + + @Override + public void addEvent(final String name) {} + + @Override + public void addEvent(final String name, final long timestamp) {} + + @Override + public void addEvent(final String name, final Map attributes) {} + + @Override + public void addEvent( + final String name, final Map attributes, final long timestamp) {} + + @Override + public void addEvent(final Event event) {} + + @Override + public void addEvent(final Event event, final long timestamp) {} + + @Override + public void setStatus(final Status status) { + if (!status.isOk()) { + delegate.setError(true); + delegate.setTag(DDTags.ERROR_MSG, status.getDescription()); + } + } + + @Override + public void updateName(final String name) { + delegate.setResourceName(name); + } + + @Override + public void end() { + delegate.finish(); + } + + @Override + public void end(final EndSpanOptions endOptions) { + if (endOptions.getEndTimestamp() > 0) { + delegate.finish(TimeUnit.NANOSECONDS.toMicros(endOptions.getEndTimestamp())); + } else { + delegate.finish(); + } + } + + @Override + public SpanContext getContext() { + return converter.toSpanContext(delegate.context()); + } + + @Override + public boolean isRecording() { + return delegate.getTraceId().toLong() != 0; + } + + public AgentSpan getDelegate() { + return delegate; + } + + @Override + public long getStartTime() { + return delegate.getStartTime(); + } + + @Override + public long getDurationNano() { + return delegate.getDurationNano(); + } + + @Override + public String getOperationName() { + return delegate.getOperationName(); + } + + @Override + public MutableSpan setOperationName(final String serviceName) { + return delegate.setOperationName(serviceName); + } + + @Override + public String getServiceName() { + return delegate.getServiceName(); + } + + @Override + public MutableSpan setServiceName(final String serviceName) { + return delegate.setServiceName(serviceName); + } + + @Override + public String getResourceName() { + return delegate.getResourceName(); + } + + @Override + public MutableSpan setResourceName(final String resourceName) { + return delegate.setResourceName(resourceName); + } + + @Override + public Integer getSamplingPriority() { + return delegate.getSamplingPriority(); + } + + @Override + public MutableSpan setSamplingPriority(final int newPriority) { + return delegate.setSamplingPriority(newPriority); + } + + @Override + public String getSpanType() { + return delegate.getSpanType(); + } + + @Override + public MutableSpan setSpanType(final String type) { + return delegate.setSpanType(type); + } + + @Override + public Map getTags() { + return delegate.getTags(); + } + + @Override + public MutableSpan setTag(final String tag, final String value) { + return delegate.setTag(tag, value); + } + + @Override + public MutableSpan setTag(final String tag, final boolean value) { + return delegate.setTag(tag, value); + } + + @Override + public MutableSpan setTag(final String tag, final Number value) { + return delegate.setTag(tag, value); + } + + @Override + public Boolean isError() { + return delegate.isError(); + } + + @Override + public MutableSpan setError(final boolean value) { + return delegate.setError(value); + } + + @Override + public MutableSpan getRootSpan() { + return delegate.getRootSpan(); + } + + @Override + public MutableSpan getLocalRootSpan() { + return delegate.getLocalRootSpan(); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpanContext.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpanContext.java new file mode 100644 index 00000000000..74a1deb2a49 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelSpanContext.java @@ -0,0 +1,47 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.TraceFlags; +import io.opentelemetry.trace.TraceId; +import io.opentelemetry.trace.TraceState; + +public class OtelSpanContext extends SpanContext { + private static final TraceFlags FLAGS = TraceFlags.builder().setIsSampled(true).build(); + private final AgentSpan.Context delegate; + + OtelSpanContext(final AgentSpan.Context delegate) { + this.delegate = delegate; + } + + @Override + public TraceId getTraceId() { + return new TraceId(0, delegate.getTraceId().toLong()); + } + + @Override + public SpanId getSpanId() { + return new SpanId(delegate.getSpanId().toLong()); + } + + @Override + public TraceFlags getTraceFlags() { + return FLAGS; + } + + @Override + public TraceState getTraceState() { + return TraceState.getDefault(); + } + + @Override + public boolean isRemote() { + // check if delegate is a ExtractedContext + return false; + } + + AgentSpan.Context getDelegate() { + return delegate; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracer.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracer.java new file mode 100644 index 00000000000..7cd64774f55 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracer.java @@ -0,0 +1,163 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import io.opentelemetry.common.AttributeValue; +import io.opentelemetry.context.Scope; +import io.opentelemetry.trace.Link; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.Tracer; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class OtelTracer implements Tracer { + private final String tracerName; + private final AgentTracer.TracerAPI tracer; + private final TypeConverter converter; + + OtelTracer( + final String tracerName, final AgentTracer.TracerAPI tracer, final TypeConverter converter) { + this.tracerName = tracerName; + this.tracer = tracer; + this.converter = converter; + } + + @Override + public Span getCurrentSpan() { + return converter.toSpan(tracer.activeSpan()); + } + + @Override + public Scope withSpan(final Span span) { + final AgentSpan agentSpan = converter.toAgentSpan(span); + final AgentScope agentScope = tracer.activateSpan(agentSpan); + return converter.toScope(agentScope); + } + + @Override + public Span.Builder spanBuilder(final String spanName) { + return new SpanBuilder(spanName); + } + + private class SpanBuilder implements Span.Builder { + private final AgentTracer.SpanBuilder delegate; + private boolean parentSet = false; + + public SpanBuilder(final String spanName) { + delegate = tracer.buildSpan(tracerName).withResourceName(spanName); + } + + @Override + public Span.Builder setParent(final Span parent) { + parentSet = true; + delegate.asChildOf(converter.toAgentSpan(parent).context()); + return this; + } + + @Override + public Span.Builder setParent(final SpanContext remoteParent) { + parentSet = true; + delegate.asChildOf(converter.toContext(remoteParent)); + return this; + } + + @Override + public Span.Builder setNoParent() { + parentSet = true; + delegate.asChildOf(null); + delegate.ignoreActiveSpan(); + return this; + } + + @Override + public Span.Builder addLink(final SpanContext spanContext) { + if (!parentSet) { + delegate.asChildOf(converter.toContext(spanContext)); + } + return this; + } + + @Override + public Span.Builder addLink( + final SpanContext spanContext, final Map attributes) { + if (!parentSet) { + delegate.asChildOf(converter.toContext(spanContext)); + } + return this; + } + + @Override + public Span.Builder addLink(final Link link) { + if (!parentSet) { + delegate.asChildOf(converter.toContext(link.getContext())); + } + return this; + } + + @Override + public Span.Builder setAttribute(final String key, final String value) { + delegate.withTag(key, value); + return this; + } + + @Override + public Span.Builder setAttribute(final String key, final long value) { + delegate.withTag(key, value); + return this; + } + + @Override + public Span.Builder setAttribute(final String key, final double value) { + delegate.withTag(key, value); + return this; + } + + @Override + public Span.Builder setAttribute(final String key, final boolean value) { + delegate.withTag(key, value); + return this; + } + + @Override + public Span.Builder setAttribute(final String key, final AttributeValue value) { + switch (value.getType()) { + case LONG: + delegate.withTag(key, value.getLongValue()); + break; + case DOUBLE: + delegate.withTag(key, value.getDoubleValue()); + break; + case STRING: + delegate.withTag(key, value.getStringValue()); + break; + case BOOLEAN: + delegate.withTag(key, value.getBooleanValue()); + break; + default: + // Unsupported.... Ignoring. + } + return this; + } + + @Override + public Span.Builder setSpanKind(final Span.Kind spanKind) { + // TODO: update delegate.operationName + delegate.withTag(Tags.SPAN_KIND, spanKind.name()); + return this; + } + + @Override + public Span.Builder setStartTimestamp(final long startTimestamp) { + delegate.withStartTimestamp(TimeUnit.NANOSECONDS.toMicros(startTimestamp)); + return this; + } + + @Override + public Span startSpan() { + return converter.toSpan(delegate.start()); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracerProvider.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracerProvider.java new file mode 100644 index 00000000000..c9bc473b619 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/OtelTracerProvider.java @@ -0,0 +1,24 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import io.opentelemetry.trace.Tracer; +import io.opentelemetry.trace.TracerProvider; + +public class OtelTracerProvider implements TracerProvider { + public static final OtelTracerProvider INSTANCE = new OtelTracerProvider(); + + private final TypeConverter converter = new TypeConverter(); + + private OtelTracerProvider() {} + + @Override + public Tracer get(final String instrumentationName) { + return get(instrumentationName, null); + } + + @Override + public Tracer get(final String instrumentationName, final String instrumentationVersion) { + // TODO: cache return value. + return new OtelTracer(instrumentationName, AgentTracer.get(), converter); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/TypeConverter.java b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/TypeConverter.java new file mode 100644 index 00000000000..89616b15ad2 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/main/java/datadog/trace/instrumentation/opentelemetry/TypeConverter.java @@ -0,0 +1,48 @@ +package datadog.trace.instrumentation.opentelemetry; + +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.SpanContext; + +// Centralized place to do conversions +public class TypeConverter { + // TODO maybe add caching to reduce new objects being created + + public AgentSpan toAgentSpan(final Span span) { + if (span instanceof OtelSpan) { + return ((OtelSpan) span).getDelegate(); + } + return null == span ? null : AgentTracer.NoopAgentSpan.INSTANCE; + } + + public Span toSpan(final AgentSpan agentSpan) { + if (agentSpan == null) { + return null; + } + return new OtelSpan(agentSpan, this); + } + + public Scope toScope(final AgentScope scope) { + if (scope == null) { + return null; + } + return new OtelScope(scope); + } + + public SpanContext toSpanContext(final AgentSpan.Context context) { + if (context == null) { + return null; + } + return new OtelSpanContext(context); + } + + public AgentSpan.Context toContext(final SpanContext spanContext) { + if (spanContext instanceof OtelSpanContext) { + return ((OtelSpanContext) spanContext).getDelegate(); + } + return null == spanContext ? null : AgentTracer.NoopContext.INSTANCE; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/src/test/groovy/OpenTelemetryTest.groovy b/dd-java-agent/instrumentation/opentelemetry/src/test/groovy/OpenTelemetryTest.groovy new file mode 100644 index 00000000000..978cb3a89a2 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/src/test/groovy/OpenTelemetryTest.groovy @@ -0,0 +1,288 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDId +import datadog.trace.api.DDTags +import datadog.trace.api.interceptor.MutableSpan +import datadog.trace.api.sampling.PrioritySampling +import datadog.trace.context.TraceScope +import datadog.trace.core.propagation.ExtractedContext +import io.grpc.Context +import io.opentelemetry.OpenTelemetry +import io.opentelemetry.context.propagation.HttpTextFormat +import io.opentelemetry.trace.Status +import io.opentelemetry.trace.TracingContextUtils +import spock.lang.Subject + +class OpenTelemetryTest extends AgentTestRunner { + static { + System.setProperty("dd.integration.opentelemetry-beta.enabled", "true") + } + + @Subject + def tracer = OpenTelemetry.tracerProvider.get("test-inst") + def httpPropagator = OpenTelemetry.getPropagators().httpTextFormat + + def "test span tags"() { + setup: + def builder = tracer.spanBuilder("some name") + if (tagBuilder) { + builder.setAttribute(DDTags.RESOURCE_NAME, "some resource") + .setAttribute("string", "a") + .setAttribute("number", 1) + .setAttribute("boolean", true) + } + def result = builder.startSpan() + if (tagSpan) { + result.setAttribute(DDTags.RESOURCE_NAME, "other resource") + result.setAttribute("string", "b") + result.setAttribute("number", 2) + result.setAttribute("boolean", false) + } + + expect: + result instanceof MutableSpan + (result as MutableSpan).localRootSpan == result.delegate + tracer.currentSpan == null + + when: + result.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + parent() + serviceName "unnamed-java-app" + operationName "test-inst" + if (tagSpan) { + resourceName "other resource" + } else if (tagBuilder) { + resourceName "some resource" + } else { + resourceName "some name" + } + errored false + tags { + if (tagSpan) { + "string" "b" + "number" 2 + "boolean" false + } else if (tagBuilder) { + "string" "a" + "number" 1 + "boolean" true + } + defaultTags() + } + metrics { + defaultMetrics() + } + } + } + } + + where: + tagBuilder | tagSpan + true | false + true | true + false | false + false | true + } + + def "test span exception"() { + setup: + def builder = tracer.spanBuilder("some name") + def result = builder.startSpan() + result.setStatus(Status.UNKNOWN) + result.setAttribute(DDTags.ERROR_MSG, (String) exception.message) + result.setAttribute(DDTags.ERROR_TYPE, (String) exception.class.name) + final StringWriter errorString = new StringWriter() + exception.printStackTrace(new PrintWriter(errorString)) + result.setAttribute(DDTags.ERROR_STACK, errorString.toString()) + + expect: + result instanceof MutableSpan + (result as MutableSpan).localRootSpan == result.delegate + (result as MutableSpan).isError() == (exception != null) + tracer.currentSpan == null + + when: + result.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + parent() + serviceName "unnamed-java-app" + operationName "test-inst" + resourceName "some name" + errored true + tags { + errorTags(exception.class) + defaultTags() + } + metrics { + defaultMetrics() + } + } + } + } + + where: + exception = new Exception() + } + + def "test span links"() { + setup: + def builder = tracer.spanBuilder("some name") + if (parentId) { + builder.setParent(tracer.converter.toSpanContext(new ExtractedContext(DDId.ONE, DDId.from(parentId), 0, null, [:], [:]))) + } + if (linkId) { + builder.addLink(tracer.converter.toSpanContext(new ExtractedContext(DDId.ONE, DDId.from(linkId), 0, null, [:], [:]))) + } + def result = builder.startSpan() + + expect: + result instanceof MutableSpan + (result as MutableSpan).localRootSpan == result.delegate + tracer.currentSpan == null + + when: + result.end() + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + if (expectedId) { + traceDDId(DDId.ONE) + parentDDId(DDId.from(expectedId)) + } else { + parent() + } + serviceName "unnamed-java-app" + operationName "test-inst" + resourceName "some name" + errored false + tags { + defaultTags(expectedId != null) + } + metrics { + defaultMetrics() + } + } + } + } + + where: + parentId | linkId | expectedId + null | null | null + 2 | null | 2 + null | 3 | 3 + 2 | 3 | 2 + } + + def "test scope"() { + setup: + def span = tracer.spanBuilder("some name").startSpan() + def scope = tracer.withSpan(span) + + expect: + scope instanceof TraceScope + tracer.currentSpan.delegate == scope.delegate.span() + + when: + scope.close() + + then: + tracer.currentSpan == null + + cleanup: + span.end() + } + + def "test continuation"() { + setup: + def span = tracer.spanBuilder("some name").startSpan() + TraceScope scope = tracer.withSpan(span) + scope.setAsyncPropagation(true) + + expect: + tracer.currentSpan.delegate == span.delegate + + when: + def continuation = scope.capture() + + then: + continuation instanceof TraceScope.Continuation + + when: + scope.close() + + then: + tracer.currentSpan == null + + when: + scope = continuation.activate() + + then: + tracer.currentSpan.delegate == span.delegate + + cleanup: + scope.close() + span.end() + } + + def "test inject extract"() { + setup: + def span = tracer.spanBuilder("some name").startSpan() + def context = TracingContextUtils.withSpan(span, Context.current()) + def textMap = [:] + + when: + span.delegate.samplingPriority = contextPriority + httpPropagator.inject(context, textMap, new TextMapSetter()) + + then: + textMap == [ + "x-datadog-trace-id" : "$span.delegate.traceId", + "x-datadog-parent-id" : "$span.delegate.spanId", + "x-datadog-sampling-priority": propagatedPriority.toString(), + ] + + when: + def extractedContext = httpPropagator.extract(context, textMap, new TextMapGetter()) + def extract = TracingContextUtils.getSpanWithoutDefault(extractedContext) + + then: + extract.context.traceId == span.context.traceId + extract.context.spanId == span.context.spanId + extract.context.delegate.samplingPriority == propagatedPriority + + cleanup: + span.end() + + where: + contextPriority | propagatedPriority + PrioritySampling.SAMPLER_DROP | PrioritySampling.SAMPLER_DROP + PrioritySampling.SAMPLER_KEEP | PrioritySampling.SAMPLER_KEEP + PrioritySampling.UNSET | PrioritySampling.SAMPLER_KEEP + PrioritySampling.USER_KEEP | PrioritySampling.USER_KEEP + PrioritySampling.USER_DROP | PrioritySampling.USER_DROP + } + + static class TextMapGetter implements HttpTextFormat.Getter> { + @Override + String get(Map carrier, String key) { + return carrier.get(key) + } + } + + static class TextMapSetter implements HttpTextFormat.Setter> { + @Override + void set(Map carrier, String key, String value) { + carrier.put(key, value) + } + } +} diff --git a/dd-java-agent/instrumentation/opentracing/api-0.31/src/main/java/datadog/trace/instrumentation/opentracing31/TypeConverter.java b/dd-java-agent/instrumentation/opentracing/api-0.31/src/main/java/datadog/trace/instrumentation/opentracing31/TypeConverter.java index e4026015a0f..4dcd27b5849 100644 --- a/dd-java-agent/instrumentation/opentracing/api-0.31/src/main/java/datadog/trace/instrumentation/opentracing31/TypeConverter.java +++ b/dd-java-agent/instrumentation/opentracing/api-0.31/src/main/java/datadog/trace/instrumentation/opentracing31/TypeConverter.java @@ -20,14 +20,10 @@ public TypeConverter(final LogHandler logHandler) { } public AgentSpan toAgentSpan(final Span span) { - if (span == null) { - return null; - } else if (span instanceof OTSpan) { + if (span instanceof OTSpan) { return ((OTSpan) span).getDelegate(); - } else { - // NOOP Span, otherwise arbitrary spans aren't supported. - return AgentTracer.NoopAgentSpan.INSTANCE; } + return null == span ? null : AgentTracer.NoopAgentSpan.INSTANCE; } public Span toSpan(final AgentSpan agentSpan) { @@ -58,12 +54,9 @@ public SpanContext toSpanContext(final AgentSpan.Context context) { } public AgentSpan.Context toContext(final SpanContext spanContext) { - if (spanContext == null) { - return null; - } else if (spanContext instanceof OTSpanContext) { + if (spanContext instanceof OTSpanContext) { return ((OTSpanContext) spanContext).getDelegate(); - } else { - return AgentTracer.NoopContext.INSTANCE; } + return null == spanContext ? null : AgentTracer.NoopContext.INSTANCE; } } diff --git a/dd-java-agent/instrumentation/opentracing/api-0.32/src/main/java/datadog/trace/instrumentation/opentracing32/TypeConverter.java b/dd-java-agent/instrumentation/opentracing/api-0.32/src/main/java/datadog/trace/instrumentation/opentracing32/TypeConverter.java index 8782eeae116..4b27a35767c 100644 --- a/dd-java-agent/instrumentation/opentracing/api-0.32/src/main/java/datadog/trace/instrumentation/opentracing32/TypeConverter.java +++ b/dd-java-agent/instrumentation/opentracing/api-0.32/src/main/java/datadog/trace/instrumentation/opentracing32/TypeConverter.java @@ -20,14 +20,10 @@ public TypeConverter(final LogHandler logHandler) { } public AgentSpan toAgentSpan(final Span span) { - if (span == null) { - return null; - } else if (span instanceof OTSpan) { + if (span instanceof OTSpan) { return ((OTSpan) span).getDelegate(); - } else { - // NOOP Span, otherwise arbitrary spans aren't supported. - return AgentTracer.NoopAgentSpan.INSTANCE; } + return null == span ? null : AgentTracer.NoopAgentSpan.INSTANCE; } public Span toSpan(final AgentSpan agentSpan) { @@ -58,12 +54,9 @@ public SpanContext toSpanContext(final AgentSpan.Context context) { } public AgentSpan.Context toContext(final SpanContext spanContext) { - if (spanContext == null) { - return null; - } else if (spanContext instanceof OTSpanContext) { + if (spanContext instanceof OTSpanContext) { return ((OTSpanContext) spanContext).getDelegate(); - } else { - return AgentTracer.NoopContext.INSTANCE; } + return null == spanContext ? null : AgentTracer.NoopContext.INSTANCE; } } diff --git a/settings.gradle b/settings.gradle index f8e760e4f79..2b16308f9db 100644 --- a/settings.gradle +++ b/settings.gradle @@ -126,6 +126,7 @@ include ':dd-java-agent:instrumentation:netty-4.0' include ':dd-java-agent:instrumentation:netty-4.1' include ':dd-java-agent:instrumentation:okhttp-2' include ':dd-java-agent:instrumentation:okhttp-3' +include ':dd-java-agent:instrumentation:opentelemetry' include ':dd-java-agent:instrumentation:opentracing' include ':dd-java-agent:instrumentation:opentracing:api-0.31' include ':dd-java-agent:instrumentation:opentracing:api-0.32'