diff --git a/AGENTS.md b/AGENTS.md index bb95f27ac646..cc9a8ab2247e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -320,6 +320,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:** @@ -346,6 +349,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/for-ai/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 9cd8d590c15a..bf39560003d8 100644 --- a/tracer/missing-nullability-files.csv +++ b/tracer/missing-nullability-files.csv @@ -92,7 +92,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/Activity/ActivityListener.cs b/tracer/src/Datadog.Trace/Activity/ActivityListener.cs index a52b703d3b49..de05a9aa86d2 100644 --- a/tracer/src/Datadog.Trace/Activity/ActivityListener.cs +++ b/tracer/src/Datadog.Trace/Activity/ActivityListener.cs @@ -155,7 +155,12 @@ public static void Initialize(CancellationToken cancellationToken) else { // if Version is 4.0.4 or greater (Uses DiagnosticListener implementation / Nuget version 4.6.0) - CreateDiagnosticSourceListenerInstance(diagnosticListenerType); + DiagnosticListeners.DiagnosticObserverFactory.SubscribeWithStaticCallback( + diagnosticListenerType, + typeof(DiagnosticObserverListener), + nameof(DiagnosticObserverListener.OnSetListener), + "Datadog.DiagnosticObserverListener.Dynamic", + typeof(ActivityListener)); } return; @@ -248,68 +253,6 @@ private static void CreateActivityListenerInstance(Type activityType) _activityListenerInstance = activityListener; } - private static void CreateDiagnosticSourceListenerInstance(Type diagnosticListenerType) - { - Log.Information("DiagnosticListener listener: {DiagnosticListenerType}", diagnosticListenerType.AssemblyQualifiedName ?? "(null)"); - - var observerDiagnosticListenerType = typeof(IObserver<>).MakeGenericType(diagnosticListenerType); - - // Initialize and subscribe to DiagnosticListener.AllListeners.Subscribe - var diagnosticObserverType = CreateDiagnosticObserverType(diagnosticListenerType, observerDiagnosticListenerType); - if (diagnosticObserverType is null) - { - throw new NullReferenceException("ActivityListener.CreateDiagnosticObserverType returned null."); - } - - var diagnosticListenerInstance = Activator.CreateInstance(diagnosticObserverType); - var allListenersPropertyInfo = diagnosticListenerType.GetProperty("AllListeners", BindingFlags.Public | BindingFlags.Static); - if (allListenersPropertyInfo is null) - { - throw new NullReferenceException("DiagnosticListener.AllListeners method cannot be found."); - } - - var subscribeMethodInfo = allListenersPropertyInfo.PropertyType.GetMethod("Subscribe", new[] { observerDiagnosticListenerType }); - subscribeMethodInfo?.Invoke(allListenersPropertyInfo.GetValue(null), new[] { diagnosticListenerInstance }); - } - - private static Type? CreateDiagnosticObserverType(Type diagnosticListenerType, Type observerDiagnosticListenerType) - { - var assemblyName = new AssemblyName("Datadog.DiagnosticObserverListener.Dynamic"); - assemblyName.Version = typeof(ActivityListener).Assembly.GetName().Version; - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); - var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); - - DuckType.EnsureTypeVisibility(moduleBuilder, typeof(ActivityListener)); - - var typeBuilder = moduleBuilder.DefineType( - "DiagnosticObserver", - TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout | TypeAttributes.Sealed, - typeof(object), - new[] { observerDiagnosticListenerType }); - - var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig; - - // OnCompleted - var onCompletedMethod = typeBuilder.DefineMethod("OnCompleted", methodAttributes, typeof(void), Type.EmptyTypes); - var onCompletedMethodIl = onCompletedMethod.GetILGenerator(); - onCompletedMethodIl.Emit(OpCodes.Ret); - - // OnError - var onErrorMethod = typeBuilder.DefineMethod("OnError", methodAttributes, typeof(void), new[] { typeof(Exception) }); - var onErrorMethodIl = onErrorMethod.GetILGenerator(); - onErrorMethodIl.Emit(OpCodes.Ret); - - // OnNext - var onSetListenerMethodInfo = typeof(DiagnosticObserverListener).GetMethod(nameof(DiagnosticObserverListener.OnSetListener), BindingFlags.Static | BindingFlags.Public)!; - var onNextMethod = typeBuilder.DefineMethod("OnNext", methodAttributes, typeof(void), new[] { diagnosticListenerType }); - var onNextMethodIl = onNextMethod.GetILGenerator(); - onNextMethodIl.Emit(OpCodes.Ldarg_1); - onNextMethodIl.EmitCall(OpCodes.Call, onSetListenerMethodInfo, null); - onNextMethodIl.Emit(OpCodes.Ret); - - return typeBuilder.CreateTypeInfo()?.AsType(); - } - private static void CreateActivityKindSetter(Type activityType) { var activityKindProperty = activityType.GetProperty("Kind"); diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs index c43a92cbde68..56673362da1b 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Quartz/QuartzCommon.cs @@ -5,7 +5,6 @@ #nullable enable using System; -using System.Linq; using Datadog.Trace.Activity; using Datadog.Trace.Activity.DuckTypes; using Datadog.Trace.Logging; @@ -44,14 +43,58 @@ internal static void SetActivityKind(IActivity5 activity) internal static void EnhanceActivityMetadata(IActivity5 activity) { activity.AddTag("operation.name", activity.DisplayName); - var jobName = activity.Tags.FirstOrDefault(kv => kv.Key == "job.name").Value ?? string.Empty; + + string? jobName = null; + foreach (var tag in activity.Tags) + { + if (tag.Key == "job.name") + { + jobName = tag.Value; + break; + } + } + if (string.IsNullOrEmpty(jobName)) { Log.Debug("Unable to update Quartz Span's resource name: job.name tag was not found."); return; } - activity.DisplayName = CreateResourceName(activity.DisplayName, jobName); + activity.DisplayName = CreateResourceName(activity.DisplayName, jobName!); + } + + internal static void EnhanceActivityMetadata(IActivity activity) + { + if (activity is IActivity5 activity5) + { + EnhanceActivityMetadata(activity5); + return; + } + + if (activity.OperationName is null) + { + return; + } + + activity.AddTag("operation.name", activity.OperationName); + + string? jobName = null; + foreach (var tag in activity.Tags) + { + if (tag.Key == "job.name") + { + jobName = tag.Value; + break; + } + } + + if (string.IsNullOrEmpty(jobName)) + { + Log.Debug("Unable to update Quartz Span's resource name: job.name tag was not found."); + return; + } + + activity.AddTag("resource.name", CreateResourceName(activity.OperationName, jobName!)); } internal static void AddException(object exceptionArg, IActivity activity) @@ -67,5 +110,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 loaded System.Diagnostics.Activity type does not have a Kind property. Unable to populate the 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; + } + } } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs index 217970b7962a..4eeaeaec458d 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs @@ -318,7 +318,6 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null) Log.Error(ex, "Error initializing Security"); } -#if !NETFRAMEWORK try { if (GlobalSettings.Instance.DiagnosticSourceEnabled) @@ -341,7 +340,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()) { @@ -467,7 +466,6 @@ private static void InitializeTracer(Stopwatch sw) } } -#if !NETFRAMEWORK private static void StartDiagnosticManager() { var observers = new List(); @@ -486,11 +484,13 @@ private static void StartDiagnosticManager() } else { +#if !NETFRAMEWORK // Tracer, Security, should both have been initialized by now. // Iast hasn't yet, but doing it now is fine // span origins is _not_ initialized yet, and we can't guarantee it will be // so just be lazy instead observers.Add(new AspNetCoreDiagnosticObserver(Tracer.Instance, Security.Instance, Iast.Iast.Instance, spanCodeOrigin: null)); +#endif observers.Add(new QuartzDiagnosticObserver()); } @@ -498,7 +498,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/DiagnosticManager.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs index fecabd43140c..4cae90023355 100644 --- a/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticManager.cs @@ -3,23 +3,28 @@ // 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; +#pragma warning disable SA1402 namespace Datadog.Trace.DiagnosticListeners { - internal sealed class DiagnosticManager : IDiagnosticManager, IObserver, IDisposable + internal sealed class DiagnosticManager : IDiagnosticManager, 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 +36,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 +45,41 @@ public void Start() if (_allListenersSubscription == null) { Log.Debug("Starting DiagnosticListener.AllListeners subscription"); - _allListenersSubscription = DiagnosticListener.AllListeners.Subscribe(this); - } - } +#if !NETFRAMEWORK + _allListenersSubscription = DiagnosticListener.AllListeners.Subscribe(new DiagnosticListenerObserver(this)); +#else + 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; + } - void IObserver.OnCompleted() - { - } + _allListenersSubscription = DiagnosticObserverFactory.SubscribeWithInstanceCallback( + diagnosticListenerType, + this, + typeof(DiagnosticManager), + nameof(OnDiagnosticListenerNext), + "Datadog.DiagnosticManager.Dynamic", + typeof(DiagnosticManager)); - void IObserver.OnError(Exception error) - { + if (_allListenersSubscription != null) + { + Log.Debug("Successfully subscribed to DiagnosticListener.AllListeners"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error starting DiagnosticListener.AllListeners subscription"); + } +#endif + } } - void IObserver.OnNext(DiagnosticListener listener) + public void OnNext(IDiagnosticListener listener) { foreach (var subscriber in _diagnosticObservers) { @@ -103,6 +130,54 @@ 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) + { + OnNext(listener); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error handling DiagnosticListener notification"); + } + } + } + +#if !NETFRAMEWORK + internal sealed class DiagnosticListenerObserver : IObserver + { + private DiagnosticManager _diagnosticManager; + + public DiagnosticListenerObserver(DiagnosticManager diagnosticManager) + { + _diagnosticManager = diagnosticManager; + } + + public void OnCompleted() + { + return; + } + + public void OnError(Exception error) + { + return; + } + + public void OnNext(DiagnosticListener value) + { + _diagnosticManager.OnDiagnosticListenerNext(value); + } } -} #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/DiagnosticObserverFactory.cs b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserverFactory.cs new file mode 100644 index 000000000000..73db366b3e05 --- /dev/null +++ b/tracer/src/Datadog.Trace/DiagnosticListeners/DiagnosticObserverFactory.cs @@ -0,0 +1,303 @@ +// +// 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 +{ + /// + /// 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. + /// Based on the implementation in ActivityListener.CreateDiagnosticObserverType. + /// + internal static class DiagnosticObserverFactory + { + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(DiagnosticObserverFactory)); + + /// + /// Creates a module builder with the specified assembly name and visibility configuration. + /// + private static ModuleBuilder CreateModuleBuilder(string assemblyName, Type visibilityType) + { + var asmName = new AssemblyName(assemblyName); + asmName.Version = visibilityType.Assembly.GetName().Version; + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); + + DuckType.EnsureTypeVisibility(moduleBuilder, visibilityType); + + return moduleBuilder; + } + + /// + /// Creates a TypeBuilder for an observer type that implements IObserver<DiagnosticListener>. + /// + private static TypeBuilder CreateObserverTypeBuilder(ModuleBuilder moduleBuilder, Type observerDiagnosticListenerType) + { + return moduleBuilder.DefineType( + "DiagnosticObserver", + TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout | TypeAttributes.Sealed, + typeof(object), + new[] { observerDiagnosticListenerType }); + } + + /// + /// Emits the OnCompleted method for the observer (empty implementation). + /// + private static void EmitOnCompletedMethod(TypeBuilder typeBuilder, MethodAttributes methodAttributes) + { + var onCompletedMethod = typeBuilder.DefineMethod("OnCompleted", methodAttributes, typeof(void), Type.EmptyTypes); + var onCompletedMethodIl = onCompletedMethod.GetILGenerator(); + onCompletedMethodIl.Emit(OpCodes.Ret); + } + + /// + /// Emits the OnError method for the observer (empty implementation). + /// + private static void EmitOnErrorMethod(TypeBuilder typeBuilder, MethodAttributes methodAttributes) + { + var onErrorMethod = typeBuilder.DefineMethod("OnError", methodAttributes, typeof(void), new[] { typeof(Exception) }); + var onErrorMethodIl = onErrorMethod.GetILGenerator(); + onErrorMethodIl.Emit(OpCodes.Ret); + } + + /// + /// Creates a constructor that accepts a manager instance and stores it in a field. + /// Returns the field that holds the manager reference. + /// + private static FieldBuilder EmitConstructorWithManagerField(TypeBuilder typeBuilder, Type managerType) + { + // Add a field to hold the manager instance + var managerField = typeBuilder.DefineField("_manager", managerType, FieldAttributes.Private | FieldAttributes.InitOnly); + + // Define constructor that takes manager as parameter + var constructor = typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + new[] { managerType }); + + 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); + + return managerField; + } + + /// + /// Creates and subscribes a dynamic observer with a static callback to DiagnosticListener.AllListeners. + /// + public static void SubscribeWithStaticCallback( + Type diagnosticListenerType, + Type callbackType, + string callbackMethodName, + string assemblyName, + Type visibilityType) + { + Log.Information("DiagnosticListener listener: {DiagnosticListenerType}", diagnosticListenerType.AssemblyQualifiedName ?? "(null)"); + + var observerDiagnosticListenerType = typeof(IObserver<>).MakeGenericType(diagnosticListenerType); + + // Initialize and subscribe to DiagnosticListener.AllListeners.Subscribe + var diagnosticObserverType = CreateObserverTypeWithStaticCallback( + diagnosticListenerType, + observerDiagnosticListenerType, + callbackType, + callbackMethodName, + assemblyName, + visibilityType); + + if (diagnosticObserverType is null) + { + throw new NullReferenceException("DiagnosticObserverFactory.CreateObserverTypeWithStaticCallback returned null."); + } + + var diagnosticListenerInstance = Activator.CreateInstance(diagnosticObserverType); + var allListenersPropertyInfo = diagnosticListenerType.GetProperty("AllListeners", BindingFlags.Public | BindingFlags.Static); + if (allListenersPropertyInfo is null) + { + throw new NullReferenceException("DiagnosticListener.AllListeners method cannot be found."); + } + + var subscribeMethodInfo = allListenersPropertyInfo.PropertyType.GetMethod("Subscribe", new[] { observerDiagnosticListenerType }); + subscribeMethodInfo?.Invoke(allListenersPropertyInfo.GetValue(null), new[] { diagnosticListenerInstance }); + } + + /// + /// Creates and subscribes a dynamic observer with an instance callback to DiagnosticListener.AllListeners. + /// Based on DiagnosticManager.Start() pattern but returns the subscription. + /// + public static IDisposable? SubscribeWithInstanceCallback( + Type diagnosticListenerType, + object managerInstance, + Type managerType, + string callbackMethodName, + string assemblyName, + Type visibilityType) + { + // Get the IObserver type + var observerDiagnosticListenerType = typeof(IObserver<>).MakeGenericType(diagnosticListenerType); + + // 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 null; + } + + // Get the value (IObservable) + var allListenersObservable = allListenersProperty.GetValue(null); + if (allListenersObservable == null) + { + Log.Warning("DiagnosticListener.AllListeners returned null"); + return null; + } + + // Create a dynamic type that implements IObserver using the shared factory + var observerType = CreateObserverTypeWithInstanceCallback( + diagnosticListenerType, + observerDiagnosticListenerType, + managerType, + callbackMethodName, + assemblyName, + visibilityType); + + if (observerType == null) + { + Log.Warning("Failed to create dynamic observer type"); + return null; + } + + // Create an instance of the dynamic observer, passing this manager instance + var observerInstance = Activator.CreateInstance(observerType, managerInstance); + if (observerInstance == null) + { + Log.Warning("Failed to create observer instance"); + return null; + } + + // 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 null; + } + + return (IDisposable?)subscribeMethod.Invoke(allListenersObservable, new object[] { observerInstance }); + } + + /// + /// Creates a dynamic type that implements IObserver<DiagnosticListener> + /// with a static method callback (no instance state). + /// Uses the exact same IL emission pattern as ActivityListener.CreateDiagnosticObserverType. + /// + /// The Type of DiagnosticListener obtained via reflection + /// The IObserver<DiagnosticListener> type + /// The type containing the static callback method + /// The name of the static method to call when OnNext is invoked + /// The name for the dynamic assembly + /// A type from the calling assembly to ensure visibility + /// A dynamically created Type that implements IObserver<DiagnosticListener>, or null if creation fails + public static Type? CreateObserverTypeWithStaticCallback( + Type diagnosticListenerType, + Type observerDiagnosticListenerType, + Type callbackType, + string callbackMethodName, + string assemblyName, + Type visibilityType) + { + var moduleBuilder = CreateModuleBuilder(assemblyName, visibilityType); + var typeBuilder = CreateObserverTypeBuilder(moduleBuilder, observerDiagnosticListenerType); + + var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig; + + EmitOnCompletedMethod(typeBuilder, methodAttributes); + EmitOnErrorMethod(typeBuilder, methodAttributes); + + // OnNext - call static method + var onSetListenerMethodInfo = callbackType.GetMethod(callbackMethodName, BindingFlags.Static | BindingFlags.Public)!; + var onNextMethod = typeBuilder.DefineMethod("OnNext", methodAttributes, typeof(void), new[] { diagnosticListenerType }); + var onNextMethodIl = onNextMethod.GetILGenerator(); + onNextMethodIl.Emit(OpCodes.Ldarg_1); + onNextMethodIl.EmitCall(OpCodes.Call, onSetListenerMethodInfo, null); + onNextMethodIl.Emit(OpCodes.Ret); + + return typeBuilder.CreateTypeInfo()?.AsType(); + } + + /// + /// Creates a dynamic type that implements IObserver<DiagnosticListener> + /// with an instance method callback (holds a reference to a manager object). + /// Uses the same IL emission pattern as the static callback version, but adds a constructor + /// and instance field to hold the manager reference. + /// + /// The Type of DiagnosticListener obtained via reflection + /// The IObserver<DiagnosticListener> type + /// The type of the manager instance to store + /// The name of the instance method to call when OnNext is invoked + /// The name for the dynamic assembly + /// A type from the calling assembly to ensure visibility + /// A dynamically created Type that implements IObserver<DiagnosticListener>, or null if creation fails + public static Type? CreateObserverTypeWithInstanceCallback( + Type diagnosticListenerType, + Type observerDiagnosticListenerType, + Type managerType, + string callbackMethodName, + string assemblyName, + Type visibilityType) + { + var moduleBuilder = CreateModuleBuilder(assemblyName, visibilityType); + var typeBuilder = CreateObserverTypeBuilder(moduleBuilder, observerDiagnosticListenerType); + + // Create constructor and get the manager field + var managerField = EmitConstructorWithManagerField(typeBuilder, managerType); + + var methodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig; + + EmitOnCompletedMethod(typeBuilder, methodAttributes); + EmitOnErrorMethod(typeBuilder, methodAttributes); + + // OnNext - call instance method on manager + var onSetListenerMethodInfo = managerType.GetMethod(callbackMethodName, BindingFlags.Instance | BindingFlags.NonPublic); + if (onSetListenerMethodInfo == null) + { + Log.Warning("Unable to find {CallbackMethodName} method on {ManagerType}", callbackMethodName, managerType.Name); + return null; + } + + var onNextMethod = typeBuilder.DefineMethod("OnNext", methodAttributes, typeof(void), new[] { diagnosticListenerType }); + var onNextMethodIl = onNextMethod.GetILGenerator(); + onNextMethodIl.Emit(OpCodes.Ldarg_0); // Load 'this' + onNextMethodIl.Emit(OpCodes.Ldfld, managerField); // Load this._manager + onNextMethodIl.Emit(OpCodes.Ldarg_1); // Load the listener parameter + onNextMethodIl.EmitCall(OpCodes.Callvirt, onSetListenerMethodInfo, null); // Call instance method + onNextMethodIl.Emit(OpCodes.Ret); + + return typeBuilder.CreateTypeInfo()?.AsType(); + } + } +} 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/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 },