Skip to content

Conversation

@chojomok
Copy link
Collaborator

@chojomok chojomok commented Oct 21, 2025

Summary of changes

To create a diagnostic observer for .NET Framework without requiting any additional dependencies.
Relying on Reflection to create an Observer instead of using the already included DiagnosticSource to have a unified method across both Core and Framework.
There is also enhancements to the spans created from OTEL for < .NET 5.0 to match the spans created versions above .NET 5.

Reason for change

To broaden support for Tracing Integrations that use the DiagnosticObserver class. Currently it's used by

  • Quartz
    • Traces are generated but in order to enhance the data in the span we need the observer class.

Implementation details

DiagnosticSource

I'll go over all the changes and the reasoning behind this.

DiagnosticManager

  • Replace the type IObserver<DiagnosticListener> to IObserver<IDiagnosticListener>
    • Create ducktype for IDiagnosticListener

DiagnosticObserver

  • update the following method to use the ducktype for DiagnosticListener:
    • public virtual IDisposable SubscribeIfMatch(DiagnosticListener diagnosticListener) to public virtual IDisposable SubscribeIfMatch(IDiagnosticListener diagnosticListener)

DiagnosticListenerObserverFactory

  • This is a new class created to use reflection to create DiagnosticListener
  • We need to use the method AllListeners defined in the DiagnosticListener class, but the type for this method is public static IObservable<System.Diagnostics.DiagnosticListener> AllListeners { get; } , creating a circular dependency for DiagnosticListener.
  • How did I come to this conclusion?
    • We need to be able to call _allListenersSubscription = DiagnosticListener.AllListeners.Subscribe(this);
    • since there's a this in the object in the subscribe method, we can't directly call AllListeners.Subscribe , we need to ducktyped. But DiagnosticListener.AllListeners is a static property - there's no instance to ducktype.
    • We needed reflection to
      • Get the DiagnosticListener type without directly referencing it
      • Access its static AllListeners property
      • Get the value from that static property

Generic Invariance Problem (The Main Issue )

This is the bigger challenge. Here's the type mismatch:

// What AllListeners returns:
IObservable<DiagnosticListener>  // Real System.Diagnostics type

// What Subscribe expects:
IObserver<DiagnosticListener>    // Must use the SAME DiagnosticListener type

// What DiagnosticManager implements:
IObserver<IDiagnosticListener>   // Our ducktyped interface

DISCLAIMER: The following explanation is from Cursor which I used to have it explain why the original method of using ducktyping didn't work - this information an be wrong but this is what I observed during testing.

Generic interfaces in C# are invariant - meaning IObserver<DiagnosticListener> and IObserver<IDiagnosticListener> are completely incompatible types, even though IDiagnosticListener is a ducktype of DiagnosticListener.

When you tried to subscribe directly:

var observable = allListeners.DuckAs<IAllListenersObservable>();
observable.Subscribe(this); // 'this' is IObserver<IDiagnosticListener>

The runtime threw: Unable to cast object of type 'DiagnosticManager' to type 'System.IObserver<System.Diagnostics.DiagnosticListener>'

Why Ducktyping Alone Couldn't Solve This:

Even if we ducktyped the observable, the Subscribe method signature still expects the exact generic type IObserver<DiagnosticListener>. Ducktyping can't bridge generic variance - it's a compile-time/runtime limitation of how generics work in .NET.

The Solution: Dynamic Type Generation

We used Reflection.Emit to dynamically create a type at runtime that:

  1. Implements the exact interface needed: IObserver<DiagnosticListener> using the actual runtime type
  2. Acts as an adapter:
    • Receives DiagnosticListener objects (the real type)
    • Ducktypes each one to IDiagnosticListener
    • Forwards to DiagnosticManager

DiagnosticListenerObserverFactory - AI Generated Code

  • This class is responsible for creating the DiagnosticListener observer type.
  • Helper methods to create the Constructor, OnNext, OnError, and OnCompleted methods.

Test coverage

Other details

@chojomok chojomok requested review from a team as code owners October 21, 2025 15:57
@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Oct 21, 2025

⚠️ Tests

Fix all issues with Cursor

⚠️ Warnings

🧪 1 Test failed

