-
Notifications
You must be signed in to change notification settings - Fork 150
[tracing] add support for DiagnosticSource for NET Framework (and Quartz) #7687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
7b206eb
c31c9ec
6da47fa
239964a
dcdaacf
6ce1321
14c5303
853b079
04bd08c
221b40e
eadcc6b
a815edd
460a2df
84cc0df
a74b9d3
34125e6
9ce6fc6
35b720b
14a8c0d
b9604e0
ec92938
24463ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| { | ||
| activity.AddTag("operation.name", activity.DisplayName); | ||
| var jobName = activity.Tags.FirstOrDefault(kv => kv.Key == "job.name").Value ?? string.Empty; | ||
|
|
@@ -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) | ||
|
|
@@ -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."); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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<DiagnosticListener> | ||
| /// 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<DiagnosticListener> | ||
| /// 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<DiagnosticListener>, 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; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 anIActivity5.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)
There was a problem hiding this comment.
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!