Skip to content

Commit 632d3ad

Browse files
mccullsamarzialideejgregordevflow.devflow-routing-intake
authored
🪞 9979 - Record detailed instrumentation errors during tests (#10300)
ExceptionHandlers: Fix code in bytecode comment InstrumentationContext: be more explicit about the reasons for failure Add instrumentation error details on test failures This puts the details front and center in the test summary and is much easier than hunting back through log messages. Note: In InstrumentationErrors, errors are only recorded when enableRecordingAndReset has been called. This is only done from the InstrumentationSpecification test specification, so we don't need to worry about the ArrayList growing without bounds in production. I considered adding a limit to the ArrayList, but opted not to for simplicity. I also wanted to avoid silently discarding some errors. Check for blocked instrumentation in tests If there is a blocked instrumentation, the test will fail and the failure message will include the same information that is logged from MuzzleCheck. Fail fast on muzzle or instrumentation errors revert asserting muzzle failures in instrumentation tests Avoid allocating a stack argument if errors are not recorded Merge branch 'master' into community-pr-9979 Merge branch 'master' into community-pr-9979 Merge branch 'master' into community-pr-9979 Rework recording of detailed instrumentation errors * Can be toggled on/off with feature-flag (on for InstrumentationSpecification) * Zero-impact when the detailed instrumentation error feature is off * Allow recording of errors indirectly via InstrumentationSpecification * Reverted change to trace asserts (we shouldn't mix concerns in asserts) Merge remote-tracking branch 'origin/master' into community-pr-9979 Update comment Update ClassFileTransformerListener to use InstrumentationErrors Clean-up reporting of instrumentation errors (installation vs per-test) Enable detailed instrumentation errors for AbstractInstrumentationTest Fix metadata/supported-configurations.json Reset InstrumentationErrors before setupSpec Co-authored-by: amarziali <andrea.marziali@datadoghq.com> Co-authored-by: deejgregor <dj@gregor.com> Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent 7a3ccbf commit 632d3ad

9 files changed

Lines changed: 119 additions & 33 deletions

File tree

‎dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/InstrumentationContext.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ private InstrumentationContext() {}
2424
public static <K, C> ContextStore<K, C> get(
2525
final Class<K> keyClass, final Class<C> contextClass) {
2626
throw new RuntimeException(
27-
"Calls to this method will be rewritten by Instrumentation Context Provider (e.g. FieldBackedProvider)");
27+
"Calls to this method will be rewritten by Instrumentation Context Provider (e.g. FieldBackedProvider)."
28+
+ " If you get this exception, this method has not been rewritten."
29+
+ " Ensure instrumentation class has a contextStore method and the call to InstrumentationContext.get happens directly in an instrumentation Advice class."
30+
+ " See how_instrumentations_work.md for details.");
2831
}
2932

3033
/**
@@ -35,6 +38,9 @@ public static <K, C> ContextStore<K, C> get(
3538
*/
3639
public static <K, C> ContextStore<K, C> get(final String keyClass, final String contextClass) {
3740
throw new RuntimeException(
38-
"Calls to this method will be rewritten by Instrumentation Context Provider (e.g. FieldBackedProvider)");
41+
"Calls to this method will be rewritten by Instrumentation Context Provider (e.g. FieldBackedProvider)."
42+
+ " If you get this exception, this method has not been rewritten."
43+
+ " Ensure instrumentation class has a contextStore method and the call to InstrumentationContext.get happens directly in an instrumentation Advice class."
44+
+ " See how_instrumentations_work.md for details.");
3945
}
4046
}
Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,61 @@
11
package datadog.trace.bootstrap;
22

3+
import java.io.PrintWriter;
4+
import java.io.StringWriter;
5+
import java.util.List;
6+
import java.util.concurrent.CopyOnWriteArrayList;
37
import java.util.concurrent.atomic.AtomicLong;
48

59
public class InstrumentationErrors {
610
private static final AtomicLong COUNTER = new AtomicLong();
711

8-
public static long getErrorCount() {
9-
return COUNTER.get();
12+
private static final class Detailed {
13+
static final List<String> ERRORS = new CopyOnWriteArrayList<>();
1014
}
1115

12-
@SuppressWarnings("unused")
13-
public static void incrementErrorCount() {
16+
private static volatile boolean detailed;
17+
18+
/** Record an error occurred without any detail about it. */
19+
public static void recordError() {
20+
COUNTER.incrementAndGet();
21+
}
22+
23+
/** Record an error occurred, including its stack trace. */
24+
public static Throwable recordError(Throwable error) {
1425
COUNTER.incrementAndGet();
26+
StringWriter detail = new StringWriter();
27+
error.printStackTrace(new PrintWriter(detail));
28+
Detailed.ERRORS.add(detail.toString());
29+
detailed = true;
30+
return error; // keep throwable at top of the stack
1531
}
1632

1733
// Visible for testing
18-
public static void resetErrorCount() {
34+
public static void resetErrors() {
1935
COUNTER.set(0);
36+
if (detailed) {
37+
Detailed.ERRORS.clear();
38+
detailed = false;
39+
}
40+
}
41+
42+
/**
43+
* @return {@code true} if no errors were recorded; otherwise {@code false}
44+
*/
45+
public static boolean noErrors() {
46+
return COUNTER.get() == 0;
47+
}
48+
49+
/**
50+
* @return a human-readable description of the errors recorded so far
51+
*/
52+
public static String describeErrors() {
53+
StringBuilder buf = new StringBuilder().append(COUNTER.get()).append(" instrumentation errors");
54+
if (detailed) {
55+
for (String error : Detailed.ERRORS) {
56+
buf.append("\n---\n").append(error);
57+
}
58+
}
59+
return buf.toString();
2060
}
2161
}

‎dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/ExceptionHandlers.java‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class ExceptionHandlers {
2727
private final Size size = new StackManipulation.Size(-1, 3);
2828
private final boolean appSecEnabled =
2929
InstrumenterConfig.get().getAppSecActivation() != ProductActivation.FULLY_DISABLED;
30+
private final boolean detailedErrors =
31+
InstrumenterConfig.get().isDetailedInstrumentationErrors();
3032

3133
@Override
3234
public boolean isValid() {
@@ -43,7 +45,7 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context)
4345
//
4446
// BlockingExceptionHandler.rethrowIfBlockingException(t);
4547
// try {
46-
// InstrumentationErrors.incrementErrorCount();
48+
// InstrumentationErrors.recordError();
4749
// org.slf4j.LoggerFactory.getLogger((Class)ExceptionLogger.class)
4850
// .debug("Failed to handle exception in instrumentation for ...", t);
4951
// } catch (Throwable t2) {
@@ -53,7 +55,7 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context)
5355
//
5456
// BlockingExceptionHandler.rethrowIfBlockingException(t);
5557
// try {
56-
// InstrumentationErrors.incrementErrorCount();
58+
// InstrumentationErrors.recordError();
5759
// org.slf4j.LoggerFactory.getLogger((Class)ExceptionLogger.class)
5860
// .error("Failed to handle exception in instrumentation for ...", t);
5961
// System.exit(1);
@@ -76,17 +78,28 @@ public Size apply(final MethodVisitor mv, final Implementation.Context context)
7678
Opcodes.INVOKESTATIC,
7779
"datadog/trace/bootstrap/blocking/BlockingExceptionHandler",
7880
"rethrowIfBlockingException",
79-
"(Ljava/lang/Throwable;)Ljava/lang/Throwable;");
81+
"(Ljava/lang/Throwable;)Ljava/lang/Throwable;",
82+
false);
8083
}
8184