TestSessionTimeoutVulnerability from Datadog.Trace.Security.IntegrationTests.Iast.AspNetCore5IastTestsRestartedSampleIastEnabled (Datadog) (Fix with Cursor)
Couldn't verify the application is ready to receive requests.

ℹ️ Info

❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 69c36c8 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@chojomok chojomok changed the title mohammad/diagnosticobserver netframework support [tracing] add support for Quartz integration for framework Oct 21, 2025
@chojomok chojomok marked this pull request as draft October 21, 2025 16:00
@dd-trace-dotnet-ci-bot
Copy link

dd-trace-dotnet-ci-bot bot commented Oct 21, 2025

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (7687) and master.

✅ No regressions detected - check the details below

Full Metrics Comparison

FakeDbCommand

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration68.16 ± (68.18 - 68.39) ms68.51 ± (68.52 - 68.77) ms+0.5%✅⬆️
.NET Framework 4.8 - Bailout
duration72.18 ± (72.02 - 72.30) ms72.05 ± (72.00 - 72.20) ms-0.2%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1004.61 ± (1008.90 - 1017.71) ms1002.45 ± (1004.36 - 1010.02) ms-0.2%
.NET Core 3.1 - Baseline
process.internal_duration_ms21.97 ± (21.93 - 22.01) ms21.93 ± (21.90 - 21.95) ms-0.2%
process.time_to_main_ms78.42 ± (78.26 - 78.57) ms78.61 ± (78.46 - 78.76) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.90 ± (10.90 - 10.91) MB10.94 ± (10.93 - 10.95) MB+0.3%✅⬆️
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms21.92 ± (21.90 - 21.94) ms21.84 ± (21.81 - 21.86) ms-0.4%
process.time_to_main_ms79.74 ± (79.65 - 79.84) ms79.73 ± (79.64 - 79.82) ms-0.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.95 ± (10.94 - 10.95) MB10.95 ± (10.95 - 10.96) MB+0.0%✅⬆️
runtime.dotnet.threads.count13 ± (13 - 13)13 ± (13 - 13)+0.0%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms241.58 ± (237.45 - 245.71) ms215.86 ± (214.80 - 216.92) ms-10.6%
process.time_to_main_ms469.82 ± (469.43 - 470.20) ms516.04 ± (515.03 - 517.04) ms+9.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.14 ± (48.12 - 48.16) MB48.79 ± (48.76 - 48.82) MB+1.4%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.5%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms20.61 ± (20.58 - 20.64) ms20.55 ± (20.52 - 20.58) ms-0.3%
process.time_to_main_ms67.90 ± (67.78 - 68.02) ms67.92 ± (67.81 - 68.03) ms+0.0%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.61 ± (10.61 - 10.61) MB10.64 ± (10.64 - 10.64) MB+0.3%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms20.52 ± (20.48 - 20.55) ms20.60 ± (20.57 - 20.62) ms+0.4%✅⬆️
process.time_to_main_ms68.72 ± (68.66 - 68.78) ms69.14 ± (69.08 - 69.21) ms+0.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.66 ± (10.66 - 10.67) MB10.74 ± (10.74 - 10.75) MB+0.7%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms243.74 ± (241.62 - 245.86) ms206.46 ± (205.56 - 207.37) ms-15.3%
process.time_to_main_ms439.37 ± (438.95 - 439.79) ms484.27 ± (483.33 - 485.21) ms+10.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.69 ± (48.66 - 48.72) MB49.16 ± (49.13 - 49.19) MB+1.0%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms18.86 ± (18.83 - 18.89) ms18.80 ± (18.77 - 18.83) ms-0.3%
process.time_to_main_ms66.96 ± (66.86 - 67.07) ms66.91 ± (66.82 - 67.01) ms-0.1%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.67 ± (7.66 - 7.68) MB7.70 ± (7.69 - 7.71) MB+0.4%✅⬆️
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms18.87 ± (18.84 - 18.89) ms18.86 ± (18.83 - 18.89) ms-0.0%
process.time_to_main_ms68.14 ± (68.09 - 68.20) ms68.14 ± (68.09 - 68.20) ms-0.0%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.73 ± (7.72 - 7.74) MB7.74 ± (7.73 - 7.75) MB+0.1%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms179.43 ± (178.40 - 180.47) ms157.35 ± (156.38 - 158.31) ms-12.3%
process.time_to_main_ms426.67 ± (426.07 - 427.28) ms447.20 ± (446.53 - 447.87) ms+4.8%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed36.33 ± (36.30 - 36.36) MB36.47 ± (36.44 - 36.49) MB+0.4%✅⬆️
runtime.dotnet.threads.count27 ± (27 - 27)27 ± (27 - 27)-0.1%

HttpMessageHandler

Metric Master (Mean ± 95% CI) Current (Mean ± 95% CI) Change Status
.NET Framework 4.8 - Baseline
duration193.33 ± (193.49 - 194.38) ms197.25 ± (197.09 - 197.99) ms+2.0%✅⬆️
.NET Framework 4.8 - Bailout
duration196.51 ± (196.65 - 197.38) ms197.63 ± (197.69 - 198.50) ms+0.6%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1105.95 ± (1108.43 - 1116.17) ms1125.29 ± (1126.22 - 1134.48) ms+1.7%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms187.65 ± (187.23 - 188.07) ms188.68 ± (188.19 - 189.17) ms+0.5%✅⬆️
process.time_to_main_ms80.43 ± (80.23 - 80.62) ms81.10 ± (80.87 - 81.34) ms+0.8%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.07 ± (16.04 - 16.10) MB16.08 ± (16.05 - 16.10) MB+0.1%✅⬆️
runtime.dotnet.threads.count20 ± (20 - 20)20 ± (20 - 20)+0.7%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms187.09 ± (186.83 - 187.35) ms191.14 ± (190.63 - 191.65) ms+2.2%✅⬆️
process.time_to_main_ms81.80 ± (81.68 - 81.93) ms83.16 ± (82.94 - 83.38) ms+1.7%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.17 ± (16.14 - 16.19) MB16.17 ± (16.15 - 16.20) MB+0.0%✅⬆️
runtime.dotnet.threads.count21 ± (21 - 21)21 ± (21 - 21)-0.2%
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms423.39 ± (420.21 - 426.57) ms397.55 ± (396.42 - 398.68) ms-6.1%
process.time_to_main_ms472.31 ± (471.64 - 472.97) ms521.28 ± (520.08 - 522.49) ms+10.4%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed58.81 ± (58.70 - 58.92) MB59.69 ± (59.63 - 59.74) MB+1.5%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 30)30 ± (30 - 30)+0.6%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms191.89 ± (191.51 - 192.28) ms192.53 ± (192.15 - 192.92) ms+0.3%✅⬆️
process.time_to_main_ms69.81 ± (69.67 - 69.95) ms70.00 ± (69.78 - 70.22) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.28 ± (16.18 - 16.37) MB16.04 ± (15.89 - 16.19) MB-1.5%
runtime.dotnet.threads.count19 ± (19 - 19)18 ± (18 - 18)-2.6%
.NET 6 - Bailout
process.internal_duration_ms191.02 ± (190.79 - 191.25) ms192.16 ± (191.81 - 192.50) ms+0.6%✅⬆️
process.time_to_main_ms70.83 ± (70.72 - 70.93) ms71.18 ± (71.02 - 71.34) ms+0.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.22 ± (16.08 - 16.35) MB16.09 ± (15.94 - 16.24) MB-0.8%
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)-0.7%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms455.89 ± (454.05 - 457.74) ms416.77 ± (415.23 - 418.32) ms-8.6%
process.time_to_main_ms444.94 ± (444.42 - 445.46) ms493.13 ± (492.11 - 494.16) ms+10.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed58.42 ± (58.30 - 58.54) MB60.23 ± (60.19 - 60.26) MB+3.1%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 30)29 ± (29 - 30)-0.2%
.NET 8 - Baseline
process.internal_duration_ms190.41 ± (190.01 - 190.81) ms190.67 ± (190.31 - 191.04) ms+0.1%✅⬆️
process.time_to_main_ms69.42 ± (69.24 - 69.59) ms69.58 ± (69.38 - 69.78) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.78 ± (11.75 - 11.81) MB11.79 ± (11.76 - 11.81) MB+0.1%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)-0.5%
.NET 8 - Bailout
process.internal_duration_ms189.67 ± (189.39 - 189.96) ms189.74 ± (189.38 - 190.10) ms+0.0%✅⬆️
process.time_to_main_ms70.53 ± (70.39 - 70.67) ms70.72 ± (70.60 - 70.84) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.81 ± (11.78 - 11.84) MB11.82 ± (11.79 - 11.85) MB+0.1%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)+0.3%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms362.18 ± (360.73 - 363.63) ms344.27 ± (342.80 - 345.74) ms-4.9%
process.time_to_main_ms428.11 ± (427.57 - 428.66) ms453.02 ± (452.26 - 453.77) ms+5.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed47.95 ± (47.92 - 47.98) MB48.38 ± (48.35 - 48.41) MB+0.9%✅⬆️
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.4%✅⬆️
Comparison explanation

Execution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:

  • Welch test with statistical test for significance of 5%
  • Only results indicating a difference greater than 5% and 5 ms are considered.

Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard.

Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph).

Duration charts
FakeDbCommand (.NET Framework 4.8)
gantt
    title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (69ms)  : 67, 70
    master - mean (68ms)  : 67, 70

    section Bailout
    This PR (7687) - mean (72ms)  : 71, 73
    master - mean (72ms)  : 71, 74

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (1,007ms)  : 967, 1047
    master - mean (1,013ms)  : 948, 1078

Loading
FakeDbCommand (.NET Core 3.1)
gantt
    title Execution time (ms) FakeDbCommand (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (106ms)  : 103, 109
    master - mean (105ms)  : 103, 108

    section Bailout
    This PR (7687) - mean (107ms)  : 106, 108
    master - mean (107ms)  : 105, 108

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (757ms)  : 738, 777
    master - mean (735ms)  : 667, 804

Loading
FakeDbCommand (.NET 6)
gantt
    title Execution time (ms) FakeDbCommand (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (93ms)  : 91, 95
    master - mean (93ms)  : 91, 95

    section Bailout
    This PR (7687) - mean (94ms)  : 93, 96
    master - mean (94ms)  : 93, 95

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (716ms)  : 697, 736
    master - mean (708ms)  : 671, 745

Loading
FakeDbCommand (.NET 8)
gantt
    title Execution time (ms) FakeDbCommand (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (92ms)  : 90, 94
    master - mean (92ms)  : 90, 94

    section Bailout
    This PR (7687) - mean (93ms)  : 92, 94
    master - mean (93ms)  : 92, 94

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (633ms)  : 617, 650
    master - mean (634ms)  : 617, 650

Loading
HttpMessageHandler (.NET Framework 4.8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (198ms)  : 192, 203
    master - mean (194ms)  : 190, 198

    section Bailout
    This PR (7687) - mean (198ms)  : 194, 202
    master - mean (197ms)  : 193, 201

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (1,130ms)  : 1070, 1191
    master - mean (1,112ms)  : 1054, 1170

Loading
HttpMessageHandler (.NET Core 3.1)
gantt
    title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (278ms)  : 272, 285
    master - mean (276ms)  : 272, 281

    section Bailout
    This PR (7687) - mean (283ms)  : 276, 290
    master - mean (277ms)  : 274, 280

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (945ms)  : 928, 963
    master - mean (925ms)  : 883, 967

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (271ms)  : 265, 277
    master - mean (270ms)  : 266, 274

    section Bailout
    This PR (7687) - mean (271ms)  : 267, 276
    master - mean (270ms)  : 267, 273

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (939ms)  : 914, 963
    master - mean (931ms)  : 898, 964

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7687) - mean (270ms)  : 264, 275
    master - mean (269ms)  : 264, 275

    section Bailout
    This PR (7687) - mean (270ms)  : 265, 274
    master - mean (270ms)  : 266, 274

    section CallTarget+Inlining+NGEN
    This PR (7687) - mean (829ms)  : 808, 851
    master - mean (822ms)  : 807, 837

Loading

@pr-commenter
Copy link

pr-commenter bot commented Oct 21, 2025

Benchmarks

Benchmark execution time: 2025-12-30 21:15:35

Comparing candidate commit 69c36c8 in PR branch mohammad/diagnosticobserver_netframework_support with baseline commit 4fb09d3 in branch master.

Found 9 performance improvements and 6 performance regressions! Performance is the same for 161 metrics, 10 unstable metrics.

scenario:Benchmarks.Trace.ActivityBenchmark.StartStopWithChild net6.0

  • 🟥 throughput [-7778.950op/s; -5323.083op/s] or [-8.041%; -5.502%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleMoreComplexBody net6.0

  • 🟩 execution_time [-19.622ms; -14.038ms] or [-9.135%; -6.535%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody net6.0

  • 🟩 execution_time [-30.691ms; -25.347ms] or [-13.717%; -11.328%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody netcoreapp3.1

  • 🟩 execution_time [-18.143ms; -11.736ms] or [-8.557%; -5.535%]

scenario:Benchmarks.Trace.Asm.AppSecWafBenchmark.RunWafRealisticBenchmark netcoreapp3.1

  • 🟥 execution_time [+24.414µs; +59.814µs] or [+5.385%; +13.194%]

scenario:Benchmarks.Trace.AspNetCoreBenchmark.SendRequest net6.0

  • 🟩 execution_time [-39.879ms; -38.603ms] or [-29.850%; -28.895%]

scenario:Benchmarks.Trace.CIVisibilityProtocolWriterBenchmark.WriteAndFlushEnrichedTraces net6.0

  • 🟩 throughput [+111.627op/s; +155.888op/s] or [+8.347%; +11.656%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSlice net6.0

  • 🟩 execution_time [-89.837µs; -83.696µs] or [-6.083%; -5.667%]
  • 🟩 throughput [+40.771op/s; +43.747op/s] or [+6.022%; +6.461%]

scenario:Benchmarks.Trace.CharSliceBenchmark.OptimizedCharSliceWithPool net472

  • 🟥 execution_time [+76.814µs; +81.066µs] or [+6.789%; +7.164%]
  • 🟥 throughput [-59.170op/s; -56.100op/s] or [-6.695%; -6.348%]

scenario:Benchmarks.Trace.Iast.StringAspectsBenchmark.StringConcatBenchmark netcoreapp3.1

  • 🟥 execution_time [+3.055µs; +6.986µs] or [+6.307%; +14.420%]
  • 🟥 throughput [-2328.935op/s; -1100.744op/s] or [-11.252%; -5.318%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishScope net6.0

  • 🟩 execution_time [-14.628ms; -14.165ms] or [-6.804%; -6.589%]

scenario:Benchmarks.Trace.SpanBenchmark.StartFinishTwoScopes netcoreapp3.1

  • 🟩 execution_time [-16.110ms; -10.592ms] or [-7.608%; -5.002%]

@github-actions
Copy link
Contributor

Snapshots difference summary

The following differences have been observed in committed snapshots. It is meant to help the reviewer.
The diff is simplistic, so please check some files anyway while we improve it.

1 occurrences of :

-    Name: internal,
-    Resource: Quartz.Job.Execute,
+    Name: quartz.job.execute,
+    Resource: execute exceptionJob,
[...]
+    Error: 1,
[...]
+      error.msg: Expected InvalidOperationException thrown,
+      error.stack:
[...]
+      error.type: Quartz.JobExecutionException,
[...]
-      job.group: group1,
-      job.name: helloJob,
-      job.type: QuartzSampleApp.Jobs.HelloJob,
+      job.group: group2,
+      job.name: exceptionJob,
+      job.type: QuartzSampleApp.Jobs.ExceptionJob,
[...]
-      otel.status_code: STATUS_CODE_UNSET,
+      otel.status_code: STATUS_CODE_ERROR,
[...]
-      trigger.group: group1,
-      trigger.name: helloTrigger,
+      trigger.group: group2,
+      trigger.name: exceptionTrigger,

1 occurrences of :

-    Name: internal,
-    Resource: Quartz.Job.Execute,
+    Name: quartz.job.execute,
+    Resource: execute helloJob,
[...]
-    Error: 1,
[...]
-      error.msg: Expected InvalidOperationException thrown,
-      error.stack:
[...]
-      error.type: Quartz.JobExecutionException,
[...]
-      job.group: group2,
-      job.name: exceptionJob,
-      job.type: QuartzSampleApp.Jobs.ExceptionJob,
+      job.group: group1,
+      job.name: helloJob,
+      job.type: QuartzSampleApp.Jobs.HelloJob,
[...]
-      otel.status_code: STATUS_CODE_ERROR,
+      otel.status_code: STATUS_CODE_UNSET,
[...]
-      trigger.group: group2,
-      trigger.name: exceptionTrigger,
+      trigger.group: group1,
+      trigger.name: helloTrigger,

@chojomok chojomok force-pushed the mohammad/diagnosticobserver_netframework_support branch from 6cbdd75 to 05709b9 Compare November 10, 2025 19:30
@chojomok chojomok changed the title [tracing] add support for Quartz integration for framework [tracing] add support for DiagnosticSource for NET Framework (and Quartz) Nov 11, 2025
namespace Datadog.Trace.DiagnosticListeners
{
/// <summary>
/// This is code written by Cursor to do the reflection needed for DiagnosticListener.
Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, but I'm not sure we need this type at all 🤔 As far as I can see we already have almost the exact same code in ActivityListener - could/should we unify those?

@chojomok chojomok marked this pull request as ready for review November 11, 2025 20:39
Copy link
Contributor

Choose a reason for hiding this comment

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

Note for reviewers: The only difference between the tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt and tracer/test/snapshots/QuartzTestsV3NETFRAMEWORK.verified.txt files that will be checked in is the stack trace content stored in the error.stack tag. Everything else is identical

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for the call out Zach!
I've discussed adding some scrubbing so the snapshots can be the same. Please let me know if this something we'd like to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

Up to you! If this test becomes flaky then it will definitely be a priority to scrub it, otherwise I don't think it's necessary for this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll just leave it as is until we see the test becoming flaky

Copy link
Contributor

@zacharycmontoya zacharycmontoya Nov 11, 2025

Choose a reason for hiding this comment

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

Note for reviewers: The only difference between the new tracer/test/snapshots/QuartzTestsV3NETCOREAPP3X.verified.txt snapshot and the existing tracer/test/snapshots/QuartzTestsV3.verified.txt (all newer runtimes running against Quartz v3) is that the NETCOREAPP3X snapshot does not have a span.kind: internal tag since System.Diagnostics.Activity does not have a Kind property before .NET 5. The rest of the span contents are identical.

<type fullname="System.Diagnostics.ActivityIdFormat" />
<type fullname="System.Diagnostics.ActivitySpanId" />
<type fullname="System.Diagnostics.ActivityTraceId" />
<type fullname="System.Diagnostics.DiagnosticListener" />
Copy link
Member

Choose a reason for hiding this comment

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

This automatic removal is actually a problem, as it means the type could be trimmed from the target application, even though we need it 🤔 Overall, I think that can be "fixed" by not relying on reflection in .NET Core (which i think we shouldn't do anyway)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this has been reverted now with adding the directive to use the built in class for !NETFRAMEWORK.

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

Choose a reason for hiding this comment

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

Agreed, but I'm not sure we need this type at all 🤔 As far as I can see we already have almost the exact same code in ActivityListener - could/should we unify those?

Comment on lines +48 to +50
try
{
// Get the DiagnosticListener type
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should be using reflection here in .NET Core + .NET Standard when these types are already available 🤔 Can we #if this so that we only take the expensive path in .NET Framework, and we use the optimized version elsewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@zacharycmontoya and I worked on adding the directive to use the builtin class instead of doing reflection from !NETFRAMEWORK.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm going to look into make the factory class more generic so it can be used for ActivityListener.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@andrewlock - made a factory class that generates the IL for a static and a instance Subscribe call. There is some reusability in the no-op methods needed in the reflection that I move to methods.
Please let me know your feedback!

@chojomok chojomok requested a review from a team as a code owner November 14, 2025 20:51
@chojomok chojomok requested a review from link04 November 14, 2025 20:51
@lucaspimentel lucaspimentel changed the title [tracing] add support for DiagnosticSource for NET Framework (and Quartz) [tracing] add support for DiagnosticSource (and Quartz) in .NET Framework Nov 17, 2025
@bm1549 bm1549 removed the request for review from link04 November 24, 2025 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants