Skip to content

Commit d0fe312

Browse files
make DD_DOTNET_TRACER_HOME optional in managed loader (#7568)
~_This PR is stacked on #7594. Merge that PR first._~ ## Summary of changes Make `DD_DOTNET_TRACER_HOME` optional. If not set, try to figure out the path from `COR_PROFILER_PATH`/`CORECLR_PROFILER_PATH` and friends. ## Reason for change One less env var for users to set manually. Easier onboarding, less error-prone. I recently worked on an escalation where `DD_DOTNET_TRACER_HOME` was set to the wrong path so the tracer was not loading. ## Implementation details The implementation makes `DD_DOTNET_TRACER_HOME` optional by adding fallback logic to derive the tracer home path from the profiler path environment variables: 1. **New method `GetTracerHomePath`** (`Startup.cs:150-178`): - First checks for explicit `DD_DOTNET_TRACER_HOME` setting (preserves backward compatibility) - Falls back to computing the path from architecture-specific profiler path env vars (e.g., `CORECLR_PROFILER_PATH_64`, `COR_PROFILER_PATH_64`) - If not found, tries the generic profiler path env var (e.g., `CORECLR_PROFILER_PATH`, `COR_PROFILER_PATH`) 2. **New method `ComputeTracerHomePathFromProfilerPath`** (`Startup.cs:180-218`): - Takes a profiler path like `C:\tracer\win-x64\Datadog.Trace.ClrProfiler.Native.dll` - Extracts the parent directory - If the parent directory name matches a known architecture folder (e.g., `win-x64`, `linux-arm64`, `osx`), goes up one more level - Returns the computed tracer home path 3. **Platform-specific helper methods**: - `GetProfilerPathEnvVarNameForArch()`: Returns the architecture-specific env var name (`CORECLR_PROFILER_PATH_64` on .NET Core x64, `COR_PROFILER_PATH_32` on .NET Framework x86, etc.) - `GetProfilerPathEnvVarNameFallback()`: Returns the generic env var name (`CORECLR_PROFILER_PATH` or `COR_PROFILER_PATH`) 4. **Architecture directory detection**: - Maintains a `HashSet<string>` of known architecture directories (`win-x64`, `win-x86`, `linux-x64`, `linux-arm64`, etc) ## Test coverage **Unit tests** (`tracer/test/Datadog.Trace.Tests/ClrProfiler/Managed/Loader/`): - Environment variable reading and fallback logic (`StartupNetCoreTests.cs`, `StartupNetFrameworkTests.cs`) - `GetTracerHomePath()` with explicit `DD_DOTNET_TRACER_HOME` (with/without whitespace) - `GetTracerHomePath()` with architecture-specific profiler path fallback - `GetTracerHomePath()` with generic profiler path fallback - `ComputeTracerHomePathFromProfilerPath()` with all architecture directories - Edge cases: empty values, whitespace, missing variables, null handling **Integration tests**: - Added `WhenOmittingTracerHome_InstrumentsApp()` test in `InstrumentationTests.cs` that explicitly omits `DD_DOTNET_TRACER_HOME` and verifies instrumentation works via fallback logic - Modified all smoke tests (`SmokeTests/SmokeTestBase.cs`) to omit `DD_DOTNET_TRACER_HOME` by default, providing comprehensive real-world validation of the fallback behavior across multiple regression scenarios ## Other details <!-- Fixes #{issue} --> ⚠️ Based on #7567, which was originally [generated by OpenAI Codex](https://chatgpt.com/codex/tasks/task_b_68d59245fcec832180db94dd256130ba), but I made substantial changes to clean up and refactor. <!-- ⚠️ Note: Where possible, please obtain 2 approvals prior to merging. Unless CODEOWNERS specifies otherwise, for external teams it is typically best to have one review from a team member, and one review from apm-dotnet. Trivial changes do not require 2 reviews. MergeQueue is NOT enabled in this repository. If you have write access to the repo, the PR has 1-2 approvals (see above), and all of the required checks have passed, you can use the Squash and Merge button to merge the PR. If you don't have write access, or you need help, reach out in the #apm-dotnet channel in Slack. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bc90bc6 commit d0fe312

File tree

8 files changed

+482
-16
lines changed

8 files changed

+482
-16
lines changed

tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetCore.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Collections.Generic;
1212
using System.IO;
1313
using System.Reflection;
14+
using System.Runtime.InteropServices;
1415

1516
namespace Datadog.Trace.ClrProfiler.Managed.Loader
1617
{
@@ -62,6 +63,23 @@ internal static string ComputeTfmDirectory(string tracerHomeDirectory)
6263
return fullPath;
6364
}
6465

66+
internal static string GetProfilerPathEnvVarNameForArch()
67+
{
68+
return RuntimeInformation.ProcessArchitecture switch
69+
{
70+
Architecture.X64 => "CORECLR_PROFILER_PATH_64",
71+
Architecture.X86 => "CORECLR_PROFILER_PATH_32",
72+
Architecture.Arm64 => "CORECLR_PROFILER_PATH_ARM64",
73+
Architecture.Arm => "CORECLR_PROFILER_PATH_ARM",
74+
_ => throw new ArgumentOutOfRangeException(nameof(RuntimeInformation.ProcessArchitecture), RuntimeInformation.ProcessArchitecture, "Unsupported architecture")
75+
};
76+
}
77+
78+
internal static string GetProfilerPathEnvVarNameFallback()
79+
{
80+
return "CORECLR_PROFILER_PATH";
81+
}
82+
6583
private static Assembly? AssemblyResolve_ManagedProfilerDependencies(object sender, ResolveEventArgs args)
6684
{
6785
return ResolveAssembly(args.Name);

tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.NetFramework.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ internal static string ComputeTfmDirectory(string tracerHomeDirectory)
2323
return Path.Combine(Path.GetFullPath(tracerHomeDirectory), "net461");
2424
}
2525

26+
internal static string GetProfilerPathEnvVarNameForArch()
27+
{
28+
return Environment.Is64BitProcess ? "COR_PROFILER_PATH_64" : "COR_PROFILER_PATH_32";
29+
}
30+
31+
internal static string GetProfilerPathEnvVarNameFallback()
32+
{
33+
return "COR_PROFILER_PATH";
34+
}
35+
2636
private static Assembly? AssemblyResolve_ManagedProfilerDependencies(object sender, ResolveEventArgs args)
2737
{
2838
try

tracer/src/Datadog.Trace.ClrProfiler.Managed.Loader/Startup.cs

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#nullable enable
77

88
using System;
9+
using System.Collections.Generic;
910
using System.IO;
1011
using System.Reflection;
1112
using System.Threading;
@@ -21,6 +22,21 @@ public partial class Startup
2122
private const string AzureAppServicesSiteExtensionKey = "DD_AZURE_APP_SERVICES"; // only set when using the AAS site extension
2223
private const string TracerHomePathKey = "DD_DOTNET_TRACER_HOME";
2324

25+
private static readonly HashSet<string> ArchitectureDirectories = new(StringComparer.OrdinalIgnoreCase)
26+
{
27+
"win-x64",
28+
"win-x86",
29+
#if NETCOREAPP
30+
"linux-x64",
31+
"linux-arm64",
32+
"linux-musl-x64",
33+
"linux-musl-arm64",
34+
"osx",
35+
"osx-arm64",
36+
"osx-x64"
37+
#endif
38+
};
39+
2440
private static int _startupCtorInitialized;
2541

2642
/// <summary>
@@ -34,10 +50,9 @@ static Startup()
3450
// Startup() was already called before in the same AppDomain, this can happen because the profiler rewrites
3551
// methods before the jitting to inject the loader. This is done until the profiler detects that the loader
3652
// has been initialized.
37-
// The piece of code injected already includes an Interlocked condition but, because the static variable is emitted
38-
// in a custom type inside the running assembly, others assemblies will also have a different type with a different static
39-
// variable, so, we still can hit an scenario where multiple loaders initialize.
40-
// With this we prevent this scenario.
53+
// The piece of code injected already includes an Interlocked condition. However, because the static variable is emitted
54+
// in a custom type inside the running assembly, other assemblies will also have a different type with a different static
55+
// variable, so we still can hit a scenario where multiple loaders initialize. This prevents this scenario.
4156
return;
4257
}
4358

@@ -58,23 +73,36 @@ static Startup()
5873
#endif
5974

6075
var envVars = new EnvironmentVariableProvider(logErrors: true);
61-
var tracerHomeDirectory = envVars.GetEnvironmentVariable(TracerHomePathKey);
76+
var tracerHomeDirectory = GetTracerHomePath(envVars);
6277

6378
if (tracerHomeDirectory is null)
6479
{
65-
StartupLogger.Log("{0} not set. Datadog SDK will be disabled.", TracerHomePathKey);
80+
// Provide a specific error message based on what was configured
81+
var explicitTracerHome = envVars.GetEnvironmentVariable(TracerHomePathKey);
82+
83+
if (string.IsNullOrWhiteSpace(explicitTracerHome))
84+
{
85+
// DD_DOTNET_TRACER_HOME was not set and automatic detection from profiler path failed
86+
StartupLogger.Log("{0} is not set and the tracer home directory could not be determined automatically. Datadog SDK will be disabled. To resolve this issue, set environment variable {0}.", TracerHomePathKey);
87+
}
88+
else
89+
{
90+
// DD_DOTNET_TRACER_HOME was set but resulted in null (shouldn't happen, but just in case)
91+
StartupLogger.Log("{0} is set to '{1}' but could not be used. Datadog SDK will be disabled.", TracerHomePathKey, explicitTracerHome);
92+
}
93+
6694
return;
6795
}
6896

6997
ManagedProfilerDirectory = ComputeTfmDirectory(tracerHomeDirectory);
7098

7199
if (!Directory.Exists(ManagedProfilerDirectory))
72100
{
73-
StartupLogger.Log("Datadog.Trace.dll TFM directory not found at '{0}'. Datadog SDK will be disabled.", ManagedProfilerDirectory);
101+
StartupLogger.Log("Datadog.Trace.dll directory not found at '{0}'. Datadog SDK will be disabled.", ManagedProfilerDirectory);
74102
return;
75103
}
76104

77-
StartupLogger.Debug("Resolved Datadog.Trace.dll TFM directory to: {0}", ManagedProfilerDirectory);
105+
StartupLogger.Debug("Resolved Datadog.Trace.dll directory to: {0}", ManagedProfilerDirectory);
78106

79107
try
80108
{
@@ -125,13 +153,89 @@ static Startup()
125153
// Nothing to do here.
126154
}
127155

128-
// If the logger fails, throw the original exception. The profiler emits code to log it.
156+
// If the logger fails, throw the original exception. The native library emits code to log it.
129157
throw;
130158
}
131159
}
132160

133161
internal static string? ManagedProfilerDirectory { get; }
134162

163+
internal static string? GetTracerHomePath<TEnvVars>(TEnvVars envVars)
164+
where TEnvVars : IEnvironmentVariableProvider
165+
{
166+
// allow override with DD_DOTNET_TRACER_HOME
167+
var tracerHomeDirectory = envVars.GetEnvironmentVariable(TracerHomePathKey);
168+
169+
if (!string.IsNullOrWhiteSpace(tracerHomeDirectory))
170+
{
171+
// Safe to use ! here because we just checked !string.IsNullOrWhiteSpace above
172+
var trimmedPath = tracerHomeDirectory!.Trim();
173+
StartupLogger.Debug("Using tracer home from {0}=\"{1}\"", TracerHomePathKey, trimmedPath);
174+
return trimmedPath;
175+
}
176+
177+
// try to compute the path from the architecture-specific "COR_PROFILER_PATH_*" or "CORECLR_PROFILER_PATH_*"
178+
var archEnvVarName = GetProfilerPathEnvVarNameForArch();
179+
180+
if (ComputeTracerHomePathFromProfilerPath(envVars, archEnvVarName) is { } archTracerHomePath)
181+
{
182+
StartupLogger.Debug("Derived tracer home from {0}=\"{1}\"", archEnvVarName, archTracerHomePath);
183+
return archTracerHomePath;
184+
}
185+
186+
// try to compute the path from "COR_PROFILER_PATH" or "CORECLR_PROFILER_PATH" (no architecture)
187+
var fallbackEnvVarName = GetProfilerPathEnvVarNameFallback();
188+
189+
if (ComputeTracerHomePathFromProfilerPath(envVars, fallbackEnvVarName) is { } fallbackTracerHomePath)
190+
{
191+
StartupLogger.Debug("Derived tracer home from {0}=\"{1}\"", fallbackEnvVarName, fallbackTracerHomePath);
192+
return fallbackTracerHomePath;
193+
}
194+
195+
return null;
196+
}
197+
198+
internal static string? ComputeTracerHomePathFromProfilerPath<TEnvVars>(TEnvVars envVars, string envVarName)
199+
where TEnvVars : IEnvironmentVariableProvider
200+
{
201+
var envVarValue = envVars.GetEnvironmentVariable(envVarName)?.Trim();
202+
203+
if (string.IsNullOrWhiteSpace(envVarValue))
204+
{
205+
return null;
206+
}
207+
208+
try
209+
{
210+
var directory = Directory.GetParent(envVarValue);
211+
212+
if (directory is null)
213+
{
214+
StartupLogger.Log("Unable to determine tracer home directory from {0}={1}", envVarName, envVarValue);
215+
return null;
216+
}
217+
218+
// if the directory name is one of the well-known "os-arch" child directories (e.g. "win-x64"), go one level higher
219+
if (ArchitectureDirectories.Contains(directory.Name))
220+
{
221+
directory = directory.Parent;
222+
223+
if (directory is null)
224+
{
225+
StartupLogger.Log("Unable to determine tracer home directory from {0}={1}", envVarName, envVarValue);
226+
return null;
227+
}
228+
}
229+
230+
return directory.FullName;
231+
}
232+
catch (Exception ex)
233+
{
234+
StartupLogger.Log(ex, "Error resolving tracer home directory from {0}={1}", envVarName, envVarValue);
235+
return null;
236+
}
237+
}
238+
135239
private static void TryInvokeManagedMethod(string typeName, string methodName, string? loaderHelperTypeName = null)
136240
{
137241
try

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/InstrumentationTests.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,25 @@ public async Task WhenUsingPathWithDotsInInTracerHome_InstrumentsApp()
251251
agent.Telemetry.Should().NotBeEmpty();
252252
}
253253

254+
[SkippableFact]
255+
[Trait("RunOnWindows", "True")]
256+
public async Task WhenOmittingTracerHome_InstrumentsApp()
257+
{
258+
// Verify that DD_DOTNET_TRACER_HOME is not set to ensure we're actually testing the fallback behavior
259+
EnvironmentHelper.CustomEnvironmentVariables.Should().NotContainKey("DD_DOTNET_TRACER_HOME");
260+
Environment.GetEnvironmentVariable("DD_DOTNET_TRACER_HOME").Should().BeNullOrEmpty();
261+
262+
SetLogDirectory();
263+
264+
// DD_DOTNET_TRACER_HOME is not set, so the tracer should derive it from the profiler path
265+
Output.WriteLine("DD_DOTNET_TRACER_HOME not set, relying on profiler path environment variables");
266+
267+
using var agent = EnvironmentHelper.GetMockAgent(useTelemetry: true);
268+
using var processResult = await RunSampleAndWaitForExit(agent, "traces 1");
269+
agent.Spans.Should().NotBeEmpty();
270+
agent.Telemetry.Should().NotBeEmpty();
271+
}
272+
254273
[SkippableTheory]
255274
[CombinatorialData]
256275
[Trait("RunOnWindows", "True")]
@@ -426,7 +445,7 @@ public async Task OnEolFrameworkInSsi_WhenForwarderPathExists_CallsForwarderWith
426445

427446
var pointsJson = """
428447
[{
429-
"name": "library_entrypoint.abort",
448+
"name": "library_entrypoint.abort",
430449
"tags": ["reason:eol_runtime"]
431450
},{
432451
"name": "library_entrypoint.abort.runtime"
@@ -459,7 +478,7 @@ public async Task OnEolFrameworkInSsi_WhenOverriden_CallsForwarderWithExpectedTe
459478

460479
var pointsJson = """
461480
[{
462-
"name": "library_entrypoint.complete",
481+
"name": "library_entrypoint.complete",
463482
"tags": ["injection_forced:true"]
464483
}]
465484
""";
@@ -498,7 +517,7 @@ public async Task OnPreviewFrameworkInSsi_CallsForwarderWithExpectedTelemetry()
498517

499518
var pointsJson = """
500519
[{
501-
"name": "library_entrypoint.complete",
520+
"name": "library_entrypoint.complete",
502521
"tags": ["injection_forced:true"]
503522
}]
504523
""";
@@ -529,7 +548,7 @@ public async Task OnPreviewFrameworkInSsi_WhenForwarderPathExists_CallsForwarder
529548

530549
var pointsJson = """
531550
[{
532-
"name": "library_entrypoint.abort",
551+
"name": "library_entrypoint.abort",
533552
"tags": ["reason:incompatible_runtime"]
534553
},{
535554
"name": "library_entrypoint.abort.runtime"
@@ -588,7 +607,7 @@ public async Task OnSupportedFrameworkInSsi_CallsForwarderWithExpectedTelemetry(
588607

589608
var pointsJson = """
590609
[{
591-
"name": "library_entrypoint.complete",
610+
"name": "library_entrypoint.complete",
592611
"tags": ["injection_forced:false"]
593612
}]
594613
""";

tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/SmokeTests/SmokeTestBase.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ protected SmokeTestBase(
2929
this.GetType(),
3030
output,
3131
samplesDirectory: "test/test-applications/regression",
32-
prependSamplesToAppName: false);
32+
prependSamplesToAppName: false)
33+
{
34+
// Don't set DD_DOTNET_TRACER_HOME in smoke tests to verify the fallback logic works
35+
SetTracerHomeEnvironmentVariable = false
36+
};
3337
}
3438

3539
protected ITestOutputHelper Output { get; }

tracer/test/Datadog.Trace.TestHelpers/EnvironmentHelper.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public EnvironmentHelper(
7070

7171
public bool DebugModeEnabled { get; set; }
7272

73+
public bool SetTracerHomeEnvironmentVariable { get; set; } = true;
74+
7375
public Dictionary<string, string> CustomEnvironmentVariables { get; set; } = new Dictionary<string, string>();
7476

7577
public string SampleName { get; }
@@ -196,7 +198,11 @@ public void SetEnvironmentVariables(
196198
bool ignoreProfilerProcessesVar = false)
197199
{
198200
string profilerEnabled = AutomaticInstrumentationEnabled ? "1" : "0";
199-
environmentVariables["DD_DOTNET_TRACER_HOME"] = MonitoringHome;
201+
202+
if (SetTracerHomeEnvironmentVariable)
203+
{
204+
environmentVariables["DD_DOTNET_TRACER_HOME"] = MonitoringHome;
205+
}
200206

201207
// see https://github.com/DataDog/dd-trace-dotnet/pull/3579
202208
environmentVariables["DD_INTERNAL_WORKAROUND_77973_ENABLED"] = "1";

0 commit comments

Comments
 (0)