From fd66292c85de9126012147e7b3a1215b3700ea68 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Mon, 24 Feb 2025 14:54:29 +0100 Subject: [PATCH 1/6] Add In-Product Enablement Introduce the ability to start and stop Debugger features: - Dynamic Instrumentation - Exception Replay - Code Origin - Distributed Debugger dynamically based on RemoteConfig record: APM_TRACING DebuggerAgent is now run every time at startup to have the base of some feature ready and be able to start the minimum required foe each feature. Ability to stop also the feature at any time to uninstall probes. Add smoke tests Refactor common initialization to be done at least once either by dynamic Instrumentation, Exception Replay or Code Origin --- .../java/datadog/trace/bootstrap/Agent.java | 29 +- .../bootstrap/debugger/DebuggerContext.java | 74 +++++ .../datadog/debugger/agent/DebuggerAgent.java | 305 ++++++++++++------ .../agent/DefaultProductConfigUpdater.java | 79 +++++ .../debugger/symbol/SymDBEnablement.java | 5 +- .../debugger/agent/CapturedSnapshotTest.java | 3 +- .../DefaultProductConfigUpdaterTest.java | 24 ++ .../ExceptionProbeInstrumentationTest.java | 8 + .../ServerDebuggerTestApplication.java | 17 + .../debugger/TestApplicationHelper.java | 11 + .../smoketest/BaseIntegrationTest.java | 63 +++- .../smoketest/CodeOriginIntegrationTest.java | 4 +- .../InProductEnablementIntegrationTest.java | 95 ++++++ .../datadog/smoketest/RemoteConfigHelper.java | 125 +++++-- .../main/java/datadog/trace/core/DDSpan.java | 6 +- .../trace/core/TracingConfigPoller.java | 29 +- .../datadog/remoteconfig/Capabilities.java | 4 + 17 files changed, 718 insertions(+), 163 deletions(-) create mode 100644 dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DefaultProductConfigUpdater.java create mode 100644 dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java create mode 100644 dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/InProductEnablementIntegrationTest.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 78a069f8c33..e55d6e99ef9 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -101,9 +101,9 @@ private enum AgentFeature { CIVISIBILITY_AGENTLESS(CiVisibilityConfig.CIVISIBILITY_AGENTLESS_ENABLED, false), USM(UsmConfig.USM_ENABLED, false), TELEMETRY(GeneralConfig.TELEMETRY_ENABLED, true), - DEBUGGER(DebuggerConfig.DYNAMIC_INSTRUMENTATION_ENABLED, false), - EXCEPTION_DEBUGGING(DebuggerConfig.EXCEPTION_REPLAY_ENABLED, false), - SPAN_ORIGIN(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, false), + DYNAMIC_INSTRUMENTATION(DebuggerConfig.DYNAMIC_INSTRUMENTATION_ENABLED, false), + EXCEPTION_REPLAY(DebuggerConfig.EXCEPTION_REPLAY_ENABLED, false), + CODE_ORIGIN(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, false), DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false), AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false); @@ -154,9 +154,10 @@ public boolean isEnabledByDefault() { private static boolean ciVisibilityEnabled = false; private static boolean usmEnabled = false; private static boolean telemetryEnabled = true; - private static boolean debuggerEnabled = false; - private static boolean exceptionDebuggingEnabled = false; - private static boolean spanOriginEnabled = false; + private static boolean dynamicInstrumentationEnabled = false; + private static boolean exceptionReplayEnabled = false; + private static boolean codeOriginEnabled = false; + private static boolean distributedDebuggerEnabled = false; private static boolean agentlessLogSubmissionEnabled = false; /** @@ -272,9 +273,9 @@ public static void start( || isFeatureEnabled(AgentFeature.DEPRECATED_REMOTE_CONFIG); cwsEnabled = isFeatureEnabled(AgentFeature.CWS); telemetryEnabled = isFeatureEnabled(AgentFeature.TELEMETRY); - debuggerEnabled = isFeatureEnabled(AgentFeature.DEBUGGER); - exceptionDebuggingEnabled = isFeatureEnabled(AgentFeature.EXCEPTION_DEBUGGING); - spanOriginEnabled = isFeatureEnabled(AgentFeature.SPAN_ORIGIN); + dynamicInstrumentationEnabled = isFeatureEnabled(AgentFeature.DYNAMIC_INSTRUMENTATION); + exceptionReplayEnabled = isFeatureEnabled(AgentFeature.EXCEPTION_REPLAY); + codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN); agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); if (profilingEnabled) { @@ -1133,7 +1134,10 @@ private static void shutdownProfilingAgent(final boolean sync) { } private static void maybeStartDebugger(Instrumentation inst, Class scoClass, Object sco) { - if (!debuggerEnabled && !exceptionDebuggingEnabled && !spanOriginEnabled) { + if (isExplicitlyDisabled(DebuggerConfig.DYNAMIC_INSTRUMENTATION_ENABLED) + && isExplicitlyDisabled(DebuggerConfig.EXCEPTION_REPLAY_ENABLED) + && isExplicitlyDisabled(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED) + && isExplicitlyDisabled(DebuggerConfig.DISTRIBUTED_DEBUGGER_ENABLED)) { return; } if (!remoteConfigEnabled) { @@ -1143,6 +1147,11 @@ private static void maybeStartDebugger(Instrumentation inst, Class scoClass, startDebuggerAgent(inst, scoClass, sco); } + private static boolean isExplicitlyDisabled(String booleanKey) { + return Config.get().configProvider().isSet(booleanKey) + && !Config.get().configProvider().getBoolean(booleanKey); + } + private static synchronized void startDebuggerAgent( Instrumentation inst, Class scoClass, Object sco) { StaticEventLogger.begin("Debugger"); diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java index 6840146f969..c524f065557 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java @@ -50,6 +50,22 @@ public String tag() { public abstract String tag(); } + public interface ProductConfigUpdater { + void updateConfig( + Boolean dynamicInstrumentationEnabled, + Boolean exceptionReplayEnabled, + Boolean codeOriginEnabled, + Boolean liveDebuggingEnabled); + + boolean isDynamicInstrumentationEnabled(); + + boolean isExceptionReplayEnabled(); + + boolean isCodeOriginEnabled(); + + boolean isDistributedDebuggerEnabled(); + } + public interface ProbeResolver { ProbeImplementation resolve(String encodedProbeId); } @@ -103,6 +119,7 @@ public interface CodeOriginRecorder { String captureCodeOrigin(Method method, boolean entry, boolean instrument); } + private static volatile ProductConfigUpdater productConfigUpdater; private static volatile ProbeResolver probeResolver; private static volatile ClassFilter classFilter; private static volatile ClassNameFilter classNameFilter; @@ -112,6 +129,10 @@ public interface CodeOriginRecorder { private static volatile ExceptionDebugger exceptionDebugger; private static volatile CodeOriginRecorder codeOriginRecorder; + public static void initProductConfigUpdater(ProductConfigUpdater productConfigUpdater) { + DebuggerContext.productConfigUpdater = productConfigUpdater; + } + public static void initProbeResolver(ProbeResolver probeResolver) { DebuggerContext.probeResolver = probeResolver; } @@ -144,6 +165,59 @@ public static void initCodeOrigin(CodeOriginRecorder codeOriginRecorder) { DebuggerContext.codeOriginRecorder = codeOriginRecorder; } + public static void updateConfig( + Boolean dynamicInstrumentationEnabled, + Boolean exceptionReplayEnabled, + Boolean codeOriginEnabled, + Boolean liveDebuggingEnabled) { + LOGGER.debug( + "Updating config: dynamicInstrumentationEnabled: {}, exceptionReplayEnabled: {}, codeOriginEnabled: {}, liveDebuggingEnabled: {}", + dynamicInstrumentationEnabled, + exceptionReplayEnabled, + codeOriginEnabled, + liveDebuggingEnabled); + ProductConfigUpdater updater = productConfigUpdater; + if (updater != null) { + updater.updateConfig( + dynamicInstrumentationEnabled, + exceptionReplayEnabled, + codeOriginEnabled, + liveDebuggingEnabled); + } + } + + public static boolean isDynamicInstrumentationEnabled() { + ProductConfigUpdater updater = productConfigUpdater; + if (updater != null) { + return updater.isDynamicInstrumentationEnabled(); + } + return Config.get().isDynamicInstrumentationEnabled(); + } + + public static boolean isExceptionReplayEnabled() { + ProductConfigUpdater updater = productConfigUpdater; + if (updater != null) { + return updater.isExceptionReplayEnabled(); + } + return Config.get().isDebuggerExceptionEnabled(); + } + + public static boolean isCodeOriginEnabled() { + ProductConfigUpdater updater = productConfigUpdater; + if (updater != null) { + return updater.isCodeOriginEnabled(); + } + return Config.get().isDebuggerCodeOriginEnabled(); + } + + public static boolean isDistributedDebuggerEnabled() { + ProductConfigUpdater updater = productConfigUpdater; + if (updater != null) { + return updater.isDistributedDebuggerEnabled(); + } + return Config.get().isDistributedDebuggerEnabled(); + } + /** * Returns the probe details based on the probe id provided. If no probe is found, try to * re-transform the class using the callingClass parameter No-op if no implementation available diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java index 4b8cef476ca..2d85f464fa6 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java @@ -1,11 +1,12 @@ package com.datadog.debugger.agent; +import static com.datadog.debugger.agent.ConfigurationAcceptor.Source.CODE_ORIGIN; +import static com.datadog.debugger.agent.ConfigurationAcceptor.Source.EXCEPTION; import static com.datadog.debugger.agent.ConfigurationAcceptor.Source.REMOTE_CONFIG; import static datadog.trace.util.AgentThreadFactory.AGENT_THREAD_GROUP; import com.datadog.debugger.codeorigin.DefaultCodeOriginRecorder; import com.datadog.debugger.exception.DefaultExceptionDebugger; -import com.datadog.debugger.exception.ExceptionProbeManager; import com.datadog.debugger.sink.DebuggerSink; import com.datadog.debugger.sink.ProbeStatusSink; import com.datadog.debugger.sink.SnapshotSink; @@ -40,6 +41,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.zip.ZipOutputStream; import org.slf4j.Logger; @@ -48,20 +51,63 @@ /** Debugger agent implementation */ public class DebuggerAgent { private static final Logger LOGGER = LoggerFactory.getLogger(DebuggerAgent.class); + private static Instrumentation instrumentation; + private static SharedCommunicationObjects sharedCommunicationObjects; private static ConfigurationPoller configurationPoller; private static DebuggerSink sink; private static String agentVersion; private static JsonSnapshotSerializer snapshotSerializer; - private static SymDBEnablement symDBEnablement; + private static volatile ClassNameFilter classNameFilter; + private static volatile SymDBEnablement symDBEnablement; + private static volatile ConfigurationUpdater configurationUpdater; + private static volatile DefaultExceptionDebugger exceptionDebugger; + private static final AtomicBoolean commonInitDone = new AtomicBoolean(); + static final AtomicBoolean dynamicInstrumentationEnabled = new AtomicBoolean(); + static final AtomicBoolean exceptionReplayEnabled = new AtomicBoolean(); + static final AtomicBoolean codeOriginEnabled = new AtomicBoolean(); + static final AtomicBoolean distributedDebuggerEnabled = new AtomicBoolean(); + private static ClassesToRetransformFinder classesToRetransformFinder; - public static synchronized void run( - Instrumentation instrumentation, SharedCommunicationObjects sco) { + public static synchronized void run(Instrumentation inst, SharedCommunicationObjects sco) { + instrumentation = inst; + sharedCommunicationObjects = sco; Config config = Config.get(); - ClassesToRetransformFinder classesToRetransformFinder = new ClassesToRetransformFinder(); + DebuggerContext.initProductConfigUpdater(new DefaultProductConfigUpdater()); + classesToRetransformFinder = new ClassesToRetransformFinder(); setupSourceFileTracking(instrumentation, classesToRetransformFinder); + if (config.isDebuggerCodeOriginEnabled()) { + startCodeOriginForSpans(); + } + if (config.isDebuggerExceptionEnabled()) { + startExceptionReplay(); + } + if (config.isDynamicInstrumentationEnabled()) { + startDynamicInstrumentation(); + if (config.isDynamicInstrumentationInstrumentTheWorld()) { + setupInstrumentTheWorldTransformer(config, instrumentation, sink); + } + } + try { + /* + Note: shutdown hooks are tricky because JVM holds reference for them forever preventing + GC for anything that is reachable from it. + */ + Runtime.getRuntime().addShutdownHook(new ShutdownHook(configurationPoller, sink)); + } catch (final IllegalStateException ex) { + // The JVM is already shutting down. + } + TracerFlare.addReporter(DebuggerAgent::addReportToFlare); + } + + private static void commonInit(Config config) { + if (!commonInitDone.compareAndSet(false, true)) { + return; + } + configurationPoller = sharedCommunicationObjects.configurationPoller(config); Redaction.addUserDefinedKeywords(config); Redaction.addUserDefinedTypes(config); - DDAgentFeaturesDiscovery ddAgentFeaturesDiscovery = sco.featuresDiscovery(config); + DDAgentFeaturesDiscovery ddAgentFeaturesDiscovery = + sharedCommunicationObjects.featuresDiscovery(config); ddAgentFeaturesDiscovery.discoverIfOutdated(); agentVersion = ddAgentFeaturesDiscovery.getVersion(); String diagnosticEndpoint = getDiagnosticEndpoint(config, ddAgentFeaturesDiscovery); @@ -70,8 +116,7 @@ public static synchronized void run( config, diagnosticEndpoint, ddAgentFeaturesDiscovery.supportsDebuggerDiagnostics()); DebuggerSink debuggerSink = createDebuggerSink(config, probeStatusSink); debuggerSink.start(); - ClassNameFilter classNameFilter = new ClassNameFiltering(config); - ConfigurationUpdater configurationUpdater = + configurationUpdater = new ConfigurationUpdater( instrumentation, DebuggerAgent::createTransformer, @@ -79,64 +124,27 @@ public static synchronized void run( debuggerSink, classesToRetransformFinder); sink = debuggerSink; - StatsdMetricForwarder statsdMetricForwarder = - new StatsdMetricForwarder(config, probeStatusSink); DebuggerContext.initProbeResolver(configurationUpdater); - DebuggerContext.initMetricForwarder(statsdMetricForwarder); + DebuggerContext.initMetricForwarder(new StatsdMetricForwarder(config, probeStatusSink)); DebuggerContext.initClassFilter(new DenyListHelper(null)); // default hard coded deny list snapshotSerializer = new JsonSnapshotSerializer(); DebuggerContext.initValueSerializer(snapshotSerializer); DebuggerContext.initTracer(new DebuggerTracer(debuggerSink.getProbeStatusSink())); - DebuggerContext.initClassNameFilter(classNameFilter); - DefaultExceptionDebugger defaultExceptionDebugger = null; - if (config.isDebuggerExceptionEnabled()) { - LOGGER.info("Starting Exception Replay"); - defaultExceptionDebugger = - new DefaultExceptionDebugger( - configurationUpdater, - classNameFilter, - Duration.ofSeconds(config.getDebuggerExceptionCaptureInterval()), - config.getDebuggerMaxExceptionPerSecond()); - DebuggerContext.initExceptionDebugger(defaultExceptionDebugger); - } - if (config.isDebuggerCodeOriginEnabled()) { - LOGGER.info("Starting Code Origin for spans"); - DebuggerContext.initCodeOrigin(new DefaultCodeOriginRecorder(config, configurationUpdater)); - } - if (config.isDynamicInstrumentationInstrumentTheWorld()) { - setupInstrumentTheWorldTransformer( - config, instrumentation, debuggerSink, statsdMetricForwarder); - } - // Dynamic Instrumentation - if (config.isDynamicInstrumentationEnabled()) { - startDynamicInstrumentation( - instrumentation, sco, config, configurationUpdater, debuggerSink, classNameFilter); - } - try { - /* - Note: shutdown hooks are tricky because JVM holds reference for them forever preventing - GC for anything that is reachable from it. - */ - Runtime.getRuntime().addShutdownHook(new ShutdownHook(configurationPoller, debuggerSink)); - } catch (final IllegalStateException ex) { - // The JVM is already shutting down. + } + + private static void initClassNameFilter() { + if (classNameFilter == null) { + classNameFilter = new ClassNameFiltering(Config.get()); } - ExceptionProbeManager exceptionProbeManager = - defaultExceptionDebugger != null - ? defaultExceptionDebugger.getExceptionProbeManager() - : null; - TracerFlare.addReporter( - new DebuggerReporter(configurationUpdater, sink, exceptionProbeManager)); } - private static void startDynamicInstrumentation( - Instrumentation instrumentation, - SharedCommunicationObjects sco, - Config config, - ConfigurationUpdater configurationUpdater, - DebuggerSink debuggerSink, - ClassNameFilter classNameFilter) { + public static void startDynamicInstrumentation() { + if (!dynamicInstrumentationEnabled.compareAndSet(false, true)) { + return; + } LOGGER.info("Starting Dynamic Instrumentation"); + Config config = Config.get(); + commonInit(config); String probeFileLocation = config.getDynamicInstrumentationProbeFile(); if (probeFileLocation != null) { Path probeFilePath = Paths.get(probeFileLocation); @@ -144,14 +152,12 @@ private static void startDynamicInstrumentation( probeFilePath, configurationUpdater, config.getDynamicInstrumentationMaxPayloadSize()); return; } - configurationPoller = sco.configurationPoller(config); if (configurationPoller != null) { if (config.isSymbolDatabaseEnabled()) { + initClassNameFilter(); SymbolAggregator symbolAggregator = new SymbolAggregator( - classNameFilter, - debuggerSink.getSymbolSink(), - config.getSymbolDatabaseFlushThreshold()); + classNameFilter, sink.getSymbolSink(), config.getSymbolDatabaseFlushThreshold()); symbolAggregator.start(); symDBEnablement = new SymDBEnablement(instrumentation, config, symbolAggregator, classNameFilter); @@ -165,6 +171,90 @@ private static void startDynamicInstrumentation( } } + public static void stopDynamicInstrumentation() { + if (!dynamicInstrumentationEnabled.compareAndSet(true, false)) { + return; + } + LOGGER.info("Stopping Dynamic Instrumentation"); + unsubscribeConfigurationPoller(); + if (configurationUpdater != null) { + // uninstall all probes by providing empty configuration + configurationUpdater.accept(REMOTE_CONFIG, Collections.emptyList()); + } + if (symDBEnablement != null) { + symDBEnablement.stopSymbolExtraction(); + symDBEnablement = null; + } + } + + public static void startExceptionReplay() { + if (!exceptionReplayEnabled.compareAndSet(false, true)) { + return; + } + LOGGER.info("Starting Exception Replay"); + Config config = Config.get(); + commonInit(config); + initClassNameFilter(); + exceptionDebugger = + new DefaultExceptionDebugger( + configurationUpdater, + classNameFilter, + Duration.ofSeconds(config.getDebuggerExceptionCaptureInterval()), + config.getDebuggerMaxExceptionPerSecond()); + DebuggerContext.initExceptionDebugger(exceptionDebugger); + } + + public static void stopExceptionReplay() { + if (!exceptionReplayEnabled.compareAndSet(true, false)) { + return; + } + LOGGER.info("Stopping Exception Replay"); + if (configurationUpdater != null) { + // uninstall all exception probes by providing empty configuration + configurationUpdater.accept(EXCEPTION, Collections.emptyList()); + } + exceptionDebugger = null; + DebuggerContext.initExceptionDebugger(null); + } + + public static void startCodeOriginForSpans() { + if (!codeOriginEnabled.compareAndSet(false, true)) { + return; + } + LOGGER.info("Starting Code Origin for spans"); + Config config = Config.get(); + commonInit(config); + initClassNameFilter(); + DebuggerContext.initClassNameFilter(classNameFilter); + DebuggerContext.initCodeOrigin(new DefaultCodeOriginRecorder(config, configurationUpdater)); + } + + public static void stopCodeOriginForSpans() { + if (!codeOriginEnabled.compareAndSet(true, false)) { + return; + } + LOGGER.info("Stopping Code Origin for spans"); + if (configurationUpdater != null) { + // uninstall all code origin probes by providing empty configuration + configurationUpdater.accept(CODE_ORIGIN, Collections.emptyList()); + } + DebuggerContext.initCodeOrigin(null); + } + + public static void startDistributedDebugger() { + if (!distributedDebuggerEnabled.compareAndSet(false, true)) { + return; + } + LOGGER.info("Starting Distributed Debugger"); + } + + public static void stopDistributedDebugger() { + if (!distributedDebuggerEnabled.compareAndSet(true, false)) { + return; + } + LOGGER.info("Sopping Distributed Debugger"); + } + private static DebuggerSink createDebuggerSink(Config config, ProbeStatusSink probeStatusSink) { String tags = getDefaultTagsMergedWithGlobalTags(config); SnapshotSink snapshotSink = @@ -256,16 +346,19 @@ private static void subscribeConfigurationPoller( } } + private static void unsubscribeConfigurationPoller() { + if (configurationPoller != null) { + configurationPoller.removeListeners(Product.LIVE_DEBUGGING); + configurationPoller.removeListeners(Product.LIVE_DEBUGGING_SYMBOL_DB); + } + } + static ClassFileTransformer setupInstrumentTheWorldTransformer( - Config config, - Instrumentation instrumentation, - DebuggerSink debuggerSink, - StatsdMetricForwarder statsdMetricForwarder) { + Config config, Instrumentation instrumentation, DebuggerSink debuggerSink) { LOGGER.info("install Instrument-The-World transformer"); DebuggerTransformer transformer = createTransformer(config, Configuration.builder().build(), null, debuggerSink); DebuggerContext.initProbeResolver(transformer::instrumentTheWorldResolver); - DebuggerContext.initMetricForwarder(statsdMetricForwarder); instrumentation.addTransformer(transformer); return transformer; } @@ -342,45 +435,49 @@ public void run() { } } - static class DebuggerReporter implements TracerFlare.Reporter { - - private final ConfigurationUpdater configurationUpdater; - private final DebuggerSink sink; - private final ExceptionProbeManager exceptionProbeManager; - - public DebuggerReporter( - ConfigurationUpdater configurationUpdater, - DebuggerSink sink, - ExceptionProbeManager exceptionProbeManager) { - this.configurationUpdater = configurationUpdater; - this.sink = sink; - this.exceptionProbeManager = exceptionProbeManager; + private static void addReportToFlare(ZipOutputStream zip) throws IOException { + String snapshotUrl = null; + String diagnosticUrl = null; + String symbolDbUrl = null; + String probeStatuses = null; + String symbolDBStats = null; + if (sink != null) { + snapshotUrl = sink.getSnapshotSink().getUrl().toString(); + diagnosticUrl = sink.getProbeStatusSink().getUrl().toString(); + symbolDbUrl = sink.getSymbolSink().getUrl().toString(); + probeStatuses = sink.getProbeStatusSink().getProbeStatuses().toString(); + symbolDBStats = sink.getSymbolSink().getStats().toString(); } - - @Override - public void addReportToFlare(ZipOutputStream zip) throws IOException { - String content = - String.join( - System.lineSeparator(), - "Snapshot url: ", - sink.getSnapshotSink().getUrl().toString(), - "Diagnostic url: ", - sink.getProbeStatusSink().getUrl().toString(), - "SymbolDB url: ", - sink.getSymbolSink().getUrl().toString(), - "Probe definitions:", - configurationUpdater.getAppliedDefinitions().toString(), - "Instrumented probes:", - configurationUpdater.getInstrumentationResults().toString(), - "Probe statuses:", - sink.getProbeStatusSink().getProbeStatuses().toString(), - "SymbolDB stats:", - sink.getSymbolSink().getStats().toString(), - "Exception Fingerprints:", - exceptionProbeManager != null - ? exceptionProbeManager.getFingerprints().toString() - : "N/A"); - TracerFlare.addText(zip, "dynamic_instrumentation.txt", content); + String probeDefinitions = null; + String instrumentedProbes = null; + if (configurationUpdater != null) { + probeDefinitions = configurationUpdater.getAppliedDefinitions().toString(); + instrumentedProbes = configurationUpdater.getInstrumentationResults().toString(); + } + String exceptionFingerprints = null; + if (exceptionDebugger != null) { + exceptionFingerprints = + exceptionDebugger.getExceptionProbeManager().getFingerprints().toString(); } + String content = + String.join( + System.lineSeparator(), + "Snapshot url: ", + snapshotUrl, + "Diagnostic url: ", + diagnosticUrl, + "SymbolDB url: ", + symbolDbUrl, + "Probe definitions:", + probeDefinitions, + "Instrumented probes:", + instrumentedProbes, + "Probe statuses:", + probeStatuses, + "SymbolDB stats:", + symbolDBStats, + "Exception Fingerprints:", + exceptionFingerprints); + TracerFlare.addText(zip, "dynamic_instrumentation.txt", content); } } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DefaultProductConfigUpdater.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DefaultProductConfigUpdater.java new file mode 100644 index 00000000000..1bfe5cc2c54 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DefaultProductConfigUpdater.java @@ -0,0 +1,79 @@ +package com.datadog.debugger.agent; + +import datadog.trace.api.Config; +import datadog.trace.api.config.DebuggerConfig; +import datadog.trace.api.config.TraceInstrumentationConfig; +import datadog.trace.bootstrap.debugger.DebuggerContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class DefaultProductConfigUpdater implements DebuggerContext.ProductConfigUpdater { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultProductConfigUpdater.class); + + @Override + public void updateConfig( + Boolean dynamicInstrumentationEnabled, + Boolean exceptionReplayEnabled, + Boolean codeOriginEnabled, + Boolean liveDebuggingEnabled) { + startOrStopFeature( + DebuggerConfig.DYNAMIC_INSTRUMENTATION_ENABLED, + dynamicInstrumentationEnabled, + DebuggerAgent::startDynamicInstrumentation, + DebuggerAgent::stopDynamicInstrumentation); + startOrStopFeature( + DebuggerConfig.EXCEPTION_REPLAY_ENABLED, + exceptionReplayEnabled, + DebuggerAgent::startExceptionReplay, + DebuggerAgent::stopExceptionReplay); + startOrStopFeature( + TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, + codeOriginEnabled, + DebuggerAgent::startCodeOriginForSpans, + DebuggerAgent::stopCodeOriginForSpans); + startOrStopFeature( + DebuggerConfig.DISTRIBUTED_DEBUGGER_ENABLED, + liveDebuggingEnabled, + DebuggerAgent::startDistributedDebugger, + DebuggerAgent::stopDistributedDebugger); + } + + @Override + public boolean isDynamicInstrumentationEnabled() { + return DebuggerAgent.dynamicInstrumentationEnabled.get(); + } + + @Override + public boolean isExceptionReplayEnabled() { + return DebuggerAgent.exceptionReplayEnabled.get(); + } + + @Override + public boolean isCodeOriginEnabled() { + return DebuggerAgent.codeOriginEnabled.get(); + } + + @Override + public boolean isDistributedDebuggerEnabled() { + return DebuggerAgent.distributedDebuggerEnabled.get(); + } + + private static boolean isExplicitlyDisabled(String booleanKey) { + return Config.get().configProvider().isSet(booleanKey) + && !Config.get().configProvider().getBoolean(booleanKey); + } + + private static void startOrStopFeature( + String booleanKey, Boolean currentStatus, Runnable start, Runnable stop) { + if (isExplicitlyDisabled(booleanKey)) { + LOGGER.debug("Feature {} is explicitly disabled", booleanKey); + return; + } + if (currentStatus != null && currentStatus) { + start.run(); + } else { + stop.run(); + } + } +} diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymDBEnablement.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymDBEnablement.java index 12b35740d05..b0a5be8c96d 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymDBEnablement.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymDBEnablement.java @@ -84,7 +84,10 @@ private static SymDbRemoteConfigRecord deserializeSymDb(byte[] content) throws I public void stopSymbolExtraction() { LOGGER.debug("Stopping symbol extraction."); - instrumentation.removeTransformer(symbolExtractionTransformer); + if (symbolExtractionTransformer != null) { + instrumentation.removeTransformer(symbolExtractionTransformer); + symbolExtractionTransformer = null; + } } long getLastUploadTimestamp() { 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 5f32095d915..bed200dfb13 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 @@ -2655,8 +2655,7 @@ private TestSnapshotListener setupInstrumentTheWorldTransformer( config, instr, new DebuggerSink( - config, new ProbeStatusSink(config, config.getFinalDebuggerSnapshotUrl(), false)), - null); + config, new ProbeStatusSink(config, config.getFinalDebuggerSnapshotUrl(), false))); DebuggerContext.initClassFilter(new DenyListHelper(null)); return listener; } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java new file mode 100644 index 00000000000..6f7e7716a01 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java @@ -0,0 +1,24 @@ +package com.datadog.debugger.agent; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class DefaultProductConfigUpdaterTest { + + @Test + public void enableDisable() { + DefaultProductConfigUpdater productConfigUpdater = new DefaultProductConfigUpdater(); + productConfigUpdater.updateConfig(null, null, null, null); + productConfigUpdater.updateConfig(true, true, true, true); + assertTrue(productConfigUpdater.isDynamicInstrumentationEnabled()); + assertTrue(productConfigUpdater.isExceptionReplayEnabled()); + assertTrue(productConfigUpdater.isCodeOriginEnabled()); + assertTrue(productConfigUpdater.isDistributedDebuggerEnabled()); + productConfigUpdater.updateConfig(false, false, false, false); + assertFalse(productConfigUpdater.isDynamicInstrumentationEnabled()); + assertFalse(productConfigUpdater.isExceptionReplayEnabled()); + assertFalse(productConfigUpdater.isCodeOriginEnabled()); + assertFalse(productConfigUpdater.isDistributedDebuggerEnabled()); + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/ExceptionProbeInstrumentationTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/ExceptionProbeInstrumentationTest.java index f895b503134..060b7b370c0 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/ExceptionProbeInstrumentationTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/exception/ExceptionProbeInstrumentationTest.java @@ -17,6 +17,7 @@ import com.datadog.debugger.agent.ClassesToRetransformFinder; import com.datadog.debugger.agent.Configuration; import com.datadog.debugger.agent.ConfigurationUpdater; +import com.datadog.debugger.agent.DebuggerAgent; import com.datadog.debugger.agent.DebuggerAgentHelper; import com.datadog.debugger.agent.DebuggerTransformer; import com.datadog.debugger.agent.JsonSnapshotSerializer; @@ -56,6 +57,7 @@ import org.joor.Reflect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -82,6 +84,11 @@ public class ExceptionProbeInstrumentationTest { private MockSampler probeSampler; private MockSampler globalSampler; + @BeforeAll + public static void beforeAll() { + setFieldInConfig(Config.get(), "agentUrl", "http://localhost:8126"); + } + @BeforeEach public void before() { CoreTracer tracer = CoreTracer.builder().build(); @@ -92,6 +99,7 @@ public void before() { ProbeRateLimiter.setSamplerSupplier(rate -> rate < 101 ? probeSampler : globalSampler); ProbeRateLimiter.setGlobalSnapshotRate(1000); // to activate the call to DebuggerContext.handleException + DebuggerAgent.startExceptionReplay(); setFieldInConfig(Config.get(), "debuggerExceptionEnabled", true); setFieldInConfig(Config.get(), "dynamicInstrumentationClassFileDumpEnabled", true); } diff --git a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java index 821ef155a03..e17753545ea 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java +++ b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/ServerDebuggerTestApplication.java @@ -97,6 +97,17 @@ protected void waitForReTransformation(String className) { } } + protected void waitForSpecificLine(String line) { + System.out.println("waitForSpecificLine..."); + try { + lastMatchedLine = + TestApplicationHelper.waitForSpecificLine(LOG_FILENAME, line, lastMatchedLine); + System.out.println("line found!"); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + protected void execute(String methodName, String arg) { Consumer method = methodsByName.get(methodName); if (method == null) { @@ -254,6 +265,12 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio app.waitForReTransformation(className); break; } + case "/app/waitForSpecificLine": + { + String feature = request.getRequestUrl().queryParameter("line"); + app.waitForSpecificLine(feature); + break; + } case "/app/execute": { String methodName = request.getRequestUrl().queryParameter("methodname"); diff --git a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/TestApplicationHelper.java b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/TestApplicationHelper.java index b19aa2175c3..b69e697b704 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/TestApplicationHelper.java +++ b/dd-smoke-tests/debugger-integration-tests/src/main/java/datadog/smoketest/debugger/TestApplicationHelper.java @@ -90,6 +90,17 @@ public static String waitForReTransformation( Duration.ofSeconds(TIMEOUT_S)); } + public static String waitForSpecificLine(String logFileName, String specificLine, String fromLine) + throws IOException { + return waitForSpecificLogLine( + Paths.get(logFileName), + fromLine != null ? line -> line.contains(fromLine) : null, + line -> line.contains(specificLine), + () -> {}, + Duration.ofMillis(SLEEP_MS), + Duration.ofSeconds(TIMEOUT_S)); + } + public static void waitForUpload(String logFileName, int expectedUploads) throws IOException { if (expectedUploads == -1) { System.out.println("wait for " + TIMEOUT_S + "s"); diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java index d366c671f48..053c331c482 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/BaseIntegrationTest.java @@ -14,7 +14,9 @@ import com.datadog.debugger.sink.Snapshot; import com.datadog.debugger.util.MoshiHelper; import com.datadog.debugger.util.MoshiSnapshotTestHelper; +import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import datadog.trace.bootstrap.debugger.CapturedContext; import datadog.trace.bootstrap.debugger.ProbeRateLimiter; @@ -80,7 +82,10 @@ public abstract class BaseIntegrationTest { protected static final MockResponse EMPTY_200_RESPONSE = new MockResponse().setResponseCode(200); private static final ByteString DIAGNOSTICS_STR = ByteString.encodeUtf8("diagnostics"); - private static final String CONFIG_ID = UUID.randomUUID().toString(); + private static final String LD_CONFIG_ID = UUID.randomUUID().toString(); + private static final String APM_CONFIG_ID = UUID.randomUUID().toString(); + public static final String LIVE_DEBUGGING_PRODUCT = "LIVE_DEBUGGING"; + public static final String APM_TRACING_PRODUCT = "APM_TRACING"; protected MockWebServer datadogAgentServer; private MockDispatcher probeMockDispatcher; @@ -90,6 +95,7 @@ public abstract class BaseIntegrationTest { protected Path logFilePath; protected Process targetProcess; private Configuration currentConfiguration; + private ConfigOverrides configOverrides; private boolean configProvided; protected final Object configLock = new Object(); protected final List> intakeRequestListeners = @@ -147,6 +153,7 @@ protected List getDebuggerCommandParams() { "-Ddd.profiling.enabled=false", "-Ddatadog.slf4j.simpleLogger.defaultLogLevel=info", "-Ddatadog.slf4j.simpleLogger.log.com.datadog.debugger=debug", + "-Ddatadog.slf4j.simpleLogger.log.datadog.remoteconfig=debug", "-Ddd.jmxfetch.start-delay=0", "-Ddd.jmxfetch.enabled=false", "-Ddd.dynamic.instrumentation.enabled=true", @@ -413,10 +420,12 @@ private MockResponse datadogAgentDispatch(RecordedRequest request) { return EMPTY_200_RESPONSE; } - private MockResponse handleConfigRequests() { + protected MockResponse handleConfigRequests() { Configuration configuration; + ConfigOverrides configOverrides; synchronized (configLock) { configuration = getCurrentConfiguration(); + configOverrides = getConfigOverrides(); configProvided = true; configLock.notifyAll(); } @@ -426,9 +435,22 @@ private MockResponse handleConfigRequests() { try { JsonAdapter adapter = MoshiConfigTestHelper.createMoshiConfig().adapter(Configuration.class); - String json = adapter.toJson(configuration); - LOG.info("Sending json config: {}", json); - String remoteConfigJson = RemoteConfigHelper.encode(json, CONFIG_ID); + String liveDebuggingJson = adapter.toJson(configuration); + LOG.info("Sending Live Debugging json: {}", liveDebuggingJson); + List remoteConfigs = new ArrayList<>(); + remoteConfigs.add( + new RemoteConfigHelper.RemoteConfig( + LIVE_DEBUGGING_PRODUCT, liveDebuggingJson, LD_CONFIG_ID)); + if (configOverrides != null) { + JsonAdapter configAdapter = + new Moshi.Builder().build().adapter(ConfigOverrides.class); + String configOverridesJson = configAdapter.toJson(configOverrides); + LOG.info("Sending configOverrides json: {}", configOverridesJson); + remoteConfigs.add( + new RemoteConfigHelper.RemoteConfig( + APM_TRACING_PRODUCT, configOverridesJson, APM_CONFIG_ID)); + } + String remoteConfigJson = RemoteConfigHelper.encode(remoteConfigs); return new MockResponse().setResponseCode(200).setBody(remoteConfigJson); } catch (Exception e) { throw new RuntimeException(e); @@ -441,6 +463,12 @@ private Configuration getCurrentConfiguration() { } } + private ConfigOverrides getConfigOverrides() { + synchronized (configLock) { + return configOverrides; + } + } + protected boolean isConfigProvided() { synchronized (configLock) { return configProvided; @@ -461,6 +489,12 @@ protected void setCurrentConfiguration(Configuration configuration) { } } + protected void setConfigOverrides(ConfigOverrides configOverrides) { + synchronized (configLock) { + this.configOverrides = configOverrides; + } + } + protected Configuration createConfig(LogProbe logProbe) { return createConfig(Arrays.asList(logProbe)); } @@ -614,4 +648,23 @@ int getPort() { return socket.getLocalPort(); } } + + static final class ConfigOverrides { + @Json(name = "lib_config") + public LibConfig libConfig; + } + + static final class LibConfig { + @Json(name = "dynamic_instrumentation_enabled") + public Boolean dynamicInstrumentationEnabled; + + @Json(name = "exception_replay_enabled") + public Boolean exceptionReplayEnabled; + + @Json(name = "code_origin_enabled") + public Boolean codeOriginEnabled; + + @Json(name = "live_debugging_enabled") + public Boolean liveDebuggingEnabled; + } } diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java index 53101ae222c..c0a1a5d21d0 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java @@ -44,8 +44,8 @@ void testCodeOriginTraceAnnotation() throws Exception { span.getMeta().get(DD_CODE_ORIGIN_FRAME_FILE)); assertEquals("runTracedMethod", span.getMeta().get(DD_CODE_ORIGIN_FRAME_METHOD)); assertEquals( - "(java.lang.String)", span.getMeta().get(DD_CODE_ORIGIN_FRAME_SIGNATURE)); - assertEquals("134", span.getMeta().get(DD_CODE_ORIGIN_FRAME_LINE)); + "(java.lang.String)", span.getMeta().get(DD_CODE_ORIGIN_FRAMES_0_SIGNATURE)); + assertEquals("145", span.getMeta().get(DD_CODE_ORIGIN_FRAMES_0_LINE)); codeOrigin.set(true); } } diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/InProductEnablementIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/InProductEnablementIntegrationTest.java new file mode 100644 index 00000000000..d80d697bcb2 --- /dev/null +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/InProductEnablementIntegrationTest.java @@ -0,0 +1,95 @@ +package datadog.smoketest; + +import static datadog.smoketest.debugger.TestApplicationHelper.waitForSpecificLine; + +import com.datadog.debugger.probe.LogProbe; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class InProductEnablementIntegrationTest extends ServerAppDebuggerIntegrationTest { + private List additionalJvmArgs = new ArrayList<>(); + + @Override + protected ProcessBuilder createProcessBuilder(Path logFilePath, String... params) { + List commandParams = getDebuggerCommandParams(); + // remove the dynamic instrumentation flag + commandParams.remove("-Ddd.dynamic.instrumentation.enabled=true"); + commandParams.addAll(additionalJvmArgs); + return ProcessBuilderHelper.createProcessBuilder( + commandParams, logFilePath, getAppClass(), params); + } + + @Test + @DisplayName("testDynamicInstrumentationEnablement") + void testDynamicInstrumentationEnablement() throws Exception { + appUrl = startAppAndAndGetUrl(); + setConfigOverrides(createConfigOverrides(true, false)); + LogProbe probe = + LogProbe.builder().probeId(PROBE_ID).where(TEST_APP_CLASS_NAME, TRACED_METHOD_NAME).build(); + setCurrentConfiguration(createConfig(probe)); + waitForFeatureStarted(appUrl, "Dynamic Instrumentation"); + waitForInstrumentation(appUrl); + // disable DI + setConfigOverrides(createConfigOverrides(false, false)); + waitForFeatureStopped(appUrl, "Dynamic Instrumentation"); + waitForReTransformation(appUrl); // wait for retransformation of removed probe + } + + @Test + @DisplayName("testDynamicInstrumentationEnablementStaticallyDisabled") + void testDynamicInstrumentationEnablementStaticallyDisabled() throws Exception { + // explicitly disable dynamic instrumentation, preventing enablement + additionalJvmArgs.add("-Ddd.dynamic.instrumentation.enabled=false"); + appUrl = startAppAndAndGetUrl(); + setConfigOverrides(createConfigOverrides(true, false)); + LogProbe probe = + LogProbe.builder().probeId(PROBE_ID).where(TEST_APP_CLASS_NAME, TRACED_METHOD_NAME).build(); + setCurrentConfiguration(createConfig(probe)); + waitForSpecificLine(appUrl, "Feature dynamic.instrumentation.enabled is explicitly disabled"); + } + + @Test + @DisplayName("testExceptionReplayEnablement") + void testExceptionReplayEnablement() throws Exception { + additionalJvmArgs.add("-Ddd.third.party.excludes=datadog.smoketest"); + appUrl = startAppAndAndGetUrl(); + setConfigOverrides(createConfigOverrides(false, true)); + waitForFeatureStarted(appUrl, "Exception Replay"); + execute(appUrl, TRACED_METHOD_NAME, "oops"); // instrumenting first exception + waitForInstrumentation(appUrl); + // disable ER + setConfigOverrides(createConfigOverrides(false, false)); + waitForFeatureStopped(appUrl, "Exception Replay"); + waitForReTransformation(appUrl); // wait for retransformation of removed probes + } + + private void waitForFeatureStarted(String appUrl, String feature) throws IOException { + String line = "INFO com.datadog.debugger.agent.DebuggerAgent - Starting " + feature; + waitForSpecificLine(appUrl, line); + LOG.info("feature {} started", feature); + } + + private void waitForFeatureStopped(String appUrl, String feature) throws IOException { + String line = "INFO com.datadog.debugger.agent.DebuggerAgent - Stopping " + feature; + waitForSpecificLine(appUrl, line); + LOG.info("feature {} stopped", feature); + } + + private void waitForSpecificLine(String appUrl, String line) throws IOException { + String url = String.format(appUrl + "/waitForSpecificLine?line=%s", line); + sendRequest(url); + } + + private static ConfigOverrides createConfigOverrides( + boolean dynamicInstrumentationEnabled, boolean exceptionReplayEnabled) { + ConfigOverrides config = new ConfigOverrides(); + config.libConfig = new LibConfig(); + config.libConfig.dynamicInstrumentationEnabled = dynamicInstrumentationEnabled; + config.libConfig.exceptionReplayEnabled = exceptionReplayEnabled; + return config; + } +} diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/RemoteConfigHelper.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/RemoteConfigHelper.java index 17fbcd73a30..d9f7c7cc85d 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/RemoteConfigHelper.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/RemoteConfigHelper.java @@ -2,50 +2,107 @@ import datadog.trace.util.Strings; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; +import java.util.StringJoiner; public class RemoteConfigHelper { - public static String encode(String config, String configId) { - String hashStr = null; - try { - hashStr = Strings.sha256(config); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); + public static class RemoteConfig { + public final String product; + public final String config; + public final String configId; + + public RemoteConfig(String product, String config, String configId) { + this.product = product; + this.config = config; + this.configId = configId; } - String targetsStr = - String.format( - "{\"signed\":\n" - + " { \"_type\":\"targets\",\n" - + " \"spec_version\": \"1.0\",\n" - + " \"version\": \"2\",\n" - + " \"custom\": { \"opaque_backend_state\": \"opaque\" },\n" - + " \"targets\":\n" - + " { \"datadog/2/LIVE_DEBUGGING/%s/config\":{" - + " \"length\": %d,\n" - + " \"custom\": { \"v\": 123 },\n" - + " \"hashes\":\n" - + " {\n" - + " \"sha256\": \"%s\"\n" - + " }" - + " }" - + " }" - + " }" - + "}", - configId, config.length(), hashStr); - String targetsEncoding = new String(Base64.getEncoder().encode(targetsStr.getBytes())); - String encodedConfig = new String(Base64.getEncoder().encode(config.getBytes())); + } + + public static String encode(List remoteConfigs) { + List hashes = buildHashes(remoteConfigs); + String targetsStr = buildTargets(hashes, remoteConfigs); + String targetsEncoded = new String(Base64.getEncoder().encode(targetsStr.getBytes())); + String targetFiles = buildTargetFiles(remoteConfigs); + String clientConfigs = buildClientConfigs(remoteConfigs); return String.format( "{\n" + "\"targets\": \"%s\",\n" + "\"target_files\": [\n" - + " {\n" - + " \"path\": \"datadog/2/LIVE_DEBUGGING/%s/config\",\n" - + " \"raw\": \"%s\"\n" - + "}]," + + " %s\n" + + "]," + "\"client_configs\": [\n" - + " \"datadog/2/LIVE_DEBUGGING/%s/config\"\n" + + " %s\n" + " ]" + "}", - targetsEncoding, configId, encodedConfig, configId); + targetsEncoded, targetFiles, clientConfigs); + } + + private static String buildClientConfigs(List remoteConfigs) { + StringJoiner sj = new StringJoiner(",\n"); + for (RemoteConfig rc : remoteConfigs) { + sj.add( + String.format( + " \"datadog/2/%s/%s/config\"\n", rc.product, rc.configId)); + } + return sj.toString(); + } + + private static String buildTargetFiles(List remoteConfigs) { + StringJoiner sj = new StringJoiner(",\n"); + for (RemoteConfig rc : remoteConfigs) { + String encodedConfig = new String(Base64.getEncoder().encode(rc.config.getBytes())); + sj.add( + String.format( + " {\n" + + " \"path\": \"datadog/2/%s/%s/config\",\n" + + " \"raw\": \"%s\"\n" + + " }\n", + rc.product, rc.configId, encodedConfig)); + } + return sj.toString(); + } + + private static List buildHashes(List remoteConfigs) { + List hashes = new ArrayList<>(); + for (RemoteConfig rc : remoteConfigs) { + try { + hashes.add(Strings.sha256(rc.config)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + return hashes; + } + + private static String buildTargets(List hashes, List remoteConfigs) { + StringJoiner sj = new StringJoiner(",\n"); + for (int i = 0; i < remoteConfigs.size(); i++) { + RemoteConfig rc = remoteConfigs.get(i); + sj.add( + String.format( + " \"datadog/2/%s/%s/config\":{" + + " \"length\": %d,\n" + + " \"custom\": { \"v\": 123 },\n" + + " \"hashes\":\n" + + " {\n" + + " \"sha256\": \"%s\"\n" + + " }" + + " }", + rc.product, rc.configId, rc.config.length(), hashes.get(i))); + } + String targets = sj.toString(); + return String.format( + "{\"signed\":\n" + + " { \"_type\":\"targets\",\n" + + " \"spec_version\": \"1.0\",\n" + + " \"version\": \"2\",\n" + + " \"custom\": { \"opaque_backend_state\": \"opaque\" },\n" + + " \"targets\":\n" + + " { %s }" + + " }" + + "}", + targets); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index 5fafa0f29a8..771d22e3758 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -356,15 +356,15 @@ public DDSpan addThrowable(Throwable error, byte errorPriority) { setTag(DDTags.ERROR_MSG, message); setTag(DDTags.ERROR_TYPE, error.getClass().getName()); - if (isExceptionDebuggingEnabled()) { + if (isExceptionReplayEnabled()) { DebuggerContext.handleException(error, this); } } return this; } - private boolean isExceptionDebuggingEnabled() { - if (!Config.get().isDebuggerExceptionEnabled()) { + private boolean isExceptionReplayEnabled() { + if (!DebuggerContext.isExceptionReplayEnabled()) { return false; } boolean captureOnlyRootSpan = diff --git a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java index bf5421617e7..60bb3a4a3bc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/TracingConfigPoller.java @@ -4,6 +4,10 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_HTTP_HEADER_TAGS; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_LOGS_INJECTION; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_DATA_STREAMS_ENABLED; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY; +import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RATE; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_SAMPLE_RULES; import static datadog.remoteconfig.Capabilities.CAPABILITY_APM_TRACING_TRACING_ENABLED; @@ -24,6 +28,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DynamicConfig; import datadog.trace.api.sampling.SamplingRule; +import datadog.trace.bootstrap.debugger.DebuggerContext; import datadog.trace.logging.GlobalLogLevelSwitcher; import datadog.trace.logging.LogLevel; import java.io.ByteArrayInputStream; @@ -65,7 +70,11 @@ public void start(Config config, SharedCommunicationObjects sco) { | CAPABILITY_APM_HTTP_HEADER_TAGS | CAPABILITY_APM_CUSTOM_TAGS | CAPABILITY_APM_TRACING_DATA_STREAMS_ENABLED - | CAPABILITY_APM_TRACING_SAMPLE_RULES); + | CAPABILITY_APM_TRACING_SAMPLE_RULES + | CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION + | CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY + | CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN + | CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING); } stopPolling = new Updater().register(config, configPoller); } @@ -211,7 +220,11 @@ void applyConfigOverrides(LibConfig libConfig) { maybeOverride(builder::setTraceSampleRate, libConfig.traceSampleRate); maybeOverride(builder::setTracingTags, parseTagListToMap(libConfig.tracingTags)); - + DebuggerContext.updateConfig( + libConfig.dynamicInstrumentationEnabled, + libConfig.exceptionReplayEnabled, + libConfig.codeOriginEnabled, + libConfig.liveDebuggingEnabled); builder.apply(); } @@ -282,6 +295,18 @@ static final class LibConfig { @Json(name = "tracing_sampling_rules") public TracingSamplingRules tracingSamplingRules; + + @Json(name = "dynamic_instrumentation_enabled") + public Boolean dynamicInstrumentationEnabled; + + @Json(name = "exception_replay_enabled") + public Boolean exceptionReplayEnabled; + + @Json(name = "code_origin_enabled") + public Boolean codeOriginEnabled; + + @Json(name = "live_debugging_enabled") + public Boolean liveDebuggingEnabled; } /** Holds the raw JSON string and the parsed rule data. */ diff --git a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java index 5aea9ca50bd..b8e1124a1b2 100644 --- a/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java +++ b/remote-config/remote-config-api/src/main/java/datadog/remoteconfig/Capabilities.java @@ -38,4 +38,8 @@ public interface Capabilities { long CAPABILITY_ASM_NETWORK_FINGERPRINT = 1L << 34; long CAPABILITY_ASM_HEADER_FINGERPRINT = 1L << 35; long CAPABILITY_ASM_RASP_CMDI = 1L << 37; + long CAPABILITY_APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION = 1L << 38; + long CAPABILITY_APM_TRACING_ENABLE_EXCEPTION_REPLAY = 1L << 39; + long CAPABILITY_APM_TRACING_ENABLE_CODE_ORIGIN = 1L << 40; + long CAPABILITY_APM_TRACING_ENABLE_LIVE_DEBUGGING = 1L << 41; } From 3b5366c079ac06c47541ad1481409a54908b8ce0 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Wed, 19 Mar 2025 17:57:56 +0100 Subject: [PATCH 2/6] fix unit test --- .../debugger/agent/DefaultProductConfigUpdaterTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java index 6f7e7716a01..36814893855 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/DefaultProductConfigUpdaterTest.java @@ -1,13 +1,22 @@ package com.datadog.debugger.agent; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import datadog.communication.ddagent.DDAgentFeaturesDiscovery; +import datadog.communication.ddagent.SharedCommunicationObjects; +import java.lang.instrument.Instrumentation; import org.junit.jupiter.api.Test; class DefaultProductConfigUpdaterTest { @Test public void enableDisable() { + SharedCommunicationObjects sco = mock(SharedCommunicationObjects.class); + when(sco.featuresDiscovery(any())).thenReturn(mock(DDAgentFeaturesDiscovery.class)); + DebuggerAgent.run(mock(Instrumentation.class), sco); DefaultProductConfigUpdater productConfigUpdater = new DefaultProductConfigUpdater(); productConfigUpdater.updateConfig(null, null, null, null); productConfigUpdater.updateConfig(true, true, true, true); From b2b62b96e7157ee4503bb8090095784d88fd421f Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Wed, 19 Mar 2025 18:00:44 +0100 Subject: [PATCH 3/6] disable source file tracking --- .../src/main/java/com/datadog/debugger/agent/DebuggerAgent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java index 2d85f464fa6..543f70e8b3d 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java @@ -74,7 +74,7 @@ public static synchronized void run(Instrumentation inst, SharedCommunicationObj Config config = Config.get(); DebuggerContext.initProductConfigUpdater(new DefaultProductConfigUpdater()); classesToRetransformFinder = new ClassesToRetransformFinder(); - setupSourceFileTracking(instrumentation, classesToRetransformFinder); + // setupSourceFileTracking(instrumentation, classesToRetransformFinder); if (config.isDebuggerCodeOriginEnabled()) { startCodeOriginForSpans(); } From 53bff4bae9c936940d0d8ed6f844f0705a3bc9c2 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 20 Mar 2025 14:27:58 +0100 Subject: [PATCH 4/6] enable source file tracking --- .../src/main/java/com/datadog/debugger/agent/DebuggerAgent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java index 543f70e8b3d..2d85f464fa6 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java @@ -74,7 +74,7 @@ public static synchronized void run(Instrumentation inst, SharedCommunicationObj Config config = Config.get(); DebuggerContext.initProductConfigUpdater(new DefaultProductConfigUpdater()); classesToRetransformFinder = new ClassesToRetransformFinder(); - // setupSourceFileTracking(instrumentation, classesToRetransformFinder); + setupSourceFileTracking(instrumentation, classesToRetransformFinder); if (config.isDebuggerCodeOriginEnabled()) { startCodeOriginForSpans(); } From 1b2a31be2dacaf19ab3999b5a87447ce0d49283d Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 21 Mar 2025 09:20:18 +0100 Subject: [PATCH 5/6] enable source file tracking conditionally --- .../main/java/com/datadog/debugger/agent/DebuggerAgent.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java index 2d85f464fa6..9f1b08fa0fe 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java @@ -74,7 +74,10 @@ public static synchronized void run(Instrumentation inst, SharedCommunicationObj Config config = Config.get(); DebuggerContext.initProductConfigUpdater(new DefaultProductConfigUpdater()); classesToRetransformFinder = new ClassesToRetransformFinder(); - setupSourceFileTracking(instrumentation, classesToRetransformFinder); + if (config.isDynamicInstrumentationEnabled() || config.isDebuggerExceptionEnabled()) { + // only activate Source File Tracking if DI or ER is enabled from the start + setupSourceFileTracking(instrumentation, classesToRetransformFinder); + } if (config.isDebuggerCodeOriginEnabled()) { startCodeOriginForSpans(); } From dc9b3ac60f70ad84c1646cbf9137747eb385ef90 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Fri, 21 Mar 2025 09:29:08 +0100 Subject: [PATCH 6/6] fix rebase --- .../java/datadog/smoketest/CodeOriginIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java index c0a1a5d21d0..473448936a1 100644 --- a/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java +++ b/dd-smoke-tests/debugger-integration-tests/src/test/java/datadog/smoketest/CodeOriginIntegrationTest.java @@ -44,8 +44,8 @@ void testCodeOriginTraceAnnotation() throws Exception { span.getMeta().get(DD_CODE_ORIGIN_FRAME_FILE)); assertEquals("runTracedMethod", span.getMeta().get(DD_CODE_ORIGIN_FRAME_METHOD)); assertEquals( - "(java.lang.String)", span.getMeta().get(DD_CODE_ORIGIN_FRAMES_0_SIGNATURE)); - assertEquals("145", span.getMeta().get(DD_CODE_ORIGIN_FRAMES_0_LINE)); + "(java.lang.String)", span.getMeta().get(DD_CODE_ORIGIN_FRAME_SIGNATURE)); + assertEquals("145", span.getMeta().get(DD_CODE_ORIGIN_FRAME_LINE)); codeOrigin.set(true); } }