diff --git a/dd-java-agent/agent-builder/build.gradle b/dd-java-agent/agent-builder/build.gradle index 6dce229a347..d014ee79196 100644 --- a/dd-java-agent/agent-builder/build.gradle +++ b/dd-java-agent/agent-builder/build.gradle @@ -7,6 +7,8 @@ excludedClassesCoverage += ['datadog.trace.agent.tooling.*'] dependencies { api project(':dd-java-agent:agent-tooling') + implementation project(':dd-java-agent:agent-otel:otel-tooling') + testImplementation project(':dd-java-agent:testing') } diff --git a/dd-java-agent/agent-builder/src/main/java/datadog/trace/agent/tooling/ExtensionFinder.java b/dd-java-agent/agent-builder/src/main/java/datadog/trace/agent/tooling/ExtensionFinder.java index 2bc4c63fe1d..925c8093e22 100644 --- a/dd-java-agent/agent-builder/src/main/java/datadog/trace/agent/tooling/ExtensionFinder.java +++ b/dd-java-agent/agent-builder/src/main/java/datadog/trace/agent/tooling/ExtensionFinder.java @@ -1,5 +1,6 @@ package datadog.trace.agent.tooling; +import static datadog.opentelemetry.tooling.OtelExtensionHandler.OPENTELEMETRY; import static datadog.trace.agent.tooling.ExtensionHandler.DATADOG; import de.thetaphi.forbiddenapis.SuppressForbidden; @@ -23,7 +24,7 @@ public final class ExtensionFinder { private static final Logger log = LoggerFactory.getLogger(ExtensionFinder.class); - private static final ExtensionHandler[] handlers = {DATADOG}; + private static final ExtensionHandler[] handlers = {OPENTELEMETRY, DATADOG}; /** * Discovers extensions on the configured path and creates a classloader for each extension. diff --git a/dd-java-agent/agent-otel/otel-bootstrap/build.gradle b/dd-java-agent/agent-otel/otel-bootstrap/build.gradle index ae5a4aed26a..b8eeb1efed3 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/build.gradle +++ b/dd-java-agent/agent-otel/otel-bootstrap/build.gradle @@ -11,6 +11,7 @@ configurations { canBeConsumed = false canBeResolved = true } + compileClasspath.extendsFrom(embeddedClasspath) instrumentPluginClasspath { visible = false canBeConsumed = false @@ -18,7 +19,7 @@ configurations { } } -instrument.plugins = ['datadog.opentelemetry.tooling.OtelShimGradlePlugin'] +instrument.plugins = ['datadog.opentelemetry.tooling.shim.OtelShimGradlePlugin'] minimumInstructionCoverage = 0.0 minimumBranchCoverage = 0.0 diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/Java8BytecodeBridge.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/Java8BytecodeBridge.java new file mode 100644 index 00000000000..ffc369acd08 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/datadog/trace/bootstrap/otel/Java8BytecodeBridge.java @@ -0,0 +1,33 @@ +package datadog.trace.bootstrap.otel; + +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; + +/** Support references to OpenTelemetry's Java8BytecodeBridge in external extensions. */ +public final class Java8BytecodeBridge { + + // Static helpers that will redirect to our embedded copy of the OpenTelemetry API + + public static Context currentContext() { + return Context.current(); + } + + public static Context rootContext() { + return Context.root(); + } + + public static Span currentSpan() { + return Span.current(); + } + + public static Span spanFromContext(Context context) { + return Span.fromContext(context); + } + + public static Baggage baggageFromContext(Context context) { + return Baggage.fromContext(context); + } + + private Java8BytecodeBridge() {} +} diff --git a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/package-info.java b/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/package-info.java deleted file mode 100644 index 3f00de3c82c..00000000000 --- a/dd-java-agent/agent-otel/otel-bootstrap/src/main/java/package-info.java +++ /dev/null @@ -1 +0,0 @@ -// placeholder to activate the compiler task diff --git a/dd-java-agent/agent-otel/otel-tooling/build.gradle b/dd-java-agent/agent-otel/otel-tooling/build.gradle index e3511f9f60c..e1d036de4d2 100644 --- a/dd-java-agent/agent-otel/otel-tooling/build.gradle +++ b/dd-java-agent/agent-otel/otel-tooling/build.gradle @@ -6,4 +6,6 @@ minimumBranchCoverage = 0.0 dependencies { api deps.bytebuddy api deps.bytebuddyagent + + compileOnly project(':dd-java-agent:agent-tooling') } diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelExtensionHandler.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelExtensionHandler.java new file mode 100644 index 00000000000..06a6f316882 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelExtensionHandler.java @@ -0,0 +1,39 @@ +package datadog.opentelemetry.tooling; + +import datadog.trace.agent.tooling.ExtensionHandler; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** Handles OpenTelemetry instrumentations, so they can be loaded into the Datadog tracer. */ +public final class OtelExtensionHandler extends ExtensionHandler { + + /** Handler for loading externally built OpenTelemetry extensions. */ + public static final OtelExtensionHandler OPENTELEMETRY = new OtelExtensionHandler(); + + private static final String OPENTELEMETRY_MODULE_DESCRIPTOR = + "META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule"; + + private static final String DATADOG_MODULE_DESCRIPTOR = + "META-INF/services/datadog.trace.agent.tooling.InstrumenterModule"; + + @Override + public JarEntry mapEntry(JarFile jar, String file) { + if (DATADOG_MODULE_DESCRIPTOR.equals(file)) { + // redirect request to include OpenTelemetry instrumentations + return super.mapEntry(jar, OPENTELEMETRY_MODULE_DESCRIPTOR); + } else { + return super.mapEntry(jar, file); + } + } + + @Override + public URLConnection mapContent(URL url, JarFile jar, JarEntry entry) { + if (entry.getName().endsWith(".class")) { + return new ClassMappingConnection(url, jar, entry, OtelInstrumentationMapper::new); + } else { + return new JarFileConnection(url, jar, entry); + } + } +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumentationMapper.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumentationMapper.java new file mode 100644 index 00000000000..3a047579ba2 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumentationMapper.java @@ -0,0 +1,109 @@ +package datadog.opentelemetry.tooling; + +import static datadog.trace.agent.tooling.ExtensionHandler.MAP_LOGGING; + +import datadog.trace.agent.tooling.InstrumenterModule; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.bytebuddy.jar.asm.ClassVisitor; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.commons.ClassRemapper; +import net.bytebuddy.jar.asm.commons.Remapper; + +/** Maps OpenTelemetry instrumentations to use the Datadog {@link InstrumenterModule} API. */ +public final class OtelInstrumentationMapper extends ClassRemapper { + + private static final Set UNSUPPORTED_TYPES = + new HashSet<>( + Arrays.asList("io/opentelemetry/javaagent/tooling/muzzle/InstrumentationModuleMuzzle")); + + private static final Set UNSUPPORTED_METHODS = + new HashSet<>( + Arrays.asList( + "getMuzzleReferences", "getMuzzleHelperClassNames", "registerMuzzleVirtualFields")); + + public OtelInstrumentationMapper(ClassVisitor classVisitor) { + super(classVisitor, Renamer.INSTANCE); + } + + @Override + public void visit( + int version, + int access, + String name, + String signature, + String superName, + String[] interfaces) { + super.visit(version, access, name, signature, superName, removeUnsupportedTypes(interfaces)); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + if (!UNSUPPORTED_METHODS.contains(name)) { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } else { + return null; // remove unsupported method + } + } + + private String[] removeUnsupportedTypes(String[] interfaces) { + List filtered = null; + for (int i = interfaces.length - 1; i >= 0; i--) { + if (UNSUPPORTED_TYPES.contains(interfaces[i])) { + if (null == filtered) { + filtered = new ArrayList<>(Arrays.asList(interfaces)); + } + filtered.remove(i); // remove unsupported interface + } + } + return null != filtered ? filtered.toArray(new String[0]) : interfaces; + } + + static final class Renamer extends Remapper { + static final Renamer INSTANCE = new Renamer(); + + private static final String OTEL_JAVAAGENT_SHADED_PREFIX = + "io/opentelemetry/javaagent/shaded/io/opentelemetry/"; + + /** Datadog equivalent of OpenTelemetry instrumentation classes. */ + private static final Map RENAMED_TYPES = new HashMap<>(); + + static { + RENAMED_TYPES.put( + "io/opentelemetry/javaagent/extension/instrumentation/InstrumentationModule", + "datadog/opentelemetry/tooling/OtelInstrumenterModule"); + RENAMED_TYPES.put( + "io/opentelemetry/javaagent/extension/instrumentation/TypeInstrumentation", + "datadog/opentelemetry/tooling/OtelInstrumenter"); + RENAMED_TYPES.put( + "io/opentelemetry/javaagent/extension/instrumentation/TypeTransformer", + "datadog/opentelemetry/tooling/OtelTransformer"); + RENAMED_TYPES.put( + "io/opentelemetry/javaagent/bootstrap/Java8BytecodeBridge", + "datadog/trace/bootstrap/otel/Java8BytecodeBridge"); + RENAMED_TYPES.put( + "io/opentelemetry/javaagent/extension/matcher/AgentElementMatchers", + "datadog/trace/agent/tooling/bytebuddy/matcher/HierarchyMatchers"); + } + + @Override + public String map(String internalName) { + String rename = RENAMED_TYPES.get(internalName); + if (null != rename) { + return rename; + } + // map OpenTelemetry's shaded API to our embedded copy + if (internalName.startsWith(OTEL_JAVAAGENT_SHADED_PREFIX)) { + return "datadog/trace/bootstrap/otel/" + + internalName.substring(OTEL_JAVAAGENT_SHADED_PREFIX.length()); + } + return MAP_LOGGING.apply(internalName); + } + } +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenter.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenter.java new file mode 100644 index 00000000000..47f6bcbb6bf --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenter.java @@ -0,0 +1,41 @@ +package datadog.opentelemetry.tooling; + +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Replaces OpenTelemetry's {@code TypeInstrumentation} callback when mapping extensions. */ +public interface OtelInstrumenter + extends Instrumenter.ForTypeHierarchy, + Instrumenter.HasMethodAdvice, + Instrumenter.HasTypeAdvice { + + @Override + default String hierarchyMarkerType() { + return null; // no hint available + } + + @Override + default ElementMatcher hierarchyMatcher() { + return typeMatcher(); + } + + @Override + default void methodAdvice(MethodTransformer methodTransformer) { + OtelTransformerState.capture(this).with(methodTransformer); + } + + @Override + default void typeAdvice(TypeTransformer typeTransformer) { + OtelTransformerState.capture(this).with(typeTransformer); + } + + ElementMatcher typeMatcher(); + + /** + * Once both transformers have been captured in {@link #methodAdvice} and {@link #typeAdvice} the + * {@code #transform} method will be called. This allows the extension to register method and type + * advice at the same time, using the single interface expected by OpenTelemetry. + */ + void transform(OtelTransformer transformer); +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenterModule.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenterModule.java new file mode 100644 index 00000000000..e88d2c09264 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelInstrumenterModule.java @@ -0,0 +1,33 @@ +package datadog.opentelemetry.tooling; + +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.InstrumenterConfig; + +/** + * Replaces OpenTelemetry's {@code InstrumentationModule} when mapping extensions. + * + *

Original instrumentation names and aliases are prefixed with {@literal "otel."}. + */ +public abstract class OtelInstrumenterModule extends InstrumenterModule.Tracing { + + public OtelInstrumenterModule(String instrumentationName, String... additionalNames) { + super(namespace(instrumentationName), namespace(additionalNames)); + } + + @Override + protected boolean defaultEnabled() { + return InstrumenterConfig.get().isTraceOtelEnabled() && super.defaultEnabled(); + } + + private static String namespace(String name) { + return "otel." + name; + } + + private static String[] namespace(String[] names) { + String[] namespaced = new String[names.length]; + for (int i = 0; i < names.length; i++) { + namespaced[i] = namespace(names[i]); + } + return namespaced; + } +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformer.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformer.java new file mode 100644 index 00000000000..f57bf753481 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformer.java @@ -0,0 +1,14 @@ +package datadog.opentelemetry.tooling; + +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** Replaces OpenTelemetry's {@code TypeTransformer} callback when mapping extensions. */ +public interface OtelTransformer { + + void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName); + + void applyTransformer(AgentBuilder.Transformer transformer); +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformerState.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformerState.java new file mode 100644 index 00000000000..04bfe4949a6 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelTransformerState.java @@ -0,0 +1,73 @@ +package datadog.opentelemetry.tooling; + +import datadog.trace.agent.tooling.Instrumenter; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * {@link OtelTransformer} state captured when processing OpenTelemetry extensions with {@code + * CombiningTransformerBuilder}. Assumes that the builder is single threaded. + * + *

OpenTelemetry has a single transformer callback with methods to register method advice and + * type transformations at the same time, whereas the Datadog tracer has separate {@link + * Instrumenter.MethodTransformer} and {@link Instrumenter.TypeTransformer} callbacks. + * + *

To map between the two we capture the Datadog method and type transformers here, from calls to + * {@link OtelInstrumenter}. Once we have captured both transformers we trigger the single transform + * request through the mapped OpenTelemetry callback. + */ +final class OtelTransformerState implements OtelTransformer { + private static final OtelTransformerState CURRENT = new OtelTransformerState(); + + private OtelInstrumenter instrumenter; + private Instrumenter.MethodTransformer methodTransformer; + private Instrumenter.TypeTransformer typeTransformer; + + static OtelTransformerState capture(OtelInstrumenter instrumenter) { + if (instrumenter != CURRENT.instrumenter) { + CURRENT.reset(); + CURRENT.instrumenter = instrumenter; + } + return CURRENT; + } + + void with(Instrumenter.MethodTransformer methodTransformer) { + this.methodTransformer = methodTransformer; + if (null != this.typeTransformer) { + triggerTransform(); + } + } + + void with(Instrumenter.TypeTransformer typeTransformer) { + this.typeTransformer = typeTransformer; + if (null != this.methodTransformer) { + triggerTransform(); + } + } + + private void triggerTransform() { + try { + instrumenter.transform(this); + } finally { + reset(); + } + } + + private void reset() { + this.instrumenter = null; + this.methodTransformer = null; + this.typeTransformer = null; + } + + @Override + public void applyAdviceToMethod( + ElementMatcher methodMatcher, String adviceClassName) { + methodTransformer.applyAdvice(methodMatcher, adviceClassName); + } + + @Override + public void applyTransformer(AgentBuilder.Transformer transformer) { + typeTransformer.applyAdvice(transformer::transform); + } +} diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimGradlePlugin.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java similarity index 68% rename from dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimGradlePlugin.java rename to dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java index ca891d25992..f23f446983d 100644 --- a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimGradlePlugin.java +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java @@ -1,8 +1,5 @@ -package datadog.opentelemetry.tooling; +package datadog.opentelemetry.tooling.shim; -import static datadog.opentelemetry.tooling.OtelShimInjector.OTEL_CONTEXT_CLASSES; -import static datadog.opentelemetry.tooling.OtelShimInjector.OTEL_CONTEXT_STORAGE_CLASSES; -import static datadog.opentelemetry.tooling.OtelShimInjector.OTEL_ENTRYPOINT_CLASSES; import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; import de.thetaphi.forbiddenapis.SuppressForbidden; @@ -21,12 +18,17 @@ public class OtelShimGradlePlugin extends Plugin.ForElementMatcher { private final File targetDir; + static final String[] OTEL_SHIM_INJECTED_CLASSES = { + "io.opentelemetry.api.DefaultOpenTelemetry", + "io.opentelemetry.api.GlobalOpenTelemetry$ObfuscatedOpenTelemetry", + "io.opentelemetry.context.ThreadLocalContextStorage", + "io.opentelemetry.context.StrictContextStorage", + "io.opentelemetry.context.ArrayBasedContext", + }; + @SuppressForbidden public OtelShimGradlePlugin(File targetDir) { - super( - namedOneOf(OTEL_ENTRYPOINT_CLASSES) - .or(namedOneOf(OTEL_CONTEXT_STORAGE_CLASSES)) - .or(namedOneOf(OTEL_CONTEXT_CLASSES))); + super(namedOneOf(OTEL_SHIM_INJECTED_CLASSES)); this.targetDir = targetDir; } diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimInjector.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimInjector.java similarity index 89% rename from dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimInjector.java rename to dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimInjector.java index 9d4c63ed0af..01390fc8bcd 100644 --- a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/OtelShimInjector.java +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimInjector.java @@ -1,4 +1,4 @@ -package datadog.opentelemetry.tooling; +package datadog.opentelemetry.tooling.shim; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; @@ -16,20 +16,6 @@ public final class OtelShimInjector implements AsmVisitorWrapper { static final OtelShimInjector INSTANCE = new OtelShimInjector(); - static final String[] OTEL_ENTRYPOINT_CLASSES = { - "io.opentelemetry.api.DefaultOpenTelemetry", - "io.opentelemetry.api.GlobalOpenTelemetry$ObfuscatedOpenTelemetry", - }; - - static final String[] OTEL_CONTEXT_STORAGE_CLASSES = { - "io.opentelemetry.context.ThreadLocalContextStorage", - "io.opentelemetry.context.StrictContextStorage", - }; - - static final String[] OTEL_CONTEXT_CLASSES = { - "io.opentelemetry.context.ArrayBasedContext", - }; - static final String TRACER_PROVIDER_DESCRIPTOR = "Lio/opentelemetry/api/trace/TracerProvider;"; static final String CONTEXT_PROPAGATORS_DESCRIPTOR = diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExtensionHandler.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExtensionHandler.java index 4dd23c26dd3..45c9f2760d4 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExtensionHandler.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExtensionHandler.java @@ -28,7 +28,7 @@ public class ExtensionHandler { new Remapper() { @Override public String map(String internalName) { - return SHADE_LOGGING.apply(internalName); + return MAP_LOGGING.apply(internalName); } }); @@ -113,7 +113,7 @@ private byte[] mapBytecode() throws IOException { } /** Maps logging references in the extension to use the tracer's embedded logger. */ - protected static final Function SHADE_LOGGING = + public static final Function MAP_LOGGING = new Function() { // substring stops string literal from being changed by shadow plugin private final String ORG_SLF4J = "_org/slf4j/".substring(1);