diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProcessContext.java b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProcessContext.java new file mode 100644 index 00000000000..7eaf7f857e7 --- /dev/null +++ b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProcessContext.java @@ -0,0 +1,36 @@ +package com.datadog.profiling.agent; + +import datadog.libs.ddprof.DdprofLibraryLoader; +import datadog.trace.api.Config; +import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ProcessContext { + private static final Logger log = LoggerFactory.getLogger(ProcessContext.class.getName()); + + public static void register(ConfigProvider configProvider) { + if (configProvider.getBoolean( + ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED, + ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT)) { + log.info("Registering process context for OTel profiler"); + DdprofLibraryLoader.OTelContextHolder holder = DdprofLibraryLoader.otelContext(); + Throwable err = holder.getReasonNotLoaded(); + if (err == null) { + Config cfg = Config.get(); + holder + .getComponent() + .setProcessContext( + cfg.getEnv(), + cfg.getHostName(), + cfg.getRuntimeId(), + cfg.getServiceName(), + cfg.getRuntimeVersion(), + cfg.getVersion()); + } else { + log.warn("Failed to register process context for OTel profiler", err); + } + } + } +} diff --git a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java index 06c03d8906a..e0ef1645509 100644 --- a/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java +++ b/dd-java-agent/agent-profiling/src/main/java/com/datadog/profiling/agent/ProfilingAgent.java @@ -96,6 +96,7 @@ public static synchronized boolean run(final boolean earlyStart, Instrumentation // Register the profiler flare before we start the profiling system, but early during the // profiler lifecycle ProfilerFlareReporter.register(); + ProcessContext.register(configProvider); boolean startForceFirst = Platform.isNativeImage() diff --git a/dd-java-agent/agent-profiling/src/test/java/com/datadog/profiling/agent/ProcessContextTest.java b/dd-java-agent/agent-profiling/src/test/java/com/datadog/profiling/agent/ProcessContextTest.java new file mode 100644 index 00000000000..4c3e3535ee2 --- /dev/null +++ b/dd-java-agent/agent-profiling/src/test/java/com/datadog/profiling/agent/ProcessContextTest.java @@ -0,0 +1,124 @@ +package com.datadog.profiling.agent; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datadoghq.profiler.OTelContext; +import datadog.libs.ddprof.DdprofLibraryLoader; +import datadog.trace.api.Config; +import datadog.trace.api.config.ProfilingConfig; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +class ProcessContextTest { + + @Test + void testRegisterSetsProcessContextValues() { + ConfigProvider configProvider = mock(ConfigProvider.class); + when(configProvider.getBoolean( + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED), + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT))) + .thenReturn(true); + + Config config = mock(Config.class); + when(config.getEnv()).thenReturn("test-env"); + when(config.getHostName()).thenReturn("test-host"); + when(config.getRuntimeId()).thenReturn("test-runtime-id"); + when(config.getServiceName()).thenReturn("test-service"); + when(config.getRuntimeVersion()).thenReturn("test-runtime-version"); + when(config.getVersion()).thenReturn("test-version"); + + OTelContext otelContext = mock(OTelContext.class); + DdprofLibraryLoader.OTelContextHolder holder = + mock(DdprofLibraryLoader.OTelContextHolder.class); + when(holder.getReasonNotLoaded()).thenReturn(null); + when(holder.getComponent()).thenReturn(otelContext); + + try (MockedStatic configMock = mockStatic(Config.class); + MockedStatic ddprofMock = mockStatic(DdprofLibraryLoader.class)) { + + configMock.when(Config::get).thenReturn(config); + ddprofMock.when(DdprofLibraryLoader::otelContext).thenReturn(holder); + + ProcessContext.register(configProvider); + + verify(otelContext) + .setProcessContext( + eq("test-env"), + eq("test-host"), + eq("test-runtime-id"), + eq("test-service"), + eq("test-runtime-version"), + eq("test-version")); + } + } + + @Test + void testRegisterSkipsWhenDisabled() { + ConfigProvider configProvider = mock(ConfigProvider.class); + when(configProvider.getBoolean( + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED), + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT))) + .thenReturn(false); + + DdprofLibraryLoader.OTelContextHolder holder = + mock(DdprofLibraryLoader.OTelContextHolder.class); + + try (MockedStatic ddprofMock = mockStatic(DdprofLibraryLoader.class)) { + ddprofMock.when(DdprofLibraryLoader::otelContext).thenReturn(holder); + + ProcessContext.register(configProvider); + + verify(holder, org.mockito.Mockito.never()).getReasonNotLoaded(); + verify(holder, org.mockito.Mockito.never()).getComponent(); + } + } + + @Test + void testRegisterSkipsByDefault() { + ConfigProvider configProvider = mock(ConfigProvider.class); + when(configProvider.getBoolean( + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED), + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT))) + .thenReturn(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT); + + DdprofLibraryLoader.OTelContextHolder holder = + mock(DdprofLibraryLoader.OTelContextHolder.class); + + try (MockedStatic ddprofMock = mockStatic(DdprofLibraryLoader.class)) { + ddprofMock.when(DdprofLibraryLoader::otelContext).thenReturn(holder); + + ProcessContext.register(configProvider); + + verify(holder, org.mockito.Mockito.never()).getReasonNotLoaded(); + verify(holder, org.mockito.Mockito.never()).getComponent(); + } + } + + @Test + void testRegisterHandlesLibraryLoadFailure() { + ConfigProvider configProvider = mock(ConfigProvider.class); + when(configProvider.getBoolean( + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED), + eq(ProfilingConfig.PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT))) + .thenReturn(true); + + Throwable loadError = new RuntimeException("Library load failed"); + DdprofLibraryLoader.OTelContextHolder holder = + mock(DdprofLibraryLoader.OTelContextHolder.class); + when(holder.getReasonNotLoaded()).thenReturn(loadError); + + try (MockedStatic ddprofMock = mockStatic(DdprofLibraryLoader.class)) { + ddprofMock.when(DdprofLibraryLoader::otelContext).thenReturn(holder); + + ProcessContext.register(configProvider); + + verify(holder).getReasonNotLoaded(); + verify(holder, org.mockito.Mockito.never()).getComponent(); + } + } +} diff --git a/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java b/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java index 5b8de0cd576..b637dc69309 100644 --- a/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java +++ b/dd-java-agent/ddprof-lib/src/main/java/datadog/libs/ddprof/DdprofLibraryLoader.java @@ -2,6 +2,7 @@ import com.datadoghq.profiler.JVMAccess; import com.datadoghq.profiler.JavaProfiler; +import com.datadoghq.profiler.OTelContext; import datadog.trace.api.config.ProfilingConfig; import datadog.trace.bootstrap.config.provider.ConfigProvider; import datadog.trace.util.TempLocationManager; @@ -91,12 +92,25 @@ public JVMAccessHolder(Supplier> initialize } } + public static final class OTelContextHolder extends ComponentHolder { + public OTelContextHolder(Supplier> initializer) { + super(initializer); + } + + OTelContextHolder(OTelContext component, Throwable reasonNotLoaded) { + super(component, reasonNotLoaded); + } + } + private static final JavaProfilerHolder PROFILER_HOLDER = new JavaProfilerHolder(DdprofLibraryLoader::initJavaProfiler); private static final JVMAccessHolder JVM_ACCESS_HOLDER = new JVMAccessHolder(DdprofLibraryLoader::initJVMAccess); + private static final OTelContextHolder OTEL_CONTEXT_HOLDER = + new OTelContextHolder(DdprofLibraryLoader::initOtelContext); + public static JavaProfilerHolder javaProfiler() { return PROFILER_HOLDER; } @@ -105,6 +119,10 @@ public static JVMAccessHolder jvmAccess() { return JVM_ACCESS_HOLDER; } + public static OTelContextHolder otelContext() { + return OTEL_CONTEXT_HOLDER; + } + private static JavaProfilerHolder initJavaProfiler() { JavaProfiler profiler; Throwable reasonNotLoaded = null; @@ -144,6 +162,26 @@ private static JVMAccessHolder initJVMAccess() { return new JVMAccessHolder(jvmAccess, reasonNotLoaded.get()); } + private static OTelContextHolder initOtelContext() { + ConfigProvider configProvider = ConfigProvider.getInstance(); + AtomicReference reasonNotLoaded = new AtomicReference<>(); + OTelContext otelContext = null; + try { + String scratchDir = getScratchDir(configProvider); + otelContext = new OTelContext(null, scratchDir, reasonNotLoaded::set); + } catch (Throwable t) { + if (reasonNotLoaded.get() == null) { + reasonNotLoaded.set(t); + } else { + // if we already have a reason, don't overwrite it + // this can happen if the OTelContext constructor throws an exception + // and then the execute method throws another one + } + otelContext = null; + } + return new OTelContextHolder(otelContext, reasonNotLoaded.get()); + } + private static String getScratchDir(ConfigProvider configProvider) throws IOException { String scratch = configProvider.getString(ProfilingConfig.PROFILING_DATADOG_PROFILER_SCRATCH); if (scratch == null) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java index 7e21ed25309..8944e7a2743 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/ProfilingConfig.java @@ -129,6 +129,10 @@ public final class ProfilingConfig { public static final String PROFILING_DATADOG_PROFILER_SCHEDULING_EVENT_INTERVAL = "profiling.experimental.ddprof.scheduling.event.interval"; + public static final String PROFILING_PROCESS_CONTEXT_ENABLED = + "profiling.experimental.process_context.enabled"; + public static final boolean PROFILING_PROCESS_CONTEXT_ENABLED_DEFAULT = false; + public static final String PROFILING_DATADOG_PROFILER_LOG_LEVEL = "profiling.ddprof.loglevel"; public static final String PROFILING_DATADOG_PROFILER_LOG_LEVEL_DEFAULT = "NONE"; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf5a07c89f3..0046a20f8b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ moshi = '1.11.0' testcontainers = '1.20.1' jmc = "8.1.0" autoservice = "1.1.1" -ddprof = "1.29.0" +ddprof = "1.31.0" asm = "9.8" cafe_crypto = "0.1.0" lz4 = "1.7.1"