Skip to content
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package datadog.opentelemetry.tooling;

import datadog.trace.agent.tooling.ExtensionHandler;
import datadog.trace.agent.tooling.muzzle.Reference;
import datadog.trace.agent.tooling.muzzle.ReferenceMatcher;
import java.net.URL;
import java.net.URLConnection;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import net.bytebuddy.jar.asm.ClassWriter;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.jar.asm.Type;

/** Handles OpenTelemetry instrumentations, so they can be loaded into the Datadog tracer. */
public final class OtelExtensionHandler extends ExtensionHandler {
Expand All @@ -23,17 +29,73 @@ 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 if (file.endsWith("$Muzzle.class")) {
return new JarEntry(file); // pretend we have a static Muzzle class
} else {
return super.mapEntry(jar, file);
}
}

@Override
public URLConnection mapContent(URL url, JarFile jar, JarEntry entry) {
if (entry.getName().endsWith(".class")) {
String file = entry.getName();
if (file.endsWith("$Muzzle.class")) {
return new EmptyMuzzleConnection(url); // generate an empty static Muzzle class
} else if (file.endsWith(".class")) {
return new ClassMappingConnection(url, jar, entry, OtelInstrumentationMapper::new);
} else {
return new JarFileConnection(url, jar, entry);
}
}

/** Generates an empty static muzzle class for OpenTelemetry instrumentations. */
static final class EmptyMuzzleConnection extends ClassMappingConnection {

private static final String REFERENCE_MATCHER_CLASS =
Type.getInternalName(ReferenceMatcher.class);

private static final String REFERENCE_CLASS = Type.getInternalName(Reference.class);

public EmptyMuzzleConnection(URL url) {
super(url, null, null, null);
}

@Override
protected byte[] doMapBytecode(String unused) {
String file = url.getFile();
// remove .class suffix and optional forward-slash prefix
String className = file.substring(file.charAt(0) == '/' ? 1 : 0, file.length() - 6);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(
Opcodes.V1_8,
Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL,
className,
null,
"java/lang/Object",
null);
MethodVisitor mv =
cw.visitMethod(
Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
"create",
"()L" + REFERENCE_MATCHER_CLASS + ";",
null,
null);
mv.visitCode();
mv.visitTypeInsn(Opcodes.NEW, REFERENCE_MATCHER_CLASS);
mv.visitInsn(Opcodes.DUP);
mv.visitInsn(Opcodes.ICONST_0);
mv.visitTypeInsn(Opcodes.ANEWARRAY, REFERENCE_CLASS);
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL,
REFERENCE_MATCHER_CLASS,
"<init>",
"([L" + REFERENCE_CLASS + ";)V",
false);
mv.visitInsn(Opcodes.ARETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ public final class OtelInstrumentationMapper extends ClassRemapper {
Collections.singleton(
"io/opentelemetry/javaagent/tooling/muzzle/InstrumentationModuleMuzzle");

private static final Set<String> UNSUPPORTED_METHODS =
Collections.singleton("getMuzzleReferences");

public OtelInstrumentationMapper(ClassVisitor classVisitor) {
super(classVisitor, Renamer.INSTANCE);
}
Expand All @@ -47,15 +44,6 @@ public void visit(
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 null; // remove unsupported method
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}

private String[] removeUnsupportedTypes(String[] interfaces) {
List<String> filtered = null;
for (int i = interfaces.length - 1; i >= 0; i--) {
Expand All @@ -75,6 +63,8 @@ static final class Renamer extends Remapper {
private static final String OTEL_JAVAAGENT_SHADED_PREFIX =
"io/opentelemetry/javaagent/shaded/io/opentelemetry/";

private static final String ASM_PREFIX = "org/objectweb/asm/";

/** Datadog equivalent of OpenTelemetry instrumentation classes. */
private static final Map<String, String> RENAMED_TYPES = new HashMap<>();

Expand All @@ -97,6 +87,30 @@ static final class Renamer extends Remapper {
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/shaded/instrumentation/api/util/VirtualField",
Type.getInternalName(ContextStore.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/ClassRefBuilder",
Type.getInternalName(OtelMuzzleRefBuilder.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/ClassRef",
Type.getInternalName(OtelMuzzleRefBuilder.ClassRef.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Flag",
Type.getInternalName(OtelMuzzleRefBuilder.Flag.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Flag$VisibilityFlag",
Type.getInternalName(OtelMuzzleRefBuilder.Flag.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Flag$MinimumVisibilityFlag",
Type.getInternalName(OtelMuzzleRefBuilder.Flag.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Flag$ManifestationFlag",
Type.getInternalName(OtelMuzzleRefBuilder.Flag.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Flag$OwnershipFlag",
Type.getInternalName(OtelMuzzleRefBuilder.Flag.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/tooling/muzzle/references/Source",
Type.getInternalName(OtelMuzzleRefBuilder.Source.class));
RENAMED_TYPES.put(
"io/opentelemetry/javaagent/bootstrap/Java8BytecodeBridge",
"datadog/trace/bootstrap/otel/Java8BytecodeBridge");
Expand All @@ -113,6 +127,10 @@ public String map(String internalName) {
return "datadog/trace/bootstrap/otel/"
+ internalName.substring(OTEL_JAVAAGENT_SHADED_PREFIX.length());
}
// map unshaded ASM types to the shaded copy in byte-buddy
if (internalName.startsWith(ASM_PREFIX)) {
return "net/bytebuddy/jar/asm/" + internalName.substring(ASM_PREFIX.length());
}
return MAP_LOGGING.apply(internalName);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package datadog.opentelemetry.tooling;

import datadog.opentelemetry.tooling.OtelMuzzleRefBuilder.ClassRef;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.agent.tooling.muzzle.Reference;
import datadog.trace.agent.tooling.muzzle.ReferenceProvider;
import datadog.trace.api.InstrumenterConfig;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.bytebuddy.pool.TypePool;

/**
* Replaces OpenTelemetry's {@code InstrumentationModule} when mapping extensions.
Expand Down Expand Up @@ -36,8 +40,17 @@ private static String[] namespace(String[] names) {
return namespaced;
}

private volatile String[] helperClassNames;

@Override
public String[] helperClassNames() {
if (null == helperClassNames) {
helperClassNames = buildHelperClassNames();
}
return helperClassNames;
}

private String[] buildHelperClassNames() {
List<String> helperClassNames;
List<String> additionalClassNames = getAdditionalHelperClassNames();
if (additionalClassNames.isEmpty()) {
Expand Down Expand Up @@ -85,4 +98,29 @@ public void registerMuzzleVirtualFields(VirtualFieldBuilder builder) {}
public interface VirtualFieldBuilder {
VirtualFieldBuilder register(String typeName, String fieldTypeName);
}

@Override
public ReferenceProvider runtimeMuzzleReferences() {
return new ReferenceProvider() {
private volatile Iterable<Reference> muzzleReferences;

@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Iterable<Reference> buildReferences(TypePool ignored) {
if (null == muzzleReferences) {
Map<String, ClassRef> muzzleMap = getMuzzleReferences();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling getMuzzleReferences() will call some OTel instrumentation code to build references that is remapped the DD OtelMuzzleRefBuilder which builds our own tooling ClassRef.
As getMuzzleReferences() returns a Map and there is type erasure, JVM don't complain at runtime we are now returning a Map with DD ClassRef instances as values, rather than the original OTel ClassRef, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We map all references to the original OTel ClassRef in OtelInstrumentationMapper to our replacement ClassRef. This replacement is binary compatible with the original, but it lets us adapt the data being built up using these ClassRefs to our expected Reference type. So from the JVM's perspective it's still dealing with a map of ClassRefs, just one that we control.

Note the getMuzzleReferences() method generated by OTel uses a builder called ClassRefBuilder, which is bootstrapped from ClassRef.builder(className) - so the process is as follows:

  • we find a potential type match involving the drop-in instrumentation
  • we want to check muzzle is ok, so we call MuzzleCheck
  • this calls loadStaticMuzzleReferences to load the static class (empty for OTel extensions)
  • and then applies the runtime reference provider
  • this calls our buildReferences method, which calls the generated OTel getMuzzleReferences method
  • getMuzzleReferences creates a ClassRefBuilder (which is binary compatible with the original OTel one) and feeds it reference data
  • we use that data to populate our ClassRefs - which are just a thin layer around Reference
  • we collect the underlying References and return them to MuzzleCheck

for (String helper : helperClassNames()) {
muzzleMap.remove(helper);
}
muzzleReferences = (Iterable) muzzleMap.values();
}
return muzzleReferences;
}
};
}

/** This method is generated by OpenTelemetry's muzzle plugin at build time. */
public Map<String, ClassRef> getMuzzleReferences() {
return Collections.emptyMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package datadog.opentelemetry.tooling;

import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_INTERFACE;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_NON_FINAL;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_NON_INTERFACE;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_NON_PRIVATE;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_NON_STATIC;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_PUBLIC;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_PUBLIC_OR_PROTECTED;
import static datadog.trace.agent.tooling.muzzle.Reference.EXPECTS_STATIC;

import datadog.trace.agent.tooling.muzzle.Reference;
import java.util.Collection;
import net.bytebuddy.jar.asm.Type;

/** Maps OpenTelemetry muzzle references to the Datadog equivalent. */
public final class OtelMuzzleRefBuilder {
private final Reference.Builder builder;

OtelMuzzleRefBuilder(String className) {
this.builder = new Reference.Builder(className);
}

public OtelMuzzleRefBuilder setSuperClassName(String superName) {
builder.withSuperName(superName);
return this;
}

public OtelMuzzleRefBuilder addInterfaceNames(Collection<String> interfaceNames) {
for (String interfaceName : interfaceNames) {
builder.withInterface(interfaceName);
}
return this;
}

public OtelMuzzleRefBuilder addInterfaceName(String interfaceName) {
builder.withInterface(interfaceName);
return this;
}

public OtelMuzzleRefBuilder addSource(String sourceName) {
builder.withSource(sourceName, 0);
return this;
}

public OtelMuzzleRefBuilder addSource(String sourceName, int line) {
builder.withSource(sourceName, line);
return this;
}

public OtelMuzzleRefBuilder addFlag(Flag flag) {
builder.withFlag(flag.bit);
return this;
}

public OtelMuzzleRefBuilder addField(
Source[] sources, Flag[] flags, String fieldName, Type fieldType, boolean isDeclared) {
builder.withField(Source.flatten(sources), Flag.flatten(flags), fieldName, fieldType);
return this;
}

public OtelMuzzleRefBuilder addMethod(
Source[] sources, Flag[] flags, String methodName, Type returnType, Type... argumentTypes) {
builder.withMethod(
Source.flatten(sources), Flag.flatten(flags), methodName, returnType, argumentTypes);
return this;
}

public ClassRef build() {
return new ClassRef(builder.build());
}

public static final class ClassRef extends Reference {
public ClassRef(Reference ref) {
super(
ref.sources,
ref.flags,
ref.className,
ref.superName,
ref.interfaces,
ref.fields,
ref.methods);
}

public static OtelMuzzleRefBuilder builder(String className) {
return new OtelMuzzleRefBuilder(className);
}
}

public enum Flag {
PUBLIC(EXPECTS_PUBLIC),
PROTECTED_OR_HIGHER(EXPECTS_PUBLIC_OR_PROTECTED),
PROTECTED(EXPECTS_PUBLIC_OR_PROTECTED),
PACKAGE_OR_HIGHER(EXPECTS_NON_PRIVATE),
PACKAGE(EXPECTS_NON_PRIVATE),
PRIVATE_OR_HIGHER(0),
PRIVATE(0),
ABSTRACT(0),
FINAL(0),
NON_FINAL(EXPECTS_NON_FINAL),
INTERFACE(EXPECTS_INTERFACE),
NON_INTERFACE(EXPECTS_NON_INTERFACE),
STATIC(EXPECTS_STATIC),
NON_STATIC(EXPECTS_NON_STATIC);

final int bit;

Flag(int bit) {
this.bit = bit;
}

public static int flatten(Flag[] flags) {
int bits = 0;
for (Flag flag : flags) {
bits |= flag.bit;
}
return bits;
}
}

public static final class Source {
final String name;
final int line;

public Source(String name, int line) {
this.name = name;
this.line = line;
}

public static String[] flatten(Source[] sources) {
String[] locations = new String[sources.length];
for (int i = 0; i < sources.length; i++) {
locations[i] = sources[i].name + ":" + sources[i].line;
}
return locations;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private byte[] mapBytecode() {
return BYTECODE_CACHE.computeIfAbsent(url.getFile(), this::doMapBytecode);
}

private byte[] doMapBytecode(String unused) {
protected byte[] doMapBytecode(String unused) {
try (InputStream in = super.getInputStream()) {
ClassReader cr = new ClassReader(in);
ClassWriter cw = new ClassWriter(cr, 0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.agent.tooling.InstrumenterState;
import datadog.trace.agent.tooling.Utils;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;
import org.slf4j.Logger;
Expand Down Expand Up @@ -55,7 +56,7 @@ private ReferenceMatcher muzzle() {
if (null == muzzle) {
muzzle =
InstrumenterModule.loadStaticMuzzleReferences(
getClass().getClassLoader(), instrumentationClass)
Utils.getExtendedClassLoader(), instrumentationClass)
.withReferenceProvider(runtimeMuzzleReferences);
}
return muzzle;
Expand Down