8285
mv.visitTryCatchBlock(logStart, logEnd, eatException, "java/lang/Throwable");
8386
mv.visitLabel(logStart);
84-
// invoke incrementAndGet on our exception counter
85-
mv.visitMethodInsn(
86-
Opcodes.INVOKESTATIC,
87-
"datadog/trace/bootstrap/InstrumentationErrors",
88-
"incrementErrorCount",
89-
"()V");
87+
// record instrumentation error
88+
if (detailedErrors) {
89+
mv.visitMethodInsn(
90+
Opcodes.INVOKESTATIC,
91+
"datadog/trace/bootstrap/InstrumentationErrors",
92+
"recordError",
93+
"(Ljava/lang/Throwable;)Ljava/lang/Throwable;",
94+
false);
95+
} else {
96+
mv.visitMethodInsn(
97+
Opcodes.INVOKESTATIC,
98+
"datadog/trace/bootstrap/InstrumentationErrors",
99+
"recordError",
100+
"()V",
101+
false);
102+
}
90103
// stack: (top) throwable
91104
mv.visitLdcInsn(Type.getType("L" + HANDLER_NAME + ";"));
92105
mv.visitMethodInsn(

‎dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/InstrumentationSpecification.groovy‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import datadog.trace.api.Pair
4747
import datadog.trace.api.ProcessTags
4848
import datadog.trace.api.TraceConfig
4949
import datadog.trace.api.config.GeneralConfig
50+
import datadog.trace.api.config.TraceInstrumentationConfig
5051
import datadog.trace.api.config.TracerConfig
5152
import datadog.trace.api.datastreams.AgentDataStreamsMonitoring
5253
import datadog.trace.api.datastreams.DataStreamsTransactionExtractor
@@ -79,7 +80,6 @@ import java.nio.ByteBuffer
7980
import java.util.concurrent.ConcurrentHashMap
8081
import java.util.concurrent.TimeUnit
8182
import java.util.concurrent.TimeoutException
82-
import java.util.concurrent.atomic.AtomicInteger
8383
import net.bytebuddy.agent.ByteBuddyAgent
8484
import net.bytebuddy.agent.builder.AgentBuilder
8585
import net.bytebuddy.description.type.TypeDescription
@@ -169,10 +169,6 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
169169
@Shared
170170
Set<TypeDescription> TRANSFORMED_CLASSES_TYPES = Sets.newConcurrentHashSet()
171171

172-
@SuppressWarnings('PropertyName')
173-
@Shared
174-
AtomicInteger INSTRUMENTATION_ERROR_COUNT = new AtomicInteger(0)
175-
176172
// don't use mocks because it will break too many exhaustive interaction-verifying tests
177173
@SuppressWarnings('PropertyName')
178174
@Shared
@@ -353,6 +349,8 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
353349

354350
@SuppressForbidden
355351
void setupSpec() {
352+
InstrumentationErrors.resetErrors()
353+
356354
AgentMeter.registerIfAbsent(
357355
STATS_D_CLIENT,
358356
new MonitoringImpl(STATS_D_CLIENT, 10, TimeUnit.SECONDS),
@@ -423,6 +421,9 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
423421
.hasNext(): "No instrumentation found"
424422
activeTransformer = AgentInstaller.installBytebuddyAgent(
425423
INSTRUMENTATION, true, AgentInstaller.getEnabledSystems(), this)
424+
425+
// check for instrumentation issues during installation
426+
assert InstrumentationErrors.noErrors(): InstrumentationErrors.describeErrors()
426427
}
427428

428429
protected String idGenerationStrategyName() {
@@ -431,12 +432,15 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
431432

432433
/** Override to set config before the agent is installed */
433434
protected void configurePreAgent() {
435+
injectSysConfig(TraceInstrumentationConfig.DETAILED_INSTRUMENTATION_ERRORS, "true")
434436
injectSysConfig(TracerConfig.SCOPE_ITERATION_KEEP_ALIVE, "1") // don't let iteration spans linger
435437
injectSysConfig(GeneralConfig.DATA_STREAMS_ENABLED, String.valueOf(isDataStreamsEnabled()))
436438
injectSysConfig(GeneralConfig.DATA_JOBS_ENABLED, String.valueOf(isDataJobsEnabled()))
437439
}
438440

439441
void setup() {
442+
InstrumentationErrors.resetErrors() // reset for each test
443+
440444
configureLoggingLevels()
441445

442446
assertThreadsEachCleanup = false
@@ -472,7 +476,6 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
472476
if (forceAppSecActive) {
473477
ActiveSubsystems.APPSEC_ACTIVE = true
474478
}
475-
InstrumentationErrors.resetErrorCount()
476479
ProcessTags.reset()
477480
}
478481

@@ -514,7 +517,9 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
514517
spanFinishLocations.clear()
515518
originalToTrackingSpan.clear()
516519
}
517-
assert InstrumentationErrors.errorCount == 0
520+
521+
// check for instrumentation issues while running each test
522+
assert InstrumentationErrors.noErrors(): InstrumentationErrors.describeErrors()
518523
}
519524

520525
private void doCheckRepeatedFinish() {
@@ -556,8 +561,6 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
556561
cleanupAfterAgent()
557562

558563
// All cleanup should happen before these assertion. If not, a failing assertion may prevent cleanup
559-
assert INSTRUMENTATION_ERROR_COUNT.get() == 0: INSTRUMENTATION_ERROR_COUNT.get() + " Instrumentation errors during test"
560-
561564
assert TRANSFORMED_CLASSES_TYPES.findAll {
562565
GlobalIgnores.isAdditionallyIgnored(it.getActualName())
563566
}.isEmpty(): "Transformed classes match global libraries ignore matcher"
@@ -658,9 +661,9 @@ abstract class InstrumentationSpecification extends DDSpecification implements A
658661
return
659662
}
660663

664+
InstrumentationErrors.recordError(throwable)
661665
println "Unexpected instrumentation error when instrumenting ${typeName} on ${classLoader}"
662666
throwable.printStackTrace()
663-
INSTRUMENTATION_ERROR_COUNT.incrementAndGet()
664667
}
665668

666669
@Override

