From 8446f40246ad0d415f8680fadb06c2a595e8bf9e Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 12:35:23 -0400 Subject: [PATCH 1/8] Implement auto-inferring helper methods --- .../tooling/CombiningTransformerBuilder.java | 2 +- dd-java-agent/agent-tooling/build.gradle | 1 + .../trace/agent/tooling/HelperScanner.java | 21 +- .../agent/tooling/InstrumenterModule.java | 30 ++- .../tooling/muzzle/HelperClassPredicate.java | 65 ++++++ .../agent/tooling/muzzle/MuzzleGenerator.java | 187 ++++++++++++++++-- .../tooling/muzzle/ReferenceCreator.java | 33 +++- .../muzzle/HelperClassPredicateTest.java | 78 ++++++++ 8 files changed, 391 insertions(+), 26 deletions(-) create mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicate.java create mode 100644 dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicateTest.java diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java index 5a7e4b9c3df..f7c1a65495a 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java @@ -130,7 +130,7 @@ private void prepareInstrumentation(InstrumenterModule module, int instrumentati adviceShader = AdviceShader.with(module); - String[] helperClassNames = module.helperClassNames(); + String[] helperClassNames = module.getAllHelperClassNames(); if (module.injectHelperDependencies()) { helperClassNames = HelperScanner.withClassDependencies(helperClassNames); } diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index 3b60e25d281..71e950f5d0e 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -49,6 +49,7 @@ dependencies { testImplementation project(':dd-java-agent:testing') testImplementation libs.bytebuddy + testImplementation libs.bundles.junit5 testImplementation group: 'com.google.guava', name: 'guava-testlib', version: '20.0' jmhImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.3.5.RELEASE' diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperScanner.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperScanner.java index 3ec0fe7c82e..66536a2ddba 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperScanner.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperScanner.java @@ -24,8 +24,7 @@ public final class HelperScanner extends ClassVisitor { static final int READER_OPTIONS = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; - static final ClassFileLocator locator = - ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()); + final ClassFileLocator locator; final MethodScanner methodScanner = new MethodScanner(); @@ -41,14 +40,30 @@ public final class HelperScanner extends ClassVisitor { Set uses; HelperScanner() { + this(ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader())); + } + + HelperScanner(ClassFileLocator locator) { super(Opcodes.ASM7, null); + this.locator = locator; } - /** Expands helper class names to include any non-bootstrap classes they depend on. */ + /** + * Expands helper class names with their non-bootstrap dependencies, via the agent class loader. + */ public static String[] withClassDependencies(String... helperClassNames) { return new HelperScanner().simulateClassLoading(helperClassNames); } + /** + * Same as above, but reads bytecode via the given locator (e.g. during build time where the agent + * loader is absent). + */ + public static String[] withClassDependencies( + ClassFileLocator locator, String... helperClassNames) { + return new HelperScanner(locator).simulateClassLoading(helperClassNames); + } + /** * Simulates class-loading by finding all classes required to load the helper classes as well as * optional classes used in method instructions that may be needed later when invoking the method. diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java index d2abbc265e5..d6438f1d30c 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java @@ -113,13 +113,31 @@ public static ReferenceMatcher loadStaticMuzzleReferences( } /** - * @return Class names of helpers to inject into the user's classloader. - *

NOTE: The order of the returned helper classes matters. If a muzzle check fails - * with a NoClassDefFoundError, as logged in build/reports/muzzle-*.txt, it is likely that one - * helper class depends on another that appears later in the list. In this case, the returned - * list must be reordered so that the referred helper class appears before the one that refers - * to it. + * @return the full, load-ordered set of helper classes to inject into the user's classloader: the + * helpers inferred at build time from the advice/helper byte-code, unioned with any {@link + * #helperClassNames() manually-declared} additions. Falls back to {@link #helperClassNames()} + * when no generated {@code $Muzzle} is available. */ + public final String[] getAllHelperClassNames() { + String[] generated = + loadStaticMuzzleHelperClassNames(getClass().getClassLoader(), getClass().getName()); + return null != generated ? generated : helperClassNames(); + } + + static String[] loadStaticMuzzleHelperClassNames( + ClassLoader classLoader, String instrumentationClass) { + String muzzleClass = instrumentationClass + "$Muzzle"; + try { + // helper class names captured at build-time; see MuzzleGenerator + return (String[]) + classLoader.loadClass(muzzleClass).getMethod("helperClassNames").invoke(null); + } catch (Throwable e) { + // no generated helper list: caller falls back to helperClassNames() + return null; + } + } + + /** Optional manual additions to the injected helper set. */ public String[] helperClassNames() { return NO_HELPERS; } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicate.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicate.java new file mode 100644 index 00000000000..713bd3f9c83 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicate.java @@ -0,0 +1,65 @@ +package datadog.trace.agent.tooling.muzzle; + +import datadog.trace.bootstrap.Constants; +import java.util.function.Predicate; + +/** + * Classifies a referenced class as an injectable tracer helper, a bootstrap class, or a library + * class — similar to OpenTelemetry's {@code HelperClassPredicate#isHelperClass}; however, the main + * signal here is whether the class was compiled from the instrumentation subproject's own output + * ({@code ownOutput}). {@link #HELPER_PREFIXES} additionally covers helpers that live in other + * tracer subprojects. + */ +public final class HelperClassPredicate { + + static final String[] HELPER_PREFIXES = { + "datadog.trace.instrumentation.", + "datadog.opentelemetry.shim.", + "datadog.trace.agent.tooling.iast.", + "datadog.trace.agent.tooling.nativeimage.", + }; + + private final Predicate ownOutput; + + /** + * @param ownOutput whether a dotted class name was compiled from the instrumentation subproject's + * own output; injected because agent-tooling cannot resolve build directories itself. + */ + public HelperClassPredicate(final Predicate ownOutput) { + this.ownOutput = ownOutput; + } + + public boolean isHelperClass(final String className) { + return !isBootstrap(className) && (ownOutput.test(className) || matchesHelperPrefix(className)); + } + + private static boolean matchesHelperPrefix(final String className) { + for (final String prefix : HELPER_PREFIXES) { + if (className.startsWith(prefix)) { + return true; + } + } + return false; + } + + /** + * Whether the class is on the bootstrap class-path (or a JDK/SLF4J type) and so never injected. + */ + public static boolean isBootstrap(final String className) { + if (className.startsWith("java.") + || className.startsWith("javax.") + || className.startsWith("jdk.") + || className.startsWith("com.sun.") + || className.startsWith("sun.") + || className.startsWith("org.slf4j.") + || className.startsWith("datadog.slf4j.")) { + return true; + } + for (final String prefix : Constants.BOOTSTRAP_PACKAGE_PREFIXES) { + if (className.startsWith(prefix)) { + return true; + } + } + return false; + } +} diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java index 69421f6d16a..8d896315ddc 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java @@ -3,23 +3,28 @@ import static java.util.Arrays.asList; import datadog.trace.agent.tooling.AdviceShader; +import datadog.trace.agent.tooling.HelperScanner; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; import net.bytebuddy.description.method.MethodList; import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.implementation.Implementation; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.ClassWriter; @@ -81,7 +86,10 @@ public ClassVisitor wrap( } private static Reference[] generateReferences( - Instrumenter.HasMethodAdvice instrumenter, AdviceShader adviceShader) { + Instrumenter.HasMethodAdvice instrumenter, + AdviceShader adviceShader, + Set allAdviceClasses, + Predicate shouldRecurse) { // track sources we've generated references from to avoid recursion final Set referenceSources = new HashSet<>(); final Map references = new LinkedHashMap<>(); @@ -93,11 +101,14 @@ private static Reference[] generateReferences( adviceClasses.addAll(asList(additionalClasses)); } }); + // remember the advice roots so callers can exclude them from the injected helper set + allAdviceClasses.addAll(adviceClasses); ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); for (String adviceClass : adviceClasses) { if (referenceSources.add(adviceClass)) { for (Map.Entry entry : - ReferenceCreator.createReferencesFrom(adviceClass, adviceShader, contextClassLoader) + ReferenceCreator.createReferencesFrom( + adviceClass, adviceShader, contextClassLoader, shouldRecurse) .entrySet()) { Reference toMerge = references.get(entry.getKey()); if (null == toMerge) { @@ -112,21 +123,56 @@ private static Reference[] generateReferences( } /** This code is generated in a separate side-class. */ - private static byte[] generateMuzzleClass(InstrumenterModule module) { + private byte[] generateMuzzleClass(InstrumenterModule module) { - Set ignoredClassNames = new HashSet<>(asList(module.muzzleIgnoredClassNames())); AdviceShader adviceShader = AdviceShader.with(module.adviceShading()); + HelperClassPredicate helperPredicate = new HelperClassPredicate(this::isOwnOutput); - List references = new ArrayList<>(); + // Crawl advice, following references into our own helper classes (even in library packages). + Set adviceClasses = new HashSet<>(); + List allReferences = new ArrayList<>(); for (Instrumenter instrumenter : module.typeInstrumentations()) { if (instrumenter instanceof Instrumenter.HasMethodAdvice) { - for (Reference reference : - generateReferences((Instrumenter.HasMethodAdvice) instrumenter, adviceShader)) { - // ignore helper classes, they will be injected by the instrumentation's HelperInjector. - if (!ignoredClassNames.contains(reference.className)) { - references.add(reference); - } - } + Collections.addAll( + allReferences, + generateReferences( + (Instrumenter.HasMethodAdvice) instrumenter, + adviceShader, + adviceClasses, + helperPredicate::isHelperClass)); + } + } + + // Advice roots are inlined into the target method, not injected, so they aren't helpers. + Set inferredHelpers = new LinkedHashSet<>(); + for (Reference reference : allReferences) { + if (!adviceClasses.contains(reference.className) + && helperPredicate.isHelperClass(reference.className)) { + inferredHelpers.add(reference.className); + } + } + + // Manual additions cover helpers the crawl can't see (reflection, SPI, advice-less injectors). + Set helperClasses = new LinkedHashSet<>(inferredHelpers); + Collections.addAll(helperClasses, module.helperClassNames()); + for (String helper : new ArrayList<>(helperClasses)) { + if (isOwnOutput(helper)) { + addNestedClasses(helper, helperClasses); + } + } + + String[] orderedHelpers = + orderHelpers(helperClasses, Thread.currentThread().getContextClassLoader()); + + writeInferenceReport(module, adviceClasses.isEmpty(), inferredHelpers, helperClasses); + + // Injected helpers are our own classes, so they must not be asserted as library references. + Set ignoredClassNames = new HashSet<>(helperClasses); + Collections.addAll(ignoredClassNames, module.muzzleIgnoredClassNames()); + List references = new ArrayList<>(); + for (Reference reference : allReferences) { + if (!ignoredClassNames.contains(reference.className)) { + references.add(reference); } } Reference[] additionalReferences = module.additionalMuzzleReferences(); @@ -179,9 +225,126 @@ private static byte[] generateMuzzleClass(InstrumenterModule module) { mv.visitMaxs(0, 0); mv.visitEnd(); + // Generated: public static String[] helperClassNames() + MethodVisitor hv = + cw.visitMethod( + Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, + "helperClassNames", + "()[Ljava/lang/String;", + null, + null); + hv.visitCode(); + writeStrings(hv, orderedHelpers); + hv.visitInsn(Opcodes.ARETURN); + hv.visitMaxs(0, 0); + hv.visitEnd(); + return cw.toByteArray(); } + /** {@code true} if the class was compiled from this instrumentation subproject's own output. */ + private boolean isOwnOutput(String className) { + return new File(targetDir, className.replace('.', '/') + ".class").isFile(); + } + + /** Adds the nested classes ({@code Foo$Bar}, {@code Foo$1}, ...) of an own-output helper. */ + private void addNestedClasses(String className, Set helperClasses) { + File classFile = new File(targetDir, className.replace('.', '/') + ".class"); + File dir = classFile.getParentFile(); + if (dir == null || !dir.isDirectory()) { + return; + } + int lastDot = className.lastIndexOf('.'); + String pkg = lastDot < 0 ? "" : className.substring(0, lastDot + 1); + String prefix = (lastDot < 0 ? className : className.substring(lastDot + 1)) + "$"; + File[] siblings = dir.listFiles(); + if (siblings == null) { + return; + } + for (File sibling : siblings) { + String fileName = sibling.getName(); + if (fileName.startsWith(prefix) && fileName.endsWith(".class")) { + helperClasses.add(pkg + fileName.substring(0, fileName.length() - ".class".length())); + } + } + } + + /** + * Load-orders helpers (dependencies first) via {@link HelperScanner}, keeping only our helpers + * (the scanner may pull in library classes) and retaining any helper it could not locate. + */ + private static String[] orderHelpers(Set helpers, ClassLoader loader) { + if (helpers.isEmpty()) { + return new String[0]; + } + List ordered = new ArrayList<>(helpers.size()); + try { + for (String name : + HelperScanner.withClassDependencies( + ClassFileLocator.ForClassLoader.of(loader), helpers.toArray(new String[0]))) { + if (helpers.contains(name) && !ordered.contains(name)) { + ordered.add(name); + } + } + } catch (Throwable ignore) { + // best-effort ordering; unordered helpers are appended below + } + for (String name : helpers) { + if (!ordered.contains(name)) { + ordered.add(name); + } + } + return ordered.toArray(new String[0]); + } + + /** + * Writes an advisory report classifying each declared helper as inferred vs. manual-only, to + * guide migration. Best-effort — never fails the build. + */ + private void writeInferenceReport( + InstrumenterModule module, + boolean adviceLess, + Set inferredHelpers, + Set allHelpers) { + try { + Set declared = new LinkedHashSet<>(asList(module.helperClassNames())); + if (declared.isEmpty() && inferredHelpers.isEmpty()) { + return; + } + File buildDir = targetDir; + while (buildDir != null && !"build".equals(buildDir.getName())) { + buildDir = buildDir.getParentFile(); + } + if (buildDir == null) { + return; + } + File reportDir = new File(buildDir, "reports/helper-inference"); + reportDir.mkdirs(); + StringBuilder sb = new StringBuilder(); + sb.append("module: ").append(module.getClass().getName()).append('\n'); + sb.append("has-advice: ").append(!adviceLess).append('\n'); + sb.append("injected-helpers: ").append(allHelpers.size()).append('\n'); + if (adviceLess && !declared.isEmpty()) { + sb.append("crawl-cannot-cover: advice-less module; declared helpers are all manual-only\n"); + } + for (String name : declared) { + sb.append(inferredHelpers.contains(name) ? " inferred " : " manual-only ") + .append(name) + .append('\n'); + } + for (String name : inferredHelpers) { + if (!declared.contains(name)) { + sb.append(" inferred-only ").append(name).append('\n'); + } + } + Files.write( + new File(reportDir, module.getClass().getName() + ".txt").toPath(), + sb.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Throwable ignore) { + // report is advisory; never fail the build over it + } + } + private static void writeReference(MethodVisitor mv, Reference reference) { if (reference instanceof OrReference) { mv.visitTypeInsn(Opcodes.NEW, "datadog/trace/agent/tooling/muzzle/OrReference"); diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java index 5fdc4d3f827..d8bbb8a7cbe 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.function.Predicate; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.FieldVisitor; @@ -38,6 +39,10 @@ public class ReferenceCreator extends ClassVisitor { */ private static final String REFERENCE_CREATION_PACKAGE = "datadog.trace.instrumentation."; + /** Default recursion rule; callers that identify helpers by other means pass their own. */ + private static final Predicate DEFAULT_SHOULD_RECURSE = + name -> name.startsWith(REFERENCE_CREATION_PACKAGE); + private static final int UNDEFINED_LINE = -1; /** Set containing name+descriptor signatures of Object methods. */ @@ -58,10 +63,31 @@ public class ReferenceCreator extends ClassVisitor { * @return Map of [referenceClassName -> Reference] * @throws IllegalStateException if class is not found or unable to be loaded. */ - @SuppressForbidden public static Map createReferencesFrom( final String entryPointClassName, final AdviceShader adviceShader, final ClassLoader loader) throws IllegalStateException { + return createReferencesFrom(entryPointClassName, adviceShader, loader, DEFAULT_SHOULD_RECURSE); + } + + /** + * Generate all references reachable from a given class, following references into classes + * accepted by {@code shouldRecurse}. + * + * @param entryPointClassName Starting point for generating references. + * @param adviceShader Optional shading to apply to the advice. + * @param loader Classloader used to read class bytes. + * @param shouldRecurse Decides whether a referenced class is one of ours to keep crawling into + * (vs. a library/bootstrap leaf that is merely recorded). + * @return Map of [referenceClassName -> Reference] + * @throws IllegalStateException if class is not found or unable to be loaded. + */ + @SuppressForbidden + public static Map createReferencesFrom( + final String entryPointClassName, + final AdviceShader adviceShader, + final ClassLoader loader, + final Predicate shouldRecurse) + throws IllegalStateException { final Set visitedSources = new HashSet<>(); final Map references = new LinkedHashMap<>(); @@ -87,9 +113,8 @@ public static Map createReferencesFrom( final Map instrumentationReferences = cv.getReferences(); for (final Map.Entry entry : instrumentationReferences.entrySet()) { - // Don't generate references created outside of the datadog instrumentation package. - if (!visitedSources.contains(entry.getKey()) - && entry.getKey().startsWith(REFERENCE_CREATION_PACKAGE)) { + // Only keep crawling into classes that are ours + if (!visitedSources.contains(entry.getKey()) && shouldRecurse.test(entry.getKey())) { instrumentationQueue.add(entry.getKey()); } Reference toMerge = references.get(entry.getKey()); diff --git a/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicateTest.java b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicateTest.java new file mode 100644 index 00000000000..42d49fb21b7 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/java/datadog/trace/agent/tooling/muzzle/HelperClassPredicateTest.java @@ -0,0 +1,78 @@ +package datadog.trace.agent.tooling.muzzle; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class HelperClassPredicateTest { + + private static HelperClassPredicate predicateWithOwnOutput(String... ownOutput) { + Set own = new HashSet<>(); + for (String className : ownOutput) { + own.add(className); + } + return new HelperClassPredicate(own::contains); + } + + @Test + void instrumentationPackageIsHelperEvenWithoutOwnOutput() { + // classes under the instrumentation package are ours regardless of where they were compiled + HelperClassPredicate predicate = predicateWithOwnOutput(); + assertTrue(predicate.isHelperClass("datadog.trace.instrumentation.foo.FooDecorator")); + } + + @Test + void allowlistedSharedSubprojectsAreHelpers() { + // helpers that live in other dd-owned tracer subprojects + HelperClassPredicate predicate = predicateWithOwnOutput(); + assertTrue(predicate.isHelperClass("datadog.opentelemetry.shim.context.OtelContext")); + assertTrue(predicate.isHelperClass("datadog.trace.agent.tooling.iast.TaintableEnumeration")); + assertTrue(predicate.isHelperClass("datadog.trace.agent.tooling.nativeimage.TracerActivation")); + } + + @Test + void ownOutputInLibraryPackageIsHelper() { + // helpers deliberately placed in a library's package are detected via ownOutput + HelperClassPredicate predicate = + predicateWithOwnOutput("redis.clients.jedis.JedisClientDecorator"); + assertTrue(predicate.isHelperClass("redis.clients.jedis.JedisClientDecorator")); + // the real library class in the same package is not ours and must remain a library reference + assertFalse(predicate.isHelperClass("redis.clients.jedis.Jedis")); + } + + @Test + void bootstrapAndJdkAreNeverHelpers() { + // bootstrap wins even if a class is reported as ownOutput + HelperClassPredicate predicate = + predicateWithOwnOutput("datadog.trace.bootstrap.InstrumentationContext"); + assertFalse(predicate.isHelperClass("datadog.trace.bootstrap.InstrumentationContext")); + assertFalse(predicate.isHelperClass("datadog.trace.api.Config")); + // datadog.trace.instrumentation.api is a bootstrap sub-package, not an injectable helper + assertFalse(predicate.isHelperClass("datadog.trace.instrumentation.api.SomeApi")); + assertFalse(predicate.isHelperClass("java.lang.String")); + assertFalse(predicate.isHelperClass("javax.servlet.http.HttpServletRequest")); + assertFalse(predicate.isHelperClass("org.slf4j.Logger")); + assertFalse(predicate.isHelperClass("datadog.slf4j.Logger")); + } + + @Test + void plainLibraryClassIsNotHelper() { + HelperClassPredicate predicate = predicateWithOwnOutput(); + assertFalse(predicate.isHelperClass("org.apache.http.HttpRequest")); + assertFalse(predicate.isHelperClass("com.datastax.oss.driver.api.core.CqlSession")); + } + + @Test + void isBootstrapMatchesBootstrapPrefixesAndJdk() { + assertTrue(HelperClassPredicate.isBootstrap("datadog.trace.instrumentation.api.X")); + assertTrue(HelperClassPredicate.isBootstrap("datadog.trace.bootstrap.Y")); + assertTrue(HelperClassPredicate.isBootstrap("datadog.trace.api.Z")); + assertTrue(HelperClassPredicate.isBootstrap("java.util.List")); + assertTrue(HelperClassPredicate.isBootstrap("jdk.internal.Foo")); + assertFalse(HelperClassPredicate.isBootstrap("datadog.trace.instrumentation.foo.Bar")); + assertFalse(HelperClassPredicate.isBootstrap("com.example.Lib")); + } +} From 42b57e38bab42ca0e5cd93b83b252dc79b00f078 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 12:38:49 -0400 Subject: [PATCH 2/8] Migrate some instrumentations --- .../cassandra4/CassandraClientInstrumentation.java | 9 --------- .../GoogleHttpClientInstrumentation.java | 7 ------- .../instrumentation/jedis40/JedisInstrumentation.java | 7 ------- .../servlet/http/HttpServletInstrumentation.java | 7 ------- .../servlet/http/HttpServletResponseInstrumentation.java | 1 - 5 files changed, 31 deletions(-) diff --git a/dd-java-agent/instrumentation/datastax-cassandra/datastax-cassandra-4.0/src/main/java/datadog/trace/instrumentation/datastax/cassandra4/CassandraClientInstrumentation.java b/dd-java-agent/instrumentation/datastax-cassandra/datastax-cassandra-4.0/src/main/java/datadog/trace/instrumentation/datastax/cassandra4/CassandraClientInstrumentation.java index 963ee8c5439..f9e0f865ad3 100644 --- a/dd-java-agent/instrumentation/datastax-cassandra/datastax-cassandra-4.0/src/main/java/datadog/trace/instrumentation/datastax/cassandra4/CassandraClientInstrumentation.java +++ b/dd-java-agent/instrumentation/datastax-cassandra/datastax-cassandra-4.0/src/main/java/datadog/trace/instrumentation/datastax/cassandra4/CassandraClientInstrumentation.java @@ -24,15 +24,6 @@ public String instrumentedType() { return "com.datastax.oss.driver.internal.core.session.DefaultSession"; } - @Override - public String[] helperClassNames() { - return new String[] { - packageName + ".CassandraClientDecorator", - packageName + ".TracingSession", - packageName + ".ContactPointsUtil", - }; - } - @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( diff --git a/dd-java-agent/instrumentation/google-http-client-1.19/src/main/java/datadog/trace/instrumentation/googlehttpclient/GoogleHttpClientInstrumentation.java b/dd-java-agent/instrumentation/google-http-client-1.19/src/main/java/datadog/trace/instrumentation/googlehttpclient/GoogleHttpClientInstrumentation.java index 769976bfc7a..84c8196b787 100644 --- a/dd-java-agent/instrumentation/google-http-client-1.19/src/main/java/datadog/trace/instrumentation/googlehttpclient/GoogleHttpClientInstrumentation.java +++ b/dd-java-agent/instrumentation/google-http-client-1.19/src/main/java/datadog/trace/instrumentation/googlehttpclient/GoogleHttpClientInstrumentation.java @@ -39,13 +39,6 @@ public String instrumentedType() { return "com.google.api.client.http.HttpRequest"; } - @Override - public String[] helperClassNames() { - return new String[] { - packageName + ".GoogleHttpClientDecorator", packageName + ".HeadersInjectAdapter" - }; - } - @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvices( diff --git a/dd-java-agent/instrumentation/jedis/jedis-4.0/src/main/java/datadog/trace/instrumentation/jedis40/JedisInstrumentation.java b/dd-java-agent/instrumentation/jedis/jedis-4.0/src/main/java/datadog/trace/instrumentation/jedis40/JedisInstrumentation.java index 24350a6a3a6..6c525ebb6ab 100644 --- a/dd-java-agent/instrumentation/jedis/jedis-4.0/src/main/java/datadog/trace/instrumentation/jedis40/JedisInstrumentation.java +++ b/dd-java-agent/instrumentation/jedis/jedis-4.0/src/main/java/datadog/trace/instrumentation/jedis40/JedisInstrumentation.java @@ -33,13 +33,6 @@ public String instrumentedType() { return "redis.clients.jedis.Connection"; } - @Override - public String[] helperClassNames() { - return new String[] { - "redis.clients.jedis.JedisClientDecorator", - }; - } - @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java index b1af9e2ea13..d409628ca29 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletInstrumentation.java @@ -45,13 +45,6 @@ public ElementMatcher hierarchyMatcher() { return extendsClass(named(hierarchyMarkerType())); } - @Override - public String[] helperClassNames() { - return new String[] { - "datadog.trace.instrumentation.servlet.SpanNameCache", packageName + ".HttpServletDecorator", - }; - } - /** * Here we are instrumenting the protected method for HttpServlet. This should ensure that this * advice is always called after Servlet3Instrumentation which is instrumenting the public method. diff --git a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java index 10e6c530212..e708d76e79a 100644 --- a/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java +++ b/dd-java-agent/instrumentation/servlet/javax-servlet/javax-servlet-common/src/main/java/datadog/trace/instrumentation/servlet/http/HttpServletResponseInstrumentation.java @@ -42,7 +42,6 @@ public ElementMatcher hierarchyMatcher() { public String[] helperClassNames() { return new String[] { "datadog.trace.instrumentation.servlet.ServletRequestSetter", - packageName + ".HttpServletResponseDecorator", }; } From 8d6cccae05d78171322a9e975880f935194f66e7 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 13:07:43 -0400 Subject: [PATCH 3/8] Increase agent jar size allowance --- metadata/agent-jar-checks.properties | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/metadata/agent-jar-checks.properties b/metadata/agent-jar-checks.properties index fd04d637e35..75de2a130bf 100644 --- a/metadata/agent-jar-checks.properties +++ b/metadata/agent-jar-checks.properties @@ -1,7 +1,10 @@ # Agent jar structural invariants — edit intentionally, commit with the change that justified it. -# Max agent jar size in bytes. Raise only when the size growth is intentional (33 MiB = 34603008). -jar.size.budget = 34603008 +# TODO: original jar size limit was set to 34603008 +# temporarily increase max agent jar size to 34 MiB = 35651584 while transitioning helper class names +# from manual definitions to auto-inferred at build-time +# jar size increases because each generated $Muzzle now carries a helperClassNames() method +jar.size.budget = 35651584 # Minimum combined class + classdata count in the assembled agent jar. # Set to ~98% of the actual count at the time of the last intentional change. From 0522c6590bb773eac324c310a091c46c106718f0 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 14:28:42 -0400 Subject: [PATCH 4/8] Fix class crawl logic --- .../agent/tooling/muzzle/MuzzleGenerator.java | 96 ++++++++++--------- .../tooling/muzzle/ReferenceCreator.java | 33 +------ 2 files changed, 56 insertions(+), 73 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java index 8d896315ddc..45b7157d6ae 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import net.bytebuddy.asm.AsmVisitorWrapper; import net.bytebuddy.description.field.FieldDescription; import net.bytebuddy.description.field.FieldList; @@ -88,8 +87,7 @@ public ClassVisitor wrap( private static Reference[] generateReferences( Instrumenter.HasMethodAdvice instrumenter, AdviceShader adviceShader, - Set allAdviceClasses, - Predicate shouldRecurse) { + Set allAdviceClasses) { // track sources we've generated references from to avoid recursion final Set referenceSources = new HashSet<>(); final Map references = new LinkedHashMap<>(); @@ -107,8 +105,7 @@ private static Reference[] generateReferences( for (String adviceClass : adviceClasses) { if (referenceSources.add(adviceClass)) { for (Map.Entry entry : - ReferenceCreator.createReferencesFrom( - adviceClass, adviceShader, contextClassLoader, shouldRecurse) + ReferenceCreator.createReferencesFrom(adviceClass, adviceShader, contextClassLoader) .entrySet()) { Reference toMerge = references.get(entry.getKey()); if (null == toMerge) { @@ -128,7 +125,7 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { AdviceShader adviceShader = AdviceShader.with(module.adviceShading()); HelperClassPredicate helperPredicate = new HelperClassPredicate(this::isOwnOutput); - // Crawl advice, following references into our own helper classes (even in library packages). + // Crawl advice for muzzle references (only recursing into the instrumentation package). Set adviceClasses = new HashSet<>(); List allReferences = new ArrayList<>(); for (Instrumenter instrumenter : module.typeInstrumentations()) { @@ -136,14 +133,11 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { Collections.addAll( allReferences, generateReferences( - (Instrumenter.HasMethodAdvice) instrumenter, - adviceShader, - adviceClasses, - helperPredicate::isHelperClass)); + (Instrumenter.HasMethodAdvice) instrumenter, adviceShader, adviceClasses)); } } - // Advice roots are inlined into the target method, not injected, so they aren't helpers. + // Inferred helpers = our classes referenced from the advice, minus the advice roots. Set inferredHelpers = new LinkedHashSet<>(); for (Reference reference : allReferences) { if (!adviceClasses.contains(reference.className) @@ -153,21 +147,26 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { } // Manual additions cover helpers the crawl can't see (reflection, SPI, advice-less injectors). - Set helperClasses = new LinkedHashSet<>(inferredHelpers); - Collections.addAll(helperClasses, module.helperClassNames()); - for (String helper : new ArrayList<>(helperClasses)) { + Set manualHelpers = new LinkedHashSet<>(asList(module.helperClassNames())); + Set seedHelpers = new LinkedHashSet<>(inferredHelpers); + seedHelpers.addAll(manualHelpers); + for (String helper : new ArrayList<>(seedHelpers)) { if (isOwnOutput(helper)) { - addNestedClasses(helper, helperClasses); + addNestedClasses(helper, seedHelpers); } } String[] orderedHelpers = - orderHelpers(helperClasses, Thread.currentThread().getContextClassLoader()); + discoverAndOrderHelpers( + seedHelpers, + manualHelpers, + helperPredicate, + Thread.currentThread().getContextClassLoader()); - writeInferenceReport(module, adviceClasses.isEmpty(), inferredHelpers, helperClasses); + writeInferenceReport(module, adviceClasses.isEmpty(), inferredHelpers, orderedHelpers); // Injected helpers are our own classes, so they must not be asserted as library references. - Set ignoredClassNames = new HashSet<>(helperClasses); + Set ignoredClassNames = new HashSet<>(asList(orderedHelpers)); Collections.addAll(ignoredClassNames, module.muzzleIgnoredClassNames()); List references = new ArrayList<>(); for (Reference reference : allReferences) { @@ -225,19 +224,21 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { mv.visitMaxs(0, 0); mv.visitEnd(); - // Generated: public static String[] helperClassNames() - MethodVisitor hv = - cw.visitMethod( - Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, - "helperClassNames", - "()[Ljava/lang/String;", - null, - null); - hv.visitCode(); - writeStrings(hv, orderedHelpers); - hv.visitInsn(Opcodes.ARETURN); - hv.visitMaxs(0, 0); - hv.visitEnd(); + // Generated: public static String[] helperClassNames() — omitted for helper-less modules. + if (orderedHelpers.length > 0) { + MethodVisitor hv = + cw.visitMethod( + Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, + "helperClassNames", + "()[Ljava/lang/String;", + null, + null); + hv.visitCode(); + writeStrings(hv, orderedHelpers); + hv.visitInsn(Opcodes.ARETURN); + hv.visitMaxs(0, 0); + hv.visitEnd(); + } return cw.toByteArray(); } @@ -270,28 +271,35 @@ private void addNestedClasses(String className, Set helperClasses) { } /** - * Load-orders helpers (dependencies first) via {@link HelperScanner}, keeping only our helpers - * (the scanner may pull in library classes) and retaining any helper it could not locate. + * Runs {@link HelperScanner} over the seed helpers to both discover their transitive helper + * dependencies and load-order the result (dependencies first). Keeps only our own helpers (plus + * manual additions), dropping library classes the scanner pulls in, and retains every seed even + * if it could not be located. */ - private static String[] orderHelpers(Set helpers, ClassLoader loader) { - if (helpers.isEmpty()) { + private static String[] discoverAndOrderHelpers( + Set seedHelpers, + Set manualHelpers, + HelperClassPredicate helperPredicate, + ClassLoader loader) { + if (seedHelpers.isEmpty()) { return new String[0]; } - List ordered = new ArrayList<>(helpers.size()); + List ordered = new ArrayList<>(); try { for (String name : HelperScanner.withClassDependencies( - ClassFileLocator.ForClassLoader.of(loader), helpers.toArray(new String[0]))) { - if (helpers.contains(name) && !ordered.contains(name)) { + ClassFileLocator.ForClassLoader.of(loader), seedHelpers.toArray(new String[0]))) { + if ((helperPredicate.isHelperClass(name) || manualHelpers.contains(name)) + && !ordered.contains(name)) { ordered.add(name); } } } catch (Throwable ignore) { - // best-effort ordering; unordered helpers are appended below + // best-effort ordering; unordered seeds are appended below } - for (String name : helpers) { - if (!ordered.contains(name)) { - ordered.add(name); + for (String seed : seedHelpers) { + if (!ordered.contains(seed)) { + ordered.add(seed); } } return ordered.toArray(new String[0]); @@ -305,7 +313,7 @@ private void writeInferenceReport( InstrumenterModule module, boolean adviceLess, Set inferredHelpers, - Set allHelpers) { + String[] injectedHelpers) { try { Set declared = new LinkedHashSet<>(asList(module.helperClassNames())); if (declared.isEmpty() && inferredHelpers.isEmpty()) { @@ -323,7 +331,7 @@ private void writeInferenceReport( StringBuilder sb = new StringBuilder(); sb.append("module: ").append(module.getClass().getName()).append('\n'); sb.append("has-advice: ").append(!adviceLess).append('\n'); - sb.append("injected-helpers: ").append(allHelpers.size()).append('\n'); + sb.append("injected-helpers: ").append(injectedHelpers.length).append('\n'); if (adviceLess && !declared.isEmpty()) { sb.append("crawl-cannot-cover: advice-less module; declared helpers are all manual-only\n"); } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java index d8bbb8a7cbe..5fdc4d3f827 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/ReferenceCreator.java @@ -14,7 +14,6 @@ import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.function.Predicate; import net.bytebuddy.jar.asm.ClassReader; import net.bytebuddy.jar.asm.ClassVisitor; import net.bytebuddy.jar.asm.FieldVisitor; @@ -39,10 +38,6 @@ public class ReferenceCreator extends ClassVisitor { */ private static final String REFERENCE_CREATION_PACKAGE = "datadog.trace.instrumentation."; - /** Default recursion rule; callers that identify helpers by other means pass their own. */ - private static final Predicate DEFAULT_SHOULD_RECURSE = - name -> name.startsWith(REFERENCE_CREATION_PACKAGE); - private static final int UNDEFINED_LINE = -1; /** Set containing name+descriptor signatures of Object methods. */ @@ -63,30 +58,9 @@ public class ReferenceCreator extends ClassVisitor { * @return Map of [referenceClassName -> Reference] * @throws IllegalStateException if class is not found or unable to be loaded. */ - public static Map createReferencesFrom( - final String entryPointClassName, final AdviceShader adviceShader, final ClassLoader loader) - throws IllegalStateException { - return createReferencesFrom(entryPointClassName, adviceShader, loader, DEFAULT_SHOULD_RECURSE); - } - - /** - * Generate all references reachable from a given class, following references into classes - * accepted by {@code shouldRecurse}. - * - * @param entryPointClassName Starting point for generating references. - * @param adviceShader Optional shading to apply to the advice. - * @param loader Classloader used to read class bytes. - * @param shouldRecurse Decides whether a referenced class is one of ours to keep crawling into - * (vs. a library/bootstrap leaf that is merely recorded). - * @return Map of [referenceClassName -> Reference] - * @throws IllegalStateException if class is not found or unable to be loaded. - */ @SuppressForbidden public static Map createReferencesFrom( - final String entryPointClassName, - final AdviceShader adviceShader, - final ClassLoader loader, - final Predicate shouldRecurse) + final String entryPointClassName, final AdviceShader adviceShader, final ClassLoader loader) throws IllegalStateException { final Set visitedSources = new HashSet<>(); final Map references = new LinkedHashMap<>(); @@ -113,8 +87,9 @@ public static Map createReferencesFrom( final Map instrumentationReferences = cv.getReferences(); for (final Map.Entry entry : instrumentationReferences.entrySet()) { - // Only keep crawling into classes that are ours - if (!visitedSources.contains(entry.getKey()) && shouldRecurse.test(entry.getKey())) { + // Don't generate references created outside of the datadog instrumentation package. + if (!visitedSources.contains(entry.getKey()) + && entry.getKey().startsWith(REFERENCE_CREATION_PACKAGE)) { instrumentationQueue.add(entry.getKey()); } Reference toMerge = references.get(entry.getKey()); From 48b4584cd25340fe550906df6f4ccd01dcde84c3 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 15:36:39 -0400 Subject: [PATCH 5/8] Clean comments --- .../trace/agent/tooling/muzzle/MuzzleGenerator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java index 45b7157d6ae..31c36014a5e 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGenerator.java @@ -137,7 +137,7 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { } } - // Inferred helpers = our classes referenced from the advice, minus the advice roots. + // Inferred helpers = our classes referenced from the advice minus the advice roots. Set inferredHelpers = new LinkedHashSet<>(); for (Reference reference : allReferences) { if (!adviceClasses.contains(reference.className) @@ -146,7 +146,7 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { } } - // Manual additions cover helpers the crawl can't see (reflection, SPI, advice-less injectors). + // Manual additions cover helpers the crawl can't see. Set manualHelpers = new LinkedHashSet<>(asList(module.helperClassNames())); Set seedHelpers = new LinkedHashSet<>(inferredHelpers); seedHelpers.addAll(manualHelpers); @@ -165,7 +165,8 @@ private byte[] generateMuzzleClass(InstrumenterModule module) { writeInferenceReport(module, adviceClasses.isEmpty(), inferredHelpers, orderedHelpers); - // Injected helpers are our own classes, so they must not be asserted as library references. + // Injected helpers are our own classes, so they don't need to be asserted as library + // references. Set ignoredClassNames = new HashSet<>(asList(orderedHelpers)); Collections.addAll(ignoredClassNames, module.muzzleIgnoredClassNames()); List references = new ArrayList<>(); From 3267e4c3f075425a654f57aa4f5204c81ec1a047 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 16:01:35 -0400 Subject: [PATCH 6/8] Make helper-name resolution lazy --- .../tooling/CombiningTransformerBuilder.java | 27 ++++++----- .../trace/agent/tooling/HelperInjector.java | 45 +++++++++++++++++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java index f7c1a65495a..5d134eefc97 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.AsmVisitorWrapper; @@ -130,18 +131,20 @@ private void prepareInstrumentation(InstrumenterModule module, int instrumentati adviceShader = AdviceShader.with(module); - String[] helperClassNames = module.getAllHelperClassNames(); - if (module.injectHelperDependencies()) { - helperClassNames = HelperScanner.withClassDependencies(helperClassNames); - } + // Resolve helper names lazily, so agent install doesn't load every module's generated $Muzzle + // class. helperTransformer = - helperClassNames.length > 0 - ? new HelperTransformer( - module.useAgentCodeSource(), - adviceShader, - module.getClass().getSimpleName(), - helperClassNames) - : null; + new HelperTransformer( + module.useAgentCodeSource(), + adviceShader, + module.getClass().getSimpleName(), + () -> { + String[] helperClassNames = module.getAllHelperClassNames(); + if (module.injectHelperDependencies()) { + helperClassNames = HelperScanner.withClassDependencies(helperClassNames); + } + return helperClassNames; + }); postProcessor = module.postProcessor(); @@ -385,7 +388,7 @@ static final class HelperTransformer extends HelperInjector implements AgentBuil boolean useAgentCodeSource, AdviceShader adviceShader, String requestingName, - String... helperClassNames) { + Supplier helperClassNames) { super(useAgentCodeSource, adviceShader, requestingName, helperClassNames); } } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java index 4cf3d26a925..782d3c6b77a 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.dynamic.DynamicType; @@ -42,7 +43,10 @@ public class HelperInjector implements Instrumenter.TransformingAdvice { private final AdviceShader adviceShader; private final String requestingName; - private final Set helperClassNames; + // Helper names are resolved lazily: when a supplier is provided, resolution is deferred to the + // first transform() so that we don't load the generated $Muzzle class at agent install. + private final Supplier helperClassNamesSupplier; + private volatile Set helperClassNames; private final Map dynamicTypeMap = new LinkedHashMap<>(); private final Map injectedClassLoaders = @@ -77,9 +81,27 @@ public HelperInjector( this.requestingName = requestingName; this.adviceShader = adviceShader; + this.helperClassNamesSupplier = null; this.helperClassNames = new LinkedHashSet<>(asList(helperClassNames)); } + /** + * Construct HelperInjector whose helper names are resolved lazily on first {@link #transform}, to + * avoid resolving them (which may load the generated {@code $Muzzle} class) at agent install. + */ + public HelperInjector( + final boolean useAgentCodeSource, + final AdviceShader adviceShader, + final String requestingName, + final Supplier helperClassNamesSupplier) { + this.useAgentCodeSource = useAgentCodeSource; + this.requestingName = requestingName; + this.adviceShader = adviceShader; + + this.helperClassNamesSupplier = helperClassNamesSupplier; + this.helperClassNames = null; + } + public HelperInjector( final boolean useAgentCodeSource, final String requestingName, @@ -88,11 +110,27 @@ public HelperInjector( this.requestingName = requestingName; this.adviceShader = null; + this.helperClassNamesSupplier = null; helperClassNames = helperMap.keySet(); dynamicTypeMap.putAll(helperMap); } - private Map getHelperMap() throws IOException { + /** Resolves helper class names, deferring to the supplier when one's provided. */ + private Set resolveHelperClassNames() { + Set names = helperClassNames; + if (names == null) { + synchronized (this) { + names = helperClassNames; + if (names == null) { + names = new LinkedHashSet<>(asList(helperClassNamesSupplier.get())); + helperClassNames = names; + } + } + } + return names; + } + + private Map getHelperMap(final Set helperClassNames) throws IOException { if (dynamicTypeMap.isEmpty()) { final Map classnameToBytes = new LinkedHashMap<>(); for (String helperName : helperClassNames) { @@ -117,6 +155,7 @@ public DynamicType.Builder transform( ClassLoader classLoader, final JavaModule module, final ProtectionDomain pd) { + final Set helperClassNames = resolveHelperClassNames(); if (!helperClassNames.isEmpty()) { if (classLoader == null) { throw new UnsupportedOperationException( @@ -135,7 +174,7 @@ public DynamicType.Builder transform( String.join(",", helperClassNames)); } - final Map classnameToBytes = getHelperMap(); + final Map classnameToBytes = getHelperMap(helperClassNames); final Collection> classes = injectClassLoader(classLoader, classnameToBytes); // all datadog helper classes are in the unnamed module From b14de7f92e91f500d520d369ec08a0735ed18e61 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Thu, 23 Jul 2026 16:38:14 -0400 Subject: [PATCH 7/8] Do not capture entire module instanc at prepareInstrumentation --- .../tooling/CombiningTransformerBuilder.java | 17 +++++++++++++---- .../trace/agent/tooling/InstrumenterModule.java | 16 ++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java index 5d134eefc97..10d35783c0a 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java @@ -131,16 +131,25 @@ private void prepareInstrumentation(InstrumenterModule module, int instrumentati adviceShader = AdviceShader.with(module); - // Resolve helper names lazily, so agent install doesn't load every module's generated $Muzzle - // class. + // Resolve helper names lazily, and capture only GC-neutral values (not the module instance) + // so that the instrumenter can still be unloaded after setup, and load $Muzzle via the + // extended class loader like the muzzle check does. + final String instrumentationClass = module.getClass().getName(); + final String[] declaredHelperClassNames = module.helperClassNames(); + final boolean injectHelperDependencies = module.injectHelperDependencies(); helperTransformer = new HelperTransformer( module.useAgentCodeSource(), adviceShader, module.getClass().getSimpleName(), () -> { - String[] helperClassNames = module.getAllHelperClassNames(); - if (module.injectHelperDependencies()) { + String[] helperClassNames = + InstrumenterModule.loadStaticMuzzleHelperClassNames( + Utils.getExtendedClassLoader(), instrumentationClass); + if (null == helperClassNames) { + helperClassNames = declaredHelperClassNames; + } + if (injectHelperDependencies) { helperClassNames = HelperScanner.withClassDependencies(helperClassNames); } return helperClassNames; diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java index d6438f1d30c..833d573cfde 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/InstrumenterModule.java @@ -113,18 +113,11 @@ public static ReferenceMatcher loadStaticMuzzleReferences( } /** - * @return the full, load-ordered set of helper classes to inject into the user's classloader: the - * helpers inferred at build time from the advice/helper byte-code, unioned with any {@link - * #helperClassNames() manually-declared} additions. Falls back to {@link #helperClassNames()} - * when no generated {@code $Muzzle} is available. + * @return the build-time inferred and manually-declared helper class names captured by {@code + * $Muzzle}, or {@code null} when none are available and fall back to {@link + * #helperClassNames()}. */ - public final String[] getAllHelperClassNames() { - String[] generated = - loadStaticMuzzleHelperClassNames(getClass().getClassLoader(), getClass().getName()); - return null != generated ? generated : helperClassNames(); - } - - static String[] loadStaticMuzzleHelperClassNames( + public static String[] loadStaticMuzzleHelperClassNames( ClassLoader classLoader, String instrumentationClass) { String muzzleClass = instrumentationClass + "$Muzzle"; try { @@ -132,7 +125,6 @@ static String[] loadStaticMuzzleHelperClassNames( return (String[]) classLoader.loadClass(muzzleClass).getMethod("helperClassNames").invoke(null); } catch (Throwable e) { - // no generated helper list: caller falls back to helperClassNames() return null; } } From dc592901be8a66fd85d92f1de02b8a85395730b5 Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 24 Jul 2026 16:49:40 -0400 Subject: [PATCH 8/8] Undo lazy loading --- .../tooling/CombiningTransformerBuilder.java | 42 ++++++++--------- .../trace/agent/tooling/HelperInjector.java | 45 ++----------------- 2 files changed, 21 insertions(+), 66 deletions(-) diff --git a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java index 10d35783c0a..71e7f454286 100644 --- a/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java +++ b/dd-java-agent/agent-installer/src/main/java/datadog/trace/agent/tooling/CombiningTransformerBuilder.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.AsmVisitorWrapper; @@ -131,29 +130,24 @@ private void prepareInstrumentation(InstrumenterModule module, int instrumentati adviceShader = AdviceShader.with(module); - // Resolve helper names lazily, and capture only GC-neutral values (not the module instance) - // so that the instrumenter can still be unloaded after setup, and load $Muzzle via the - // extended class loader like the muzzle check does. - final String instrumentationClass = module.getClass().getName(); - final String[] declaredHelperClassNames = module.helperClassNames(); - final boolean injectHelperDependencies = module.injectHelperDependencies(); + // Resolve helper names eagerly at install. Only the resolved String[] is captured. + String[] helperClassNames = + InstrumenterModule.loadStaticMuzzleHelperClassNames( + Utils.getExtendedClassLoader(), module.getClass().getName()); + if (null == helperClassNames) { + helperClassNames = module.helperClassNames(); + } + if (module.injectHelperDependencies()) { + helperClassNames = HelperScanner.withClassDependencies(helperClassNames); + } helperTransformer = - new HelperTransformer( - module.useAgentCodeSource(), - adviceShader, - module.getClass().getSimpleName(), - () -> { - String[] helperClassNames = - InstrumenterModule.loadStaticMuzzleHelperClassNames( - Utils.getExtendedClassLoader(), instrumentationClass); - if (null == helperClassNames) { - helperClassNames = declaredHelperClassNames; - } - if (injectHelperDependencies) { - helperClassNames = HelperScanner.withClassDependencies(helperClassNames); - } - return helperClassNames; - }); + helperClassNames.length > 0 + ? new HelperTransformer( + module.useAgentCodeSource(), + adviceShader, + module.getClass().getSimpleName(), + helperClassNames) + : null; postProcessor = module.postProcessor(); @@ -397,7 +391,7 @@ static final class HelperTransformer extends HelperInjector implements AgentBuil boolean useAgentCodeSource, AdviceShader adviceShader, String requestingName, - Supplier helperClassNames) { + String... helperClassNames) { super(useAgentCodeSource, adviceShader, requestingName, helperClassNames); } } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java index 782d3c6b77a..4cf3d26a925 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java @@ -21,7 +21,6 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Supplier; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.dynamic.DynamicType; @@ -43,10 +42,7 @@ public class HelperInjector implements Instrumenter.TransformingAdvice { private final AdviceShader adviceShader; private final String requestingName; - // Helper names are resolved lazily: when a supplier is provided, resolution is deferred to the - // first transform() so that we don't load the generated $Muzzle class at agent install. - private final Supplier helperClassNamesSupplier; - private volatile Set helperClassNames; + private final Set helperClassNames; private final Map dynamicTypeMap = new LinkedHashMap<>(); private final Map injectedClassLoaders = @@ -81,27 +77,9 @@ public HelperInjector( this.requestingName = requestingName; this.adviceShader = adviceShader; - this.helperClassNamesSupplier = null; this.helperClassNames = new LinkedHashSet<>(asList(helperClassNames)); } - /** - * Construct HelperInjector whose helper names are resolved lazily on first {@link #transform}, to - * avoid resolving them (which may load the generated {@code $Muzzle} class) at agent install. - */ - public HelperInjector( - final boolean useAgentCodeSource, - final AdviceShader adviceShader, - final String requestingName, - final Supplier helperClassNamesSupplier) { - this.useAgentCodeSource = useAgentCodeSource; - this.requestingName = requestingName; - this.adviceShader = adviceShader; - - this.helperClassNamesSupplier = helperClassNamesSupplier; - this.helperClassNames = null; - } - public HelperInjector( final boolean useAgentCodeSource, final String requestingName, @@ -110,27 +88,11 @@ public HelperInjector( this.requestingName = requestingName; this.adviceShader = null; - this.helperClassNamesSupplier = null; helperClassNames = helperMap.keySet(); dynamicTypeMap.putAll(helperMap); } - /** Resolves helper class names, deferring to the supplier when one's provided. */ - private Set resolveHelperClassNames() { - Set names = helperClassNames; - if (names == null) { - synchronized (this) { - names = helperClassNames; - if (names == null) { - names = new LinkedHashSet<>(asList(helperClassNamesSupplier.get())); - helperClassNames = names; - } - } - } - return names; - } - - private Map getHelperMap(final Set helperClassNames) throws IOException { + private Map getHelperMap() throws IOException { if (dynamicTypeMap.isEmpty()) { final Map classnameToBytes = new LinkedHashMap<>(); for (String helperName : helperClassNames) { @@ -155,7 +117,6 @@ public DynamicType.Builder transform( ClassLoader classLoader, final JavaModule module, final ProtectionDomain pd) { - final Set helperClassNames = resolveHelperClassNames(); if (!helperClassNames.isEmpty()) { if (classLoader == null) { throw new UnsupportedOperationException( @@ -174,7 +135,7 @@ public DynamicType.Builder transform( String.join(",", helperClassNames)); } - final Map classnameToBytes = getHelperMap(helperClassNames); + final Map classnameToBytes = getHelperMap(); final Collection> classes = injectClassLoader(classLoader, classnameToBytes); // all datadog helper classes are in the unnamed module