From faf02d9cd073ae962a2cdfeb83f0873c06d1f867 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 28 Apr 2026 19:34:42 +0100 Subject: [PATCH 01/25] [WCF] Add test for .NET Framework (#4318) --- .../TelemetryEndpointBehaviorTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/OpenTelemetry.Instrumentation.Wcf.Tests/TelemetryEndpointBehaviorTests.cs b/test/OpenTelemetry.Instrumentation.Wcf.Tests/TelemetryEndpointBehaviorTests.cs index d1a9b0a7d7..6f96b9aa08 100644 --- a/test/OpenTelemetry.Instrumentation.Wcf.Tests/TelemetryEndpointBehaviorTests.cs +++ b/test/OpenTelemetry.Instrumentation.Wcf.Tests/TelemetryEndpointBehaviorTests.cs @@ -34,6 +34,31 @@ public void ApplyClientBehaviorToClientRuntime_WithNullActionOperation_DoesNotTh } } +#if NETFRAMEWORK + [Fact] + public void ApplyDispatchBehaviorToEndpoint_WithNullActionOperation_DoesNotThrow() + { + // Arrange + var endpointDispatcher = new EndpointDispatcher( + new EndpointAddress("http://localhost/something"), + contractName: "Service", + contractNamespace: "http://opentelemetry.io/"); + + endpointDispatcher.DispatchRuntime.Operations.Add( + new DispatchOperation( + endpointDispatcher.DispatchRuntime, + name: "NullActionOperation", + action: null)); + + // Act + var exception = Record.Exception(() => TelemetryEndpointBehavior.ApplyDispatchBehaviorToEndpoint(endpointDispatcher)); + + // Assert + Assert.Null(exception); + Assert.Single(endpointDispatcher.DispatchRuntime.MessageInspectors); + } +#endif + private sealed class InjectNullActionOperationBehavior : IEndpointBehavior { public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) From 9c0bc5189e7b1aa807140f38d07e7bb126359c8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:46:28 +0000 Subject: [PATCH 02/25] Bump postgres:18.3 Docker digest to 7848165 (#4319) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../postgres.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/postgres.Dockerfile b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/postgres.Dockerfile index 75b31bc5a0..561a5acd7e 100644 --- a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/postgres.Dockerfile +++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/postgres.Dockerfile @@ -1 +1 @@ -FROM postgres:18.3@sha256:059fa0289cc5a184034e05a1f4f6d6fd79f69dc718b8b04ab60b6b469eed411e +FROM postgres:18.3@sha256:78481659c47e862334611ccdaf7c369c986b3046da9857112f3b309114a65fb4 From 25c8b13c4737ebe31ca1bc1561aae2dd21f9b3b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:46:52 +0000 Subject: [PATCH 03/25] Bump redis:8.6.2 Docker digest to 832d778 (#4320) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../redis.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/redis.Dockerfile b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/redis.Dockerfile index 22f4de1a66..2549b13632 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/redis.Dockerfile +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/redis.Dockerfile @@ -1 +1 @@ -FROM redis:8.6.2@sha256:d372cf7cd5ab47fb6ad1a73c45ea1104d6f3fa11cc833ff0b3ac997890b4ccec +FROM redis:8.6.2@sha256:832d7785830f3f4b559300e6191fc914b15642c1935252338825cf4332200148 From 0e7a630f34e417eb1784e08d5e57102ff3d4e6cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:55:03 +0000 Subject: [PATCH 04/25] Bump dependency FsCheck.Xunit to 3.3.3 (#4323) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index aae79f23db..2c1dd1a92e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -101,7 +101,7 @@ - + From 90e81d328ab400e010026b27c7effbb50a79ebef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:55:59 +0000 Subject: [PATCH 05/25] Bump mysql Docker tag to v9.7.0 (#4329) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../mysql.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/mysql.Dockerfile b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/mysql.Dockerfile index 94e946e721..0903677f7c 100644 --- a/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/mysql.Dockerfile +++ b/test/OpenTelemetry.Instrumentation.EntityFrameworkCore.Tests/mysql.Dockerfile @@ -1 +1 @@ -FROM mysql:9.6.0@sha256:c5df04bee1a42b74a5841c6409e669cf62126cd0416f00c1cea8ab933b9361b9 +FROM mysql:9.7.0@sha256:c9e48b0c008f1936d4139d1c0dcd5950a9dbe57d4d40f383013cde432fa6d6aa From 668e2680a335f2b4075f55a07b191a8f5dd7b03f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:57:27 +0000 Subject: [PATCH 06/25] Bump dependency JetBrains.Profiler.Api to 1.4.12 (#4324) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c1dd1a92e..6ec971f028 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -111,7 +111,7 @@ - + From fe86eb1b362c8aff4d87a2241584f34b2ce0a751 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 08:59:46 +0000 Subject: [PATCH 07/25] Bump dependency Microsoft.Data.SqlClient to 7.0.1 (#4326) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6ec971f028..f92192ebee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -121,7 +121,7 @@ - + From b60724ebad5ce433a8576a5aa32148b8e14c4df5 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 10:14:15 +0100 Subject: [PATCH 08/25] [Instrumentation.Process] Add schema URL (#4088) --- .../CHANGELOG.md | 9 ++++++ .../MeterProviderBuilderExtensions.cs | 2 +- ...enTelemetry.Instrumentation.Process.csproj | 1 + .../ProcessMetrics.cs | 20 ++---------- .../README.md | 18 ----------- .../ProcessMetricsTests.cs | 32 ++++++++++++------- 6 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Process/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Process/CHANGELOG.md index 30b280b3b1..99335df9fb 100644 --- a/src/OpenTelemetry.Instrumentation.Process/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Process/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +* Add instrumentation scope version and schema URL to metrics. + ([#4088](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4088)) + +* Removed the `process.cpu.count` metric as it is not part of + the semantic conventions. Use the + [`dotnet.process.cpu.count`](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/runtime/dotnet-metrics.md#metric-dotnetprocesscpucount) + metric as an alternative. + ([#4088](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4088)) + ## 1.15.1-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.Process/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Process/MeterProviderBuilderExtensions.cs index de2f9e97df..71160e78c3 100644 --- a/src/OpenTelemetry.Instrumentation.Process/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Process/MeterProviderBuilderExtensions.cs @@ -21,7 +21,7 @@ public static MeterProviderBuilder AddProcessInstrumentation( { Guard.ThrowIfNull(builder); - builder.AddMeter(ProcessMetrics.MeterName); + builder.AddMeter(ProcessMetrics.MeterInstance.Name); return builder.AddInstrumentation(() => new ProcessMetrics()); } } diff --git a/src/OpenTelemetry.Instrumentation.Process/OpenTelemetry.Instrumentation.Process.csproj b/src/OpenTelemetry.Instrumentation.Process/OpenTelemetry.Instrumentation.Process.csproj index 49f9e9cb35..fd75263cbc 100644 --- a/src/OpenTelemetry.Instrumentation.Process/OpenTelemetry.Instrumentation.Process.csproj +++ b/src/OpenTelemetry.Instrumentation.Process/OpenTelemetry.Instrumentation.Process.csproj @@ -25,6 +25,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs b/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs index 05142e8bc1..7f0d993495 100644 --- a/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs @@ -2,21 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; -using System.Reflection; -using OpenTelemetry.Internal; using Diagnostics = System.Diagnostics; namespace OpenTelemetry.Instrumentation.Process; internal sealed class ProcessMetrics { - internal static readonly Assembly Assembly = typeof(ProcessMetrics).Assembly; - internal static readonly AssemblyName AssemblyName = Assembly.GetName(); -#pragma warning disable IDE0370 // Suppression is unnecessary - internal static readonly string MeterName = AssemblyName.Name!; -#pragma warning restore IDE0370 // Suppression is unnecessary - - private static readonly Meter MeterInstance = new(MeterName, Assembly.GetPackageVersion()); + internal static readonly Version SemanticConventionsVersion = new(1, 25, 0); + internal static readonly Meter MeterInstance = Metrics.MeterFactory.Create(SemanticConventionsVersion); static ProcessMetrics() { @@ -54,15 +47,6 @@ static ProcessMetrics() unit: "s", description: "Total CPU seconds broken down by different states."); - MeterInstance.CreateObservableUpDownCounter( - "process.cpu.count", - () => - { - return Environment.ProcessorCount; - }, - unit: "{processors}", - description: "The number of processors (CPU cores) available to the current process."); - MeterInstance.CreateObservableUpDownCounter( "process.thread.count", () => diff --git a/src/OpenTelemetry.Instrumentation.Process/README.md b/src/OpenTelemetry.Instrumentation.Process/README.md index 69b95a2cf0..6e17593a7c 100644 --- a/src/OpenTelemetry.Instrumentation.Process/README.md +++ b/src/OpenTelemetry.Instrumentation.Process/README.md @@ -118,24 +118,6 @@ Gets the user processor time for this process. * [Process.PrivilegedProcessorTime](https://learn.microsoft.com/dotnet/api/system.diagnostics.process.privilegedprocessortime): Gets the privileged processor time for this process. -### process.cpu.count - -The number of processors (CPU cores) available to the current process. - -| Units | Instrument Type | Value Type | -| ------------- | ----------------------- | ---------- | -| `{processors}`| ObservableUpDownCounter | `Int32` | - -The API used to retrieve the value is -[System.Environment.ProcessorCount](https://learn.microsoft.com/dotnet/api/system.environment.processorcount). - -> [!NOTE] -> This metric is under -> [discussion](https://github.com/open-telemetry/opentelemetry-specification/issues/3200) -and not part of the [Process Metrics -Spec](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/system/process-metrics.md) -at this time. - ### process.thread.count Process threads count. diff --git a/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs b/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs index f10a36d8b7..2c778e9a04 100644 --- a/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs +++ b/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs @@ -21,17 +21,15 @@ public void ProcessMetricsAreCaptured() meterProviderA.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(5, exportedItemsA.Count); var physicalMemoryMetric = exportedItemsA.FirstOrDefault(i => i.Name == "process.memory.usage"); Assert.NotNull(physicalMemoryMetric); var virtualMemoryMetric = exportedItemsA.FirstOrDefault(i => i.Name == "process.memory.virtual"); Assert.NotNull(virtualMemoryMetric); var cpuTimeMetric = exportedItemsA.FirstOrDefault(i => i.Name == "process.cpu.time"); Assert.NotNull(cpuTimeMetric); - var processorCountMetric = exportedItemsA.FirstOrDefault(i => i.Name == "process.cpu.count"); - Assert.NotNull(processorCountMetric); var threadMetric = exportedItemsA.FirstOrDefault(i => i.Name == "process.thread.count"); Assert.NotNull(threadMetric); + Assert.Equal(4, exportedItemsA.Count); exportedItemsA.Clear(); @@ -51,8 +49,11 @@ public void ProcessMetricsAreCaptured() meterProviderB.ForceFlush(MaxTimeToAllowForFlush); - Assert.Equal(5, exportedItemsA.Count); - Assert.Equal(5, exportedItemsB.Count); + Assert.Equal(4, exportedItemsA.Count); + Assert.Equal(4, exportedItemsB.Count); + + AssertMetrics(exportedItemsA); + AssertMetrics(exportedItemsB); } [Fact] @@ -129,29 +130,28 @@ public async Task ProcessMetricsAreCapturedWhenTasksOverlap() await Task.WhenAll(tasks); - Assert.Equal(5, exportedItemsA.Count); var physicalMemoryMetricA = exportedItemsA.FirstOrDefault(i => i.Name == "process.memory.usage"); Assert.NotNull(physicalMemoryMetricA); var virtualMemoryMetricA = exportedItemsA.FirstOrDefault(i => i.Name == "process.memory.virtual"); Assert.NotNull(virtualMemoryMetricA); var cpuTimeMetricA = exportedItemsA.FirstOrDefault(i => i.Name == "process.cpu.time"); Assert.NotNull(cpuTimeMetricA); - var processorCountMetricA = exportedItemsA.FirstOrDefault(i => i.Name == "process.cpu.count"); - Assert.NotNull(processorCountMetricA); var threadMetricA = exportedItemsA.FirstOrDefault(i => i.Name == "process.thread.count"); Assert.NotNull(threadMetricA); + Assert.Equal(4, exportedItemsA.Count); - Assert.Equal(5, exportedItemsB.Count); var physicalMemoryMetricB = exportedItemsB.FirstOrDefault(i => i.Name == "process.memory.usage"); Assert.NotNull(physicalMemoryMetricB); var virtualMemoryMetricB = exportedItemsB.FirstOrDefault(i => i.Name == "process.memory.virtual"); Assert.NotNull(virtualMemoryMetricB); var cpuTimeMetricB = exportedItemsB.FirstOrDefault(i => i.Name == "process.cpu.time"); Assert.NotNull(cpuTimeMetricB); - var processorCountMetricB = exportedItemsB.FirstOrDefault(i => i.Name == "process.cpu.count"); - Assert.NotNull(processorCountMetricB); var threadMetricB = exportedItemsB.FirstOrDefault(i => i.Name == "process.thread.count"); Assert.NotNull(threadMetricB); + Assert.Equal(4, exportedItemsB.Count); + + AssertMetrics(exportedItemsA); + AssertMetrics(exportedItemsB); } [Fact] @@ -221,4 +221,14 @@ private static double GetValue(Metric metric) return sum; } + + private static void AssertMetrics(IEnumerable metrics) + { + foreach (var metric in metrics) + { + Assert.NotNull(metric.MeterVersion); + Assert.NotEmpty(metric.MeterVersion); + Assert.StartsWith("https://opentelemetry.io/schemas/", metric.MeterSchemaUrl); + } + } } From 09b516ac69537b95d95ddb764970e6f47a5a078b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:24:06 +0000 Subject: [PATCH 09/25] Bump NuGet/login action to v1.2.0 (#4330) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/publish-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index d67b6ae584..0bf547b31a 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -179,7 +179,7 @@ jobs: run: dotnet nuget push *.nupkg --api-key ${env:API_KEY} --skip-duplicate --source ${env:SOURCE} - name: NuGet log in - uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.1.0 + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 env: NUGET_USER_EXISTS: ${{ secrets.NUGET_USER != '' }} id: nuget-login From e9ea02bd9016e9aad68ae073f826dd8186bf003f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:43:07 +0000 Subject: [PATCH 10/25] Bump dependency Meziantou.Framework.NuGetPackageValidation.Tool to 1.0.49 (#4325) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/publish-packages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 0bf547b31a..5c425887d2 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -135,7 +135,7 @@ jobs: # renovate: datasource=nuget depName=dotnet-validate DOTNET_VALIDATE_VERSION: '0.0.1-preview.582' # renovate: datasource=nuget depName=Meziantou.Framework.NuGetPackageValidation.Tool - MEZIANTOU_VALIDATE_NUGET_PACKAGE_VERSION: '1.0.47' + MEZIANTOU_VALIDATE_NUGET_PACKAGE_VERSION: '1.0.49' run: | dotnet tool install --global dotnet-validate --version ${env:DOTNET_VALIDATE_VERSION} --allow-roll-forward dotnet tool install --global Meziantou.Framework.NuGetPackageValidation.Tool --version ${env:MEZIANTOU_VALIDATE_NUGET_PACKAGE_VERSION} --allow-roll-forward From 6099206366935aab8ffbf75d96cd9e22afaf0d89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:43:35 +0000 Subject: [PATCH 11/25] Bump crate-ci/typos action to v1.45.2 (#4322) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/sanitycheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index 360f754bd5..76ae6e04fd 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: check for typos - uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1 + uses: crate-ci/typos@7c572958218557a3272c2d6719629443b5cc26fd # v1.45.2 run-sanitycheck: runs-on: ubuntu-24.04 From 55ec10d4bb4913570979c34cfd3aab81053e3e58 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:49:46 +0000 Subject: [PATCH 12/25] Bump aws-sdk-net monorepo (#4321) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f92192ebee..f8de289a69 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -91,14 +91,14 @@ - - - - - - - - + + + + + + + + From 0e995e60b638d56479e2fe4afcf6436def473784 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:49:54 +0000 Subject: [PATCH 13/25] Bump DavidAnson/markdownlint-cli2-action action to v23.1.0 (#4328) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/markdownlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml index dd360f38df..19e7d3b814 100644 --- a/.github/workflows/markdownlint.yml +++ b/.github/workflows/markdownlint.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: run markdownlint - uses: DavidAnson/markdownlint-cli2-action@ce4853d43830c74c1753b39f3cf40f71c2031eb9 # v23.0.0 + uses: DavidAnson/markdownlint-cli2-action@6b51ade7a9e4a75a7ad929842dd298a3804ebe8b # v23.1.0 with: globs: | **/*.md From bf729bfea0db158f3fdfa62ebf895650ddab7caf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:07:51 +0100 Subject: [PATCH 14/25] Bump dependency Microsoft.NET.Test.Sdk to 18.5.1 (#4327) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f8de289a69..6cb826ceb2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -126,7 +126,7 @@ - + From 052ddf2bf0b00788d0f2bae5d4be12f44a4d2ae7 Mon Sep 17 00:00:00 2001 From: Yevhenii Solomchenko Date: Wed, 29 Apr 2026 15:43:22 +0200 Subject: [PATCH 15/25] [Shared.SqlProcessor] Add support for handling unterminated escaped identifiers in sanitization (#4317) --- .../CHANGELOG.md | 4 +++ .../CHANGELOG.md | 4 +++ src/Shared/SqlProcessor.cs | 25 ++++++++++++- .../SqlProcessorTests.cs | 35 +++++++++++++++++++ .../SqlProcessorTests.cs | 10 ++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md index 6a503bdea0..0200fbf7e8 100644 --- a/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Fix SQL query text sanitization for malformed bracketed identifiers in `FROM` + clauses to avoid leaking following literal values. + ([#4317](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4317)) + ## 1.15.1-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index b49dcf7d71..1229991981 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -9,6 +9,10 @@ * Add support for native AoT on .NET 8+. ([#4062](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4062)) +* Fix SQL query text sanitization for malformed bracketed identifiers in `FROM` + clauses to avoid leaking following literal values. + ([#4317](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4317)) + ## 1.15.2 Released 2026-Apr-21 diff --git a/src/Shared/SqlProcessor.cs b/src/Shared/SqlProcessor.cs index 5bce9757d7..a382a79f7d 100644 --- a/src/Shared/SqlProcessor.cs +++ b/src/Shared/SqlProcessor.cs @@ -223,6 +223,27 @@ private static bool IsValidTokenCharacter(ReadOnlySpan sql, int currentPos return (currentChar != DotChar || indexInToken != 0) && IsUnescapedIdentifierChar(currentChar); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HasTerminatingEscapedIdentifier(ReadOnlySpan sql, int start) + { + for (var i = start + 1; i < sql.Length; i++) + { + if (sql[i] == CloseSquareBracketChar) + { + if (i + 1 < sql.Length && sql[i + 1] == CloseSquareBracketChar) + { + i++; + } + else + { + return true; + } + } + } + + return false; + } + private static SqlStatementInfo SanitizeSql(string sql) { var sqlSpan = sql.AsSpan(); @@ -611,7 +632,9 @@ private static void ParseNextToken( // Brackets may occur when using schema-qualified or delimited identifiers. state.CaptureNextNonKeywordTokenAsIdentifier = state.InFromClause && (currentChar is CommaChar or OpenSquareBracketChar or DotChar); - if (state.CaptureNextNonKeywordTokenAsIdentifier && currentChar is OpenSquareBracketChar) + if (state.CaptureNextNonKeywordTokenAsIdentifier + && currentChar is OpenSquareBracketChar + && HasTerminatingEscapedIdentifier(sql, state.ParsePosition)) { state.InEscapedIdentifier = true; AppendSummaryChar(OpenSquareBracketChar, ref state); diff --git a/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs b/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs index a7d3f28d6e..268bb83270 100644 --- a/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs +++ b/test/OpenTelemetry.Contrib.Shared.FuzzTests/SqlProcessorTests.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Globalization; using FsCheck; using FsCheck.Xunit; using Xunit; @@ -377,4 +378,38 @@ public static void GetSanitizedSql_Multiple_Values_In_In_Clause(PositiveInt valu var questionMarkCount = result.SanitizedSql.Count((p) => p == '?'); Assert.Equal(1, questionMarkCount); } + + [Property(MaxTest = MaxValue)] + public static void GetSanitizedSql_Unterminated_Escaped_Identifier_In_From_Clause_Sanitizes_Following_Literals( + NonEmptyString input, + PositiveInt number, + NonNegativeInt variant) + { + // Arrange + var secret = "secret_" + new string([.. input.Get.Where(char.IsLetterOrDigit).Take(32)]); + if (secret.Length == "secret_".Length) + { + secret += "value"; + } + + var numericLiteral = number.Get + 100_000; + var numericLiteralString = numericLiteral.ToString(CultureInfo.InvariantCulture); + var sqlPrefix = (variant.Get % 3) switch + { + 0 => "SELECT * FROM [Orders", + 1 => "SELECT * FROM dbo.[Orders", + _ => "SELECT * FROM Customers, [Orders", + }; + + var sql = $"{sqlPrefix} WHERE Name = '{secret}' AND Id = {numericLiteralString} AND Token = 0xDEADBEEF"; + + // Act + var result = SqlProcessor.GetSanitizedSql(sql); + + // Assert + Assert.Contains("?", result.SanitizedSql); + Assert.DoesNotContain(secret, result.SanitizedSql); + Assert.DoesNotContain(numericLiteralString, result.SanitizedSql); + Assert.DoesNotContain("DEADBEEF", result.SanitizedSql); + } } diff --git a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs index 9832c15742..fa70c365df 100644 --- a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs +++ b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs @@ -28,6 +28,16 @@ public void GetSanitizedSql_CreateTableWithTrailingIdentifier_DoesNotThrow() Assert.Equal(sql, sqlStatementInfo.DbQuerySummary); } + [Fact] + public void GetSanitizedSql_UnterminatedEscapedIdentifierInFromClause_SanitizesLiterals() + { + var sql = "SELECT * FROM [Orders WHERE CustomerName = 'secret-name' AND Id = 123 AND Token = 0xDEADBEEF"; + + var sqlStatementInfo = SqlProcessor.GetSanitizedSql(sql); + + Assert.Equal("SELECT * FROM [Orders WHERE CustomerName = ? AND Id = ? AND Token = ?", sqlStatementInfo.SanitizedSql); + } + [SkippableTheory] [MemberData(nameof(TestData))] public void TestGetSanitizedSql(SqlProcessorTestCases.TestCase testCase) From 8ec3b95b26b592c12e9bd52e2e3f00445487eaca Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 15:06:37 +0100 Subject: [PATCH 16/25] [SqlClient] Polyfill Stopwatch.GetElapsedTime (#4300) --- opentelemetry-dotnet-contrib.slnx | 1 + .../Implementation/SqlTelemetryHelper.cs | 14 +---------- ...Telemetry.Instrumentation.SqlClient.csproj | 1 + src/Shared/StopwatchExtensions.cs | 25 +++++++++++++++++++ 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/Shared/StopwatchExtensions.cs diff --git a/opentelemetry-dotnet-contrib.slnx b/opentelemetry-dotnet-contrib.slnx index 27d2c4e400..36dbe78af2 100644 --- a/opentelemetry-dotnet-contrib.slnx +++ b/opentelemetry-dotnet-contrib.slnx @@ -214,6 +214,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlTelemetryHelper.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlTelemetryHelper.cs index a0d8f78011..554ea7a2ca 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlTelemetryHelper.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlTelemetryHelper.cs @@ -93,17 +93,5 @@ public static TagList GetTagListFromConnectionInfo(string? dataSource, string? d } internal static double CalculateDurationFromTimestamp(long begin) - { -#if NET - var duration = Stopwatch.GetElapsedTime(begin); -#else - var end = Stopwatch.GetTimestamp(); - var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - var delta = end - begin; - var ticks = (long)(timestampToTicks * delta); - var duration = new TimeSpan(ticks); -#endif - - return duration.TotalSeconds; - } + => Stopwatch.GetElapsedTime(begin).TotalSeconds; } diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj index 84763fa654..c26dad640c 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj +++ b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Shared/StopwatchExtensions.cs b/src/Shared/StopwatchExtensions.cs new file mode 100644 index 0000000000..0f88139981 --- /dev/null +++ b/src/Shared/StopwatchExtensions.cs @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET + +namespace System.Diagnostics; + +internal static class StopwatchExtensions +{ + extension(Stopwatch) + { + public static TimeSpan GetElapsedTime(long begin) + { + var end = Stopwatch.GetTimestamp(); + + var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + var delta = end - begin; + var ticks = (long)(timestampToTicks * delta); + + return new TimeSpan(ticks); + } + } +} + +#endif From c78ac9eb675bb94a31f40cb34a1f4592ddce630c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:03:52 +0000 Subject: [PATCH 17/25] Bump confluentinc/cp-kafka Docker tag to v8 (#4331) Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: martincostello --- .../KafkaFixture.cs | 6 +++++- .../kafka.Dockerfile | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/KafkaFixture.cs b/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/KafkaFixture.cs index 3a00202cd1..22a7ae9d12 100644 --- a/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/KafkaFixture.cs +++ b/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/KafkaFixture.cs @@ -10,5 +10,9 @@ public sealed class KafkaFixture : XunitContainerFixture { protected override string DockerfileName => "kafka.Dockerfile"; - protected override KafkaContainer CreateContainer() => new KafkaBuilder(this.GetImage()).Build(); + protected override KafkaContainer CreateContainer() => + new KafkaBuilder(this.GetImage()) + .WithListener("127.0.0.1:19092") + .WithKRaft() + .Build(); } diff --git a/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/kafka.Dockerfile b/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/kafka.Dockerfile index 4ec0de3bda..a607480c85 100644 --- a/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/kafka.Dockerfile +++ b/test/OpenTelemetry.Instrumentation.ConfluentKafka.Tests/kafka.Dockerfile @@ -1 +1 @@ -FROM confluentinc/cp-kafka:7.9.6@sha256:9e178783dba8cf44cd8b1d260f548a587a21235921957edd4ed7aa7d060b0852 +FROM confluentinc/cp-kafka:8.2.0@sha256:acbbf674f2ed40e5d0a8ca51beb0f00692c866fc22b5ce06f8cadbdc54cd4436 From a606b1b972475c7208bf0f17842179ea67311f7e Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 16:48:26 +0100 Subject: [PATCH 18/25] [Repo] Update CHANGELOGs (#4333) --- src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md | 3 ++- src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md | 1 + src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md | 1 + src/OpenTelemetry.OpAmp.Client/CHANGELOG.md | 3 ++- src/OpenTelemetry.Resources.AWS/CHANGELOG.md | 3 ++- src/OpenTelemetry.Resources.Azure/CHANGELOG.md | 3 ++- src/OpenTelemetry.Sampler.AWS/CHANGELOG.md | 3 ++- 7 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md b/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md index 7aa93f468e..50f5618af3 100644 --- a/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md @@ -7,7 +7,8 @@ Released 2026-Apr-21 * Limit how much of the response body is read when export fails using the HTTP - JSON transport and informational logging is enabled. + JSON transport and informational logging is enabled to resolve + [GHSA-55m9-299j-53c7](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/security/advisories/GHSA-55m9-299j-53c7). ([#4117](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4117)) * Updated OpenTelemetry core component version(s) to `1.15.3`. diff --git a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md index 3d30b5a60d..ee9ae16b5a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md @@ -163,6 +163,7 @@ Released 2024-Apr-17 `?key1=value1&key2=value2` becomes `?key1=Redacted&key2=Redacted`. You can disable this redaction by setting the environment variable `OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION` to `true`. + Resolves [GHSA-vh2m-22xx-q94f](https://github.com/advisories/GHSA-vh2m-22xx-q94f). ([#1656](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1656)) ## 1.8.0-beta.1 diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index 17bb0ea404..49f4235554 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -108,6 +108,7 @@ Released 2024-Apr-12 `?key1=value1&key2=value2` becomes `?key1=Redacted&key2=Redacted`. You can disable this redaction by setting the environment variable `OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` to `true`. + Resolves [GHSA-vh2m-22xx-q94f](https://github.com/advisories/GHSA-vh2m-22xx-q94f). ([#5532](https://github.com/open-telemetry/opentelemetry-dotnet/pull/5532)) ## 1.8.0 diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index 4c3d04515f..1524f62b43 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -26,7 +26,8 @@ Released 2026-Apr-21 * Add support for sticky HTTP connections via the `OpAMP-Instance-UID` header. ([#3830](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3830)) -* Apply response size limits for oversized OpAMP responses. +* Apply response size limits for oversized OpAMP responses to resolve + [GHSA-w2jh-77fq-7gp8](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/security/advisories/GHSA-w2jh-77fq-7gp8). ([#4116](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4116)) * Harden WebSocket transport: diff --git a/src/OpenTelemetry.Resources.AWS/CHANGELOG.md b/src/OpenTelemetry.Resources.AWS/CHANGELOG.md index 6aec7bda69..da9a6a6094 100644 --- a/src/OpenTelemetry.Resources.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Resources.AWS/CHANGELOG.md @@ -13,7 +13,8 @@ Released 2026-Apr-21 Windows containers running on AWS ECS. ([#4028](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4028)) -* Limit how much of the response body is consumed from metadata service HTTP responses. +* Limit how much of the response body is consumed from metadata service HTTP responses + to resolve [GHSA-28xm-prxc-5866](https://github.com/advisories/GHSA-28xm-prxc-5866). ([#4122](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4122)) * Fix ECS Metadata V4 cluster ARN normalization when the `Cluster` field returns diff --git a/src/OpenTelemetry.Resources.Azure/CHANGELOG.md b/src/OpenTelemetry.Resources.Azure/CHANGELOG.md index 7226b8453c..9351ec2b5d 100644 --- a/src/OpenTelemetry.Resources.Azure/CHANGELOG.md +++ b/src/OpenTelemetry.Resources.Azure/CHANGELOG.md @@ -9,7 +9,8 @@ Released 2026-Apr-21 -* Limit how much of the response body is consumed from metadata service HTTP responses. +* Limit how much of the response body is consumed from metadata service HTTP responses + to resolve [GHSA-vc24-j8c5-2vw4](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/security/advisories/GHSA-vc24-j8c5-2vw4). ([#4121](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4121)) * Updated OpenTelemetry core component version(s) to `1.15.3`. diff --git a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md index 15cf482976..9aa1d03158 100644 --- a/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Sampler.AWS/CHANGELOG.md @@ -23,7 +23,8 @@ Released 2026-Apr-14 * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) -* Limit the max size read for response body getting the sampling rules to 1MB. +* Limit the max size read for response body getting the sampling rules to 1MB to + resolve [GHSA-28xm-prxc-5866](https://github.com/advisories/GHSA-28xm-prxc-5866). ([#4100](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4100)) ## 0.1.0-alpha.7 From e0e10ccc27c54e589c09137303c760ca6fcae167 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 17:31:52 +0100 Subject: [PATCH 19/25] [Infra] Disallow instance string.Equals methods (#4332) --- build/BannedSymbols.txt | 2 ++ .../Internal/ConnectionStringBuilder.cs | 8 ++++---- .../Metrics/GenevaMetricExporterOptions.cs | 4 ++-- .../Metrics/TlvMetricExporter.cs | 8 ++++---- .../Implementation/AWSServiceType.cs | 14 +++++++------- .../Implementation/Utils.cs | 18 ++++++------------ .../SamplingRuleApplier.cs | 18 +++++++++--------- 7 files changed, 34 insertions(+), 38 deletions(-) diff --git a/build/BannedSymbols.txt b/build/BannedSymbols.txt index 2a2ab5b8c0..e3af14dac0 100644 --- a/build/BannedSymbols.txt +++ b/build/BannedSymbols.txt @@ -44,6 +44,8 @@ M:System.SByte.TryParse(System.ReadOnlySpan{System.Char},System.SByte@); Use ove M:System.SByte.TryParse(System.String,System.SByte@); Use overloads that specify CultureInfo.InvariantCulture. M:System.Single.TryParse(System.ReadOnlySpan{System.Char},System.Single@); Use overloads that specify CultureInfo.InvariantCulture. M:System.Single.TryParse(System.String,System.Single@); Use overloads that specify CultureInfo.InvariantCulture. +M:System.String.Equals(System.String); Use the static string.Equals(string, string) method instead to avoid potential NullReferenceException. +M:System.String.Equals(System.String,System.StringComparison); Use the static string.Equals(string, string, StringComparison) method instead to avoid potential NullReferenceException. M:System.TimeOnly.TryParse(System.ReadOnlySpan{System.Char},System.TimeOnly@); Use overloads that specify CultureInfo.InvariantCulture. M:System.TimeOnly.TryParse(System.String,System.TimeOnly@); Use overloads that specify CultureInfo.InvariantCulture. M:System.TimeSpan.TryParse(System.ReadOnlySpan{System.Char},System.TimeSpan@); Use overloads that specify CultureInfo.InvariantCulture. diff --git a/src/OpenTelemetry.Exporter.Geneva/Internal/ConnectionStringBuilder.cs b/src/OpenTelemetry.Exporter.Geneva/Internal/ConnectionStringBuilder.cs index a92db78036..51f4b1379b 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Internal/ConnectionStringBuilder.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Internal/ConnectionStringBuilder.cs @@ -71,16 +71,16 @@ public string EtwSession } public bool PrivatePreviewEnableTraceLoggingDynamic => this.parts.TryGetValue(nameof(this.PrivatePreviewEnableTraceLoggingDynamic), out var value) - && bool.TrueString.Equals(value, StringComparison.OrdinalIgnoreCase); + && string.Equals(bool.TrueString, value, StringComparison.OrdinalIgnoreCase); public bool PrivatePreviewEnableOtlpProtobufEncoding => this.parts.TryGetValue(nameof(this.PrivatePreviewEnableOtlpProtobufEncoding), out var value) - && bool.TrueString.Equals(value, StringComparison.OrdinalIgnoreCase); + && string.Equals(bool.TrueString, value, StringComparison.OrdinalIgnoreCase); public bool PrivatePreviewEnableUserEvents => this.parts.TryGetValue(nameof(this.PrivatePreviewEnableUserEvents), out var value) - && bool.TrueString.Equals(value, StringComparison.OrdinalIgnoreCase); + && string.Equals(bool.TrueString, value, StringComparison.OrdinalIgnoreCase); public bool PrivatePreviewEnableAFDCorrelationIdEnrichment => this.parts.TryGetValue(nameof(this.PrivatePreviewEnableAFDCorrelationIdEnrichment), out var value) - && bool.TrueString.Equals(value, StringComparison.OrdinalIgnoreCase); + && string.Equals(bool.TrueString, value, StringComparison.OrdinalIgnoreCase); public int PrivatePreviewLogMessagePackStringSizeLimit => !this.parts.TryGetValue(nameof(this.PrivatePreviewLogMessagePackStringSizeLimit), out var value) diff --git a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs index 6b4930cad9..1fc4e8b744 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs @@ -50,8 +50,8 @@ public IReadOnlyDictionary? PrepopulatedMetricDimensions foreach (var entry in value) { - if (entry.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || - entry.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(entry.Key, GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || + string.Equals(entry.Key, GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException($"The dimension: {entry.Key} is reserved and cannot be used as a prepopulated dimension."); } diff --git a/src/OpenTelemetry.Exporter.Geneva/Metrics/TlvMetricExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Metrics/TlvMetricExporter.cs index ac0e55398a..38caf916b3 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Metrics/TlvMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Metrics/TlvMetricExporter.cs @@ -646,8 +646,8 @@ private void SerializeDimensionsAndGetCustomAccountNamespace(in ReadOnlyTagColle // TODO: Data Validation } - if (tag.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || - tag.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(tag.Key, GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || + string.Equals(tag.Key, GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) { reservedTags++; continue; @@ -667,7 +667,7 @@ private void SerializeDimensionsAndGetCustomAccountNamespace(in ReadOnlyTagColle // Serialize MetricPoint Dimension values foreach (var tag in tags) { - if (tag.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsAccount) + if (string.Equals(tag.Key, GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsAccount) { if (!string.IsNullOrWhiteSpace(metricsAccount)) { @@ -677,7 +677,7 @@ private void SerializeDimensionsAndGetCustomAccountNamespace(in ReadOnlyTagColle continue; } - if (tag.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsNamespace) + if (string.Equals(tag.Key, GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsNamespace) { if (!string.IsNullOrWhiteSpace(metricsNamespace)) { diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs index 98f64525ab..f19850653f 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs @@ -15,23 +15,23 @@ internal class AWSServiceType internal const string BedrockRuntimeService = "Bedrock Runtime"; internal static bool IsDynamoDbService(string service) - => DynamoDbService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(DynamoDbService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsSqsService(string service) - => SQSService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(SQSService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsSnsService(string service) - => SNSService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(SNSService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsBedrockService(string service) - => BedrockService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(BedrockService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsBedrockAgentService(string service) - => BedrockAgentService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(BedrockAgentService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsBedrockAgentRuntimeService(string service) - => BedrockAgentRuntimeService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(BedrockAgentRuntimeService, service, StringComparison.OrdinalIgnoreCase); internal static bool IsBedrockRuntimeService(string service) - => BedrockRuntimeService.Equals(service, StringComparison.OrdinalIgnoreCase); + => string.Equals(BedrockRuntimeService, service, StringComparison.OrdinalIgnoreCase); } diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/Utils.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/Utils.cs index 3919ea16bd..a7dce8578e 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/Utils.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/Utils.cs @@ -11,7 +11,7 @@ internal class Utils { foreach (var tag in activity.TagObjects) { - if (tag.Key.Equals(tagName, StringComparison.Ordinal)) + if (string.Equals(tag.Key, tagName, StringComparison.Ordinal)) { return tag.Value; } @@ -20,15 +20,13 @@ internal class Utils return null; } - internal static string RemoveSuffix(string originalString, string suffix) - { - return string.IsNullOrEmpty(originalString) + internal static string RemoveSuffix(string originalString, string suffix) => + string.IsNullOrEmpty(originalString) ? string.Empty : originalString.EndsWith(suffix, StringComparison.Ordinal) ? originalString.Substring(0, originalString.Length - suffix.Length) : originalString; - } /// /// Removes amazon prefix from service name. There are two type of service name. @@ -39,14 +37,10 @@ internal static string RemoveSuffix(string originalString, string suffix) /// Name of the service. /// String after removing Amazon prefix. internal static string RemoveAmazonPrefixFromServiceName(string serviceName) - { - return RemovePrefix(RemovePrefix(serviceName, "Amazon"), "."); - } + => RemovePrefix(RemovePrefix(serviceName, "Amazon"), "."); - private static string RemovePrefix(string originalString, string prefix) - { - return string.IsNullOrEmpty(originalString) ? string.Empty : + private static string RemovePrefix(string originalString, string prefix) => + string.IsNullOrEmpty(originalString) ? string.Empty : originalString.StartsWith(prefix, StringComparison.Ordinal) ? originalString.Substring(prefix.Length) : originalString; - } } diff --git a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs index 2deb456fdd..ab4d69de8e 100644 --- a/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs +++ b/src/OpenTelemetry.Sampler.AWS/SamplingRuleApplier.cs @@ -94,19 +94,19 @@ public bool Matches(SamplingParameters samplingParameters, Resource resource) { foreach (var tag in samplingParameters.Tags) { - if (tag.Key.Equals(SemanticConventions.AttributeUrlPath, StringComparison.Ordinal)) + if (string.Equals(tag.Key, SemanticConventions.AttributeUrlPath, StringComparison.Ordinal)) { httpTarget = (string?)tag.Value; } - else if (tag.Key.Equals(SemanticConventions.AttributeUrlFull, StringComparison.Ordinal)) + else if (string.Equals(tag.Key, SemanticConventions.AttributeUrlFull, StringComparison.Ordinal)) { httpUrl = (string?)tag.Value; } - else if (tag.Key.Equals(SemanticConventions.AttributeHttpRequestMethod, StringComparison.Ordinal)) + else if (string.Equals(tag.Key, SemanticConventions.AttributeHttpRequestMethod, StringComparison.Ordinal)) { httpMethod = (string?)tag.Value; } - else if (tag.Key.Equals(SemanticConventions.AttributeHttpHost, StringComparison.Ordinal)) + else if (string.Equals(tag.Key, SemanticConventions.AttributeHttpHost, StringComparison.Ordinal)) { httpHost = (string?)tag.Value; } @@ -128,7 +128,7 @@ public bool Matches(SamplingParameters samplingParameters, Resource resource) } var serviceName = (string)resource.Attributes.FirstOrDefault(kvp => - kvp.Key.Equals("service.name", StringComparison.Ordinal)).Value; + string.Equals(kvp.Key, "service.name", StringComparison.Ordinal)).Value; return Matcher.AttributeMatch(samplingParameters.Tags, this.Rule.Attributes) && Matcher.WildcardMatch(httpTarget, this.Rule.UrlPath) && @@ -224,7 +224,7 @@ public SamplingRuleApplier WithTarget(SamplingTargetDocument target, DateTimeOff private static string GetServiceType(Resource resource) { var cloudPlatform = (string)resource.Attributes.FirstOrDefault(kvp => - kvp.Key.Equals("cloud.platform", StringComparison.Ordinal)).Value; + string.Equals(kvp.Key, "cloud.platform", StringComparison.Ordinal)).Value; return cloudPlatform == null ? string.Empty : Matcher.XRayCloudPlatform.TryGetValue(cloudPlatform, out var value) ? value : string.Empty; @@ -234,16 +234,16 @@ private static string GetArn(in SamplingParameters samplingParameters, Resource { // currently the aws resource detectors only capture ARNs for ECS and Lambda environments. var arn = (string?)resource.Attributes.FirstOrDefault(kvp => - kvp.Key.Equals("aws.ecs.container.arn", StringComparison.Ordinal)).Value; + string.Equals(kvp.Key, "aws.ecs.container.arn", StringComparison.Ordinal)).Value; if (arn != null) { return arn; } - if (GetServiceType(resource).Equals("AWS::Lambda::Function", StringComparison.Ordinal)) + if (string.Equals(GetServiceType(resource), "AWS::Lambda::Function", StringComparison.Ordinal)) { - arn = (string?)samplingParameters.Tags?.FirstOrDefault(kvp => kvp.Key.Equals("faas.id", StringComparison.Ordinal)).Value; + arn = (string?)samplingParameters.Tags?.FirstOrDefault(kvp => string.Equals(kvp.Key, "faas.id", StringComparison.Ordinal)).Value; if (arn != null) { From d71991406e44293cb8c722ec3237590b6e6e8d8a Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Wed, 29 Apr 2026 17:33:11 +0100 Subject: [PATCH 20/25] [SqlClient] Use SearchValues more in SqlProcessor (#4303) --- src/Shared/SqlProcessor.cs | 155 ++++++++++-------- .../SqlProcessorTests.cs | 11 ++ 2 files changed, 96 insertions(+), 70 deletions(-) diff --git a/src/Shared/SqlProcessor.cs b/src/Shared/SqlProcessor.cs index a382a79f7d..a8ec92f185 100644 --- a/src/Shared/SqlProcessor.cs +++ b/src/Shared/SqlProcessor.cs @@ -33,8 +33,13 @@ internal static class SqlProcessor private static readonly ConcurrentDictionary Cache = new(); private static readonly char[] WhitespaceChars = [SpaceChar, TabChar, CarriageReturnChar, NewLineChar]; +#if !NET + private static readonly char[] LineBreakChars = [CarriageReturnChar, NewLineChar]; +#endif #if NET + private static readonly SearchValues AsciiLetterSearchValues = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); + private static readonly SearchValues LineBreakSearchValues = SearchValues.Create("\n\r"); private static readonly SearchValues WhitespaceSearchValues = SearchValues.Create(WhitespaceChars); #endif @@ -428,6 +433,15 @@ private static void ParseNextToken( // Determine the length of the next contiguous ascii-letter run. // This allows some fast paths in the comparisons below. +#if NET + var asciiLetterLength = sql.Slice(start, remaining) + .IndexOfAnyExcept(AsciiLetterSearchValues); + + if (asciiLetterLength < 0) + { + asciiLetterLength = remaining; + } +#else var asciiLetterLength = 1; while (asciiLetterLength < remaining) { @@ -443,6 +457,7 @@ private static void ParseNextToken( asciiLetterLength++; } +#endif // IMPLEMENTATION NOTE: At one stage we tried checking if the length was between 2 and 12 (inclusive) // the range of shortest and longest keywords. This ended up being slower in practice @@ -669,38 +684,42 @@ static bool IsCaseInsensitiveMatch(ReadOnlySpan sql, int tokenStart, int t private static bool ParseWhitespace(ReadOnlySpan sql, Span buffer, ref ParseState state) { var start = state.ParsePosition; - var foundWhitespace = false; +#if NET + var remaining = sql.Slice(start); + var length = remaining.IndexOfAnyExcept(WhitespaceSearchValues); + if (length == 0) + { + return false; + } - // Find the end of whitespace run first + if (length < 0) + { + length = remaining.Length; + } +#else var i = start; while (i < sql.Length) { var currentChar = sql[i]; - -#if NET - if (WhitespaceSearchValues.Contains(currentChar)) -#else - if (currentChar is SpaceChar or TabChar or CarriageReturnChar or NewLineChar) -#endif + if (currentChar is not (SpaceChar or TabChar or CarriageReturnChar or NewLineChar)) { - foundWhitespace = true; - i++; - continue; + break; } - break; + i++; } - // Bulk copy whitespace if found - if (foundWhitespace) + var length = i - start; + if (length == 0) { - var length = i - start; - sql.Slice(start, length).CopyTo(buffer.Slice(state.SanitizedPosition)); - state.SanitizedPosition += length; - state.ParsePosition = i; + return false; } +#endif - return foundWhitespace; + sql.Slice(start, length).CopyTo(buffer.Slice(state.SanitizedPosition)); + state.SanitizedPosition += length; + state.ParsePosition = start + length; + return true; } private static bool SkipComment(ReadOnlySpan sql, ref ParseState state) @@ -715,20 +734,24 @@ private static bool SkipComment(ReadOnlySpan sql, ref ParseState state) // Scan past multi-line comment if (ch == '/' && iPlusOne < length && sql[iPlusOne] == AsteriskChar) { - // Use index arithmetic instead of slicing - var searchPos = iPlusTwo; - while (searchPos < length) + var remainingComment = sql.Slice(iPlusTwo); + var searchOffset = 0; + while (searchOffset < remainingComment.Length) { - if (sql[searchPos] == AsteriskChar) + var asteriskIndex = remainingComment.Slice(searchOffset).IndexOf(AsteriskChar); + if (asteriskIndex < 0) { - if (searchPos + 1 < length && sql[searchPos + 1] == ForwardSlashChar) - { - state.ParsePosition = searchPos + 2; - return true; - } + break; + } + + searchOffset += asteriskIndex; + if (searchOffset + 1 < remainingComment.Length && remainingComment[searchOffset + 1] == ForwardSlashChar) + { + state.ParsePosition = iPlusTwo + searchOffset + 2; + return true; } - searchPos++; + searchOffset++; } // Unterminated comment, consume to end @@ -739,19 +762,16 @@ private static bool SkipComment(ReadOnlySpan sql, ref ParseState state) // Scan past single-line comment if (ch == DashChar && iPlusOne < length && sql[iPlusOne] == DashChar) { - // Find next line break efficiently using index arithmetic - var searchPosition = iPlusTwo; - while (searchPosition < length) +#if NET + var lineBreakIndex = sql.Slice(iPlusTwo).IndexOfAny(LineBreakSearchValues); +#else + var lineBreakIndex = sql.Slice(iPlusTwo).IndexOfAny(LineBreakChars); +#endif + if (lineBreakIndex >= 0) { - var currentChar = sql[searchPosition]; - if (currentChar is CarriageReturnChar or NewLineChar) - { - // Position at the newline so ParseWhitespace can copy it - state.ParsePosition = searchPosition; - return true; - } - - searchPosition++; + // Position at the newline so ParseWhitespace can copy it + state.ParsePosition = iPlusTwo + lineBreakIndex; + return true; } state.ParsePosition = length; @@ -775,32 +795,33 @@ private static bool SanitizeStringLiteral(ReadOnlySpan sql, Span buf // If so, we want to skip the Unicode prefix when sanitizing. var isUnicode = state.ParsePosition >= 1 && sql[state.ParsePosition - 1] is UnicodePrefixChar; - // Use index arithmetic instead of slicing var searchPos = state.ParsePosition + 1; while (searchPos < sql.Length) { - if (sql[searchPos] == SingleQuoteChar) + var quoteIndex = sql.Slice(searchPos).IndexOf(SingleQuoteChar); + if (quoteIndex < 0) { - if (searchPos + 1 < sql.Length && sql[searchPos + 1] == SingleQuoteChar) - { - // Skip escaped quote ('') - searchPos += 2; - continue; - } + break; + } - // Found terminating quote - if (isUnicode) - { - // Skip the Unicode prefix by overwriting the previous position instead - state.SanitizedPosition--; - } + searchPos += quoteIndex; + if (searchPos + 1 < sql.Length && sql[searchPos + 1] == SingleQuoteChar) + { + // Skip escaped quote ('') + searchPos += 2; + continue; + } - state.ParsePosition = searchPos + 1; - buffer[state.SanitizedPosition++] = SanitizationPlaceholder; - return true; + // Found terminating quote + if (isUnicode) + { + // Skip the Unicode prefix by overwriting the previous position instead + state.SanitizedPosition--; } - searchPos++; + state.ParsePosition = searchPos + 1; + buffer[state.SanitizedPosition++] = SanitizationPlaceholder; + return true; } state.ParsePosition = sql.Length; @@ -948,18 +969,12 @@ private static bool TrySanitizeLiteralsForInClause(ReadOnlySpan sql, Span< return false; } - // Use index arithmetic instead of slicing - var searchPosition = parsePosition; - while (searchPosition < sql.Length) + var closeParenIndex = sql.Slice(parsePosition).IndexOf(CloseParenChar); + if (closeParenIndex >= 0) { - if (sql[searchPosition] == CloseParenChar) - { - state.ParsePosition = searchPosition; - buffer[state.SanitizedPosition++] = SanitizationPlaceholder; - return true; - } - - searchPosition++; + state.ParsePosition = parsePosition + closeParenIndex; + buffer[state.SanitizedPosition++] = SanitizationPlaceholder; + return true; } } diff --git a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs index fa70c365df..90ed5e3c40 100644 --- a/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs +++ b/test/OpenTelemetry.Contrib.Shared.Tests/SqlProcessorTests.cs @@ -28,6 +28,17 @@ public void GetSanitizedSql_CreateTableWithTrailingIdentifier_DoesNotThrow() Assert.Equal(sql, sqlStatementInfo.DbQuerySummary); } + [Fact] + public void GetSanitizedSql_SingleLineCommentWithCarriageReturnLineFeed_PreservesLineBreak() + { + var sql = "SELECT * FROM table -- comment\r\nWHERE id = 42"; + + var sqlStatementInfo = SqlProcessor.GetSanitizedSql(sql); + + Assert.Equal("SELECT * FROM table \r\nWHERE id = ?", sqlStatementInfo.SanitizedSql); + Assert.Equal("SELECT table", sqlStatementInfo.DbQuerySummary); + } + [Fact] public void GetSanitizedSql_UnterminatedEscapedIdentifierInFromClause_SanitizesLiterals() { From 41a289c229fe0246ba887e98ec277b1718090a71 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 30 Apr 2026 10:53:25 +0100 Subject: [PATCH 21/25] [Sampler.AWS] Fix flaky tests (#4297) --- .../AWSXRayRemoteSampler.cs | 178 ++++++++++++++---- .../BasicTests.cs | 2 +- .../TestAWSXRayRemoteSampler.cs | 74 +++++--- test/Shared/TestHttpServer.cs | 2 +- 4 files changed, 187 insertions(+), 69 deletions(-) diff --git a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs index f5652225f7..3e28f32868 100644 --- a/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs +++ b/src/OpenTelemetry.Sampler.AWS/AWSXRayRemoteSampler.cs @@ -14,8 +14,13 @@ public sealed class AWSXRayRemoteSampler : Trace.Sampler, IDisposable { internal static readonly TimeSpan DefaultTargetInterval = TimeSpan.FromSeconds(10); + private const string ClientIdCharacters = "0123456789abcdef"; + private static readonly Random Random = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly SemaphoreSlim pollerLock = new(1, 1); private bool isFallBackEventToWriteSwitch = true; + private int disposed; [SuppressMessage("Performance", "CA5394: Do not use insecure randomness", Justification = "Secure random is not required for jitters.")] internal AWSXRayRemoteSampler(Resource resource, TimeSpan pollingInterval, string endpoint, Clock clock) @@ -73,10 +78,7 @@ internal AWSXRayRemoteSampler(Resource resource, TimeSpan pollingInterval, strin /// to identify the service attributes for sampling. This resource should /// be the same as what the OpenTelemetry SDK is configured with. /// an instance of . - public static AWSXRayRemoteSamplerBuilder Builder(Resource resource) - { - return new AWSXRayRemoteSamplerBuilder(resource); - } + public static AWSXRayRemoteSamplerBuilder Builder(Resource resource) => new(resource); /// public override SamplingResult ShouldSample(in SamplingParameters samplingParameters) @@ -105,53 +107,54 @@ public void Dispose() GC.SuppressFinalize(this); } - [SuppressMessage( - "Usage", - "CA5394: Do not use insecure randomness", - Justification = "using insecure random is fine here since clientId doesn't need to be secure.")] - private static string GenerateClientId() + internal async Task ExecutePollAsync(Func pollAsync) { - char[] hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; - var clientIdChars = new char[24]; - for (var i = 0; i < clientIdChars.Length; i++) + var lockTaken = false; + + try { - clientIdChars[i] = hex[Random.Next(hex.Length)]; - } + await this.pollerLock.WaitAsync(this.cancellationTokenSource.Token).ConfigureAwait(false); + lockTaken = true; - return new string(clientIdChars); - } + if (Volatile.Read(ref this.disposed) != 0) + { + return; + } - private void Dispose(bool disposing) - { - if (disposing) + await pollAsync(this.cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (this.cancellationTokenSource.IsCancellationRequested) { - this.RulePollerTimer?.Dispose(); - this.Client?.Dispose(); - this.RulesCache?.Dispose(); + // Sampler is shutting down. + } + catch (ObjectDisposedException) when (Volatile.Read(ref this.disposed) != 0) + { + // Sampler is shutting down. + } + catch (Exception ex) + { + AWSSamplerEventSource.Log.ExceptionFromSampler(ex.Message); + } + finally + { + if (lockTaken) + { + this.pollerLock.Release(); + } } } - private async void GetAndUpdateRules(object? state) - { - var rules = await this.Client.GetSamplingRules().ConfigureAwait(false); - - this.RulesCache.UpdateRules(rules); - - // schedule the next rule poll. - this.RulePollerTimer.Change(this.PollingInterval.Add(this.RulePollerJitter), Timeout.InfiniteTimeSpan); - } - - private async void GetAndUpdateTargets(object? state) + internal async Task GetAndUpdateTargetsAsync(CancellationToken cancellationToken) { - await this.GetAndUpdateTargetsAsync().ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); - private async Task GetAndUpdateTargetsAsync() - { var statistics = this.RulesCache.Snapshot(this.Clock.Now()); var request = new GetSamplingTargetsRequest(statistics); - var response = await this.Client.GetSamplingTargets(request).ConfigureAwait(false); + var response = await this.Client.GetSamplingTargets(request, cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + if (response != null) { Dictionary targets = []; @@ -169,7 +172,8 @@ private async Task GetAndUpdateTargetsAsync() { var lastRuleModificationTime = this.Clock.ToDateTime(response.LastRuleModification); - if (lastRuleModificationTime > this.RulesCache.GetUpdatedAt()) + if (!cancellationToken.IsCancellationRequested && + lastRuleModificationTime > this.RulesCache.GetUpdatedAt()) { // rules have been updated. fetch the new ones right away. this.RulePollerTimer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan); @@ -185,6 +189,100 @@ private async Task GetAndUpdateTargetsAsync() nextTargetFetchInterval = DefaultTargetInterval; } - this.TargetPollerTimer.Change(nextTargetFetchInterval.Add(this.TargetPollerJitter), Timeout.InfiniteTimeSpan); + if (!cancellationToken.IsCancellationRequested) + { + this.TargetPollerTimer.Change(nextTargetFetchInterval.Add(this.TargetPollerJitter), Timeout.InfiniteTimeSpan); + } + } + + [SuppressMessage( + "Usage", + "CA5394: Do not use insecure randomness", + Justification = "using insecure random is fine here since clientId doesn't need to be secure.")] + private static string GenerateClientId() + { + const int ClientIdLength = 24; + +#if NET + Span buffer = stackalloc char[ClientIdLength]; + + Random.GetItems(ClientIdCharacters, buffer); + + return new(buffer); +#else + var buffer = new char[ClientIdLength]; + for (var i = 0; i < buffer.Length; i++) + { + buffer[i] = ClientIdCharacters[Random.Next(ClientIdCharacters.Length)]; + } + + return new string(buffer); +#endif + } + + private static void DisposeTimer(Timer? timer) + { + if (timer == null) + { + return; + } + + using var disposedEvent = new ManualResetEvent(false); + if (timer.Dispose(disposedEvent)) + { + disposedEvent.WaitOne(); + } + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Exchange(ref this.disposed, 1) != 0) + { + return; + } + + this.cancellationTokenSource.Cancel(); + DisposeTimer(this.RulePollerTimer); + DisposeTimer(this.TargetPollerTimer); + + this.pollerLock.Wait(); + + try + { + this.Client?.Dispose(); + this.RulesCache?.Dispose(); + } + finally + { + this.pollerLock.Release(); + this.pollerLock.Dispose(); + this.cancellationTokenSource.Dispose(); + } + } + } + + private void GetAndUpdateRules(object? state) => + _ = this.ExecutePollAsync(this.GetAndUpdateRulesAsync); + + private void GetAndUpdateTargets(object? state) => + _ = this.ExecutePollAsync(this.GetAndUpdateTargetsAsync); + + private async Task GetAndUpdateRulesAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var rules = await this.Client.GetSamplingRules(cancellationToken).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + this.RulesCache.UpdateRules(rules); + + if (!cancellationToken.IsCancellationRequested) + { + // schedule the next rule poll. + this.RulePollerTimer.Change(this.PollingInterval.Add(this.RulePollerJitter), Timeout.InfiniteTimeSpan); + } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 194358052c..4a3de6b062 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -1326,7 +1326,7 @@ private static void WaitForActivityExport(List exportedItems, int coun Thread.Sleep(10); return exportedItems.Count >= count; }, - TimeSpan.FromSeconds(1)), + TimeSpan.FromSeconds(5)), $"Actual: {exportedItems.Count} Expected: {count}"); private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) diff --git a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs index 4c2a7e3a2f..09b6c1c244 100644 --- a/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs +++ b/test/OpenTelemetry.Sampler.AWS.Tests/TestAWSXRayRemoteSampler.cs @@ -22,14 +22,12 @@ public void TestSamplerWithConfiguration() .SetEndpoint(endpoint) .Build(); - var rootSamplerFieldInfo = typeof(ParentBasedSampler).GetField("rootSampler", BindingFlags.NonPublic | BindingFlags.Instance); - - var xraySampler = (AWSXRayRemoteSampler?)rootSamplerFieldInfo?.GetValue(parentBasedSampler); + using var xraySampler = GetRemoteSampler(parentBasedSampler); - Assert.Equal(pollingInterval, xraySampler?.PollingInterval); - Assert.Equal(endpoint, xraySampler?.Endpoint); - Assert.NotNull(xraySampler?.RulePollerTimer); - Assert.NotNull(xraySampler?.Client); + Assert.Equal(pollingInterval, xraySampler.PollingInterval); + Assert.Equal(endpoint, xraySampler.Endpoint); + Assert.NotNull(xraySampler.RulePollerTimer); + Assert.NotNull(xraySampler.Client); } [Fact] @@ -37,14 +35,12 @@ public void TestSamplerWithDefaults() { var parentBasedSampler = AWSXRayRemoteSampler.Builder(ResourceBuilder.CreateEmpty().Build()).Build(); - var rootSamplerFieldInfo = typeof(ParentBasedSampler).GetField("rootSampler", BindingFlags.NonPublic | BindingFlags.Instance); - - var xraySampler = (AWSXRayRemoteSampler?)rootSamplerFieldInfo?.GetValue(parentBasedSampler); + using var xraySampler = GetRemoteSampler(parentBasedSampler); - Assert.Equal(TimeSpan.FromMinutes(5), xraySampler?.PollingInterval); - Assert.Equal("http://localhost:2000", xraySampler?.Endpoint); - Assert.NotNull(xraySampler?.RulePollerTimer); - Assert.NotNull(xraySampler?.Client); + Assert.Equal(TimeSpan.FromMinutes(5), xraySampler.PollingInterval); + Assert.Equal("http://localhost:2000", xraySampler.Endpoint); + Assert.NotNull(xraySampler.RulePollerTimer); + Assert.NotNull(xraySampler.Client); } [Fact] @@ -66,6 +62,8 @@ public async Task TestSamplerUpdateAndSample() .SetClock(clock) .Build(); + using var remoteSampler = GetRemoteSampler(sampler); + // the sampler will use fallback sampler until rules are fetched. Assert.Equal(SamplingDecision.RecordAndSample, this.DoSample(sampler, "cat-service")); @@ -122,26 +120,48 @@ public async Task TestSamplerUpdateTargetsWithMissingTargetDocumentsDoesNotThrow .SetClock(clock) .Build(); - var rootSamplerFieldInfo = typeof(ParentBasedSampler).GetField("rootSampler", BindingFlags.NonPublic | BindingFlags.Instance); - var sampler = (AWSXRayRemoteSampler?)rootSamplerFieldInfo?.GetValue(parentBasedSampler); - - Assert.NotNull(sampler); + using var sampler = GetRemoteSampler(parentBasedSampler); requestHandler.SetResponse("/SamplingTargets", "{\"LastRuleModification\":1530920505.0}"); - var getAndUpdateTargetsAsyncMethod = typeof(AWSXRayRemoteSampler).GetMethod("GetAndUpdateTargetsAsync", BindingFlags.NonPublic | BindingFlags.Instance); - var getAndUpdateTargetsAsyncTask = (Task?)getAndUpdateTargetsAsyncMethod?.Invoke(sampler, null); + await sampler.GetAndUpdateTargetsAsync(CancellationToken.None); + } - Assert.NotNull(getAndUpdateTargetsAsyncTask); + [Fact] + public async Task ExecutePollAsyncDoesNotBlockCaller() + { + using var sampler = GetRemoteSampler(AWSXRayRemoteSampler.Builder(ResourceBuilder.CreateEmpty().Build()).Build()); - try - { - await getAndUpdateTargetsAsyncTask!; - } - finally + var pollStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releasePoll = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var executePollAsyncMethod = typeof(AWSXRayRemoteSampler).GetMethod("ExecutePollAsync", BindingFlags.NonPublic | BindingFlags.Instance); + + Assert.NotNull(executePollAsyncMethod); + + Task PollAsync(CancellationToken cancellationToken) { - sampler.Dispose(); + pollStarted.TrySetResult(true); + cancellationToken.Register(() => releasePoll.TrySetCanceled(cancellationToken)); + return releasePoll.Task; } + + var stopwatch = Stopwatch.StartNew(); + var executePollTask = sampler.ExecutePollAsync(PollAsync); + stopwatch.Stop(); + + await pollStarted.Task; + Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1), $"Expected ExecutePollAsync to return without waiting for the poll to finish, but it took {stopwatch.Elapsed}."); + + releasePoll.TrySetResult(true); + await executePollTask; + } + + private static AWSXRayRemoteSampler GetRemoteSampler(Trace.Sampler sampler) + { + var rootSamplerFieldInfo = typeof(ParentBasedSampler).GetField("rootSampler", BindingFlags.NonPublic | BindingFlags.Instance); + var remoteSampler = (AWSXRayRemoteSampler?)rootSamplerFieldInfo?.GetValue(sampler); + + return remoteSampler ?? throw new InvalidOperationException("Unable to get AWSXRayRemoteSampler from ParentBasedSampler."); } private SamplingDecision DoSample(Trace.Sampler sampler, string serviceName) diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index 3ace5275b0..333ea62520 100644 --- a/test/Shared/TestHttpServer.cs +++ b/test/Shared/TestHttpServer.cs @@ -105,7 +105,7 @@ private static bool IsResponseAlreadyClosedException(Exception exception) return true; } - if (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 1 or 6 or 995 or 10057)) + if (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 1 or 6 or 995 or 1229 or 10057)) { return true; } From 3a1bf243f09361fe21c8021e6e1fb5e7f31db53b Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 30 Apr 2026 10:54:15 +0100 Subject: [PATCH 22/25] [Redis] Add schema URL to traces (#4095) Co-authored-by: Matthew Hensley <130569+matt-hensley@users.noreply.github.com> --- .../CHANGELOG.md | 3 +++ .../RedisProfilerEntryToActivityConverter.cs | 9 ++++++++- ....Instrumentation.StackExchangeRedis.csproj | 1 + ...kExchangeRedisConnectionInstrumentation.cs | 18 +++++++++++------- .../TracerProviderBuilderExtensions.cs | 2 +- ...kExchangeRedisCallsInstrumentationTests.cs | 19 +++++++++++++++++++ 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md index c554b48d37..0ee7d06d9f 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md @@ -6,6 +6,9 @@ the new database semantic conventions are enabled. ([#4245](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4245)) +* Add instrumentation scope version and schema URL to traces. + ([#4095](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4095)) + ## 1.15.1-beta.1 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs index f180ae92d6..9759e619d5 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs @@ -94,7 +94,14 @@ static bool GetCommandAndKey( name = StackExchangeRedisConnectionInstrumentation.ActivityName; } - var activity = StackExchangeRedisConnectionInstrumentation.ActivitySource.StartActivity( + var activitySource = + options.EmitNewAttributes && options.EmitOldAttributes ? + StackExchangeRedisConnectionInstrumentation.ActivitySourceBoth : + options.EmitNewAttributes ? + StackExchangeRedisConnectionInstrumentation.ActivitySourceNew : + StackExchangeRedisConnectionInstrumentation.ActivitySource; + + var activity = activitySource.StartActivity( name, ActivityKind.Client, parentActivity?.Context ?? default, diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj index bfd6fb23b5..1b3ee6e5f4 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj @@ -18,6 +18,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs index 374c7395ce..ddcee2961a 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; -using System.Reflection; using OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; using OpenTelemetry.Internal; using OpenTelemetry.Trace; @@ -18,12 +17,17 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis; internal sealed class StackExchangeRedisConnectionInstrumentation : IDisposable { internal const string RedisDatabaseIndexKeyName = "db.redis.database_index"; - internal static readonly Assembly Assembly = typeof(StackExchangeRedisConnectionInstrumentation).Assembly; -#pragma warning disable IDE0370 // Suppression is unnecessary - internal static readonly string ActivitySourceName = Assembly.GetName().Name!; -#pragma warning restore IDE0370 // Suppression is unnecessary - internal static readonly string ActivityName = ActivitySourceName + ".Execute"; - internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion()); + + internal static readonly Version SemanticConventionsVersion = new(1, 23, 0); + internal static readonly ActivitySource ActivitySource = ActivitySourceFactory.Create(SemanticConventionsVersion); + + internal static readonly Version SemanticConventionsVersionNew = new(1, 28, 0); + internal static readonly ActivitySource ActivitySourceNew = ActivitySourceFactory.Create(SemanticConventionsVersionNew); + + internal static readonly ActivitySource ActivitySourceBoth = ActivitySourceFactory.Create(null); + + internal static readonly string ActivityName = $"{ActivitySource.Name}.Execute"; + internal static readonly IEnumerable> OldCreationTags = [ new(SemanticConventions.AttributeDbSystem, "redis") diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs index d9a450a2a6..7f46ab8a01 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs @@ -151,7 +151,7 @@ public static TracerProviderBuilder AddRedisInstrumentation( } return builder - .AddSource(StackExchangeRedisConnectionInstrumentation.ActivitySourceName) + .AddSource(StackExchangeRedisConnectionInstrumentation.ActivitySource.Name) .AddInstrumentation(sp => { var instrumentation = sp.GetRequiredService(); diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs index 816ededb9c..d33e6d0ff1 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs @@ -162,6 +162,21 @@ public void SuccessfulCommandTest( // TODO VerifySamplingParameters(sampler.LatestSamplingParameters); } + + string? expectedSchemaUrl = (emitOldAttributes, emitNewAttributes) switch + { + (false, true) => "https://opentelemetry.io/schemas/1.28.0", + (true, false) => "https://opentelemetry.io/schemas/1.23.0", + _ => null, + }; + + foreach (var activity in exportedItems) + { + Assert.Equal("OpenTelemetry.Instrumentation.StackExchangeRedis", activity.Source.Name); + Assert.NotNull(activity.Source.Version); + Assert.NotEmpty(activity.Source.Version); + Assert.Equal(expectedSchemaUrl, activity.Source.TelemetrySchemaUrl); + } } [EnabledOnDockerPlatformFact(DockerPlatform.Linux)] @@ -551,6 +566,10 @@ private static void VerifyNewActivityData(Activity activity, bool isSet, EndPoin Assert.Equal(dbOperationName, activity.GetTagValue(SemanticConventions.AttributeDbOperationName)); Assert.Equal(dbQueryText, activity.GetTagValue(SemanticConventions.AttributeDbQueryText)); + Assert.Equal("OpenTelemetry.Instrumentation.StackExchangeRedis", activity.Source.Name); + Assert.NotNull(activity.Source.Version); + Assert.NotEmpty(activity.Source.Version); + VerifyEndPoint(activity, endPoint); } From dc7e36a04f6ec14e1e823824b9fcef2fff95d17c Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 30 Apr 2026 10:55:07 +0100 Subject: [PATCH 23/25] [Infra] Skip netfx reference assemblies on Windows (#4287) --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6cb826ceb2..80707291cf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,8 +33,8 @@ - - + + From fd2c66625a0198aadfe87778e1291c3a0732e4c8 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Thu, 30 Apr 2026 13:46:02 +0100 Subject: [PATCH 24/25] [AspNetCore] Avoid adding tags for traces (#3993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Piotr Kiełkowicz --- .../AspNetCoreInstrumentation.cs | 6 +- ...entationTracerProviderBuilderExtensions.cs | 8 +- .../CHANGELOG.md | 6 + .../Implementation/HttpInListener.cs | 87 ++++++++----- .../BasicTests.cs | 7 -- .../EndToEndTests.cs | 114 ++++++++++++++++++ .../MetricTests.cs | 10 -- .../RouteTests/TestApplication/RouteInfo.cs | 8 +- .../TestApplication/TestApplicationFactory.cs | 2 - test/TestApp.AspNetCore/Program.cs | 2 + 10 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EndToEndTests.cs diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs index d7d02ecdc3..a9ec2c8939 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -19,7 +19,7 @@ internal sealed class AspNetCoreInstrumentation : IDisposable "Microsoft.AspNetCore.Hosting.UnhandledException" ]; - private readonly Func isEnabled = (eventName, _, _) + private readonly Func isEnabled = static (eventName, _, _) => DiagnosticSourceEvents.Contains(eventName); private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; @@ -32,7 +32,5 @@ public AspNetCoreInstrumentation(HttpInListener httpInListener) /// public void Dispose() - { - this.diagnosticSourceSubscriber?.Dispose(); - } + => this.diagnosticSourceSubscriber?.Dispose(); } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs index d18e17357a..8b2a728b5e 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationTracerProviderBuilderExtensions.cs @@ -75,9 +75,7 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation( return builder.AddInstrumentation(sp => { var options = sp.GetRequiredService>().Get(name); - - return new AspNetCoreInstrumentation( - new HttpInListener(options)); + return new AspNetCoreInstrumentation(new HttpInListener(options)); }); } @@ -102,9 +100,9 @@ private static void AddAspNetCoreInstrumentationSources( string optionsName, IServiceProvider? serviceProvider = null) { - // For .NET7.0 onwards activity will be created using activitySource. + // For .NET 7.0+ the activity will be created using activitySource. // https://github.com/dotnet/aspnetcore/blob/bf3352f2422bf16fa3ca49021f0e31961ce525eb/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L327 - // For .NET6.0 and below, we will continue to use legacy way. + // For .NET 6.0 and below, we will continue to use legacy way. if (HttpInListener.Net7OrGreater) { // TODO: Check with .NET team to see if this can be prevented diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index 4c934aecb9..89bcc1fdfb 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +* Avoid duplicative work to add tags to traces when they are already natively supported + by ASP.NET Core itself. When using ASP.NET Core 10, performance can be + improved by setting the `Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData` + AppContext switch to `false` (its default value is `true`). + ([#3993](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3993)) + ## 1.15.2 Released 2026-Apr-21 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index c452c1fad9..cb1eb98e71 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -30,6 +30,7 @@ internal class HttpInListener : ListenerHandler #pragma warning restore IDE0370 // Suppression is unnecessary internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); internal static readonly bool Net7OrGreater = Environment.Version.Major >= 7; + internal static readonly bool Net10OrGreater = Environment.Version.Major >= 10; private const string DiagnosticSourceName = "Microsoft.AspNetCore"; @@ -47,6 +48,7 @@ internal class HttpInListener : ListenerHandler private static readonly PropertyFetcher ExceptionPropertyFetcher = new("Exception"); private readonly AspNetCoreTraceInstrumentationOptions options; + private readonly bool nativeAspNetCoreOpenTelemetryEnabled; public HttpInListener(AspNetCoreTraceInstrumentationOptions options) : base(DiagnosticSourceName) @@ -54,6 +56,7 @@ public HttpInListener(AspNetCoreTraceInstrumentationOptions options) Guard.ThrowIfNull(options); this.options = options; + this.nativeAspNetCoreOpenTelemetryEnabled = AspNetCoreHasNativeOpenTelemetryTags(); } public override void OnEventWritten(string name, object? payload) @@ -63,24 +66,18 @@ public override void OnEventWritten(string name, object? payload) switch (name) { case OnStartEvent: - { - this.OnStartActivity(activity, payload); - } - + this.OnStartActivity(activity, payload); break; - case OnStopEvent: - { - this.OnStopActivity(activity, payload); - } + case OnStopEvent: + this.OnStopActivity(activity, payload); break; + case OnUnhandledHostingExceptionEvent: case OnUnHandledDiagnosticsExceptionEvent: - { - this.OnException(activity, payload); - } - + this.OnException(activity, payload); break; + default: break; } @@ -176,19 +173,38 @@ public void OnStartActivity(Activity activity, object? payload) ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); } + // See the spec: https://github.com/open-telemetry/semantic-conventions/blob/v1.40.0/docs/http/http-spans.md var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method); - // see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.23.0/docs/http/http-spans.md + // ASP.NET Core 10 does not support OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS so we + // still need to set the HTTP method tag so that any override by the user is honoured. + TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method); - if (request.Host.HasValue) + if (!Net10OrGreater || !this.nativeAspNetCoreOpenTelemetryEnabled) { - activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Host); + if (request.Host.HasValue) + { + activity.SetTag(SemanticConventions.AttributeServerAddress, request.Host.Value); + + if (request.Host.Port is { } port) + { + activity.SetTag(SemanticConventions.AttributeServerPort, port); + } + } - if (request.Host.Port.HasValue) + if (request.Headers.TryGetValue("User-Agent", out var values)) { - activity.SetTag(SemanticConventions.AttributeServerPort, request.Host.Port.Value); + var userAgent = values.Count > 0 ? values[0] : null; + if (!string.IsNullOrEmpty(userAgent)) + { + activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); + } } + + activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); + activity.SetTag(SemanticConventions.AttributeUrlPath, path); } if (request.QueryString.HasValue) @@ -203,21 +219,8 @@ public void OnStartActivity(Activity activity, object? payload) } } - TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method); - - activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); - activity.SetTag(SemanticConventions.AttributeUrlPath, path); activity.SetTag(SemanticConventions.AttributeNetworkProtocolVersion, RequestDataHelper.GetHttpProtocolVersion(request.Protocol)); - if (request.Headers.TryGetValue("User-Agent", out var values)) - { - var userAgent = values.Count > 0 ? values[0] : null; - if (!string.IsNullOrEmpty(userAgent)) - { - activity.SetTag(SemanticConventions.AttributeUserAgentOriginal, userAgent); - } - } - try { this.options.EnrichWithHttpRequest?.Invoke(activity, request); @@ -394,4 +397,28 @@ private static void AddGrpcAttributes(Activity activity, string grpcMethod, Http } } } + + // ASP.NET Core 10 does not generate OpenTelemetry tags by default so we can only take + // the optimal path if the user has explicitly opted-out of suppressing the OpenTelemetry data. + private static bool AspNetCoreHasNativeOpenTelemetryTags() + { +#if NET10_0_OR_GREATER + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var suppressed)) + { + return !suppressed; + } +#endif +#if NET10_0 + // In ASP.NET Core 10 OpenTelemetry tags are suppressed by default, + // see https://github.com/dotnet/aspnetcore/blob/7387de91234d3ef751fa50b3d1bfede4130213ff/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L59-L67. + return false; +#elif NET11_0_OR_GREATER + // In ASP.NET Core 11+ OpenTelemetry tags are emitted by default, + // see https://github.com/dotnet/aspnetcore/blob/655f41d52f2fc75992eac41496b8e9cc119e1b54/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L59-L67. + return true; +#else + // In ASP.NET Core 8 and 9 the feature switch does not exist and there are no native OpenTelemetry tags + return false; +#endif + } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 4a3de6b062..2e2bfb07af 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -725,7 +725,6 @@ public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShould Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName); } -#if NET [Fact] public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore() { @@ -766,7 +765,6 @@ void ConfigureTestServices(IServiceCollection services) Assert.Equal("UserRegisteredActivitySource", activity.Source.Name); } -#endif [Theory] [InlineData(1)] @@ -1332,14 +1330,9 @@ private static void WaitForActivityExport(List exportedItems, int coun private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) { Assert.Equal(ActivityKind.Server, activityToValidate.Kind); -#if NET Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); Assert.NotNull(activityToValidate.Source.Version); Assert.Empty(activityToValidate.Source.Version); -#else - Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); - Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); -#endif Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string); } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EndToEndTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EndToEndTests.cs new file mode 100644 index 0000000000..09fc5df6d5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/EndToEndTests.cs @@ -0,0 +1,114 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Tests; + +[Collection("AspNetCore")] +public sealed class EndToEndTests + : IClassFixture>, IDisposable +{ + private readonly WebApplicationFactory factory; + private TracerProvider? tracerProvider; + + public EndToEndTests(WebApplicationFactory factory) + { + this.factory = factory; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task HttpRequestActivityIsCorrectWithFeatureSwitch(bool isEnabled) + { + bool? originalValue = null; + + if (AppContext.TryGetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", out var existingValue)) + { + originalValue = existingValue; + } + + AppContext.SetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", isEnabled); + + try + { + var exportedItems = new List(); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build(); + } + + // Arrange + using var client = this.factory + .WithWebHostBuilder(builder => + { + builder.ConfigureTestServices(ConfigureTestServices); + builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders()); + }) + .CreateClient(); + + client.DefaultRequestHeaders.UserAgent.Add(new("OpenTelemetry.Instrumentation.AspNetCore.Tests", "1.0")); + + _ = await client.GetStringAsync(new Uri("/ping", UriKind.Relative)); + + WaitForActivityExport(exportedItems, 1); + + var activity = Assert.Single(exportedItems); + + ValidateAspNetCoreActivity(activity, "/ping"); + + Assert.Equal("GET /ping", activity.DisplayName); + Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod)); + Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeServerAddress)); + Assert.Equal("OpenTelemetry.Instrumentation.AspNetCore.Tests/1.0", activity.GetTagValue(SemanticConventions.AttributeUserAgentOriginal)); + Assert.Equal("http", activity.GetTagValue(SemanticConventions.AttributeUrlScheme)); + Assert.Equal("/ping", activity.GetTagValue(SemanticConventions.AttributeUrlPath)); + } + finally + { + if (originalValue is { } previousValue) + { + AppContext.SetSwitch("Microsoft.AspNetCore.Hosting.SuppressActivityOpenTelemetryData", previousValue); + } + } + } + + public void Dispose() + => this.tracerProvider?.Dispose(); + + private static void WaitForActivityExport(List exportedItems, int count) + => Assert.True( + SpinWait.SpinUntil( + () => + { + // We need to let End callback execute as it is executed AFTER response was returned. + // In unit tests environment there may be a lot of parallel unit tests executed, so + // giving some breathing room for the End callback to complete + Thread.Sleep(10); + return exportedItems.Count >= count; + }, + TimeSpan.FromSeconds(5)), + $"Actual: {exportedItems.Count} Expected: {count}"); + + private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath) + { + Assert.Equal(ActivityKind.Server, activityToValidate.Kind); + Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name); + Assert.NotNull(activityToValidate.Source.Version); + Assert.Empty(activityToValidate.Source.Version); + Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 0ea35508eb..48dd47f26a 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -1,22 +1,14 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#if NET using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; -#endif using Microsoft.AspNetCore.Hosting; -#if NET using Microsoft.AspNetCore.Http; -#endif using Microsoft.AspNetCore.Mvc.Testing; -#if NET using Microsoft.AspNetCore.RateLimiting; -#endif -#if NET using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -#endif using Microsoft.Extensions.Logging; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -38,7 +30,6 @@ public void AddAspNetCoreInstrumentation_BadArgs() Assert.Throws(builder!.AddAspNetCoreInstrumentation); } -#if NET [Fact] public async Task ValidateNetMetricsAsync() { @@ -178,7 +169,6 @@ static string GetTicks() await app.DisposeAsync(); } -#endif [Theory] [InlineData("/api/values/2", "api/Values/{id}", null, 200)] diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs index ad2e92e737..d02ba23b77 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/RouteInfo.cs @@ -3,9 +3,7 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; -#if NET using Microsoft.AspNetCore.Http.Metadata; -#endif using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Routing; @@ -38,9 +36,7 @@ public void SetValues(HttpContext context) this.Path = $"{context.Request.Path}{context.Request.QueryString}"; var endpoint = context.GetEndpoint(); this.RawText = (endpoint as RouteEndpoint)?.RoutePattern.RawText; -#if NET this.RouteDiagnosticMetadata = endpoint?.Metadata.GetMetadata()?.Route; -#endif this.RouteData = new Dictionary(); foreach (var value in context.GetRouteData().Values) { @@ -48,8 +44,6 @@ public void SetValues(HttpContext context) } } - public void SetValues(ActionDescriptor actionDescriptor) - { + public void SetValues(ActionDescriptor actionDescriptor) => this.ActionDescriptor ??= new ActionDescriptorInfo(actionDescriptor); - } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs index a0e99e5775..478fe00c05 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/TestApplication/TestApplicationFactory.cs @@ -129,11 +129,9 @@ private static WebApplication CreateMinimalApiApplication() app.MapGet("/MinimalApi", () => Results.Ok()); app.MapGet("/MinimalApi/{id}", (int id) => Results.Ok()); -#if NET var api = app.MapGroup("/MinimalApiUsingMapGroup"); api.MapGet("/", () => Results.Ok()); api.MapGet("/{id}", (int id) => Results.Ok()); -#endif return app; } diff --git a/test/TestApp.AspNetCore/Program.cs b/test/TestApp.AspNetCore/Program.cs index 110693b6fa..b3c2de565d 100644 --- a/test/TestApp.AspNetCore/Program.cs +++ b/test/TestApp.AspNetCore/Program.cs @@ -53,6 +53,8 @@ public static void Main(string[] args) app.AddTestMiddleware(); + app.MapGet("/ping", () => "pong"); + app.Run(); } } From f1246c53cdc6fac9e9e783de8e9f5cc160f8425c Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 21 Mar 2026 13:36:47 +0000 Subject: [PATCH 25/25] [AspNetCore] Do not add built-in tags for v11+ Do not add tags to activities for ASP.NET Core 11 that are already added by the framework. Relates to open-telemetry#3808. --- .../Implementation/HttpInListener.cs | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index cb1eb98e71..8cea0212b7 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -31,6 +31,7 @@ internal class HttpInListener : ListenerHandler internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Version.ToString()); internal static readonly bool Net7OrGreater = Environment.Version.Major >= 7; internal static readonly bool Net10OrGreater = Environment.Version.Major >= 10; + internal static readonly bool Net11OrGreater = Environment.Version.Major >= 11; private const string DiagnosticSourceName = "Microsoft.AspNetCore"; @@ -101,6 +102,8 @@ public void OnStartActivity(Activity activity, object? payload) return; } + string? path = null; + // Ensure context extraction irrespective of sampling decision var request = context.Request; var textMapPropagator = Propagators.DefaultTextMapPropagator; @@ -140,6 +143,14 @@ public void OnStartActivity(Activity activity, object? payload) // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity activity.IsAllDataRequested = false; activity = newOne; + + if (Net11OrGreater) + { + // ASP.NET Core 11 will set the url.path attribute, but only if the + // activity is sampled. As we skip setting it for ASP.NET Core 11 + // we need to set it here otherwise it will not get set at all. + SetUrlPathAttribute(request, activity); + } } Baggage.Current = ctx.Baggage; @@ -173,13 +184,14 @@ public void OnStartActivity(Activity activity, object? payload) ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); } - // See the spec: https://github.com/open-telemetry/semantic-conventions/blob/v1.40.0/docs/http/http-spans.md - var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; - - TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method); + if (!Net11OrGreater || !this.nativeAspNetCoreOpenTelemetryEnabled) + { + TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method); + } - // ASP.NET Core 10 does not support OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS so we - // still need to set the HTTP method tag so that any override by the user is honoured. + // ASP.NET Core does not support OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS so we + // still need to set the display name and HTTP method tag so that any override + // by the user is honoured. See https://github.com/dotnet/aspnetcore/issues/65873. TelemetryHelper.RequestDataHelper.SetHttpMethodTag(activity, request.Method); if (!Net10OrGreater || !this.nativeAspNetCoreOpenTelemetryEnabled) @@ -204,7 +216,8 @@ public void OnStartActivity(Activity activity, object? payload) } activity.SetTag(SemanticConventions.AttributeUrlScheme, request.Scheme); - activity.SetTag(SemanticConventions.AttributeUrlPath, path); + + SetUrlPathAttribute(request, activity); } if (request.QueryString.HasValue) @@ -230,6 +243,13 @@ public void OnStartActivity(Activity activity, object? payload) AspNetCoreInstrumentationEventSource.Log.EnrichmentException(nameof(HttpInListener), nameof(this.OnStartActivity), activity.OperationName, ex); } } + + void SetUrlPathAttribute(HttpRequest request, Activity activity) + { + // See the spec: https://github.com/open-telemetry/semantic-conventions/blob/v1.40.0/docs/http/http-spans.md + path ??= (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; + activity.SetTag(SemanticConventions.AttributeUrlPath, path); + } } public void OnStopActivity(Activity activity, object? payload) @@ -244,25 +264,28 @@ public void OnStopActivity(Activity activity, object? payload) var response = context.Response; -#if !NETSTANDARD - var routePattern = context.GetHttpRoute(); - if (!string.IsNullOrEmpty(routePattern)) + if (!Net11OrGreater || !this.nativeAspNetCoreOpenTelemetryEnabled) { - TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern); - activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); - } +#if NET + var routePattern = context.GetHttpRoute(); + if (!string.IsNullOrEmpty(routePattern)) + { + TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern); + activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern); + } #endif - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, TelemetryHelper.GetBoxedStatusCode(response.StatusCode)); - if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) - { - AddGrpcAttributes(activity, grpcMethod, context); + if (activity.Status == ActivityStatusCode.Unset) + { + activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + } } - if (activity.Status == ActivityStatusCode.Unset) + if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) { - activity.SetStatus(SpanHelper.ResolveActivityStatusForHttpStatusCode(activity.Kind, response.StatusCode)); + AddGrpcAttributes(activity, grpcMethod, context); } try