Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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)) {
Comment on lines +14 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

❔ question: ‏Is there a reason to use ConfigProvider instead of creating an entry into Config for the new PROFILING_PROCESS_CONTEXT_ENABLED? Is that because it's an experimental flag only that won't stay for long?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. That's the main reason.
Also, Config is a kind of abomination in its current form :) Is there anything particularly important I am missing by using the ConfigProvider directly?

Copy link

Choose a reason for hiding this comment

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

I tried answering the same question when looking at the flare reporter & came to the conclusion that anything profiling-specific should be dumped in ProfilingConfig, while features that could/should affect other components are better off in Config. I think given the experimental nature & profiling-specific func., we should keep it here for now.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything particularly important I am missing by using the ConfigProvider directly?

Not really. Telemetry and config inversion will still work using ConfigProvider.
It might be a bit harder (not even sure) for newcomers to find out where is config is setup if it's handled on the side.

We really failed at providing useful / helpful config API so that's totally fine as it is right now. Maybe at some point we should advocate for product to create their own Config when it does not interact with the others.

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link

Choose a reason for hiding this comment

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

Do we want to report any issues with registration via the flare or telemetry similar to what's currently in the exception handling blocks? I see the registration method itself does some warn-level logging, the extended reporting could live there as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we will add the logging once the upstream is finalized.
As it is now, this is a very raw PoC that is disabled by default, the config key is not published anywhere and it directly spells 'experimental', so whoever enables this are on their own.
Once this becomes a product feature the logs should be treated as a part of initialization and saved for profiler flare.


boolean startForceFirst =
Platform.isNativeImage()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Config> configMock = mockStatic(Config.class);
MockedStatic<DdprofLibraryLoader> 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<DdprofLibraryLoader> 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<DdprofLibraryLoader> 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<DdprofLibraryLoader> ddprofMock = mockStatic(DdprofLibraryLoader.class)) {
ddprofMock.when(DdprofLibraryLoader::otelContext).thenReturn(holder);

ProcessContext.register(configProvider);

verify(holder).getReasonNotLoaded();
verify(holder, org.mockito.Mockito.never()).getComponent();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,12 +92,25 @@ public JVMAccessHolder(Supplier<? extends ComponentHolder<JVMAccess>> initialize
}
}

public static final class OTelContextHolder extends ComponentHolder<OTelContext> {
public OTelContextHolder(Supplier<? extends ComponentHolder<OTelContext>> 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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -144,6 +162,26 @@ private static JVMAccessHolder initJVMAccess() {
return new JVMAccessHolder(jvmAccess, reasonNotLoaded.get());
}

private static OTelContextHolder initOtelContext() {
ConfigProvider configProvider = ConfigProvider.getInstance();
AtomicReference<Throwable> reasonNotLoaded = new AtomicReference<>();
OTelContext otelContext = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

❔ question: ‏Did they name it using the same name / semantic than their Context API? Aren't we be confused with the existing datadog.opentelemetry.shim.context.OtelContext from the Context API (or even datadog.opentelemetry.shim.trace.OtelSpanContext / datadog.trace.instrumentation.opentelemetry.OtelSpanContext from the Tracing API)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a good question.
There is no explicit naming anywhere, AFAICT

Go went with 'process context' (DataDog/dd-trace-go#3937) - but, the long-term plan is to support both process-level as well as thread-level context transfer to the ebpf profiler. So, I decided to use OTelContext as the wrapper for accessing the native functionality related to that concept. Only later I found the OtelContext shim :(

This is all still in progress - if this is confusing we can change the epbf context propagator native wrapper class name to something else, before making this GA.

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading