From eb62934f2cb4897bab1ea243bccf2038cd515137 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Wed, 4 Feb 2026 12:45:17 +0100 Subject: [PATCH 1/7] Fix method parameters JVM bug Prevent classfiles with Method Parameters attribute (javac -parameters) to be (re)transformed when on JDK < 19. Spring 6+ or SpringBoot3+ rely exclusively on method parameters to get param name and if the attribute is not present throw an exception and returns 500 on endpoints. see OpenJDK bug JDK-8240908. we are scanning method with at least on parameter to detect if the class was compiled with method parameters attribute and if the JDK is < 19 we prevent instrumentation to happen. Even at load time we prevent it because we need to call retransform to remove instrumentation. Therefore the attribute can be strip at that time. --- .../debugger/agent/ConfigurationUpdater.java | 48 +++++++++++++++++++ .../debugger/agent/DebuggerTransformer.java | 36 ++++++++++++++ .../debugger/agent/CapturedSnapshotTest.java | 24 ++++++++++ .../agent/ConfigurationUpdaterTest.java | 30 ++++++++++++ .../java/utils/InstrumentationTestHelper.java | 12 ++++- .../src/test/java/utils/SourceCompiler.java | 8 +++- 6 files changed, 155 insertions(+), 3 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java index 16c8f0c03ad..ae6387565d9 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java @@ -13,6 +13,7 @@ import com.datadog.debugger.probe.Sampling; import com.datadog.debugger.sink.DebuggerSink; import com.datadog.debugger.util.ExceptionHelper; +import datadog.environment.JavaVirtualMachine; import datadog.trace.api.Config; import datadog.trace.bootstrap.debugger.DebuggerContext; import datadog.trace.bootstrap.debugger.ProbeId; @@ -21,6 +22,9 @@ import datadog.trace.relocate.api.RatelimitedLogger; import datadog.trace.util.TagsHelper; import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; @@ -41,6 +45,8 @@ */ public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor { + private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19); + public interface TransformerSupplier { DebuggerTransformer supply( Config tracerConfig, @@ -178,6 +184,7 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne } List> changedClasses = finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes); + changedClasses = detectMethodParameters(changedClasses); retransformClasses(changedClasses); // ensures that we have at least re-transformed 1 class if (changedClasses.size() > 0) { @@ -185,6 +192,47 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne } } + /* + * Because of this bug (https://bugs.openjdk.org/browse/JDK-8240908), classes compiled with + * method parameters (javac -parameters) strip this attribute once retransformed + * Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception + * if no attribute found. + */ + private List> detectMethodParameters(List> changedClasses) { + if (JAVA_AT_LEAST_19) { + // bug is fixed since JDK19, no need to perform detection + return changedClasses; + } + List> result = new ArrayList<>(); + for (Class changedClass : changedClasses) { + Method[] declaredMethods = changedClass.getDeclaredMethods(); + boolean addClass = true; + // capping scanning of methods to 100 to avoid generated class with thousand of methods + // assuming that in those first 100 methods there is at least one with at least one parameter + for (int methodIdx = 0; methodIdx < declaredMethods.length && methodIdx < 100; methodIdx++) { + Method method = declaredMethods[methodIdx]; + Parameter[] parameters = method.getParameters(); + if (parameters.length == 0) { + continue; + } + if (parameters[0].isNamePresent()) { + LOGGER.debug( + "Detecting method parameter: method={} param={}, Skipping retransforming this class", + method.getName(), + parameters[0].getName()); + // skip the class: compiled with -parameters + addClass = false; + } + // we found at leat a method with one parameter if name is not present we can stop there + break; + } + if (addClass) { + result.add(changedClass); + } + } + return result; + } + private void reportReceived(ConfigurationComparer changes) { for (ProbeDefinition def : changes.getAddedDefinitions()) { if (def instanceof ExceptionProbe) { diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index 66e8b76cacb..240eac50bc0 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -24,6 +24,7 @@ import com.datadog.debugger.uploader.BatchUploader; import com.datadog.debugger.util.ClassFileLines; import com.datadog.debugger.util.DebuggerMetrics; +import datadog.environment.JavaVirtualMachine; import datadog.environment.SystemProperties; import datadog.trace.agent.tooling.AgentStrategies; import datadog.trace.api.Config; @@ -61,6 +62,7 @@ import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.commons.JSRInlinerAdapter; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; @@ -90,6 +92,7 @@ public class DebuggerTransformer implements ClassFileTransformer { SpanDecorationProbe.class, SpanProbe.class); private static final String JAVA_IO_TMPDIR = "java.io.tmpdir"; + private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19); public static Path DUMP_PATH = Paths.get(SystemProperties.get(JAVA_IO_TMPDIR), "debugger"); @@ -258,6 +261,7 @@ public byte[] transform( return null; } ClassNode classNode = parseClassFile(classFilePath, classfileBuffer); + checkMethodParameters(classNode); boolean transformed = performInstrumentation(loader, fullyQualifiedClassName, definitions, classNode); if (transformed) { @@ -276,6 +280,38 @@ public byte[] transform( return null; } + /* + * Because of this bug (https://bugs.openjdk.org/browse/JDK-8240908), classes compiled with + * method parameters (javac -parameters) strip this attribute once retransformed + * Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception + * if no attribute found. + * Note: Even if the attribute is preserved when transforming at load time, the fact that we have + * instrumented the class, we will retransform for removing the instrumentation and then the + * attribute is stripped. That's why we are preventing it even at load time. + */ + private void checkMethodParameters(ClassNode classNode) { + if (JAVA_AT_LEAST_19) { + // bug is fixed since JDK19, no need to perform check + return; + } + // capping scanning of methods to 100 to avoid generated class with thousand of methods + // assuming that in those first 100 methods there is at least one with at least one parameter + for (int methodIdx = 0; methodIdx < classNode.methods.size() && methodIdx < 100; methodIdx++) { + MethodNode methodNode = classNode.methods.get(methodIdx); + int argumentCount = Type.getArgumentCount(methodNode.desc); + if (argumentCount == 0) { + continue; + } + if (methodNode.parameters != null && !methodNode.parameters.isEmpty()) { + throw new RuntimeException( + "Method Parameters attribute detected, cannot instrument class " + classNode.name); + } else { + // we found at leat a method with one parameter if name is not present we can stop there + break; + } + } + } + private boolean skipInstrumentation(ClassLoader loader, String classFilePath) { if (definitionMatcher.isEmpty()) { LOGGER.debug("No debugger definitions present."); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index ff05a21845e..07c87e35643 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -44,6 +44,7 @@ import com.datadog.debugger.util.MoshiSnapshotTestHelper; import com.datadog.debugger.util.TestSnapshotListener; import com.datadog.debugger.util.TestTraceInterceptor; +import datadog.environment.JavaVirtualMachine; import datadog.trace.agent.tooling.TracerInstaller; import datadog.trace.api.Config; import datadog.trace.api.interceptor.MutableSpan; @@ -2807,6 +2808,29 @@ public void captureExpressionsWithCaptureLimits() throws IOException, URISyntaxE assertEquals("depth", fldValue.getNotCapturedReason()); } + @Test + public void methodParametersAttribute() throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot01"; + TestSnapshotListener listener = + installMethodProbeAtExit(CLASS_NAME, "main", "int (java.lang.String)"); + Map buffers = + compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters")); + Class testClass = loadClass(CLASS_NAME, buffers); + int result = Reflect.onClass(testClass).call("main", "1").get(); + assertEquals(3, result); + if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { + Snapshot snapshot = assertOneSnapshot(listener); + assertCaptureArgs(snapshot.getCaptures().getReturn(), "arg", String.class.getTypeName(), "1"); + } else { + assertEquals(0, listener.snapshots.size()); + ArgumentCaptor probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class); + ArgumentCaptor strCaptor = ArgumentCaptor.forClass(String.class); + verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture()); + assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId()); + assertEquals("Instrumentation fails for CapturedSnapshot01", strCaptor.getAllValues().get(0)); + } + } + private TestSnapshotListener setupInstrumentTheWorldTransformer( String excludeFileName, String includeFileName) { Config config = mock(Config.class); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java index 3bc4cf4e4df..4ff7e69c925 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java @@ -13,6 +13,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import static utils.InstrumentationTestHelper.compile; +import static utils.InstrumentationTestHelper.loadClass; import com.datadog.debugger.el.DSL; import com.datadog.debugger.el.ProbeCondition; @@ -23,11 +25,14 @@ import com.datadog.debugger.probe.SpanProbe; import com.datadog.debugger.sink.DebuggerSink; import com.datadog.debugger.sink.ProbeStatusSink; +import datadog.environment.JavaVirtualMachine; import datadog.trace.api.Config; import datadog.trace.bootstrap.debugger.ProbeId; import datadog.trace.bootstrap.debugger.ProbeImplementation; +import java.io.IOException; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; +import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; @@ -39,6 +44,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import utils.SourceCompiler; @ExtendWith(MockitoExtension.class) public class ConfigurationUpdaterTest { @@ -623,6 +629,30 @@ public void handleException() { verify(probeStatusSink).addError(eq(ProbeId.from(PROBE_ID.getId() + ":0")), eq(ex)); } + @Test + public void methodParametersAttribute() + throws IOException, URISyntaxException, UnmodifiableClassException { + final String CLASS_NAME = "CapturedSnapshot01"; + Map buffers = + compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "8", Arrays.asList("-parameters")); + Class testClass = loadClass(CLASS_NAME, buffers); + when(inst.getAllLoadedClasses()).thenReturn(new Class[] {testClass}); + ConfigurationUpdater configurationUpdater = createConfigUpdater(debuggerSinkWithMockStatusSink); + configurationUpdater.accept( + REMOTE_CONFIG, + singletonList( + LogProbe.builder().probeId(PROBE_ID).where("CapturedSnapshot01", "main").build())); + if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { + ArgumentCaptor[]> captor = ArgumentCaptor.forClass(Class[].class); + verify(inst, times(1)).retransformClasses(captor.capture()); + List[]> allValues = captor.getAllValues(); + assertEquals(testClass, allValues.get(0)); + } else { + verify(inst).getAllLoadedClasses(); + verify(inst, times(0)).retransformClasses(any()); + } + } + private DebuggerTransformer createTransformer( Config tracerConfig, Configuration configuration, diff --git a/dd-java-agent/agent-debugger/src/test/java/utils/InstrumentationTestHelper.java b/dd-java-agent/agent-debugger/src/test/java/utils/InstrumentationTestHelper.java index 99690c388f9..303d976d69b 100644 --- a/dd-java-agent/agent-debugger/src/test/java/utils/InstrumentationTestHelper.java +++ b/dd-java-agent/agent-debugger/src/test/java/utils/InstrumentationTestHelper.java @@ -12,6 +12,7 @@ import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -52,8 +53,17 @@ public static Map compile(String className, String version) public static Map compile( String className, SourceCompiler.DebugInfo debugInfo, String version) throws IOException, URISyntaxException { + return compile(className, debugInfo, version, Collections.emptyList()); + } + + public static Map compile( + String className, + SourceCompiler.DebugInfo debugInfo, + String version, + List additionalOptions) + throws IOException, URISyntaxException { String classSource = getFixtureContent("/" + className.replace('.', '/') + ".java"); - return SourceCompiler.compile(className, classSource, debugInfo, version); + return SourceCompiler.compile(className, classSource, debugInfo, version, additionalOptions); } public static Class loadClass(String className, String classFileName) throws IOException { diff --git a/dd-java-agent/agent-debugger/src/test/java/utils/SourceCompiler.java b/dd-java-agent/agent-debugger/src/test/java/utils/SourceCompiler.java index e4828719720..e2ed7f9b6cb 100644 --- a/dd-java-agent/agent-debugger/src/test/java/utils/SourceCompiler.java +++ b/dd-java-agent/agent-debugger/src/test/java/utils/SourceCompiler.java @@ -18,7 +18,11 @@ public enum DebugInfo { } public static Map compile( - String className, String source, DebugInfo debug, String version) { + String className, + String source, + DebugInfo debug, + String version, + List additionalOptions) { JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); if (jc == null) throw new RuntimeException("Compiler unavailable"); @@ -26,7 +30,7 @@ public static Map compile( Iterable fileObjects = Collections.singletonList(jsfs); - List options = new ArrayList<>(); + List options = new ArrayList<>(additionalOptions); switch (debug) { case ALL: { From 122ca2a2e4b37c7e91a275a51562f9628bcfdd00 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Wed, 4 Feb 2026 13:37:40 +0100 Subject: [PATCH 2/7] disable Scala test for JDK < 21 --- .../java/com/datadog/debugger/agent/CapturedSnapshotTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index 07c87e35643..8d18e0d8378 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -599,6 +599,7 @@ public void methodProbeLineProbeMix() throws IOException, URISyntaxException { } @Test + @EnabledForJreRange(min = JRE.JAVA_21) public void sourceFileProbeScala() throws IOException, URISyntaxException { final String CLASS_NAME = "CapturedSnapshot101"; final String FILE_NAME = CLASS_NAME + SCALA_EXT; From 9a2007976d8ea9729263e34b70f2f2b035b58030 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Wed, 4 Feb 2026 17:08:34 +0100 Subject: [PATCH 3/7] Add special case for Record canonical record constructor has method parameters attribute --- .../debugger/agent/DebuggerTransformer.java | 7 +++++ .../debugger/instrumentation/ASMHelper.java | 4 +++ .../debugger/agent/CapturedSnapshotTest.java | 27 ++++++++++++++++++ .../agent/ConfigurationUpdaterTest.java | 28 +++++++++++++++++-- 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index 240eac50bc0..26ff249ebb7 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -5,6 +5,7 @@ import static java.util.stream.Collectors.toList; import com.datadog.debugger.el.ProbeCondition; +import com.datadog.debugger.instrumentation.ASMHelper; import com.datadog.debugger.instrumentation.DiagnosticMessage; import com.datadog.debugger.instrumentation.InstrumentationResult; import com.datadog.debugger.instrumentation.MethodInfo; @@ -294,6 +295,7 @@ private void checkMethodParameters(ClassNode classNode) { // bug is fixed since JDK19, no need to perform check return; } + boolean isRecord = ASMHelper.isRecord(classNode); // capping scanning of methods to 100 to avoid generated class with thousand of methods // assuming that in those first 100 methods there is at least one with at least one parameter for (int methodIdx = 0; methodIdx < classNode.methods.size() && methodIdx < 100; methodIdx++) { @@ -302,6 +304,11 @@ private void checkMethodParameters(ClassNode classNode) { if (argumentCount == 0) { continue; } + if (isRecord && methodNode.name.equals("")) { + // skip record constructors, cannot rely on them because of the canonical one + // use the equals method for this + continue; + } if (methodNode.parameters != null && !methodNode.parameters.isEmpty()) { throw new RuntimeException( "Method Parameters attribute detected, cannot instrument class " + classNode.name); diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java index 94ac5264259..b011eca087c 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java @@ -138,6 +138,10 @@ public static boolean isFinalField(Field field) { return Modifier.isFinal(field.getModifiers()); } + public static boolean isRecord(ClassNode classNode) { + return (classNode.access & Opcodes.ACC_RECORD) > 0; + } + public static void invokeStatic( InsnList insnList, org.objectweb.asm.Type owner, diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index 8d18e0d8378..4a9c9c40b6a 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -2832,6 +2832,33 @@ public void methodParametersAttribute() throws IOException, URISyntaxException { } } + @Test + @EnabledForJreRange(min = JRE.JAVA_17) + public void methodParametersAttributeRecord() throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot29"; + final String RECORD_NAME = "com.datadog.debugger.MyRecord1"; + TestSnapshotListener listener = installMethodProbeAtExit(RECORD_NAME, "", null); + Map buffers = + compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "17", Arrays.asList("-parameters")); + Class testClass = loadClass(CLASS_NAME, buffers); + int result = Reflect.onClass(testClass).call("main", "1").get(); + assertEquals(42, result); + if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { + Snapshot snapshot = assertOneSnapshot(listener); + assertCaptureArgs( + snapshot.getCaptures().getReturn(), "firstName", String.class.getTypeName(), "john"); + } else { + assertEquals(0, listener.snapshots.size()); + ArgumentCaptor probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class); + ArgumentCaptor strCaptor = ArgumentCaptor.forClass(String.class); + verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture()); + assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId()); + assertEquals( + "Instrumentation fails for com.datadog.debugger.MyRecord1", + strCaptor.getAllValues().get(0)); + } + } + private TestSnapshotListener setupInstrumentTheWorldTransformer( String excludeFileName, String includeFileName) { Config config = mock(Config.class); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java index 4ff7e69c925..ba9db6b2a87 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java @@ -40,6 +40,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -640,8 +642,7 @@ public void methodParametersAttribute() ConfigurationUpdater configurationUpdater = createConfigUpdater(debuggerSinkWithMockStatusSink); configurationUpdater.accept( REMOTE_CONFIG, - singletonList( - LogProbe.builder().probeId(PROBE_ID).where("CapturedSnapshot01", "main").build())); + singletonList(LogProbe.builder().probeId(PROBE_ID).where(CLASS_NAME, "main").build())); if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { ArgumentCaptor[]> captor = ArgumentCaptor.forClass(Class[].class); verify(inst, times(1)).retransformClasses(captor.capture()); @@ -653,6 +654,29 @@ public void methodParametersAttribute() } } + @Test + @EnabledForJreRange(min = JRE.JAVA_17) + public void methodParametersAttributeRecord() + throws IOException, URISyntaxException, UnmodifiableClassException { + // make sure record method are not detected as having methodParameters attribute. + // /!\ record canonical constructor has the MethodParameters attribute, + // but not returned by Class::getDeclaredMethods() + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot29"; + final String RECORD_NAME = "com.datadog.debugger.MyRecord1"; + Map buffers = compile(CLASS_NAME, SourceCompiler.DebugInfo.ALL, "17"); + Class testClass = loadClass(RECORD_NAME, buffers); + when(inst.getAllLoadedClasses()).thenReturn(new Class[] {testClass}); + ConfigurationUpdater configurationUpdater = createConfigUpdater(debuggerSinkWithMockStatusSink); + configurationUpdater.accept( + REMOTE_CONFIG, + singletonList(LogProbe.builder().probeId(PROBE_ID).where(RECORD_NAME, "").build())); + verify(inst).getAllLoadedClasses(); + ArgumentCaptor[]> captor = ArgumentCaptor.forClass(Class[].class); + verify(inst, times(1)).retransformClasses(captor.capture()); + List[]> allValues = captor.getAllValues(); + assertEquals(testClass, allValues.get(0)); + } + private DebuggerTransformer createTransformer( Config tracerConfig, Configuration configuration, From 4bfed3c9e82940d13283433f788104bffcbf92e0 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 5 Feb 2026 10:39:55 +0100 Subject: [PATCH 4/7] add error messages --- .../debugger/agent/ConfigurationUpdater.java | 19 +++++++++++++++++-- .../debugger/agent/DebuggerTransformer.java | 13 +++++++------ .../datadog/debugger/sink/DebuggerSink.java | 4 ++++ .../debugger/agent/CapturedSnapshotTest.java | 4 +++- .../agent/ConfigurationUpdaterTest.java | 7 +++++++ .../agent/DebuggerTransformerTest.java | 18 +++++++++++++++--- 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java index ae6387565d9..4fb678b3cde 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java @@ -184,7 +184,7 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne } List> changedClasses = finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes); - changedClasses = detectMethodParameters(changedClasses); + changedClasses = detectMethodParameters(changes, changedClasses); retransformClasses(changedClasses); // ensures that we have at least re-transformed 1 class if (changedClasses.size() > 0) { @@ -198,7 +198,8 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne * Spring 6/Spring boot 3 rely exclusively on this attribute and may throw an exception * if no attribute found. */ - private List> detectMethodParameters(List> changedClasses) { + private List> detectMethodParameters( + ConfigurationComparer changes, List> changedClasses) { if (JAVA_AT_LEAST_19) { // bug is fixed since JDK19, no need to perform detection return changedClasses; @@ -221,6 +222,10 @@ private List> detectMethodParameters(List> changedClasses) { method.getName(), parameters[0].getName()); // skip the class: compiled with -parameters + reportError( + changes, + "Method Parameters detected, instrumentation not supported for " + + changedClass.getTypeName()); addClass = false; } // we found at leat a method with one parameter if name is not present we can stop there @@ -246,6 +251,16 @@ private void reportReceived(ConfigurationComparer changes) { } } + private void reportError(ConfigurationComparer changes, String errorMsg) { + for (ProbeDefinition def : changes.getAddedDefinitions()) { + if (def instanceof ExceptionProbe) { + // do not report received for exception probes + continue; + } + sink.addError(def.getProbeId(), errorMsg); + } + } + private void installNewDefinitions(Configuration newConfiguration) { DebuggerContext.initClassFilter(new DenyListHelper(newConfiguration.getDenyList())); if (appliedDefinitions.isEmpty()) { diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index 26ff249ebb7..6b7a7f9112c 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -82,7 +82,7 @@ public class DebuggerTransformer implements ClassFileTransformer { private static final Logger LOGGER = LoggerFactory.getLogger(DebuggerTransformer.class); private static final String CANNOT_FIND_METHOD = "Cannot find method %s::%s%s"; - private static final String INSTRUMENTATION_FAILS = "Instrumentation fails for %s"; + private static final String INSTRUMENTATION_FAILS = "Instrumentation failed for %s: %s"; private static final String CANNOT_FIND_LINE = "No executable code was found at %s:L%s"; private static final Pattern COMMA_PATTERN = Pattern.compile(","); private static final List> PROBE_ORDER = @@ -276,7 +276,7 @@ public byte[] transform( "type {} matched but no transformation for definitions: {}", classFilePath, definitions); } catch (Throwable ex) { LOGGER.warn("Cannot transform: ", ex); - reportInstrumentationFails(definitions, fullyQualifiedClassName); + reportInstrumentationFails(definitions, fullyQualifiedClassName, ex.toString()); } return null; } @@ -311,7 +311,7 @@ private void checkMethodParameters(ClassNode classNode) { } if (methodNode.parameters != null && !methodNode.parameters.isEmpty()) { throw new RuntimeException( - "Method Parameters attribute detected, cannot instrument class " + classNode.name); + "Method Parameters attribute detected, instrumentation not supported"); } else { // we found at leat a method with one parameter if name is not present we can stop there break; @@ -534,7 +534,7 @@ private byte[] writeClassFile( classNode.accept(visitor); } catch (Throwable t) { LOGGER.error("Cannot write classfile for class: {} Exception: ", classFilePath, t); - reportInstrumentationFails(definitions, Strings.getClassName(classFilePath)); + reportInstrumentationFails(definitions, Strings.getClassName(classFilePath), t.toString()); return null; } byte[] data = writer.toByteArray(); @@ -658,8 +658,9 @@ private void reportLocationNotFound( // on a separate class files because probe was set on an inner/top-level class } - private void reportInstrumentationFails(List definitions, String className) { - String msg = String.format(INSTRUMENTATION_FAILS, className); + private void reportInstrumentationFails( + List definitions, String className, String errorMsg) { + String msg = String.format(INSTRUMENTATION_FAILS, className, errorMsg); reportErrorForAllProbes(definitions, msg); } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/sink/DebuggerSink.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/sink/DebuggerSink.java index f9ac43b5d0e..e84a732eacd 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/sink/DebuggerSink.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/sink/DebuggerSink.java @@ -208,6 +208,10 @@ public void addBlocked(ProbeId probeId) { probeStatusSink.addBlocked(probeId); } + public void addError(ProbeId probeId, String msg) { + probeStatusSink.addError(probeId, msg); + } + public void removeDiagnostics(ProbeId probeId) { probeStatusSink.removeDiagnostics(probeId); } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index 4a9c9c40b6a..fa4bb49ea2a 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -2828,7 +2828,9 @@ public void methodParametersAttribute() throws IOException, URISyntaxException { ArgumentCaptor strCaptor = ArgumentCaptor.forClass(String.class); verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture()); assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId()); - assertEquals("Instrumentation fails for CapturedSnapshot01", strCaptor.getAllValues().get(0)); + assertEquals( + "Instrumentation failed for CapturedSnapshot01: java.lang.RuntimeException: Method Parameters attribute detected, instrumentation not supported", + strCaptor.getAllValues().get(0)); } } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java index ba9db6b2a87..ac42dd7d8e3 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/ConfigurationUpdaterTest.java @@ -651,6 +651,13 @@ public void methodParametersAttribute() } else { verify(inst).getAllLoadedClasses(); verify(inst, times(0)).retransformClasses(any()); + ArgumentCaptor probeIdCaptor = ArgumentCaptor.forClass(ProbeId.class); + ArgumentCaptor strCaptor = ArgumentCaptor.forClass(String.class); + verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture()); + assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId()); + assertEquals( + "Method Parameters detected, instrumentation not supported for CapturedSnapshot01", + strCaptor.getAllValues().get(0)); } } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java index 133df1394a2..4ad93c4b140 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java @@ -338,9 +338,21 @@ public void classGenerationFailed() { assertEquals("logprobe1", probeIdCaptor.getAllValues().get(0).getId()); assertEquals("logprobe2", probeIdCaptor.getAllValues().get(1).getId()); assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(2).getId()); - assertEquals("Instrumentation fails for " + CLASS_NAME, strCaptor.getAllValues().get(0)); - assertEquals("Instrumentation fails for " + CLASS_NAME, strCaptor.getAllValues().get(1)); - assertEquals("Instrumentation fails for " + CLASS_NAME, strCaptor.getAllValues().get(2)); + assertEquals( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", + strCaptor.getAllValues().get(0)); + assertEquals( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", + strCaptor.getAllValues().get(1)); + assertEquals( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", + strCaptor.getAllValues().get(2)); } @Test From 4e61d63424a7a5f36524fbe22933a134e9e1d556 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 5 Feb 2026 11:27:44 +0100 Subject: [PATCH 5/7] fix test --- .../agent/DebuggerTransformerTest.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java index 4ad93c4b140..7d259b77980 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; @@ -158,10 +159,10 @@ public void testDump() { ArrayList.class, null, getClassFileBytes(ArrayList.class)); - Assertions.assertTrue(instrumentedClassFile.exists()); - Assertions.assertTrue(origClassFile.exists()); - Assertions.assertTrue(instrumentedClassFile.delete()); - Assertions.assertTrue(origClassFile.delete()); + assertTrue(instrumentedClassFile.exists()); + assertTrue(origClassFile.exists()); + assertTrue(instrumentedClassFile.delete()); + assertTrue(origClassFile.delete()); } finally { DebuggerTransformer.DUMP_PATH = initialTmpDir; } @@ -264,7 +265,7 @@ public void testBlockedProbes() { getClassFileBytes(String.class)); assertNull(newClassBuffer); Assertions.assertNotNull(lastResult.get()); - Assertions.assertTrue(lastResult.get().isBlocked()); + assertTrue(lastResult.get().isBlocked()); Assertions.assertFalse(lastResult.get().isInstalled()); assertEquals("java.lang.String", lastResult.get().getTypeName()); } @@ -294,7 +295,7 @@ public void classBeingRedefinedNull() { Assertions.assertNotNull(newClassBuffer); Assertions.assertNotNull(lastResult.get()); Assertions.assertFalse(lastResult.get().isBlocked()); - Assertions.assertTrue(lastResult.get().isInstalled()); + assertTrue(lastResult.get().isInstalled()); assertEquals("java.util.ArrayList", lastResult.get().getTypeName()); } @@ -338,21 +339,15 @@ public void classGenerationFailed() { assertEquals("logprobe1", probeIdCaptor.getAllValues().get(0).getId()); assertEquals("logprobe2", probeIdCaptor.getAllValues().get(1).getId()); assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(2).getId()); - assertEquals( - "Instrumentation failed for " + assertTrue(strCaptor.getAllValues().get(0).startsWith("Instrumentation failed for " + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", - strCaptor.getAllValues().get(0)); - assertEquals( - "Instrumentation failed for " - + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", - strCaptor.getAllValues().get(1)); - assertEquals( - "Instrumentation failed for " - + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 0", - strCaptor.getAllValues().get(2)); + + ": java.lang.ArrayIndexOutOfBoundsException:")); + assertTrue(strCaptor.getAllValues().get(1).startsWith("Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException:")); + assertTrue(strCaptor.getAllValues().get(2).startsWith("Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException:")); } @Test From 81aeac7c97d5abee3c4a47d66cd0e4b565fbb5be Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 5 Feb 2026 11:43:39 +0100 Subject: [PATCH 6/7] spotless --- .../agent/DebuggerTransformerTest.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java index 7d259b77980..3000236300e 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DebuggerTransformerTest.java @@ -339,15 +339,30 @@ public void classGenerationFailed() { assertEquals("logprobe1", probeIdCaptor.getAllValues().get(0).getId()); assertEquals("logprobe2", probeIdCaptor.getAllValues().get(1).getId()); assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(2).getId()); - assertTrue(strCaptor.getAllValues().get(0).startsWith("Instrumentation failed for " - + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException:")); - assertTrue(strCaptor.getAllValues().get(1).startsWith("Instrumentation failed for " - + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException:")); - assertTrue(strCaptor.getAllValues().get(2).startsWith("Instrumentation failed for " - + CLASS_NAME - + ": java.lang.ArrayIndexOutOfBoundsException:")); + assertTrue( + strCaptor + .getAllValues() + .get(0) + .startsWith( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException:")); + assertTrue( + strCaptor + .getAllValues() + .get(1) + .startsWith( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException:")); + assertTrue( + strCaptor + .getAllValues() + .get(2) + .startsWith( + "Instrumentation failed for " + + CLASS_NAME + + ": java.lang.ArrayIndexOutOfBoundsException:")); } @Test From f0f6ad14007d851f92dd02ae9fd154fce2e200e9 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 5 Feb 2026 12:30:37 +0100 Subject: [PATCH 7/7] fix test --- .../java/com/datadog/debugger/agent/CapturedSnapshotTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index fa4bb49ea2a..97d20a83c7f 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -2856,7 +2856,7 @@ public void methodParametersAttributeRecord() throws IOException, URISyntaxExcep verify(probeStatusSink, times(1)).addError(probeIdCaptor.capture(), strCaptor.capture()); assertEquals(PROBE_ID.getId(), probeIdCaptor.getAllValues().get(0).getId()); assertEquals( - "Instrumentation fails for com.datadog.debugger.MyRecord1", + "Instrumentation failed for com.datadog.debugger.MyRecord1: java.lang.RuntimeException: Method Parameters attribute detected, instrumentation not supported", strCaptor.getAllValues().get(0)); } }