diff --git a/AGENTS.md b/AGENTS.md index c26bdfbc1d87..bbb6eac01884 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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:** @@ -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 diff --git a/docs/development/TracerDebugging.md b/docs/development/TracerDebugging.md new file mode 100644 index 000000000000..9b4de387bc59 --- /dev/null +++ b/docs/development/TracerDebugging.md @@ -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 .csproj` | ❌ **Not defined** (no solution context) | +| Command line: `dotnet test .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 + +$(SolutionDir)shared\bin\monitoring-home + + +C:\Users\your.name\DDRepos\dd-trace-dotnet\shared\bin\monitoring-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 diff --git a/tracer/missing-nullability-files.csv b/tracer/missing-nullability-files.csv index 0e16a61fbc1e..510614da09d0 100644 --- a/tracer/missing-nullability-files.csv +++ b/tracer/missing-nullability-files.csv @@ -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 diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index 6611cdd628ff..afdee4977f8d 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -244,7 +244,6 @@ - diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs index ac162a55cd92..b53998357695 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs @@ -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"); } + + /// + /// Handles Quartz diagnostic events. + /// This method is shared between the DiagnosticObserver (modern .NET) and reflection-based observer (.NET Framework). + /// + 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."); + } + + 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; + } + } } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs index a46ddcb7f4e4..ae8453f0b200 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs @@ -316,7 +316,6 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null) Log.Error(ex, "Error initializing Security"); } -#if !NETFRAMEWORK try { if (GlobalSettings.Instance.DiagnosticSourceEnabled) @@ -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()) { @@ -465,7 +464,6 @@ private static void InitializeTracer(Stopwatch sw) } } -#if !NETFRAMEWORK private static void StartDiagnosticManager() { var observers = new List(); @@ -484,7 +482,9 @@ private static void StartDiagnosticManager() } else { +#if !NETFRAMEWORK observers.Add(new AspNetCoreDiagnosticObserver()); +#endif observers.Add(new QuartzDiagnosticObserver()); } @@ -492,7 +492,6 @@ private static void StartDiagnosticManager() diagnosticManager.Start(); DiagnosticManager.Instance = diagnosticManager; } -#endif private static void InitializeDebugger(TracerSettings tracerSettings) { diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticListenerObserverFactory.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticListenerObserverFactory.cs new file mode 100644 index 000000000000..f9b1dbbc0102 --- /dev/null +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticListenerObserverFactory.cs @@ -0,0 +1,168 @@ +// +// 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. +// + +#nullable enable + +using System; +using System.Reflection; +using System.Reflection.Emit; +using Datadog.Trace.DuckTyping; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.DiagnosticListeners +{ + /// + /// This is code written by Cursor to do the reflection needed for DiagnosticListener. + /// 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. + /// + internal static class DiagnosticListenerObserverFactory + { + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(DiagnosticListenerObserverFactory)); + + /// + /// Creates a dynamic type that implements IObserver<DiagnosticListener> + /// to receive notifications about new DiagnosticListeners. + /// + /// The Type of DiagnosticListener obtained via reflection + /// A dynamically created Type that implements IObserver<DiagnosticListener>, or null if creation fails + public static Type? CreateObserverType(Type diagnosticListenerType) + { + try + { + // Get the IObserver 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 + 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; + } + } + + /// + /// Creates the constructor for the observer type. + /// Constructor signature: .ctor(DiagnosticManager manager) + /// + 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); + } + + /// + /// Creates the OnCompleted method (no-op implementation). + /// + 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); + } + + /// + /// Creates the OnError method (no-op implementation). + /// + 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); + } + + /// + /// Creates the OnNext method that forwards to DiagnosticManager.OnDiagnosticListenerNext. + /// Method signature: void OnNext(DiagnosticListener listener) + /// Implementation: this._manager.OnDiagnosticListenerNext(listener); + /// + 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; + } + } +} diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs index fecabd43140c..ab3136739531 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs @@ -3,23 +3,27 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -#if !NETFRAMEWORK +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics; +using System.Reflection; +using Datadog.Trace.DiagnosticListeners.DuckTypes; +using Datadog.Trace.DuckTyping; using Datadog.Trace.Logging; using Datadog.Trace.Util; using Datadog.Trace.Vendors.Serilog.Events; namespace Datadog.Trace.DiagnosticListeners { - internal sealed class DiagnosticManager : IDiagnosticManager, IObserver, IDisposable + internal sealed class DiagnosticManager : IDiagnosticManager, IObserver, IDisposable { private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); private readonly IEnumerable _diagnosticObservers; private readonly List _subscriptions = new List(); - private IDisposable _allListenersSubscription; + private IDisposable? _allListenersSubscription; public DiagnosticManager(IEnumerable diagnosticSubscribers) { @@ -31,7 +35,7 @@ public DiagnosticManager(IEnumerable diagnosticSubscribers) _diagnosticObservers = diagnosticSubscribers; } - public static DiagnosticManager Instance { get; set; } + public static DiagnosticManager? Instance { get; set; } public bool IsRunning => _allListenersSubscription != null; @@ -40,19 +44,76 @@ public void Start() if (_allListenersSubscription == null) { Log.Debug("Starting DiagnosticListener.AllListeners subscription"); - _allListenersSubscription = DiagnosticListener.AllListeners.Subscribe(this); + + try + { + // Get the DiagnosticListener type + var diagnosticListenerType = Type.GetType("System.Diagnostics.DiagnosticListener, System.Diagnostics.DiagnosticSource"); + if (diagnosticListenerType == null) + { + Log.Warning("Unable to find DiagnosticListener type"); + return; + } + + // Get the AllListeners static property + var allListenersProperty = diagnosticListenerType.GetProperty("AllListeners", BindingFlags.Public | BindingFlags.Static); + if (allListenersProperty == null) + { + Log.Warning("Unable to find DiagnosticListener.AllListeners property"); + return; + } + + // Get the value (IObservable) + var allListenersObservable = allListenersProperty.GetValue(null); + if (allListenersObservable == null) + { + Log.Warning("DiagnosticListener.AllListeners returned null"); + return; + } + + // Create a dynamic type that implements IObserver + var observerType = DiagnosticListenerObserverFactory.CreateObserverType(diagnosticListenerType); + if (observerType == null) + { + Log.Warning("Failed to create dynamic observer type"); + return; + } + + // Create an instance of the dynamic observer, passing this manager instance + var observerInstance = Activator.CreateInstance(observerType, this); + if (observerInstance == null) + { + Log.Warning("Failed to create observer instance"); + return; + } + + // Use reflection to call Subscribe with the observer + var subscribeMethod = allListenersProperty.PropertyType.GetMethod("Subscribe"); + if (subscribeMethod == null) + { + Log.Warning("Unable to find Subscribe method on AllListeners"); + return; + } + + _allListenersSubscription = (IDisposable?)subscribeMethod.Invoke(allListenersObservable, new object[] { observerInstance }); + Log.Debug("Successfully subscribed to DiagnosticListener.AllListeners"); + } + catch (Exception ex) + { + Log.Error(ex, "Error starting DiagnosticListener.AllListeners subscription"); + } } } - void IObserver.OnCompleted() + void IObserver.OnCompleted() { } - void IObserver.OnError(Exception error) + void IObserver.OnError(Exception error) { } - void IObserver.OnNext(DiagnosticListener listener) + void IObserver.OnNext(IDiagnosticListener listener) { foreach (var subscriber in _diagnosticObservers) { @@ -103,6 +164,27 @@ public void Dispose() { Stop(); } + + /// + /// Called when a new DiagnosticListener is created. + /// This method is called by the dynamically created observer type via reflection. + /// + /// The DiagnosticListener instance (actual System.Diagnostics type) + internal void OnDiagnosticListenerNext(object diagnosticListener) + { + try + { + // Duck type the actual DiagnosticListener to our interface + var listener = diagnosticListener.DuckAs(); + if (listener?.Instance != null) + { + ((IObserver)this).OnNext(listener); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error handling DiagnosticListener notification"); + } + } } } -#endif diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserver.cs index 470938a15cb9..226152d25996 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserver.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserver.cs @@ -3,11 +3,11 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -#if !NETFRAMEWORK using System; using System.Collections.Generic; using System.Diagnostics; using Datadog.Trace.AppSec; +using Datadog.Trace.DiagnosticListeners.DuckTypes; using Datadog.Trace.Logging; namespace Datadog.Trace.DiagnosticListeners @@ -17,9 +17,9 @@ internal abstract class DiagnosticObserver : IObserver(); /// - /// Gets the name of the that should be instrumented. + /// Gets the name of the DiagnosticListener that should be instrumented. /// - /// The name of the that should be instrumented. + /// The name of the DiagnosticListener that should be instrumented. protected abstract string ListenerName { get; } public virtual bool IsSubscriberEnabled() @@ -27,7 +27,7 @@ public virtual bool IsSubscriberEnabled() return true; } - public virtual IDisposable SubscribeIfMatch(DiagnosticListener diagnosticListener) + public virtual IDisposable SubscribeIfMatch(IDiagnosticListener diagnosticListener) { if (diagnosticListener.Name == ListenerName) { @@ -70,4 +70,3 @@ protected virtual bool IsEventEnabled(string eventName) protected abstract void OnNext(string eventName, object arg); } } -#endif diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/DuckTypes/IDiagnosticListener.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DuckTypes/IDiagnosticListener.cs new file mode 100644 index 000000000000..d1aa2ff6360d --- /dev/null +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DuckTypes/IDiagnosticListener.cs @@ -0,0 +1,27 @@ +// +// 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. +// + +using System; +using System.Collections.Generic; +using Datadog.Trace.DuckTyping; + +namespace Datadog.Trace.DiagnosticListeners.DuckTypes; + +#nullable enable +/// +/// Ducktyping for DiagnosticListener +/// +public interface IDiagnosticListener : IDuckType +{ + /// + /// Gets a value of DiagnosticListener.Name + /// + string Name { get; } + + /// + /// Ducktype for Subscribe + /// + IDisposable Subscribe(IObserver> observer, Predicate? isEnabled); +} diff --git a/tracer/src/Datadog.Trace/DiagnosticListeners/QuartzDiagnosticObserver.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/QuartzDiagnosticObserver.cs index db86d8565df6..486b83c1a0f9 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/QuartzDiagnosticObserver.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/QuartzDiagnosticObserver.cs @@ -3,67 +3,28 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // -using System; -using System.Linq; -using Datadog.Trace.Activity; -using Datadog.Trace.Activity.DuckTypes; using Datadog.Trace.ClrProfiler.AutoInstrumentation.Quartz; using Datadog.Trace.Logging; -#nullable enable -// Currently to our DiagnosticObserver class isn't available for .NET Framework. -// Our QuartzDiagnosticObserver only works for .NET Framework due to this limitation -// We are purposely avoiding adding a dependency to System.Diagnostics.DiagnosticSource -#if !NETFRAMEWORK -namespace Datadog.Trace.DiagnosticListeners -{ - /// - /// Instruments Quartz.NET job scheduler. - /// - /// This observer listens to Quartz diagnostic events to trace job execution, - /// scheduling, and other Quartz-related operations. - /// - internal sealed class QuartzDiagnosticObserver : DiagnosticObserver - { - private const string DiagnosticListenerName = "Quartz"; - private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); +#nullable enable - protected override string ListenerName => DiagnosticListenerName; +namespace Datadog.Trace.DiagnosticListeners; - protected override void OnNext(string eventName, object arg) - { - switch (eventName) - { - case "Quartz.Job.Execute.Start": - case "Quartz.Job.Veto.Start": - var currentActivity = ActivityListener.GetCurrentActivity(); - if (currentActivity is IActivity5 activity5) - { - QuartzCommon.EnhanceActivityMetadata(activity5); - QuartzCommon.SetActivityKind(activity5); - } - else - { - Log.Debug("The activity was not Activity5 (Less than .NET 5.0). Unable enhance the span metadata."); - } +/// +/// Instruments Quartz.NET job scheduler. +/// +/// This observer listens to Quartz diagnostic events to trace job execution, +/// scheduling, and other Quartz-related operations. +/// +internal sealed class QuartzDiagnosticObserver : DiagnosticObserver +{ + private const string DiagnosticListenerName = "Quartz"; + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); - 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) - { - QuartzCommon.AddException(arg, closingActivity); - } + protected override string ListenerName => DiagnosticListenerName; - break; - } - } + protected override void OnNext(string eventName, object arg) + { + QuartzCommon.HandleDiagnosticEvent(eventName, arg); } } - -#endif diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/QuartzTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/QuartzTests.cs index 61be93880608..568cbb83dd2f 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/QuartzTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/QuartzTests.cs @@ -50,9 +50,6 @@ public static IEnumerable GetData() [MemberData(nameof(GetData))] public async Task SubmitsTraces(string packageVersion) { -#if NETFRAMEWORK - Skip.If(true, "Quartz instrumentation is not supported on .NET Framework - DiagnosticObserver infrastructure is excluded with #if !NETFRAMEWORK"); -#endif SetEnvironmentVariable("DD_TRACE_OTEL_ENABLED", "true"); using (var telemetry = this.ConfigureTelemetry()) @@ -104,6 +101,8 @@ private static Tuple GetSuffix(string packageVersion) { #if NETCOREAPP3_0 || NETCOREAPP3_1 return new("V3NETCOREAPP3X", 2); +#elif NETFRAMEWORK + return new("V3NETFRAMEWORK", 2); #else return new("V3", 2); #endif @@ -114,6 +113,8 @@ private static Tuple GetSuffix(string packageVersion) { } v when v >= new Version("4.0.0") => new("V4", 3), #if NETCOREAPP3_0 || NETCOREAPP3_1 { } v when v >= new Version("3.0.0") => new("V3NETCOREAPP3X", 2), +#elif NETFRAMEWORK + { } v when v >= new Version("3.0.0") => new("V3NETFRAMEWORK", 2), #endif _ => new("V3", 2) }; diff --git a/tracer/test/Datadog.Trace.Tests/Snapshots/PublicApiTests.Datadog.Trace.AssemblyReferencesHaveNotChanged.netstandard2.0.verified.txt b/tracer/test/Datadog.Trace.Tests/Snapshots/PublicApiTests.Datadog.Trace.AssemblyReferencesHaveNotChanged.netstandard2.0.verified.txt index ac9af1c7c5d1..e3903e0a3ca0 100644 --- a/tracer/test/Datadog.Trace.Tests/Snapshots/PublicApiTests.Datadog.Trace.AssemblyReferencesHaveNotChanged.netstandard2.0.verified.txt +++ b/tracer/test/Datadog.Trace.Tests/Snapshots/PublicApiTests.Datadog.Trace.AssemblyReferencesHaveNotChanged.netstandard2.0.verified.txt @@ -6,7 +6,6 @@ Microsoft.AspNetCore.Routing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=a Microsoft.AspNetCore.Routing.Abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60 Microsoft.Extensions.Primitives, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60 Microsoft.Net.Http.Headers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60 -System.Diagnostics.DiagnosticSource, Version=4.0.2.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 System.Reflection.Emit, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Reflection.Emit.ILGeneration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a System.Reflection.Emit.Lightweight, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt b/tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt index 63a88141b0b6..968a50862843 100644 --- a/tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt +++ b/tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt @@ -2,24 +2,32 @@ { TraceId: Id_1, SpanId: Id_2, - Name: internal, - Resource: Quartz.Job.Execute, + Name: quartz.job.execute, + Resource: execute exceptionJob, Service: Samples.Quartz, Type: custom, + Error: 1, Tags: { env: integration_tests, + error.msg: Expected InvalidOperationException thrown, + error.stack: +Parameters: refire = False, unscheduleFiringTrigger = False, unscheduleAllTriggers = False +Quartz.JobExecutionException: Expected InvalidOperationException thrown +---> System.InvalidOperationException: Expected InvalidOperationException thrown +at QuartzSampleApp.Jobs.ExceptionJob.Quartz.IJob.Execute(IJobExecutionContext context), + error.type: Quartz.JobExecutionException, fire.instance.id: , - job.group: group1, - job.name: helloJob, - job.type: QuartzSampleApp.Jobs.HelloJob, + job.group: group2, + job.name: exceptionJob, + job.type: QuartzSampleApp.Jobs.ExceptionJob, language: dotnet, - otel.status_code: STATUS_CODE_UNSET, + otel.status_code: STATUS_CODE_ERROR, otel.trace_id: Guid_1, runtime-id: Guid_2, scheduler.id: NON_CLUSTERED, scheduler.name: DefaultQuartzScheduler, - trigger.group: group1, - trigger.name: helloTrigger, + trigger.group: group2, + trigger.name: exceptionTrigger, version: 1.0.0 }, Metrics: { @@ -32,32 +40,24 @@ { TraceId: Id_3, SpanId: Id_4, - Name: internal, - Resource: Quartz.Job.Execute, + Name: quartz.job.execute, + Resource: execute helloJob, Service: Samples.Quartz, Type: custom, - Error: 1, Tags: { env: integration_tests, - error.msg: Expected InvalidOperationException thrown, - error.stack: -Parameters: refire = False, unscheduleFiringTrigger = False, unscheduleAllTriggers = False -Quartz.JobExecutionException: Expected InvalidOperationException thrown ----> System.InvalidOperationException: Expected InvalidOperationException thrown -at QuartzSampleApp.Jobs.ExceptionJob.Quartz.IJob.Execute(IJobExecutionContext context), - error.type: Quartz.JobExecutionException, fire.instance.id: , - job.group: group2, - job.name: exceptionJob, - job.type: QuartzSampleApp.Jobs.ExceptionJob, + job.group: group1, + job.name: helloJob, + job.type: QuartzSampleApp.Jobs.HelloJob, language: dotnet, - otel.status_code: STATUS_CODE_ERROR, + otel.status_code: STATUS_CODE_UNSET, otel.trace_id: Guid_3, runtime-id: Guid_2, scheduler.id: NON_CLUSTERED, scheduler.name: DefaultQuartzScheduler, - trigger.group: group2, - trigger.name: exceptionTrigger, + trigger.group: group1, + trigger.name: helloTrigger, version: 1.0.0 }, Metrics: { diff --git a/tracer/test/snapshots/QuartzTestsV3NETFRAMEWORK.verified.txt b/tracer/test/snapshots/QuartzTestsV3NETFRAMEWORK.verified.txt new file mode 100644 index 000000000000..c4e3e8d7ad64 --- /dev/null +++ b/tracer/test/snapshots/QuartzTestsV3NETFRAMEWORK.verified.txt @@ -0,0 +1,69 @@ +[ + { + TraceId: Id_1, + SpanId: Id_2, + Name: quartz.job.execute, + Resource: execute exceptionJob, + Service: Samples.Quartz, + Type: custom, + Error: 1, + Tags: { + env: integration_tests, + error.msg: Expected InvalidOperationException thrown, + error.stack: +Parameters: refire = False, unscheduleFiringTrigger = False, unscheduleAllTriggers = False +Quartz.JobExecutionException: Expected InvalidOperationException thrown ---> System.InvalidOperationException: Expected InvalidOperationException thrown +at QuartzSampleApp.Jobs.ExceptionJob.d__0.MoveNext(), + error.type: Quartz.JobExecutionException, + fire.instance.id: , + job.group: group2, + job.name: exceptionJob, + job.type: QuartzSampleApp.Jobs.ExceptionJob, + language: dotnet, + otel.status_code: STATUS_CODE_ERROR, + otel.trace_id: Guid_1, + runtime-id: Guid_2, + scheduler.id: NON_CLUSTERED, + scheduler.name: DefaultQuartzScheduler, + trigger.group: group2, + trigger.name: exceptionTrigger, + version: 1.0.0 + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_3, + SpanId: Id_4, + Name: quartz.job.execute, + Resource: execute helloJob, + Service: Samples.Quartz, + Type: custom, + Tags: { + env: integration_tests, + fire.instance.id: , + job.group: group1, + job.name: helloJob, + job.type: QuartzSampleApp.Jobs.HelloJob, + language: dotnet, + otel.status_code: STATUS_CODE_UNSET, + otel.trace_id: Guid_3, + runtime-id: Guid_2, + scheduler.id: NON_CLUSTERED, + scheduler.name: DefaultQuartzScheduler, + trigger.group: group1, + trigger.name: helloTrigger, + version: 1.0.0 + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + } +] \ No newline at end of file diff --git a/tracer/test/test-applications/integrations/Samples.Quartz/Program.cs b/tracer/test/test-applications/integrations/Samples.Quartz/Program.cs index 4f950821bcfa..be65364ef76e 100644 --- a/tracer/test/test-applications/integrations/Samples.Quartz/Program.cs +++ b/tracer/test/test-applications/integrations/Samples.Quartz/Program.cs @@ -1,4 +1,5 @@ -ο»Ώusing Quartz; +ο»Ώusing System.Diagnostics; +using Quartz; using Quartz.Impl; using Quartz.Impl.Matchers; using QuartzSampleApp.Infrastructure; diff --git a/tracer/test/test-applications/integrations/Samples.Quartz/Properties/launchSettings.json b/tracer/test/test-applications/integrations/Samples.Quartz/Properties/launchSettings.json index e4777dab67df..d53f251d7b8d 100644 --- a/tracer/test/test-applications/integrations/Samples.Quartz/Properties/launchSettings.json +++ b/tracer/test/test-applications/integrations/Samples.Quartz/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "WithDatadog": { + "WithDatadogMac": { "commandName": "Project", "environmentVariables": { "DD_VERSION": "", @@ -16,7 +16,27 @@ "DD_DOTNET_TRACER_HOME": "$(SolutionDir)shared/bin/monitoring-home", "DD_TRACE_AGENT_PORT": "8136", "DD_SERVICE": "Samples.Quartz", - "DD_TRACE_DEBUG": "false" + "DD_TRACE_DEBUG": "true" + }, + "nativeDebugging": false + }, + "WithDatadogWindows": { + "commandName": "Project", + "environmentVariables": { + "DD_TRACE_OTEL_ENABLED": "true", + "DD_VERSION": "", + "COR_ENABLE_PROFILING": "1", + "COR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "COR_PROFILER_PATH": "$(SolutionDir)shared\\bin\\monitoring-home\\win-x64\\Datadog.Trace.ClrProfiler.Native.dll", + + "CORECLR_ENABLE_PROFILING": "1", + "CORECLR_PROFILER": "{846F5F1C-F9AE-4B07-969E-05C26BC060D8}", + "CORECLR_PROFILER_PATH": "$(SolutionDir)shared\\bin\\monitoring-home\\win-x64\\Datadog.Trace.ClrProfiler.Native.dll", + + "DD_DOTNET_TRACER_HOME": "$(SolutionDir)shared\\bin\\monitoring-home", + "DD_TRACE_AGENT_PORT": "8136", + "DD_SERVICE": "Samples.Quartz", + "DD_TRACE_DEBUG": "true" }, "nativeDebugging": false },