Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime drop-in support for OpenTelemetry instrumentations #7086

Merged
merged 5 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dd-java-agent/agent-builder/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion dd-java-agent/agent-otel/otel-bootstrap/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ configurations {
canBeConsumed = false
canBeResolved = true
}
compileClasspath.extendsFrom(embeddedClasspath)
instrumentPluginClasspath {
visible = false
canBeConsumed = false
canBeResolved = true
}
}

instrument.plugins = ['datadog.opentelemetry.tooling.OtelShimGradlePlugin']
instrument.plugins = ['datadog.opentelemetry.tooling.shim.OtelShimGradlePlugin']

minimumInstructionCoverage = 0.0
minimumBranchCoverage = 0.0
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}

This file was deleted.

2 changes: 2 additions & 0 deletions dd-java-agent/agent-otel/otel-tooling/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ minimumBranchCoverage = 0.0
dependencies {
api deps.bytebuddy
api deps.bytebuddyagent

compileOnly project(':dd-java-agent:agent-tooling')
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> UNSUPPORTED_TYPES =
new HashSet<>(
Arrays.asList("io/opentelemetry/javaagent/tooling/muzzle/InstrumentationModuleMuzzle"));

private static final Set<String> 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<String> 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<String, String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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<TypeDescription> 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);
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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;
}
}
Original file line number Diff line number Diff line change
@@ -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<? super MethodDescription> methodMatcher, String adviceClassName);

void applyTransformer(AgentBuilder.Transformer transformer);
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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<? super MethodDescription> methodMatcher, String adviceClassName) {
methodTransformer.applyAdvice(methodMatcher, adviceClassName);
}

@Override
public void applyTransformer(AgentBuilder.Transformer transformer) {
typeTransformer.applyAdvice(transformer::transform);
}
}
Loading
Loading