Skip to content

[DON'T MERGE][Azure Functions] Fix span parenting in isolated functions using ASP.NET Core integration#7628

Open
lucaspimentel wants to merge 29 commits intomasterfrom
lpimentel/APMSVLS-58-azfunc-host-parenting
Open

[DON'T MERGE][Azure Functions] Fix span parenting in isolated functions using ASP.NET Core integration#7628
lucaspimentel wants to merge 29 commits intomasterfrom
lpimentel/APMSVLS-58-azfunc-host-parenting

Conversation

@lucaspimentel
Copy link
Member

@lucaspimentel lucaspimentel commented Oct 6, 2025

Summary of changes

Fixes incorrect span parenting in isolated Azure Functions when using ASP.NET Core integration. Worker process spans are now correctly parented to the ASP.NET Core request span, instead of being incorrectly parented to the root host span.

Reason for change

When using isolated Azure Functions with ASP.NET Core integration and HTTP proxying enabled, spans created in the worker process were being parented to the wrong span, causing disconnected or incorrectly structured traces. This made it difficult to understand the complete request flow and latency attribution.

Current (incorrect) behavior:

ROOT: azure_functions.invoke: GET /api/httptest [HOST]
  ├─ http.request: GET localhost:40521/api/HttpTest [HOST → WORKER]
  └─ azure_functions.invoke: Http HttpTest [WORKER] ❌ wrong parent
      └─ (worker child spans)

Fixed (correct) behavior:

ROOT: azure_functions.invoke: GET /api/httptest [HOST]
  └─ http.request: GET localhost:40521/api/HttpTest [HOST → WORKER]
      └─ aspnet_core.request [WORKER]
          └─ azure_functions.invoke: Http HttpTest [WORKER] ✅ correct parent
              └─ (worker child spans)

Implementation details

The root cause was that AsyncLocal context doesn't flow correctly through Azure Functions middleware, causing the worker's azure_functions.invoke span to have no local parent. The instrumentation would then fall back to extracting trace context from gRPC message headers, which contained stale context (the host's root span context), resulting in incorrect parenting.

The Fix: Use HttpContext.Items as an explicit bridge to pass scope between ASP.NET Core and Azure Functions middleware layers:

  1. Store scope in HttpContext.Items (AspNetCoreHttpRequestHandler.cs:159-171)

    • After creating the aspnet_core.request scope, store it in HttpContext.Items[HttpContextActiveScopeKey]
    • Only done when running in Azure Functions isolated worker
  2. Skip stale gRPC header extraction (AzureFunctionsCommon.cs:243-259)

    • Detect ASP.NET Core integration by checking for "HttpRequestContext" key in FunctionContext.Items
    • Skip extracting trace context from gRPC message headers (which contain stale host root span context)
    • Only extract headers in non-ASP.NET Core mode (timer triggers, non-proxying HTTP triggers)
  3. Retrieve scope from HttpContext.Items (AzureFunctionsCommon.cs:287-367)

    • When tracer.InternalActiveScope is null (AsyncLocal didn't flow), call GetAspNetCoreScope()
    • Get HttpContext from FunctionContext.Items["HttpRequestContext"] (set by FunctionsHttpProxyingMiddleware)
    • Get scope from HttpContext.Items[HttpContextActiveScopeKey]
    • Use retrieved scope as parent if found, otherwise fall back to extracted context or create root
  4. Added Items property to IFunctionContext (IFunctionContext.cs:21)

    • Added IDictionary<object, object>? Items { get; } for duck-typed access to FunctionContext.Items

This preserves existing behavior for non-proxying scenarios (timer triggers, non-ASP.NET Core HTTP triggers) while fixing the proxying case.

Test coverage

Covered by existing tests in AzureFunctionsTests.cs.

Fixed test snapshots to reflect correct span counts and hierarchy.

Screenshots

Before

image

https://dd-dotnet.datadoghq.com/apm/trace/69129102000000009bd07f7872769a84

After

image

https://dd-dotnet.datadoghq.com/apm/trace/69150c5500000000cb655a7e34732dd6

More recent examples with these changes:

Other details

Fixes APMSVLS-58

@lucaspimentel lucaspimentel changed the title Fix span parenting for isolated Azure Functions with ASP.NET Core integration [Azure Functions] Fix span parenting for isolated functions with ASP.NET Core integration Oct 6, 2025
@datadog-datadog-prod-us1

This comment has been minimized.

@dd-trace-dotnet-ci-bot
Copy link

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

Execution-Time Benchmarks Report ⏱️

Execution-time results for samples comparing This PR (7628) 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
duration69.28 ± (69.28 - 69.51) ms69.09 ± (69.18 - 69.45) ms-0.3%
.NET Framework 4.8 - Bailout
duration73.43 ± (73.36 - 73.59) ms73.06 ± (73.02 - 73.29) ms-0.5%
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1036.60 ± (1040.06 - 1047.35) ms1036.80 ± (1037.66 - 1043.25) ms+0.0%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms21.92 ± (21.88 - 21.96) ms21.92 ± (21.89 - 21.95) ms+0.0%✅⬆️
process.time_to_main_ms79.80 ± (79.64 - 79.95) ms80.07 ± (79.91 - 80.23) ms+0.3%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.91 ± (10.91 - 10.92) MB10.93 ± (10.92 - 10.93) MB+0.1%✅⬆️
runtime.dotnet.threads.count12 ± (12 - 12)12 ± (12 - 12)+0.0%
.NET Core 3.1 - Bailout
process.internal_duration_ms21.73 ± (21.70 - 21.75) ms21.84 ± (21.82 - 21.86) ms+0.5%✅⬆️
process.time_to_main_ms80.93 ± (80.76 - 81.10) ms81.01 ± (80.87 - 81.15) ms+0.1%✅⬆️
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_ms247.71 ± (244.64 - 250.77) ms255.07 ± (252.52 - 257.62) ms+3.0%✅⬆️
process.time_to_main_ms467.67 ± (467.13 - 468.20) ms470.53 ± (470.09 - 470.97) ms+0.6%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed47.70 ± (47.68 - 47.72) MB47.73 ± (47.70 - 47.75) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.9%✅⬆️
.NET 6 - Baseline
process.internal_duration_ms20.69 ± (20.66 - 20.73) ms20.63 ± (20.60 - 20.66) ms-0.3%
process.time_to_main_ms69.42 ± (69.28 - 69.55) ms69.47 ± (69.34 - 69.61) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.63 ± (10.63 - 10.64) MB10.63 ± (10.63 - 10.63) MB-0.0%
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 6 - Bailout
process.internal_duration_ms20.70 ± (20.66 - 20.74) ms20.59 ± (20.57 - 20.62) ms-0.5%
process.time_to_main_ms70.77 ± (70.66 - 70.89) ms70.29 ± (70.18 - 70.41) ms-0.7%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed10.73 ± (10.72 - 10.73) MB10.73 ± (10.73 - 10.73) MB+0.0%✅⬆️
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms253.53 ± (252.60 - 254.45) ms251.92 ± (251.06 - 252.78) ms-0.6%
process.time_to_main_ms448.80 ± (448.36 - 449.25) ms449.37 ± (448.90 - 449.84) ms+0.1%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed48.42 ± (48.39 - 48.45) MB48.49 ± (48.46 - 48.52) MB+0.2%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 28)28 ± (28 - 28)+0.0%
.NET 8 - Baseline
process.internal_duration_ms18.90 ± (18.87 - 18.93) ms18.96 ± (18.93 - 18.98) ms+0.3%✅⬆️
process.time_to_main_ms68.50 ± (68.37 - 68.63) ms68.64 ± (68.53 - 68.75) ms+0.2%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.67 ± (7.67 - 7.68) MB7.67 ± (7.66 - 7.67) MB-0.1%
runtime.dotnet.threads.count10 ± (10 - 10)10 ± (10 - 10)+0.0%
.NET 8 - Bailout
process.internal_duration_ms18.93 ± (18.90 - 18.96) ms18.91 ± (18.88 - 18.94) ms-0.1%
process.time_to_main_ms69.60 ± (69.51 - 69.70) ms69.45 ± (69.36 - 69.54) ms-0.2%
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed7.75 ± (7.74 - 7.76) MB7.73 ± (7.72 - 7.74) MB-0.2%
runtime.dotnet.threads.count11 ± (11 - 11)11 ± (11 - 11)+0.0%
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms180.12 ± (179.14 - 181.10) ms179.93 ± (178.94 - 180.91) ms-0.1%
process.time_to_main_ms429.68 ± (429.11 - 430.25) ms431.52 ± (430.90 - 432.15) ms+0.4%✅⬆️
runtime.dotnet.exceptions.count0 ± (0 - 0)0 ± (0 - 0)+0.0%
runtime.dotnet.mem.committed35.86 ± (35.83 - 35.90) MB35.94 ± (35.91 - 35.97) MB+0.2%✅⬆️
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
duration191.84 ± (191.77 - 192.50) ms194.95 ± (194.81 - 195.63) ms+1.6%✅⬆️
.NET Framework 4.8 - Bailout
duration196.78 ± (196.41 - 197.12) ms198.53 ± (198.55 - 199.29) ms+0.9%✅⬆️
.NET Framework 4.8 - CallTarget+Inlining+NGEN
duration1139.81 ± (1138.81 - 1145.14) ms1149.31 ± (1151.45 - 1159.74) ms+0.8%✅⬆️
.NET Core 3.1 - Baseline
process.internal_duration_ms186.60 ± (186.24 - 186.96) ms188.10 ± (187.71 - 188.50) ms+0.8%✅⬆️
process.time_to_main_ms81.01 ± (80.79 - 81.23) ms81.47 ± (81.26 - 81.68) ms+0.6%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.23 ± (16.21 - 16.26) MB16.07 ± (16.04 - 16.10) MB-1.0%
runtime.dotnet.threads.count20 ± (19 - 20)20 ± (20 - 20)+0.8%✅⬆️
.NET Core 3.1 - Bailout
process.internal_duration_ms185.90 ± (185.58 - 186.22) ms189.02 ± (188.58 - 189.47) ms+1.7%✅⬆️
process.time_to_main_ms81.78 ± (81.65 - 81.91) ms82.91 ± (82.74 - 83.07) ms+1.4%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed16.20 ± (16.17 - 16.23) MB16.15 ± (16.12 - 16.18) MB-0.3%
runtime.dotnet.threads.count21 ± (20 - 21)21 ± (21 - 21)+0.2%✅⬆️
.NET Core 3.1 - CallTarget+Inlining+NGEN
process.internal_duration_ms438.06 ± (436.09 - 440.04) ms436.27 ± (433.42 - 439.12) ms-0.4%
process.time_to_main_ms472.05 ± (471.45 - 472.66) ms475.83 ± (475.21 - 476.44) ms+0.8%✅⬆️
runtime.dotnet.exceptions.count3 ± (3 - 3)3 ± (3 - 3)+0.0%
runtime.dotnet.mem.committed57.93 ± (57.82 - 58.05) MB57.87 ± (57.75 - 58.00) MB-0.1%
runtime.dotnet.threads.count29 ± (29 - 30)29 ± (29 - 29)-0.1%
.NET 6 - Baseline
process.internal_duration_ms190.99 ± (190.59 - 191.39) ms193.62 ± (193.14 - 194.09) ms+1.4%✅⬆️
process.time_to_main_ms70.51 ± (70.32 - 70.70) ms71.05 ± (70.86 - 71.24) ms+0.8%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed16.07 ± (15.92 - 16.22) MB16.26 ± (16.18 - 16.34) MB+1.2%✅⬆️
runtime.dotnet.threads.count18 ± (18 - 18)19 ± (19 - 19)+4.3%✅⬆️
.NET 6 - Bailout
process.internal_duration_ms189.61 ± (189.28 - 189.94) ms196.44 ± (195.93 - 196.96) ms+3.6%✅⬆️
process.time_to_main_ms71.19 ± (71.04 - 71.34) ms73.37 ± (73.17 - 73.57) ms+3.1%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed15.83 ± (15.65 - 16.01) MB16.41 ± (16.39 - 16.43) MB+3.7%✅⬆️
runtime.dotnet.threads.count19 ± (19 - 19)20 ± (20 - 20)+7.2%✅⬆️
.NET 6 - CallTarget+Inlining+NGEN
process.internal_duration_ms448.57 ± (447.12 - 450.01) ms452.76 ± (451.12 - 454.39) ms+0.9%✅⬆️
process.time_to_main_ms448.07 ± (447.50 - 448.63) ms454.96 ± (454.15 - 455.76) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed58.24 ± (58.14 - 58.35) MB57.89 ± (57.80 - 57.99) MB-0.6%
runtime.dotnet.threads.count29 ± (29 - 29)29 ± (29 - 29)+0.1%✅⬆️
.NET 8 - Baseline
process.internal_duration_ms188.14 ± (187.79 - 188.48) ms190.86 ± (190.51 - 191.21) ms+1.4%✅⬆️
process.time_to_main_ms69.35 ± (69.16 - 69.55) ms70.55 ± (70.36 - 70.74) ms+1.7%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.78 ± (11.75 - 11.81) MB11.74 ± (11.71 - 11.76) MB-0.3%
runtime.dotnet.threads.count18 ± (18 - 18)18 ± (18 - 18)+0.2%✅⬆️
.NET 8 - Bailout
process.internal_duration_ms187.55 ± (187.15 - 187.96) ms191.51 ± (191.03 - 191.99) ms+2.1%✅⬆️
process.time_to_main_ms70.52 ± (70.38 - 70.65) ms71.60 ± (71.46 - 71.74) ms+1.5%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed11.84 ± (11.81 - 11.87) MB11.78 ± (11.75 - 11.80) MB-0.6%
runtime.dotnet.threads.count19 ± (19 - 19)19 ± (19 - 19)+0.4%✅⬆️
.NET 8 - CallTarget+Inlining+NGEN
process.internal_duration_ms363.88 ± (362.51 - 365.24) ms369.55 ± (368.18 - 370.92) ms+1.6%✅⬆️
process.time_to_main_ms431.73 ± (430.99 - 432.46) ms440.00 ± (439.12 - 440.87) ms+1.9%✅⬆️
runtime.dotnet.exceptions.count4 ± (4 - 4)4 ± (4 - 4)+0.0%
runtime.dotnet.mem.committed47.70 ± (47.67 - 47.74) MB47.77 ± (47.74 - 47.80) MB+0.1%✅⬆️
runtime.dotnet.threads.count28 ± (28 - 29)29 ± (29 - 29)+1.6%✅⬆️
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 (7628) - mean (69ms)  : 67, 71
    master - mean (69ms)  : 68, 71

    section Bailout
    This PR (7628) - mean (73ms)  : 72, 74
    master - mean (73ms)  : 72, 75

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (1,040ms)  : 1000, 1081
    master - mean (1,044ms)  : 990, 1098

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 (7628) - mean (108ms)  : 105, 110
    master - mean (108ms)  : 105, 110

    section Bailout
    This PR (7628) - mean (108ms)  : 106, 110
    master - mean (108ms)  : 106, 110

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (751ms)  : 711, 791
    master - mean (744ms)  : 699, 789

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

    section Bailout
    This PR (7628) - mean (96ms)  : 95, 97
    master - mean (97ms)  : 95, 99

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (727ms)  : 706, 747
    master - mean (736ms)  : 710, 762

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

    section Bailout
    This PR (7628) - mean (95ms)  : 93, 97
    master - mean (95ms)  : 93, 97

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (640ms)  : 628, 652
    master - mean (638ms)  : 626, 651

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 (7628) - mean (195ms)  : 191, 199
    master - mean (192ms)  : 189, 196

    section Bailout
    This PR (7628) - mean (199ms)  : 195, 202
    master - mean (197ms)  : 193, 200

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (1,156ms)  : 1096, 1215
    master - mean (1,142ms)  : 1096, 1188

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 (7628) - mean (278ms)  : 272, 284
    master - mean (276ms)  : 270, 282

    section Bailout
    This PR (7628) - mean (280ms)  : 275, 286
    master - mean (276ms)  : 272, 280

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (941ms)  : 894, 988
    master - mean (942ms)  : 906, 978

Loading
HttpMessageHandler (.NET 6)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 6)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7628) - mean (273ms)  : 264, 283
    master - mean (270ms)  : 264, 276

    section Bailout
    This PR (7628) - mean (278ms)  : 268, 289
    master - mean (269ms)  : 265, 273

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (937ms)  : 903, 970
    master - mean (926ms)  : 901, 951

Loading
HttpMessageHandler (.NET 8)
gantt
    title Execution time (ms) HttpMessageHandler (.NET 8)
    dateFormat  x
    axisFormat %Q
    todayMarker off
    section Baseline
    This PR (7628) - mean (271ms)  : 266, 276
    master - mean (267ms)  : 262, 272

    section Bailout
    This PR (7628) - mean (273ms)  : 266, 280
    master - mean (267ms)  : 262, 273

    section CallTarget+Inlining+NGEN
    This PR (7628) - mean (841ms)  : 811, 870
    master - mean (828ms)  : 813, 844

Loading

@lucaspimentel lucaspimentel force-pushed the lpimentel/APMSVLS-58-azfunc-host-parenting branch 2 times, most recently from 12a23e2 to 0c87ab6 Compare October 8, 2025 15:35
@pr-commenter
Copy link

pr-commenter bot commented Oct 8, 2025

Benchmarks

Benchmark execution time: 2026-02-26 19:46:03

Comparing candidate commit d12ffc5 in PR branch lpimentel/APMSVLS-58-azfunc-host-parenting with baseline commit 8b4147e in branch master.

Found 9 performance improvements and 7 performance regressions! Performance is the same for 155 metrics, 21 unstable metrics.

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

  • 🟩 execution_time [-15.792ms; -11.562ms] or [-7.463%; -5.464%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.AllCycleSimpleBody net472

  • 🟩 throughput [+90825.552op/s; +95711.515op/s] or [+10.036%; +10.576%]

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

  • 🟩 execution_time [-19.048ms; -13.975ms] or [-8.922%; -6.546%]

scenario:Benchmarks.Trace.Asm.AppSecBodyBenchmark.ObjectExtractorSimpleBody net472

  • 🟩 throughput [+192030.009op/s; +212909.678op/s] or [+6.105%; +6.769%]

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

  • 🟩 execution_time [-23.707ms; -17.828ms] or [-10.865%; -8.170%]

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

  • 🟥 execution_time [+18.535ms; +24.063ms] or [+9.458%; +12.278%]

scenario:Benchmarks.Trace.Asm.AppSecEncoderBenchmark.EncodeLegacyArgs netcoreapp3.1

  • 🟥 execution_time [+21.703ms; +22.227ms] or [+12.102%; +12.394%]

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

  • 🟥 execution_time [+17.685µs; +41.576µs] or [+5.452%; +12.817%]

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

  • 🟩 execution_time [-126.705µs; -121.335µs] or [-8.118%; -7.774%]
  • 🟩 throughput [+54.090op/s; +56.513op/s] or [+8.443%; +8.821%]

scenario:Benchmarks.Trace.DbCommandBenchmark.ExecuteNonQuery netcoreapp3.1

  • 🟥 execution_time [+13.371ms; +19.344ms] or [+6.828%; +9.879%]
  • 🟥 throughput [-43023.956op/s; -31552.421op/s] or [-10.746%; -7.881%]

scenario:Benchmarks.Trace.ElasticsearchBenchmark.CallElasticsearchAsync netcoreapp3.1

  • 🟥 throughput [-49415.829op/s; -35672.016op/s] or [-10.061%; -7.263%]

scenario:Benchmarks.Trace.Log4netBenchmark.EnrichedLog netcoreapp3.1

  • 🟥 execution_time [+41.179ms; +45.432ms] or [+25.925%; +28.603%]

scenario:Benchmarks.Trace.NLogBenchmark.EnrichedLog net6.0

  • 🟩 throughput [+9750.626op/s; +11238.199op/s] or [+5.093%; +5.870%]

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

  • 🟩 execution_time [-19.693ms; -14.063ms] or [-9.111%; -6.506%]

@lucaspimentel lucaspimentel force-pushed the lpimentel/APMSVLS-58-azfunc-host-parenting branch 6 times, most recently from 9dd669c to 27ba999 Compare October 10, 2025 21:55
@lucaspimentel lucaspimentel changed the title [Azure Functions] Fix span parenting for isolated functions with ASP.NET Core integration [WIP] [Azure Functions] Fix span parenting for isolated functions with ASP.NET Core integration Oct 10, 2025
@lucaspimentel lucaspimentel force-pushed the lpimentel/APMSVLS-58-azfunc-host-parenting branch from 27ba999 to 584134a Compare October 14, 2025 20:15
@lucaspimentel lucaspimentel force-pushed the lpimentel/APMSVLS-58-azfunc-host-parenting branch 8 times, most recently from 24a8537 to aae2cfb Compare November 5, 2025 22:25
@lucaspimentel lucaspimentel force-pushed the lpimentel/APMSVLS-58-azfunc-host-parenting branch 7 times, most recently from 974a81e to 613d6bb Compare November 11, 2025 23:52
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a7dc9745ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// just update the existing root span's tags to make it a "serverless" span.
// This matches the behavior for isolated worker scenario where we detect
// an existing scope and update it rather than creating a duplicate.
scope = activeScope;

Choose a reason for hiding this comment

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

P1 Badge Create a new function span when ASP.NET scope is active

Reusing activeScope here means OnIsolatedFunctionBegin can return the existing aspnet_core.request scope instead of creating azure_functions.invoke; when that happens, FunctionExecutionMiddlewareInvokeIntegration.OnAsyncMethodEnd disposes the ASP.NET scope via state.Scope, which can prematurely close the request span and drop the function-level span entirely. This is triggered whenever AsyncLocal context does flow (for example on worker/runtime variants where middleware context propagation differs), so the trace shape regresses back to missing/incorrect function spans.

Useful? React with 👍 / 👎.

httpContext.Items[HttpContextTrackingKey] = new RequestTrackingFeature(originalPath, scope, proxyContext?.Scope);
#endif

if (EnvironmentHelpers.IsAzureFunctions())

Choose a reason for hiding this comment

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

P2 Badge Cache Azure Functions detection outside request hot path

This check runs on every ASP.NET Core request, but EnvironmentHelpers.IsAzureFunctions() performs environment-variable probing each time; adding repeated env reads in request instrumentation increases per-request overhead for all apps even though the result is process-constant. Given this method is in a critical hot path, compute the flag once (startup/static cache) and reuse it.

Useful? React with 👍 / 👎.

Copy link
Member Author

Choose a reason for hiding this comment

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

We are aware of this issue. I'm doing this for all the Azure Functions environment variable probes in a separate PR. Thanks.

lucaspimentel and others added 29 commits February 26, 2026 12:52
AsyncLocal context doesn't flow through Azure Functions middleware,
causing worker's azure_functions.invoke span to be incorrectly
parented. Use HttpContext.Items as an explicit bridge to pass the
AspNetCore scope to the Azure Functions middleware.

Changes:
- Store AspNetCore scope in HttpContext.Items after creation
- Add Items property to IFunctionContext duck type interface
- Retrieve scope from HttpContext.Items when creating azure_functions.invoke span
- Only use HttpContext.Items fallback when tracer.InternalActiveScope is null

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Extracted context from gRPC propagation headers must take
priority over InternalActiveScope. Enabling the
AspNetCoreDiagnosticObserver in isolated workers caused
InternalActiveScope to be a gRPC listener span with an
unrelated trace ID, breaking host-to-worker context flow.

🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
When ASP.NET Core integration is active, check if the retrieved scope
is already the active scope before creating a new span. If it's active,
reuse it and update the root span tags instead of creating a duplicate
azure_functions.invoke span. This prevents extra spans that break
integration tests expecting specific span counts.

🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
Replace direct Microsoft.AspNetCore.Http.HttpContext type reference
with IHttpContextItems duck type to avoid FileNotFoundException
in non-ASP.NET Core Azure Functions workers where the
Microsoft.AspNetCore.Http.Abstractions assembly is not available.

🤖 Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:serverless status:do-not-merge Work is done. Can review, but do not merge yet.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant