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
7b206eb
framework snapshot
chojomok Oct 20, 2025
c31c9ec
renaming snapshot
chojomok Oct 20, 2025
6da47fa
create observer via reflection for framework
chojomok Oct 20, 2025
239964a
update diagnostic observer structure and enhance metadata for <IActiv…
chojomok Oct 20, 2025
dcdaacf
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Oct 21, 2025
6ce1321
update coreapp snapshot
chojomok Oct 21, 2025
14c5303
using ducktype for .NET core
chojomok Oct 23, 2025
853b079
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Oct 24, 2025
04bd08c
fix typo
chojomok Oct 24, 2025
221b40e
use reflection + ducktyping to remove reference from DiagnosticListen…
chojomok Oct 24, 2025
eadcc6b
update structure for reflection code and remove if !NETFRAMEWORK dire…
chojomok Oct 24, 2025
a815edd
add diagnosticsource to sample app
chojomok Oct 27, 2025
460a2df
trying to add compatible DiagnosticSource version for framework
chojomok Oct 28, 2025
84cc0df
fix error with minimum diagnosticsource version required for Quartz
chojomok Oct 29, 2025
a74b9d3
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Oct 29, 2025
34125e6
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Oct 30, 2025
9ce6fc6
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Nov 6, 2025
35b720b
add diagnosticmanager for framework
chojomok Nov 10, 2025
14a8c0d
this got skipped somehow?
chojomok Nov 10, 2025
b9604e0
add ai agent documentation for testing the tracer
chojomok Nov 10, 2025
ec92938
Merge branch 'master' into mohammad/diagnosticobserver_netframework_s…
chojomok Nov 10, 2025
24463ae
Update PublicApiTests.Datadog.Trace.AssemblyReferencesHaveNotChanged.…
chojomok Nov 10, 2025
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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ The tracer runs in-process with customer applications and must have minimal perf
- Use struct implementations with generic constraints for zero-allocation production code
- Example: Managed loader tests use `MockEnvironmentVariableProvider` (see `tracer/test/Datadog.Trace.Tests/ClrProfiler/Managed/Loader/`)

📖 **Load when**: Debugging the tracer locally, setting up IDE debugging configurations, or troubleshooting tracer loading issues
- **`docs/development/TracerDebugging.md`** — Local debugging techniques, launchSettings.json configuration, $(SolutionDir) path issues, IDE-specific tips, and troubleshooting common tracer loading problems

## Commit & Pull Request Guidelines

**Commits:**
Expand All @@ -314,6 +317,7 @@ The tracer runs in-process with customer applications and must have minimal perf
**Development guides:**
- `docs/development/AutomaticInstrumentation.md` — Creating integrations
- `docs/development/DuckTyping.md` — Duck typing guide
- `docs/development/TracerDebugging.md` — Local debugging, IDE configuration, path issues, and troubleshooting
- `docs/development/AzureFunctions.md` — Azure Functions integration
- `docs/development/AzureFunctions-Architecture.md` — Azure Functions architecture deep dive
- `docs/development/Serverless.md` — Serverless instrumentation
Expand Down
70 changes: 70 additions & 0 deletions docs/development/TracerDebugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Tracer Debugging Guide

This guide covers how to configure environment variables for debugging the Datadog .NET tracer locally.

## Understanding `$(SolutionDir)` and Path References

When setting up debugging configurations (e.g., in `launchSettings.json` or `.runsettings` files), you may need to reference tracer DLLs and other build artifacts.

**Key Issue:** The `$(SolutionDir)` MSBuild property is **not always defined**, depending on how you're running tests or building:

| Scenario | `$(SolutionDir)` Availability |
|----------|------------------------------|
| Visual Studio (running solution) | ✅ Defined |
| JetBrains Rider | ❌ **Not always defined** |
| Command line: `dotnet test <project>.csproj` | ❌ **Not defined** (no solution context) |
| Command line: `dotnet test <solution>.sln` | ✅ Defined |
| MSBuild with solution | ✅ Defined |

**Recommendation:** For local debugging configurations that you won't commit, use **absolute paths** instead of `$(SolutionDir)` if you find that the tracer isn't loaded into your sample app during testing:

```xml
<!-- ❌ Unreliable - only works in some scenarios -->
<DD_DOTNET_TRACER_HOME>$(SolutionDir)shared\bin\monitoring-home</DD_DOTNET_TRACER_HOME>

<!-- ✅ Reliable - works in all scenarios (for local use only) -->
<DD_DOTNET_TRACER_HOME>C:\Users\your.name\DDRepos\dd-trace-dotnet\shared\bin\monitoring-home</DD_DOTNET_TRACER_HOME>
```

⚠️ **Important:** Never commit absolute paths containing your personal username or machine-specific paths (e.g., `C:\Users\john.doe\...`), as they won't work for other developers.

## Platform-Specific Paths

**Windows:**
```json
"DD_DOTNET_TRACER_HOME": "C:\\Users\\your.name\\DDRepos\\dd-trace-dotnet\\shared\\bin\\monitoring-home",
"CORECLR_PROFILER_PATH": "C:\\Users\\your.name\\DDRepos\\dd-trace-dotnet\\shared\\bin\\monitoring-home\\win-x64\\Datadog.Trace.ClrProfiler.Native.dll"
```

**Linux:**
```json
"DD_DOTNET_TRACER_HOME": "/home/your.name/DDRepos/dd-trace-dotnet/shared/bin/monitoring-home",
"CORECLR_PROFILER_PATH": "/home/your.name/DDRepos/dd-trace-dotnet/shared/bin/monitoring-home/linux-x64/Datadog.Trace.ClrProfiler.Native.so"
```

**macOS:**
```json
"DD_DOTNET_TRACER_HOME": "/Users/your.name/DDRepos/dd-trace-dotnet/shared/bin/monitoring-home",
"CORECLR_PROFILER_PATH": "/Users/your.name/DDRepos/dd-trace-dotnet/shared/bin/monitoring-home/osx-x64/Datadog.Trace.ClrProfiler.Native.dylib"
```

## Required Environment Variables

When debugging with the tracer locally, set these environment variables:

### Core Variables (Required)
- `DD_DOTNET_TRACER_HOME` - Path to the monitoring-home directory
- `CORECLR_ENABLE_PROFILING=1` - Enable CLR profiling (use `COR_ENABLE_PROFILING` for .NET Framework)
- `CORECLR_PROFILER={846F5F1C-F9AE-4B07-969E-05C26BC060D8}` - Tracer profiler GUID (use `COR_PROFILER` for .NET Framework)
- `CORECLR_PROFILER_PATH` - Path to native profiler DLL/SO (use `COR_PROFILER_PATH` for .NET Framework)

### Optional Variables
- `DD_TRACE_DEBUG=1` - Enable verbose debug logging
- `DD_TRACE_LOG_DIRECTORY=/path/to/logs` - Log output directory

## Related Documentation

- [AutomaticInstrumentation.md](AutomaticInstrumentation.md) - Creating and testing integrations
- [DuckTyping.md](DuckTyping.md) - Duck typing patterns for instrumentation
- [../CONTRIBUTING.md](../CONTRIBUTING.md) - General contribution guidelines
- [../../tracer/README.MD](../../tracer/README.MD) - Build and development setup
1 change: 0 additions & 1 deletion tracer/missing-nullability-files.csv
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ src/Datadog.Trace/DatabaseMonitoring/MultiPartIdentifier.cs
src/Datadog.Trace/DatabaseMonitoring/VendoredSqlHelpers.cs
src/Datadog.Trace/DataStreamsMonitoring/CheckpointKind.cs
src/Datadog.Trace/DiagnosticListeners/AspNetCoreDiagnosticObserver.cs
src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs
src/Datadog.Trace/DiagnosticListeners/DiagnosticObserver.cs
src/Datadog.Trace/DiagnosticListeners/EndpointFeatureProxy.cs
src/Datadog.Trace/DiagnosticListeners/IDiagnosticManager.cs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@
<type fullname="System.Diagnostics.ActivityIdFormat" />
<type fullname="System.Diagnostics.ActivitySpanId" />
<type fullname="System.Diagnostics.ActivityTraceId" />
<type fullname="System.Diagnostics.DiagnosticListener" />
<type fullname="System.Diagnostics.DistributedContextPropagator" />
<type fullname="System.Diagnostics.Metrics.Instrument" />
<type fullname="System.Diagnostics.Metrics.MeasurementCallback`1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal static void SetActivityKind(IActivity5 activity)
ActivityListener.SetActivityKind(activity, GetActivityKind(activity));
}

internal static void EnhanceActivityMetadata(IActivity5 activity)
internal static void EnhanceActivityMetadata5(IActivity5 activity)
Copy link
Member

@lucaspimentel lucaspimentel Nov 11, 2025

Choose a reason for hiding this comment

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

I would call this one EnhanceActivity5Metadata() since it's enhancing the metadata on an IActivity5.

But also, does it really need to be renamed, or can we just have two overloads (same name, different parameters)? (not a big deal either way)

EnhanceActivityMetadata(IActivity5 activity)
EnhanceActivityMetadata(IActivity activity)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like the overload idea!

{
activity.AddTag("operation.name", activity.DisplayName);
var jobName = activity.Tags.FirstOrDefault(kv => kv.Key == "job.name").Value ?? string.Empty;
Expand All @@ -54,6 +54,22 @@ internal static void EnhanceActivityMetadata(IActivity5 activity)
activity.DisplayName = CreateResourceName(activity.DisplayName, jobName);
}

internal static void EnhanceActivityMetadata(IActivity activity)
{
if (activity is IActivity5 activity5)
{
EnhanceActivityMetadata5(activity5);
return;
}

if (activity.OperationName is not null)
{
// enhancing span metadata for < IActivity5
activity.AddTag("operation.name", activity.OperationName);
activity.AddTag("resource.name", CreateResourceName(activity.OperationName, activity.Tags.FirstOrDefault(kv => kv.Key == "job.name").Value ?? string.Empty));
}
}

internal static void AddException(object exceptionArg, IActivity activity)
{
if (exceptionArg is not Exception exception)
Expand All @@ -67,5 +83,47 @@ internal static void AddException(object exceptionArg, IActivity activity)
activity.AddTag(Tags.ErrorStack, exception.ToString());
activity.AddTag("otel.status_code", "STATUS_CODE_ERROR");
}

/// <summary>
/// Handles Quartz diagnostic events.
/// This method is shared between the DiagnosticObserver (modern .NET) and reflection-based observer (.NET Framework).
/// </summary>
internal static void HandleDiagnosticEvent(string eventName, object arg)
{
switch (eventName)
{
case "Quartz.Job.Execute.Start":
case "Quartz.Job.Veto.Start":
var activity = ActivityListener.GetCurrentActivity();
if (activity is IActivity5 activity5)
{
SetActivityKind(activity5);
}
else
{
Log.Debug("The activity was not Activity5 (Less than .NET 5.0). Unable to set span kind.");
Copy link
Member

@lucaspimentel lucaspimentel Nov 11, 2025

Choose a reason for hiding this comment

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

I don't think IActivity5 is specific to .NET 5.0 (you can reference a different version of the nuget package), so maybe remove the (Less than .NET 5.0) part.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1. You could note something like "The loaded System.Diagnostics.Activity type does not have a Kind property.".

}

if (activity?.Instance is not null)
{
EnhanceActivityMetadata(activity);
}

break;
case "Quartz.Job.Execute.Stop":
case "Quartz.Job.Veto.Stop":
break;
case "Quartz.Job.Execute.Exception":
case "Quartz.Job.Veto.Exception":
// setting an exception manually
var closingActivity = ActivityListener.GetCurrentActivity();
if (closingActivity?.Instance is not null)
{
AddException(arg, closingActivity);
}

break;
}
}
}
}
7 changes: 3 additions & 4 deletions tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,6 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null)
Log.Error(ex, "Error initializing Security");
}

