Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2940f6a
Expose OTel thread/process context without requiring profiling
jandro996 Sep 17, 2026
113eaf9
Defer ddprof context integration construction past premain for AppSec…
jandro996 Sep 18, 2026
28ef434
Drop dedicated OTel context exposure config flag, derive purely from …
jandro996 Sep 18, 2026
385c029
review: pre-PR checks
jandro996 Sep 18, 2026
060ff1f
review: remove decisions.md from tracked files
jandro996 Sep 18, 2026
8652de2
review: defer ddprof context construction with a startup delay
jandro996 Sep 21, 2026
68e2e09
review: defer profiling context engine tag until deferred constructio…
jandro996 Sep 21, 2026
271e8cb
review: limit testRuntimeActivation to the OTel exposure spec
jandro996 Sep 21, 2026
6b1c10d
review: fix thread-safety race in CoreTracer local root span tags
jandro996 Sep 22, 2026
b1d3bbb
revert: remove AppSec runtime-activation trigger for OTel context exp…
jandro996 Sep 23, 2026
8e7988b
Merge branch 'master' into otel-context-without-profiling
jandro996 Sep 23, 2026
ba4757f
fix: unwrap LocalRootSpanTags in DDTracerAPITest reflection
jandro996 Sep 23, 2026
92339b3
docs: remove stale remote-config activation paragraph from DeferredPr…
jandro996 Sep 23, 2026
33352ec
fix: forward virtual-thread context binding in deferred wrapper; tigh…
jandro996 Sep 23, 2026
a75de83
fix: exclude AWS Lambda from ddprof context-exposure path; cover fact…
jandro996 Sep 23, 2026
6d290fc
docs: trim verbose Javadocs and inline comments per reviewer feedback
jandro996 Sep 24, 2026
7c502c2
refactor: self-document OTel context config naming per reviewer feedback
jandro996 Sep 24, 2026
c7c52f9
refactor: split ddprof context integration into load/defer paths per …
jandro996 Sep 24, 2026
7d3d3e7
refactor: drop redundant Windows check in createProfilingContextInteg…
jandro996 Sep 25, 2026
ae334b4
test: cover null-returning factory in DeferredProfilingContextIntegra…
jandro996 Sep 25, 2026
3100f8a
test: rename disabledInAnEnvironmentWhereTheDatadogProfilerIsUnsafe
jandro996 Sep 25, 2026
01ade6b
Merge branch 'master' into otel-context-without-profiling
jandro996 Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import datadog.trace.api.profiling.ProfilingEnablement;
import datadog.trace.api.scopemanager.ScopeListener;
import datadog.trace.bootstrap.benchmark.StaticEventLogger;
import datadog.trace.bootstrap.config.provider.ConfigProvider;
import datadog.trace.bootstrap.config.provider.StableConfigSource;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI;
Expand Down Expand Up @@ -1489,35 +1490,93 @@ public void withTracer(TracerAPI tracer) {
* {@see com.datadog.profiling.ddprof.DatadogProfilingIntegration} must not be modified to depend
* on JFR.
*/
private static ProfilingContextIntegration createProfilingContextIntegration() {
if (Config.get().isProfilingEnabled()) {
if (Config.get().isDatadogProfilerEnabled() && !OperatingSystem.isWindows()) {
try {
return (ProfilingContextIntegration)
AGENT_CLASSLOADER
.loadClass("com.datadog.profiling.ddprof.DatadogProfilingIntegration")
.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
log.debug("ddprof-based profiling context labeling not available. {}", t.getMessage());
static ProfilingContextIntegration createProfilingContextIntegration() {
Config config = Config.get();
// Windows is already excluded by Config (isDatadogProfilerSafeAndConfigured), so only AWS
// Lambda needs to be excluded here: it has no ddprof native library support, same as
// startProfilingAgent().
if (!isAwsLambdaRuntime()) {
if (config.isDatadogProfilerEnabled()) {
// The profiler itself is running: load ddprof now, and let ProfilingAgent.run() register
// the process context as it always has.
ProfilingContextIntegration integration = loadDdprofContextIntegration(AGENT_CLASSLOADER);
if (integration != null) {
return integration;
}
} else if (config.isOtelThreadContextEnabled()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If I read this code correctly, when the 'ddprof' profiler library is disabled and the otel thread context is enabled, we short-circuit back to ddprof context integration, effectively disabling the fall-back profiler context integration based on JFR events for timeline and trace-to-profile, breaking those features.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correct me if I’m wrong, but the new branch only kicks in when isProfilingEnabled() == false, while the JFR fallback requires isProfilingEnabled() == true. So they’re mutually exclusive, and we can never lose the JFR fallback, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This branch kicks in for isDatadogProfilerEnabled() == false not isProfilingEnabled() == false - at least I can not find that guard anywhere.

The isOtelThreadContextEnabled may become true only if isProfilingEnabled() == true, so if isProfilingEnabled() == false we would not be entering this branch at all.

My point is - we have profiling enabled, but not the 'ddprof' native library based implementation. Therefore, in the original code we did fall back to JFR event based context representation so our timeline and trace-to-profile can still work, even though with lowered precission.

But if we return unconditinally the wrapper from this branch, the JFR event based context representation will stay disabled and break our timeline and trace-to-profile.

Sadly, the JVM JFR can not use the otel/native thread context and because of that we need to have the alternative, JFR event based implementation available.

// No profiler, we only want the context exposed: loading ddprof pulls in the native
// library and touches java.nio.file, which must not happen on the primordial premain
// thread, so it is deferred.
return deferDdprofContextIntegration(AGENT_CLASSLOADER);
}
if (Config.get().isProfilingTimelineEventsEnabled()) {
// important: note that this will not initialise JFR until onStart is called
try {
return (ProfilingContextIntegration)
AGENT_CLASSLOADER
.loadClass("com.datadog.profiling.controller.openjdk.JFREventContextIntegration")
.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
log.debug("JFR event-based profiling context labeling not available. {}", t.getMessage());
}
}
if (config.isProfilingEnabled() && config.isProfilingTimelineEventsEnabled()) {
// important: note that this will not initialise JFR until onStart is called
try {
return (ProfilingContextIntegration)
AGENT_CLASSLOADER
.loadClass("com.datadog.profiling.controller.openjdk.JFREventContextIntegration")
.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
log.debug("JFR event-based profiling context labeling not available. {}", t.getMessage());
}
}
return ProfilingContextIntegration.NoOp.INSTANCE;
}

/**
* Loads the ddprof-based profiling context integration on the calling thread, for when the
* Datadog profiler is running. Returns {@code null} when it isn't available, so the caller can
* fall back to another integration.
*/
static ProfilingContextIntegration loadDdprofContextIntegration(final ClassLoader classLoader) {
try {
return newDdprofContextIntegration(classLoader);
} catch (Throwable t) {
log.debug("ddprof-based profiling context labeling not available. {}", t.getMessage());
return null;
}
}

/**
* Returns a placeholder integration that loads the ddprof-based one off the calling thread and
* registers the OTel process context alongside it. Only used when the profiler isn't running:
* otherwise {@code ProfilingAgent.run()} registers the process context itself.
*/
static ProfilingContextIntegration deferDdprofContextIntegration(final ClassLoader classLoader) {
DeferredProfilingContextIntegration deferred =
new DeferredProfilingContextIntegration(
"ddprof",
() -> {
ProfilingContextIntegration integration = newDdprofContextIntegration(classLoader);
registerProcessContext(classLoader);
return integration;
});
deferred.scheduleInitialization();
return deferred;
}

private static ProfilingContextIntegration newDdprofContextIntegration(
final ClassLoader classLoader) throws ReflectiveOperationException {
return (ProfilingContextIntegration)
classLoader
.loadClass("com.datadog.profiling.ddprof.DatadogProfilingIntegration")
.getDeclaredConstructor()
.newInstance();
}

private static void registerProcessContext(final ClassLoader classLoader) {
try {
classLoader
.loadClass("com.datadog.profiling.agent.ProcessContext")
.getMethod("register", ConfigProvider.class)
.invoke(null, ConfigProvider.getInstance());
} catch (Throwable t) {
log.debug("Process context registration not available. {}", t.getMessage());
}
}

private static boolean startProfilingAgent(
final boolean earlyStart, final boolean firstAttempt, Instrumentation inst) {
if (isAwsLambdaRuntime()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package datadog.trace.bootstrap;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

import datadog.context.Context;
import datadog.trace.api.EndpointTracker;
import datadog.trace.api.Stateful;
import datadog.trace.api.profiling.ProfilingContextAttribute;
import datadog.trace.api.profiling.ProfilingScope;
import datadog.trace.api.profiling.Timing;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.ProfilerContext;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import datadog.trace.util.AgentTaskScheduler;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@link ProfilingContextIntegration} handed out synchronously during {@code premain} while the
* real ddprof-based integration is constructed later, off the premain thread, to avoid loading the
* ddprof native library (and touching {@code java.nio.file}) before {@code main} gets a chance to
* set its own {@code java.nio.file.spi.DefaultFileSystemProvider}. Delegates to {@link
* ProfilingContextIntegration.NoOp} until the swap happens; stays a no-op forever if construction
* fails.
*/
final class DeferredProfilingContextIntegration implements ProfilingContextIntegration {
private static final Logger log =
LoggerFactory.getLogger(DeferredProfilingContextIntegration.class);

/**
* Delay before the deferred construction runs, giving {@code main} a chance to install its own
* {@code java.nio.file.spi.DefaultFileSystemProvider} first; not user-tunable since losing the
* first second of context exposure is not observable.
*/
private static final long INITIALIZATION_DELAY_MILLIS = 1_000;

private final String name;
private final Callable<ProfilingContextIntegration> factory;

/**
* Swapped to the real integration once construction succeeds; volatile since scopes may already
* be running when the swap happens.
*/
private volatile ProfilingContextIntegration delegate = ProfilingContextIntegration.NoOp.INSTANCE;

/**
* Callbacks queued via {@link #whenAvailable(Runnable)} before the swap; guarded by {@code this}
* together with the {@link #delegate} write so none is run twice or dropped.
*/
private final List<Runnable> pendingAvailabilityCallbacks = new ArrayList<>(1);

/**
* @param name the name reported by {@link #name()}, i.e. the name of the integration being
* deferred.
* @param factory creates the real integration; invoked at most once, off the premain thread.
*/
DeferredProfilingContextIntegration(
final String name, final Callable<ProfilingContextIntegration> factory) {
this.name = name;
this.factory = factory;
}

/**
* Schedules the deferred construction to run off this (premain) thread, after {@link
* #INITIALIZATION_DELAY_MILLIS}.
*/
void scheduleInitialization() {
AgentTaskScheduler.get().schedule(this::initialize, INITIALIZATION_DELAY_MILLIS, MILLISECONDS);
Comment thread
jandro996 marked this conversation as resolved.
Comment thread
jandro996 marked this conversation as resolved.
Comment thread
jandro996 marked this conversation as resolved.
}

/**
* Runs the deferred construction; called exactly once per instance, from {@link
* #scheduleInitialization()}. On failure this instance keeps behaving as {@link
* ProfilingContextIntegration.NoOp} forever; a background failure must never propagate.
*/
void initialize() {
try {
final ProfilingContextIntegration integration = factory.call();
Comment thread
jandro996 marked this conversation as resolved.
if (integration == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Sphinx Review — LOW] Mutation candidate (null-guard) on the integration==null check in initialize() has no test exercising a factory that returns null. Deleting the guard (mutant) would set delegate=null and make pass-through methods throw NPE after initialize(); no existing test constructs DeferredProfilingContextIntegration with a null-returning factory and then calls a pass-through method, so the mutant survives.

Suggestion: Add a test with a factory returning null; assert initialize() completes and pass-through calls still behave as NoOp.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch - added staysNoOpWhenTheFactoryReturnsNull(), which constructs the deferred wrapper with a factory returning null and asserts initialize() completes cleanly with no callbacks run and the wrapper still behaving as NoOp. Fixed in ae334b4.

return;
}
final List<Runnable> callbacks;
synchronized (this) {
delegate = integration;
callbacks = new ArrayList<>(pendingAvailabilityCallbacks);
pendingAvailabilityCallbacks.clear();
}
for (final Runnable callback : callbacks) {
try {
callback.run();
} catch (final Throwable t) {
log.debug("Availability callback for {} profiling context failed.", name, t);
}
}
} catch (final Throwable t) {
// toString() because failures here (UnsatisfiedLinkError etc.) often carry no message.
log.info("Deferred {} profiling context labeling not available. {}", name, t.toString());
}
}

/**
* Runs {@code callback} once the real integration is swapped in, or immediately if it already is;
* never runs it if the deferred construction failed.
*/
@Override
public void whenAvailable(final Runnable callback) {
// double-checked: delegate is volatile, so a post-swap caller never takes the lock
if (delegate == ProfilingContextIntegration.NoOp.INSTANCE) {
synchronized (this) {
if (delegate == ProfilingContextIntegration.NoOp.INSTANCE) {
pendingAvailabilityCallbacks.add(callback);
return;
}
}
}
callback.run();
}

/**
* The name of the deferred integration, not of the current delegate: read once at tracer build
* time, possibly before the deferred construction completes.
*/
@Override
public String name() {
return name;
}

@Override
public void onStart() {
delegate.onStart();
}

@Override
public void onAttach() {
delegate.onAttach();
}

@Override
public void onDetach() {
delegate.onDetach();
}
Comment thread
jandro996 marked this conversation as resolved.

@Override
public boolean isThreadContextBindingRequired() {
return delegate.isThreadContextBindingRequired();
}

@Override
public void setContext(final Context context) {
delegate.setContext(context);
}

@Override
public Stateful newScopeState(final ProfilerContext profilerContext) {
return delegate.newScopeState(profilerContext);
}

@Override
public int encode(final CharSequence constant) {
return delegate.encode(constant);
}

@Override
public int encodeOperationName(final CharSequence constant) {
return delegate.encodeOperationName(constant);
}

@Override
public int encodeResourceName(final CharSequence constant) {
return delegate.encodeResourceName(constant);
}

@Override
public ProfilingContextAttribute createContextAttribute(final String attribute) {
return delegate.createContextAttribute(attribute);
}

@Override
public ProfilingScope newScope() {
return delegate.newScope();
}

@Override
public void onRootSpanFinished(final AgentSpan rootSpan, final EndpointTracker tracker) {
delegate.onRootSpanFinished(rootSpan, tracker);
}

@Override
public EndpointTracker onRootSpanStarted(final AgentSpan rootSpan) {
return delegate.onRootSpanStarted(rootSpan);
}

@Override
public Timing start(final TimerType type) {
return delegate.start(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package datadog.trace.bootstrap;

import static datadog.trace.api.config.AppSecConfig.APPSEC_ENABLED;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_DATADOG_PROFILER_ENABLED;
import static datadog.trace.api.config.ProfilingConfig.PROFILING_ENABLED;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import datadog.trace.api.Config;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import datadog.trace.test.junit.utils.config.WithConfig;
import datadog.trace.test.junit.utils.config.WithConfigExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
* Profiling is explicitly unsupported in AWS Lambda runtimes ({@code Agent#startProfilingAgent}
* bails out there). The AppSec-driven OTel context exposure path must honour the same exclusion, so
* that enabling AppSec inside a Lambda function never loads the ddprof native library.
*
* <p>Forked because {@link WithConfigExtension} swaps the process-wide environment variable
* provider.
*/
@ExtendWith(WithConfigExtension.class)
class AgentLambdaProfilingContextForkedTest {

@Test
@WithConfig(key = APPSEC_ENABLED, value = "true")
@WithConfig(key = PROFILING_ENABLED, value = "false")
@WithConfig(key = PROFILING_DATADOG_PROFILER_ENABLED, value = "true")
@WithConfig(
key = "AWS_LAMBDA_FUNCTION_NAME",
value = "my-function",
env = true,
addPrefix = false)
void doesNotCreateTheDdprofIntegrationInAwsLambda() {
// The exclusion is only observable when the configuration would otherwise have triggered the
// ddprof context integration; the Datadog profiler is vetoed on some platforms and JVMs.
assumeTrue(
Config.get().isOtelThreadContextEnabled(),
"OTel context exposure is unavailable on this platform/JVM version");

// AGENT_CLASSLOADER is null in this unit test, so reaching the ddprof branch at all would fail
// loudly rather than silently return the no-op integration.
assertSame(
ProfilingContextIntegration.NoOp.INSTANCE, Agent.createProfilingContextIntegration());
}
}
Loading
Loading