‎dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
import datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers;
1414
import datadog.trace.api.Config;
1515
import datadog.trace.api.IdGenerationStrategy;
16+
import datadog.trace.bootstrap.InstrumentationErrors;
1617
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1718
import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI;
1819
import datadog.trace.common.writer.ListWriter;
1920
import datadog.trace.core.CoreTracer;
2021
import datadog.trace.core.DDSpan;
2122
import datadog.trace.core.PendingTrace;
2223
import datadog.trace.core.TraceCollector;
24+
import datadog.trace.junit.utils.config.WithConfig;
2325
import datadog.trace.junit.utils.context.AllowContextTestingExtension;
2426
import java.lang.instrument.ClassFileTransformer;
2527
import java.lang.instrument.Instrumentation;
@@ -51,6 +53,7 @@
5153
* <li>{@code @AfterAll}: Closes the tracer and removes the agent transformer
5254
* </ul>
5355
*/
56+
@WithConfig(key = "detailed.instrumentation.errors", value = "true")
5457
@ExtendWith({TestClassShadowingExtension.class, AllowContextTestingExtension.class})
5558
public abstract class AbstractInstrumentationTest {
5659
static final Instrumentation INSTRUMENTATION = ByteBuddyAgent.getInstrumentation();
@@ -66,6 +69,8 @@ public abstract class AbstractInstrumentationTest {
6669

6770
@BeforeAll
6871
static void initAll() {
72+
InstrumentationErrors.resetErrors();
73+
6974
// If this fails, it's likely the result of another test loading Config before it can be
7075
// injected into the bootstrap classpath.
7176
assertNull(Config.class.getClassLoader(), "Config must load on the bootstrap classpath.");
@@ -97,17 +102,24 @@ static void initAll() {
97102
activeTransformer =
98103
AgentInstaller.installBytebuddyAgent(
99104
INSTRUMENTATION, true, AgentInstaller.getEnabledSystems(), transformerListener);
105+
106+
// check for instrumentation issues during installation
107+
assertTrue(InstrumentationErrors.noErrors(), InstrumentationErrors::describeErrors);
100108
}
101109

102110
@BeforeEach
103111
public void init() {
112+
InstrumentationErrors.resetErrors(); // reset for each test
104113
tracer.flush();
105114
writer.start();
106115
}
107116

108117
@AfterEach
109118
public void tearDown() {
110119
tracer.flush();
120+
121+
// check for instrumentation issues while running each test
122+
assertTrue(InstrumentationErrors.noErrors(), InstrumentationErrors::describeErrors);
111123
}
112124

113125
@AfterAll

‎dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/ClassFileTransformerListener.java‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package datadog.trace.agent.test;
22

3-
import static org.junit.jupiter.api.Assertions.assertEquals;
43
import static org.junit.jupiter.api.Assertions.assertTrue;
54

65
import com.google.common.collect.Sets;
76
import datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnores;
7+
import datadog.trace.bootstrap.InstrumentationErrors;
88
import de.thetaphi.forbiddenapis.SuppressForbidden;
99
import java.util.Set;
10-
import java.util.concurrent.atomic.AtomicInteger;
1110
import net.bytebuddy.agent.builder.AgentBuilder;
1211
import net.bytebuddy.description.type.TypeDescription;
1312
import net.bytebuddy.dynamic.DynamicType;
@@ -18,7 +17,6 @@ public class ClassFileTransformerListener implements AgentBuilder.Listener {
1817

1918
final Set<String> transformedClassesNames = Sets.newConcurrentHashSet();
2019
final Set<TypeDescription> transformedClassesTypes = Sets.newConcurrentHashSet();
21-
final AtomicInteger instrumentationErrorCount = new AtomicInteger(0);
2220

2321
@Override
2422
public void onTransformation(
@@ -45,10 +43,10 @@ public void onError(
4543
return;
4644
}
4745

46+
InstrumentationErrors.recordError(throwable);
4847
System.out.println(
4948
"Unexpected instrumentation error when instrumenting " + typeName + " on " + classLoader);
5049
throwable.printStackTrace();
51-
instrumentationErrorCount.incrementAndGet();
5250
}
5351

5452
@Override
@@ -70,9 +68,6 @@ public void onComplete(
7068
}
7169

7270
public void verify() {
73-
// Check instrumentation errors
74-
int errorCount = this.instrumentationErrorCount.get();
75-
assertEquals(0, errorCount, errorCount + " instrumentation errors during test");
7671
// Check effectively transformed classes that should have been ignored
7772
assertTrue(
7873
this.transformedClassesTypes.stream()

‎dd-trace-api/src/main/java/datadog/trace/api/config/TraceInstrumentationConfig.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public final class TraceInstrumentationConfig {
1515
public static final String CODE_ORIGIN_MAX_USER_FRAMES = "code.origin.max.user.frames";
1616
public static final String TRACE_ENABLED = "trace.enabled";
1717
public static final String INTEGRATIONS_ENABLED = "integrations.enabled";
18+
public static final String DETAILED_INSTRUMENTATION_ERRORS = "detailed.instrumentation.errors";
1819

1920
public static final String TRACE_EXTENSIONS_PATH = "trace.extensions.path";
2021

0 commit comments

Comments
 (0)