#if !NETFRAMEWORK
try
{
if (GlobalSettings.Instance.DiagnosticSourceEnabled)
Expand All @@ -339,7 +338,7 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null)
{
// ignore
}

#if !NETFRAMEWORK
// we only support Service Fabric Service Remoting instrumentation on .NET Core (including .NET 5+)
if (FrameworkDescription.Instance.IsCoreClr())
{
Expand Down Expand Up @@ -465,7 +464,6 @@ private static void InitializeTracer(Stopwatch sw)
}
}

#if !NETFRAMEWORK
private static void StartDiagnosticManager()
{
var observers = new List<DiagnosticObserver>();
Expand All @@ -484,15 +482,16 @@ private static void StartDiagnosticManager()
}
else
{
#if !NETFRAMEWORK
observers.Add(new AspNetCoreDiagnosticObserver());
#endif
observers.Add(new QuartzDiagnosticObserver());
}

var diagnosticManager = new DiagnosticManager(observers);
diagnosticManager.Start();
DiagnosticManager.Instance = diagnosticManager;
}
#endif

private static void InitializeDebugger(TracerSettings tracerSettings)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// <copyright file="DiagnosticListenerObserverFactory.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;
using System.Reflection;
using System.Reflection.Emit;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Logging;

namespace Datadog.Trace.DiagnosticListeners
{
/// <summary>
/// This is code written by Cursor to do the reflection needed for DiagnosticListener.
Copy link
Member

Choose a reason for hiding this comment

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

Not a blocker, but I don't think we need to starting adding comments everywhere we generated code with AI assistance.

/// Factory for creating dynamic observer types that can subscribe to DiagnosticListener.AllListeners.
/// Uses Reflection.Emit to generate types at runtime that implement IObserver&lt;DiagnosticListener&gt;
/// without directly referencing the DiagnosticSource assembly.
/// </summary>
internal static class DiagnosticListenerObserverFactory
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(DiagnosticListenerObserverFactory));

/// <summary>
/// Creates a dynamic type that implements IObserver&lt;DiagnosticListener&gt;
/// to receive notifications about new DiagnosticListeners.
/// </summary>
/// <param name="diagnosticListenerType">The Type of DiagnosticListener obtained via reflection</param>
/// <returns>A dynamically created Type that implements IObserver&lt;DiagnosticListener&gt;, or null if creation fails</returns>
public static Type? CreateObserverType(Type diagnosticListenerType)
{
try
{
// Get the IObserver<DiagnosticListener> type
var observerType = typeof(IObserver<>).MakeGenericType(diagnosticListenerType);

// Create a dynamic assembly to hold our observer type
var assemblyName = new AssemblyName("Datadog.DiagnosticManager.Dynamic");
assemblyName.Version = typeof(DiagnosticManager).Assembly.GetName().Version;
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

// Ensure type visibility for DuckType infrastructure
DuckType.EnsureTypeVisibility(moduleBuilder, typeof(DiagnosticManager));

// Define the observer type that will implement IObserver<DiagnosticListener>
var typeBuilder = moduleBuilder.DefineType(
"DiagnosticListenerObserver",
TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout | TypeAttributes.Sealed,
typeof(object),
new[] { observerType });

// Add a field to hold the DiagnosticManager instance that will handle callbacks
var managerField = typeBuilder.DefineField("_manager", typeof(DiagnosticManager), FieldAttributes.Private | FieldAttributes.InitOnly);

// Define constructor that takes DiagnosticManager as parameter
CreateConstructor(typeBuilder, managerField);

// Define the three IObserver methods
CreateOnCompletedMethod(typeBuilder);
CreateOnErrorMethod(typeBuilder);
var success = CreateOnNextMethod(typeBuilder, managerField, diagnosticListenerType);

if (!success)
{
return null;
}

// Create and return the final type
var createdType = typeBuilder.CreateTypeInfo()?.AsType();
return createdType;
}
catch (Exception ex)
{
Log.Error(ex, "Error creating dynamic DiagnosticListener observer type");
return null;
}
}

/// <summary>
/// Creates the constructor for the observer type.
/// Constructor signature: .ctor(DiagnosticManager manager)
/// </summary>
private static void CreateConstructor(TypeBuilder typeBuilder, FieldInfo managerField)
{
var constructor = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
new[] { typeof(DiagnosticManager) });

var ctorIl = constructor.GetILGenerator();

// Call base object constructor
ctorIl.Emit(OpCodes.Ldarg_0);
var baseConstructor = typeof(object).GetConstructor(Type.EmptyTypes);
if (baseConstructor is null)
{
throw new NullReferenceException("Could not get Object constructor.");
}

ctorIl.Emit(OpCodes.Call, baseConstructor);

// Store the manager field: this._manager = manager;
ctorIl.Emit(OpCodes.Ldarg_0);
ctorIl.Emit(OpCodes.Ldarg_1);
ctorIl.Emit(OpCodes.Stfld, managerField);
ctorIl.Emit(OpCodes.Ret);
}

/// <summary>
/// Creates the OnCompleted method (no-op implementation).
/// </summary>
private static void CreateOnCompletedMethod(TypeBuilder typeBuilder)
{
var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig;
var onCompletedMethod = typeBuilder.DefineMethod("OnCompleted", methodAttributes, typeof(void), Type.EmptyTypes);
var il = onCompletedMethod.GetILGenerator();
il.Emit(OpCodes.Ret);
}

/// <summary>
/// Creates the OnError method (no-op implementation).
/// </summary>
private static void CreateOnErrorMethod(TypeBuilder typeBuilder)
{
var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig;
var onErrorMethod = typeBuilder.DefineMethod("OnError", methodAttributes, typeof(void), new[] { typeof(Exception) });
var il = onErrorMethod.GetILGenerator();
il.Emit(OpCodes.Ret);
}

/// <summary>
/// Creates the OnNext method that forwards to DiagnosticManager.OnDiagnosticListenerNext.
/// Method signature: void OnNext(DiagnosticListener listener)
/// Implementation: this._manager.OnDiagnosticListenerNext(listener);
/// </summary>
private static bool CreateOnNextMethod(TypeBuilder typeBuilder, FieldInfo managerField, Type diagnosticListenerType)
{
var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig;

// Find the callback method on DiagnosticManager
var onDiagnosticListenerNextMethodInfo = typeof(DiagnosticManager).GetMethod(
nameof(DiagnosticManager.OnDiagnosticListenerNext),
BindingFlags.Instance | BindingFlags.NonPublic);

if (onDiagnosticListenerNextMethodInfo == null)
{
Log.Warning("Unable to find OnDiagnosticListenerNext method on DiagnosticManager");
return false;
}

// Define OnNext method
var onNextMethod = typeBuilder.DefineMethod("OnNext", methodAttributes, typeof(void), new[] { diagnosticListenerType });
var il = onNextMethod.GetILGenerator();

// Generate: this._manager.OnDiagnosticListenerNext(listener);
il.Emit(OpCodes.Ldarg_0); // Load 'this'
il.Emit(OpCodes.Ldfld, managerField); // Load this._manager
il.Emit(OpCodes.Ldarg_1); // Load the listener parameter
il.EmitCall(OpCodes.Callvirt, onDiagnosticListenerNextMethodInfo, null); // Call the method
il.Emit(OpCodes.Ret); // Return

return true;
}
}
}
Loading
Loading