From 2122abb5b78cc8b68581d06e431ff7c9b477eac4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 9 Feb 2026 13:53:04 +1300 Subject: [PATCH 1/6] Added support to send OTEL traces via OTLP Resolves: #4859 - #4859 --- .../Program.cs | 11 ++- ...ry.Samples.OpenTelemetry.AspNetCore.csproj | 1 + .../Sentry.OpenTelemetry.csproj | 2 +- .../SentryOptionsExtensions.cs | 4 +- .../TracerProviderBuilderExtensions.cs | 88 ++++++++++++++----- src/Sentry/Dsn.cs | 2 + 6 files changed, 83 insertions(+), 25 deletions(-) diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs index e53d2b20b2..05fc13690a 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs @@ -7,6 +7,13 @@ var builder = WebApplication.CreateBuilder(args); +// Read the Sentry DSN from the environment variable, if it's not already set in code. +#if SENTRY_DSN_DEFINED_IN_ENV +var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN") ?? throw new InvalidOperationException("SENTRY_DSN environment variable is not set"); +#else +var dsn = SamplesShared.Dsn; +#endif + // OpenTelemetry Configuration // See https://opentelemetry.io/docs/instrumentation/net/getting-started/ builder.Services.AddOpenTelemetry() @@ -20,8 +27,8 @@ // The two lines below take care of configuring sources for ASP.NET Core and HttpClient .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - // Finally we configure OpenTelemetry to send traces to Sentry - .AddSentry() + // Finally, we configure OpenTelemetry to send traces to Sentry + .AddSentry(dsn) ); builder.WebHost.UseSentry(options => diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj index a07956f015..4b2791c341 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Sentry.Samples.OpenTelemetry.AspNetCore.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj index 6067738ff9..e3c498e746 100644 --- a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj +++ b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 2035787a19..7633ea8adc 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -41,8 +41,8 @@ public static void UseOpenTelemetry( /// /// Configures Sentry to use OpenTelemetry for distributed tracing. /// - /// Note: if you are using this method to configure Sentry to work with OpenTelemetry you will also have to call - /// when building your + /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call + /// TracerProviderBuilderExtensions.AddSentry when building your /// to ensure OpenTelemetry sends trace information to Sentry. /// /// diff --git a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs index 96472eda98..970daf8e77 100644 --- a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Exporter; using OpenTelemetry.Trace; using Sentry.Extensibility; @@ -11,27 +12,74 @@ namespace Sentry.OpenTelemetry; /// public static class TracerProviderBuilderExtensions { - /// - /// Ensures OpenTelemetry trace information is sent to Sentry. - /// - /// . - /// - /// The default TextMapPropagator to be used by OpenTelemetry. - /// - /// If this parameter is not supplied, the will be used, which propagates the - /// baggage header as well as Sentry trace headers. - /// - /// - /// The is required for Sentry's OpenTelemetry integration to work but you - /// could wrap this in a if you needed other propagators as well. - /// - /// - /// The supplied for chaining. - public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, TextMapPropagator? defaultTextMapPropagator = null) + extension(TracerProviderBuilder tracerProviderBuilder) { - defaultTextMapPropagator ??= new SentryPropagator(); - Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); - return tracerProviderBuilder.AddProcessor(ImplementationFactory); + /// + /// Ensures OpenTelemetry trace information is sent to Sentry. + /// + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + /// The supplied for chaining. + public TracerProviderBuilder AddSentry(TextMapPropagator? defaultTextMapPropagator = null) + { + defaultTextMapPropagator ??= new SentryPropagator(); + Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); + return tracerProviderBuilder.AddProcessor(ImplementationFactory); + } + + /// + /// Ensures OpenTelemetry trace information is sent to the Sentry OTLP endpoint. + /// + /// The DSN for your Sentry project + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + /// The supplied for chaining. + public TracerProviderBuilder AddSentry(string dsnString, TextMapPropagator? defaultTextMapPropagator = null) + { + if (string.IsNullOrWhiteSpace(dsnString)) + { + throw new ArgumentException("OTLP endpoint must be provided", nameof(dsnString)); + } + + defaultTextMapPropagator ??= new SentryPropagator(); + Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); + + if (Dsn.TryParse(dsnString) is not { } dsn) + { + return tracerProviderBuilder; + } + + tracerProviderBuilder.AddOtlpExporter(options => + { + options.Endpoint = dsn.GetOtlpTracesEndpointUri(); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); + return client; + }; + }); + return tracerProviderBuilder.AddProcessor(ImplementationFactory); + } } internal static BaseProcessor ImplementationFactory(IServiceProvider services) diff --git a/src/Sentry/Dsn.cs b/src/Sentry/Dsn.cs index 011af39169..a132d89945 100644 --- a/src/Sentry/Dsn.cs +++ b/src/Sentry/Dsn.cs @@ -58,6 +58,8 @@ private Dsn( public Uri GetEnvelopeEndpointUri() => new(ApiBaseUri, "envelope/"); + public Uri GetOtlpTracesEndpointUri() => new(ApiBaseUri, "integration/otlp/v1/traces"); + public override string ToString() => Source; public static bool IsDisabled(string? dsn) => From a2fb0e48e0bfd990249937cabd8be843474a0e3b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 10 Feb 2026 10:08:22 +1300 Subject: [PATCH 2/6] Added OTEL propagation context to link OTEL spans to events etc. --- .../OpenTelemetryTransactionProcessor.cs | 1 + .../OtelPropagationContext.cs | 24 ++++++ .../Sentry.OpenTelemetry.csproj | 2 + .../SentryOptionsExtensions.cs | 78 +++++++++---------- src/Sentry.OpenTelemetry/SentryPropagator.cs | 3 +- src/Sentry/DynamicSamplingContext.cs | 5 +- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/Internal/IPropagationContext.cs | 12 +++ src/Sentry/Scope.cs | 14 ++-- src/Sentry/SentryOptions.cs | 6 ++ src/Sentry/SentryPropagationContext.cs | 16 ++-- .../OpenTelemetryExtensionsTests.cs | 2 + .../OpenTelemetryTransactionProcessorTests.cs | 2 + .../SentryPropagatorTests.cs | 1 + test/Sentry.Tests/HubTests.cs | 12 +-- .../SentryPropagationContextTests.cs | 22 +++--- 16 files changed, 121 insertions(+), 81 deletions(-) create mode 100644 src/Sentry.OpenTelemetry/OtelPropagationContext.cs create mode 100644 src/Sentry/Internal/IPropagationContext.cs diff --git a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs index 4ab485dde1..a6423ef254 100644 --- a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs +++ b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry; diff --git a/src/Sentry.OpenTelemetry/OtelPropagationContext.cs b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs new file mode 100644 index 0000000000..1d74a3f086 --- /dev/null +++ b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs @@ -0,0 +1,24 @@ +using Sentry.Extensibility; +using Sentry.Internal; + +namespace Sentry.OpenTelemetry; + +internal class OtelPropagationContext : IPropagationContext +{ + public DynamicSamplingContext? DynamicSamplingContext { get; private set; } + + public SentryId TraceId => Activity.Current?.TraceId.AsSentryId() ?? default; + public SpanId SpanId => Activity.Current?.SpanId.AsSentrySpanId() ?? default; + public SpanId? ParentSpanId => Activity.Current?.ParentSpanId.AsSentrySpanId(); + + public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession) + { + if (DynamicSamplingContext is null) + { + options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context."); + DynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); + } + + return DynamicSamplingContext; + } +} diff --git a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj index e3c498e746..ef2d1fb369 100644 --- a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj +++ b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj @@ -5,6 +5,8 @@ $(PackageTags);OpenTelemetry $(CurrentTfms);netstandard2.1;netstandard2.0;net462 enable + + $(NoWarn);AD0001 diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 7633ea8adc..576b333760 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -8,50 +8,44 @@ namespace Sentry.OpenTelemetry; /// public static class SentryOptionsExtensions { - /// - /// Enables OpenTelemetry instrumentation with Sentry - /// - /// instance - /// - /// - /// The default TextMapPropagator to be used by OpenTelemetry. - /// - /// If this parameter is not supplied, the will be used, which propagates the - /// baggage header as well as Sentry trace headers. - /// - /// - /// The is required for Sentry's OpenTelemetry integration to work but you - /// could wrap this in a if you needed other propagators as well. - /// - /// - public static void UseOpenTelemetry( - this SentryOptions options, - TracerProviderBuilder traceProviderBuilder, - TextMapPropagator? defaultTextMapPropagator = null - ) + extension(SentryOptions options) { - options.Instrumenter = Instrumenter.OpenTelemetry; - options.AddTransactionProcessor( - new OpenTelemetryTransactionProcessor() - ); - - traceProviderBuilder.AddSentry(defaultTextMapPropagator); - } + /// + /// Enables OpenTelemetry instrumentation with Sentry + /// + /// + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + public void UseOpenTelemetry(TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) + { + options.UseOpenTelemetry(); + builder.AddSentry(textMapPropagator); + } - /// - /// Configures Sentry to use OpenTelemetry for distributed tracing. - /// - /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call - /// TracerProviderBuilderExtensions.AddSentry when building your - /// to ensure OpenTelemetry sends trace information to Sentry. - /// - /// - /// instance - public static void UseOpenTelemetry(this SentryOptions options) - { - options.Instrumenter = Instrumenter.OpenTelemetry; - options.AddTransactionProcessor( - new OpenTelemetryTransactionProcessor() + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// + /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call + /// when building your + /// to ensure OpenTelemetry sends trace information to Sentry. + /// + /// + public void UseOpenTelemetry() + { + options.Instrumenter = Instrumenter.OpenTelemetry; + options.PropagationContextFactory = _ => new OtelPropagationContext(); + options.AddTransactionProcessor( + new OpenTelemetryTransactionProcessor() ); + } } } diff --git a/src/Sentry.OpenTelemetry/SentryPropagator.cs b/src/Sentry.OpenTelemetry/SentryPropagator.cs index 78fd960d6b..f8b4696c21 100644 --- a/src/Sentry.OpenTelemetry/SentryPropagator.cs +++ b/src/Sentry.OpenTelemetry/SentryPropagator.cs @@ -2,6 +2,7 @@ using OpenTelemetry; using OpenTelemetry.Context.Propagation; using Sentry.Extensibility; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry; @@ -54,7 +55,7 @@ public override PropagationContext Extract(PropagationContext context, T carr Options?.LogDebug("SentryPropagator.Extract"); var result = base.Extract(context, carrier, getter); - var baggage = result.Baggage; // The Otel .NET SDK takes care of baggage headers alread + var baggage = result.Baggage; // The Otel .NET SDK takes care of baggage headers already Options?.LogDebug("Baggage"); foreach (var entry in baggage) diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index f39d7fa5a7..683e809e74 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -1,5 +1,4 @@ using Sentry.Internal; -using Sentry.Internal.Extensions; namespace Sentry; @@ -228,7 +227,7 @@ public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTra replaySession); } - public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) + public static DynamicSamplingContext CreateFromPropagationContext(IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) { var traceId = propagationContext.TraceId; var publicKey = options.ParsedDsn.PublicKey; @@ -257,6 +256,6 @@ public static DynamicSamplingContext CreateDynamicSamplingContext(this Transacti public static DynamicSamplingContext CreateDynamicSamplingContext(this UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromUnsampledTransaction(transaction, options, replaySession); - public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) + public static DynamicSamplingContext CreateDynamicSamplingContext(this IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index be71b2f1d3..e39e6fd0f7 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -478,7 +478,7 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span) } } - private void ApplyTraceContextToEvent(SentryEvent evt, SentryPropagationContext propagationContext) + private void ApplyTraceContextToEvent(SentryEvent evt, IPropagationContext propagationContext) { evt.Contexts.Trace.TraceId = propagationContext.TraceId; evt.Contexts.Trace.SpanId = propagationContext.SpanId; diff --git a/src/Sentry/Internal/IPropagationContext.cs b/src/Sentry/Internal/IPropagationContext.cs new file mode 100644 index 0000000000..00f2c781af --- /dev/null +++ b/src/Sentry/Internal/IPropagationContext.cs @@ -0,0 +1,12 @@ +namespace Sentry.Internal; + +internal interface IPropagationContext +{ + public DynamicSamplingContext? DynamicSamplingContext { get; } + + public SentryId TraceId { get; } + public SpanId SpanId { get; } + public SpanId? ParentSpanId { get; } + + public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession); +} diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 04fab75ff1..4638a4255a 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; using Sentry.Extensibility; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Internal.OpenTelemetry; namespace Sentry; @@ -249,7 +245,7 @@ public ITransactionTracer? Transaction } } - internal SentryPropagationContext PropagationContext { get; private set; } + internal IPropagationContext PropagationContext { get; private set; } internal SessionUpdate? SessionUpdate { get; set; } @@ -297,10 +293,10 @@ public Scope(SentryOptions? options) { } - internal Scope(SentryOptions? options, SentryPropagationContext? propagationContext) + internal Scope(SentryOptions? options, IPropagationContext? propagationContext) { Options = options ?? new SentryOptions(); - PropagationContext = new SentryPropagationContext(propagationContext); + PropagationContext = Options.PropagationContextFactory(propagationContext); } // For testing. Should explicitly require SentryOptions. @@ -420,7 +416,7 @@ public void Clear() _extra.Clear(); _tags.Clear(); ClearAttachments(); - PropagationContext = new(); + PropagationContext = Options.PropagationContextFactory(null); } /// diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 7fc69a600f..29bc351511 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1156,6 +1156,12 @@ public StackTraceMode StackTraceMode /// internal Instrumenter Instrumenter { get; set; } = Instrumenter.Sentry; + /// + /// The default factory creates SentryPropagationContext instances... this should be replaced when using OTEL + /// + internal Func PropagationContextFactory { get; set; } = sourceContext => + new SentryPropagationContext(sourceContext); + /// /// /// Set to `true` to prevents Sentry from automatically registering . diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs index 6183262241..2ae4adba09 100644 --- a/src/Sentry/SentryPropagationContext.cs +++ b/src/Sentry/SentryPropagationContext.cs @@ -3,23 +3,23 @@ namespace Sentry; -internal class SentryPropagationContext +internal class SentryPropagationContext : IPropagationContext { public SentryId TraceId { get; } public SpanId SpanId { get; } public SpanId? ParentSpanId { get; } - internal DynamicSamplingContext? _dynamicSamplingContext; + public DynamicSamplingContext? DynamicSamplingContext { get; private set; } public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession) { - if (_dynamicSamplingContext is null) + if (DynamicSamplingContext is null) { options.LogDebug("Creating the Dynamic Sampling Context from the Propagation Context."); - _dynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); + DynamicSamplingContext = this.CreateDynamicSamplingContext(options, replaySession); } - return _dynamicSamplingContext; + return DynamicSamplingContext; } internal SentryPropagationContext( @@ -30,7 +30,7 @@ internal SentryPropagationContext( TraceId = traceId; SpanId = SpanId.Create(); ParentSpanId = parentSpanId; - _dynamicSamplingContext = dynamicSamplingContext; + DynamicSamplingContext = dynamicSamplingContext; } public SentryPropagationContext() @@ -39,13 +39,13 @@ public SentryPropagationContext() SpanId = SpanId.Create(); } - public SentryPropagationContext(SentryPropagationContext? other) + public SentryPropagationContext(IPropagationContext? other) { TraceId = other?.TraceId ?? SentryId.Create(); SpanId = other?.SpanId ?? SpanId.Create(); ParentSpanId = other?.ParentSpanId; - _dynamicSamplingContext = other?._dynamicSamplingContext; + DynamicSamplingContext = other?.DynamicSamplingContext; } public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession) diff --git a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs index 636a54f9fb..580002204d 100644 --- a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryExtensionsTests.cs @@ -1,3 +1,5 @@ +using Sentry.Internal.OpenTelemetry; + namespace Sentry.OpenTelemetry.Tests; public class OpenTelemetryExtensionsTests diff --git a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs index 714caf1cb7..cffe5eb6c4 100644 --- a/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/OpenTelemetryTransactionProcessorTests.cs @@ -1,3 +1,5 @@ +using Sentry.Internal.OpenTelemetry; + namespace Sentry.OpenTelemetry.Tests; public class OpenTelemetryTransactionProcessorTests : ActivitySourceTests diff --git a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs index 693619aece..cdd24560a3 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Primitives; using OpenTelemetry; using OpenTelemetry.Context.Propagation; +using Sentry.Internal.OpenTelemetry; namespace Sentry.OpenTelemetry.Tests; diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index c8bb6ea702..11171d8e0c 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1510,8 +1510,8 @@ public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransa { scope.PropagationContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); scope.PropagationContext.ParentSpanId.Should().Be(SpanId.Parse("2000000000000000")); - Assert.NotNull(scope.PropagationContext._dynamicSamplingContext); - scope.PropagationContext._dynamicSamplingContext.Items.Should().Contain(baggageHeader.GetSentryMembers()); + Assert.NotNull(scope.PropagationContext.DynamicSamplingContext); + scope.PropagationContext.DynamicSamplingContext.Items.Should().Contain(baggageHeader.GetSentryMembers()); }); transactionContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); @@ -1531,7 +1531,7 @@ public void ContinueTrace_DoesNotReceiveHeaders_CreatesRootTrace() hub.ScopeManager.ConfigureScope(scope => { Assert.Null(scope.PropagationContext.ParentSpanId); - Assert.Null(scope.PropagationContext._dynamicSamplingContext); + Assert.Null(scope.PropagationContext.DynamicSamplingContext); }); transactionContext.Name.Should().Be("test-name"); @@ -1564,8 +1564,8 @@ public void ContinueTrace_ReceivesHeadersAsStrings_SetsPropagationContextAndRetu { scope.PropagationContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); scope.PropagationContext.ParentSpanId.Should().Be(SpanId.Parse("2000000000000000")); - Assert.NotNull(scope.PropagationContext._dynamicSamplingContext); - scope.PropagationContext._dynamicSamplingContext.ToBaggageHeader().Members.Should().Contain(BaggageHeader.TryParse(baggageHeader)!.Members); + Assert.NotNull(scope.PropagationContext.DynamicSamplingContext); + scope.PropagationContext.DynamicSamplingContext.ToBaggageHeader().Members.Should().Contain(BaggageHeader.TryParse(baggageHeader)!.Members); }); transactionContext.TraceId.Should().Be(SentryId.Parse("5bd5f6d346b442dd9177dce9302fd737")); @@ -1585,7 +1585,7 @@ public void ContinueTrace_DoesNotReceiveHeadersAsStrings_CreatesRootTrace() hub.ScopeManager.ConfigureScope(scope => { Assert.Null(scope.PropagationContext.ParentSpanId); - Assert.Null(scope.PropagationContext._dynamicSamplingContext); + Assert.Null(scope.PropagationContext.DynamicSamplingContext); }); transactionContext.Name.Should().Be("test-name"); diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs index bb90359d0a..fa307db89a 100644 --- a/test/Sentry.Tests/SentryPropagationContextTests.cs +++ b/test/Sentry.Tests/SentryPropagationContextTests.cs @@ -32,12 +32,12 @@ public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) Assert.Equal(original.TraceId, copy.TraceId); Assert.Equal(original.SpanId, copy.SpanId); - Assert.Equal(original._dynamicSamplingContext!.Items.Count, copy._dynamicSamplingContext!.Items.Count); - foreach (var dscItem in original._dynamicSamplingContext!.Items) + Assert.Equal(original.DynamicSamplingContext!.Items.Count, copy.DynamicSamplingContext!.Items.Count); + foreach (var dscItem in original.DynamicSamplingContext!.Items) { if (dscItem.Key == "replay_id") { - copy._dynamicSamplingContext!.Items["replay_id"].Should().Be(replaySessionIsActive + copy.DynamicSamplingContext!.Items["replay_id"].Should().Be(replaySessionIsActive // We overwrite the replay_id when we have an active replay session ? _fixture.ActiveReplayId.ToString() // Otherwise we propagate whatever was in the baggage header @@ -45,7 +45,7 @@ public void CopyConstructor_CreatesCopyWithReplayId(bool replaySessionIsActive) } else { - copy._dynamicSamplingContext!.Items.Should() + copy.DynamicSamplingContext!.Items.Should() .Contain(kvp => kvp.Key == dscItem.Key && kvp.Value == dscItem.Value); } } @@ -59,18 +59,18 @@ public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_Creat var options = new SentryOptions { Dsn = ValidDsn }; var propagationContext = new SentryPropagationContext(); - Assert.Null(propagationContext._dynamicSamplingContext); // Sanity check + Assert.Null(propagationContext.DynamicSamplingContext); // Sanity check _ = propagationContext.GetOrCreateDynamicSamplingContext(options, replaySessionIsActive ? _fixture.ActiveReplaySession : _fixture.InactiveReplaySession); - Assert.NotNull(propagationContext._dynamicSamplingContext); + Assert.NotNull(propagationContext.DynamicSamplingContext); if (replaySessionIsActive) { // We add the replay_id automatically when we have an active replay session - Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", propagationContext._dynamicSamplingContext.Items)); + Assert.Equal(_fixture.ActiveReplayId.ToString(), Assert.Contains("replay_id", propagationContext.DynamicSamplingContext.Items)); } else { - Assert.DoesNotContain("replay_id", propagationContext._dynamicSamplingContext.Items); + Assert.DoesNotContain("replay_id", propagationContext.DynamicSamplingContext.Items); } } @@ -95,7 +95,7 @@ public void CreateFromHeaders_HeadersNull_CreatesPropagationContextWithTraceAndS Assert.NotEqual(propagationContext.TraceId, SentryId.Empty); Assert.NotEqual(propagationContext.SpanId, SpanId.Empty); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Fact] @@ -108,7 +108,7 @@ public void CreateFromHeaders_TraceHeaderNotNull_CreatesPropagationContextFromTr Assert.Equal(traceHeader.TraceId, propagationContext.TraceId); Assert.NotEqual(traceHeader.SpanId, propagationContext.SpanId); // Sanity check Assert.Equal(traceHeader.SpanId, propagationContext.ParentSpanId); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Fact] @@ -124,7 +124,7 @@ public void CreateFromHeaders_BaggageExistsButTraceHeaderNull_CreatesPropagation var propagationContext = SentryPropagationContext.CreateFromHeaders(null, null, baggageHeader, _fixture.InactiveReplaySession); - Assert.Null(propagationContext._dynamicSamplingContext); + Assert.Null(propagationContext.DynamicSamplingContext); } [Theory] From 442fc7d20700c2b0565abcb73b33b547d110dc0c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 10 Feb 2026 13:54:34 +1300 Subject: [PATCH 3/6] Disable Sentry tracing instrumentation if OTEL is enabled --- .../SentryDiagnosticListenerIntegration.cs | 2 +- .../DbInterceptionIntegration.cs | 2 +- .../SentryOptionsExtensions.cs | 10 +++++++++- src/Sentry/Integrations/ISdkIntegration.cs | 8 ++++++++ src/Sentry/Internal/Hub.cs | 7 +++++++ src/Sentry/SentryGraphQLHttpMessageHandler.cs | 6 ++++++ src/Sentry/SentryHttpMessageHandler.cs | 6 ++++++ src/Sentry/SentryOptions.cs | 6 +++++- 8 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs b/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs index 39ced41aa5..45da25e6bc 100644 --- a/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs +++ b/src/Sentry.DiagnosticSource/Internal/DiagnosticSource/SentryDiagnosticListenerIntegration.cs @@ -3,7 +3,7 @@ namespace Sentry.Internal.DiagnosticSource; -internal class SentryDiagnosticListenerIntegration : ISdkIntegration +internal class SentryDiagnosticListenerIntegration : ISdkIntegration, ISentryTracingIntegration { public void Register(IHub hub, SentryOptions options) { diff --git a/src/Sentry.EntityFramework/DbInterceptionIntegration.cs b/src/Sentry.EntityFramework/DbInterceptionIntegration.cs index bf561ef039..a75b873625 100644 --- a/src/Sentry.EntityFramework/DbInterceptionIntegration.cs +++ b/src/Sentry.EntityFramework/DbInterceptionIntegration.cs @@ -1,6 +1,6 @@ namespace Sentry.EntityFramework; -internal class DbInterceptionIntegration : ISdkIntegration +internal class DbInterceptionIntegration : ISdkIntegration, ISentryTracingIntegration { // Internal for testing. internal IDbInterceptor? SqlInterceptor { get; private set; } diff --git a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs index 2f22bcbbbd..e2e1ca7c00 100644 --- a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs +++ b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs @@ -1,3 +1,4 @@ +using Sentry.Extensibility; using Sentry.Maui.CommunityToolkit.Mvvm; namespace Sentry.Maui; @@ -12,7 +13,14 @@ public static class SentryOptionsExtensions /// public static SentryMauiOptions AddCommunityToolkitIntegration(this SentryMauiOptions options) { - options.AddIntegrationEventBinder(); + if (options.Instrumenter == Instrumenter.OpenTelemetry) + { + options.LogWarning("Skipping CommunityToolkit.Mvvm integration since OpenTelemetry instrumentation is enabled."); + } + else + { + options.AddIntegrationEventBinder(); + } return options; } } diff --git a/src/Sentry/Integrations/ISdkIntegration.cs b/src/Sentry/Integrations/ISdkIntegration.cs index 898e5b39db..1a1eadc558 100644 --- a/src/Sentry/Integrations/ISdkIntegration.cs +++ b/src/Sentry/Integrations/ISdkIntegration.cs @@ -15,3 +15,11 @@ public interface ISdkIntegration /// The options. public void Register(IHub hub, SentryOptions options); } + +/// +/// Marker interface to indicate that an integration provides native Sentry tracing capabilities. We do NOT initialise +/// these integrations when using OTEL instrumentation. +/// +internal interface ISentryTracingIntegration +{ +} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index e39e6fd0f7..e7ac1e2360 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -185,6 +185,13 @@ internal ITransactionTracer StartTransaction( return NoOpTransaction.Instance; } + if (_options.Instrumenter == Instrumenter.OpenTelemetry) + { + _options.LogWarning("This transaction will not be sent to Sentry. " + + "Please instrument traces using the OpenTelemetry APIs when using Sentry's OpenTelemetry integration."); + return NoOpTransaction.Instance; + } + bool? isSampled = null; double? sampleRate = null; DiscardReason? discardReason = null; diff --git a/src/Sentry/SentryGraphQLHttpMessageHandler.cs b/src/Sentry/SentryGraphQLHttpMessageHandler.cs index 7666e46455..2d2339f550 100644 --- a/src/Sentry/SentryGraphQLHttpMessageHandler.cs +++ b/src/Sentry/SentryGraphQLHttpMessageHandler.cs @@ -50,6 +50,12 @@ internal SentryGraphQLHttpMessageHandler(IHub? hub, SentryOptions? options, } request.SetFused(graphQlRequestContent); + if (_options?.Instrumenter == Instrumenter.OpenTelemetry) + { + _options.LogDebug("Skipping span creation in SentryGraphQLHttpMessageHandler because Instrumenter is set to OpenTelemetry"); + return null; + } + // Start a span that tracks this request // (may be null if transaction is not set on the scope) var span = _hub.GetSpan()?.StartChild( diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index f75f958b8a..9b2d694af2 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -65,6 +65,12 @@ internal SentryHttpMessageHandler(IHub? hub, SentryOptions? options, HttpMessage /// protected internal override ISpan? ProcessRequest(HttpRequestMessage request, string method, string url) { + if (_options?.Instrumenter == Instrumenter.OpenTelemetry) + { + _options.LogDebug("Skipping span creation in SentryHttpMessageHandler because Instrumenter is set to OpenTelemetry"); + return null; + } + // Start a span that tracks this request // (may be null if transaction is not set on the scope) var span = _hub.GetSpan()?.StartChild( diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 29bc351511..3da1e8674c 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -206,7 +206,7 @@ internal IEnumerable Integrations #endif #if HAS_DIAGNOSTIC_INTEGRATION - if ((_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) + if (Instrumenter == Instrumenter.Sentry && (_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) { yield return new SentryDiagnosticListenerIntegration(); } @@ -222,6 +222,10 @@ internal IEnumerable Integrations foreach (var integration in _integrations) { + if (Instrumenter == Instrumenter.OpenTelemetry && integration is ISentryTracingIntegration) + { + continue; + } yield return integration; } } From 08d87118c810d1f050aa1bb1f07b8fd8515b79e7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 10 Feb 2026 14:05:49 +1300 Subject: [PATCH 4/6] Revert to classic extension methods --- .../SentryOptionsExtensions.cs | 73 ++++++----- .../TracerProviderBuilderExtensions.cs | 121 +++++++++--------- 2 files changed, 96 insertions(+), 98 deletions(-) diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 576b333760..65d04a66c7 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -8,44 +8,43 @@ namespace Sentry.OpenTelemetry; /// public static class SentryOptionsExtensions { - extension(SentryOptions options) + /// + /// Enables OpenTelemetry instrumentation with Sentry + /// + /// The instance. + /// + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) { - /// - /// Enables OpenTelemetry instrumentation with Sentry - /// - /// - /// - /// The default TextMapPropagator to be used by OpenTelemetry. - /// - /// If this parameter is not supplied, the will be used, which propagates the - /// baggage header as well as Sentry trace headers. - /// - /// - /// The is required for Sentry's OpenTelemetry integration to work but you - /// could wrap this in a if you needed other propagators as well. - /// - /// - public void UseOpenTelemetry(TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) - { - options.UseOpenTelemetry(); - builder.AddSentry(textMapPropagator); - } + options.UseOpenTelemetry(); + builder.AddSentry(textMapPropagator); + } - /// - /// Configures Sentry to use OpenTelemetry for distributed tracing. - /// - /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call - /// when building your - /// to ensure OpenTelemetry sends trace information to Sentry. - /// - /// - public void UseOpenTelemetry() - { - options.Instrumenter = Instrumenter.OpenTelemetry; - options.PropagationContextFactory = _ => new OtelPropagationContext(); - options.AddTransactionProcessor( - new OpenTelemetryTransactionProcessor() - ); - } + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// + /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call + /// when building your + /// to ensure OpenTelemetry sends trace information to Sentry. + /// + /// + /// The instance. + public static void UseOpenTelemetry(this SentryOptions options) + { + options.Instrumenter = Instrumenter.OpenTelemetry; + options.PropagationContextFactory = _ => new OtelPropagationContext(); + options.AddTransactionProcessor( + new OpenTelemetryTransactionProcessor() + ); } } diff --git a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs index 970daf8e77..980311d855 100644 --- a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -12,74 +12,73 @@ namespace Sentry.OpenTelemetry; /// public static class TracerProviderBuilderExtensions { - extension(TracerProviderBuilder tracerProviderBuilder) + /// + /// Ensures OpenTelemetry trace information is sent to Sentry. + /// + /// The . + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + /// The supplied for chaining. + public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, TextMapPropagator? defaultTextMapPropagator = null) { - /// - /// Ensures OpenTelemetry trace information is sent to Sentry. - /// - /// - /// The default TextMapPropagator to be used by OpenTelemetry. - /// - /// If this parameter is not supplied, the will be used, which propagates the - /// baggage header as well as Sentry trace headers. - /// - /// - /// The is required for Sentry's OpenTelemetry integration to work but you - /// could wrap this in a if you needed other propagators as well. - /// - /// - /// The supplied for chaining. - public TracerProviderBuilder AddSentry(TextMapPropagator? defaultTextMapPropagator = null) - { - defaultTextMapPropagator ??= new SentryPropagator(); - Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); - return tracerProviderBuilder.AddProcessor(ImplementationFactory); - } + defaultTextMapPropagator ??= new SentryPropagator(); + Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); + return tracerProviderBuilder.AddProcessor(ImplementationFactory); + } - /// - /// Ensures OpenTelemetry trace information is sent to the Sentry OTLP endpoint. - /// - /// The DSN for your Sentry project - /// - /// The default TextMapPropagator to be used by OpenTelemetry. - /// - /// If this parameter is not supplied, the will be used, which propagates the - /// baggage header as well as Sentry trace headers. - /// - /// - /// The is required for Sentry's OpenTelemetry integration to work but you - /// could wrap this in a if you needed other propagators as well. - /// - /// - /// The supplied for chaining. - public TracerProviderBuilder AddSentry(string dsnString, TextMapPropagator? defaultTextMapPropagator = null) + /// + /// Ensures OpenTelemetry trace information is sent to the Sentry OTLP endpoint. + /// + /// The . + /// The DSN for your Sentry project + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + /// The supplied for chaining. + public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, string dsnString, TextMapPropagator? defaultTextMapPropagator = null) + { + if (string.IsNullOrWhiteSpace(dsnString)) { - if (string.IsNullOrWhiteSpace(dsnString)) - { - throw new ArgumentException("OTLP endpoint must be provided", nameof(dsnString)); - } + throw new ArgumentException("OTLP endpoint must be provided", nameof(dsnString)); + } - defaultTextMapPropagator ??= new SentryPropagator(); - Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); + defaultTextMapPropagator ??= new SentryPropagator(); + Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); - if (Dsn.TryParse(dsnString) is not { } dsn) - { - return tracerProviderBuilder; - } + if (Dsn.TryParse(dsnString) is not { } dsn) + { + return tracerProviderBuilder; + } - tracerProviderBuilder.AddOtlpExporter(options => + tracerProviderBuilder.AddOtlpExporter(options => + { + options.Endpoint = dsn.GetOtlpTracesEndpointUri(); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => { - options.Endpoint = dsn.GetOtlpTracesEndpointUri(); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.HttpClientFactory = () => - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); - return client; - }; - }); - return tracerProviderBuilder.AddProcessor(ImplementationFactory); - } + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); + return client; + }; + }); + return tracerProviderBuilder.AddProcessor(ImplementationFactory); } internal static BaseProcessor ImplementationFactory(IServiceProvider services) From 907850bc3ff1f00ed139a7c5dfa4d1392d11c66f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 12 Feb 2026 13:35:13 +1300 Subject: [PATCH 5/6] Added option to disable sentry tracing --- .../Program.cs | 11 +-- .../SentryOptionsExtensions.cs | 4 +- .../SentryOptionsExtensions.cs | 83 +++++++++++++++++-- .../TracerProviderBuilderExtensions.cs | 73 ++++++++++------ src/Sentry/Internal/Hub.cs | 5 +- src/Sentry/SentryGraphQLHttpMessageHandler.cs | 4 +- src/Sentry/SentryHttpMessageHandler.cs | 4 +- src/Sentry/SentryOptions.cs | 12 ++- 8 files changed, 147 insertions(+), 49 deletions(-) diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs index 05fc13690a..dde8955089 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs @@ -7,9 +7,10 @@ var builder = WebApplication.CreateBuilder(args); -// Read the Sentry DSN from the environment variable, if it's not already set in code. +// Read the Sentry DSN from the environment variable if it's not already set in code. #if SENTRY_DSN_DEFINED_IN_ENV -var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN") ?? throw new InvalidOperationException("SENTRY_DSN environment variable is not set"); +var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN") + ?? throw new InvalidOperationException("SENTRY_DSN environment variable is not set"); #else var dsn = SamplesShared.Dsn; #endif @@ -27,8 +28,8 @@ // The two lines below take care of configuring sources for ASP.NET Core and HttpClient .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() - // Finally, we configure OpenTelemetry to send traces to Sentry - .AddSentry(dsn) + // Finally, we configure OpenTelemetry over OTLP to send traces to Sentry + .AddSentryOTLP(dsn) ); builder.WebHost.UseSentry(options => @@ -42,7 +43,7 @@ options.Debug = builder.Environment.IsDevelopment(); options.SendDefaultPii = true; options.TracesSampleRate = 1.0; - options.UseOpenTelemetry(); // <-- Configure Sentry to use OpenTelemetry trace information + options.UseOTLP(); // <-- Configure Sentry to use OpenTelemetry trace information }); builder.Services diff --git a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs index e2e1ca7c00..eebe40e6d6 100644 --- a/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs +++ b/src/Sentry.Maui.CommunityToolkit.Mvvm/SentryOptionsExtensions.cs @@ -13,9 +13,9 @@ public static class SentryOptionsExtensions /// public static SentryMauiOptions AddCommunityToolkitIntegration(this SentryMauiOptions options) { - if (options.Instrumenter == Instrumenter.OpenTelemetry) + if (options.DisableSentryTracing) { - options.LogWarning("Skipping CommunityToolkit.Mvvm integration since OpenTelemetry instrumentation is enabled."); + options.LogWarning("Skipping CommunityToolkit.Mvvm integration because OpenTelemetry is enabled."); } else { diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 65d04a66c7..52bd50e98e 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -9,7 +9,7 @@ namespace Sentry.OpenTelemetry; public static class SentryOptionsExtensions { /// - /// Enables OpenTelemetry instrumentation with Sentry + /// Configures Sentry to use OpenTelemetry for distributed tracing. /// /// The instance. /// @@ -24,27 +24,98 @@ public static class SentryOptionsExtensions /// could wrap this in a if you needed other propagators as well. /// /// - public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) + /// Whether to disable traces created using Sentry's tracing instrumentation. + /// It's recommended that you set this to true since mixing OpenTelemetry and Sentry traces may yield + /// unexpected results. It is false by default for backward compatibility only. + /// + /// + /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. + /// We recommend you use instead. + /// + public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBuilder builder, + TextMapPropagator? textMapPropagator = null, bool disableSentryTracing = false) { - options.UseOpenTelemetry(); + options.UseOpenTelemetry(disableSentryTracing); builder.AddSentry(textMapPropagator); } /// - /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// Configures Sentry to use OpenTelemetry for distributed tracing. + /// /// - /// Note: if you are using this method to configure Sentry to work with OpenTelemetry, you will also have to call + /// Note: if you are using this overload to configure Sentry to work with OpenTelemetry, you will also have to call /// when building your /// to ensure OpenTelemetry sends trace information to Sentry. /// /// /// The instance. - public static void UseOpenTelemetry(this SentryOptions options) + /// Whether to disable traces created using Sentry's tracing instrumentation. + /// It's recommended that you set this to true since mixing OpenTelemetry and Sentry traces may yield + /// unexpected results. It is false by default for backward compatibility only. + /// + /// + /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. + /// We recommend you use instead. + /// + public static void UseOpenTelemetry(this SentryOptions options, bool disableSentryTracing = false) { options.Instrumenter = Instrumenter.OpenTelemetry; + options.DisableSentryTracing = disableSentryTracing; options.PropagationContextFactory = _ => new OtelPropagationContext(); options.AddTransactionProcessor( new OpenTelemetryTransactionProcessor() ); } + + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. Sentry instrumented traces will be + /// disabled (so all tracing instrumentation must be done using the OpenTelemetry classes). + /// + /// + /// This is the recommended way to set up Sentry's OpenTelemetry integration. + /// + /// + /// The instance. + /// + /// + /// The default TextMapPropagator to be used by OpenTelemetry. + /// + /// If this parameter is not supplied, the will be used, which propagates the + /// baggage header as well as Sentry trace headers. + /// + /// + /// The is required for Sentry's OpenTelemetry integration to work but you + /// could wrap this in a if you needed other propagators as well. + /// + /// + public static void UseOTLP(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) + { + if (string.IsNullOrWhiteSpace(options.Dsn)) + { + throw new ArgumentException("Sentry DSN must be set before calling `SentryOptions.UseOTLP`", nameof(options.Dsn)); + } + builder.AddSentryOTLP(options.Dsn, textMapPropagator); + options.UseOTLP(); + } + + /// + /// Configures Sentry to use OpenTelemetry for distributed tracing. Sentry instrumented traces will be + /// disabled (so all tracing instrumentation must be done using the OpenTelemetry classes). + /// + /// + /// This is the recommended way to set up Sentry's OpenTelemetry integration. + /// + /// + /// + /// Note: if you are using this overload to configure Sentry to work with OpenTelemetry, you will also have to call + /// , when building your + /// to ensure OpenTelemetry sends trace information to Sentry. + /// + /// The instance. + public static void UseOTLP(this SentryOptions options) + { + options.Instrumenter = Instrumenter.OpenTelemetry; + options.DisableSentryTracing = true; + options.PropagationContextFactory = _ => new OtelPropagationContext(); + } } diff --git a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs index 980311d855..1b2cb75115 100644 --- a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -13,7 +13,17 @@ namespace Sentry.OpenTelemetry; public static class TracerProviderBuilderExtensions { /// - /// Ensures OpenTelemetry trace information is sent to Sentry. + /// + /// Ensures OpenTelemetry trace information is sent to Sentry. OpenTelemetry spans will be converted to Sentry spans + /// using a span processor. This is no longer recommended. SDK users should consider using + /// instead, which is the recommended + /// way to send OpenTelemetry trace information to Sentry moving forward. + /// + /// + /// Note that if you use this method to configure the trace builder, you will also need to call + /// when initialising Sentry, for Sentry + /// to work properly with OpenTelemetry. + /// /// /// The . /// @@ -28,15 +38,45 @@ public static class TracerProviderBuilderExtensions /// /// /// The supplied for chaining. - public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, TextMapPropagator? defaultTextMapPropagator = null) + public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, + TextMapPropagator? defaultTextMapPropagator = null) { defaultTextMapPropagator ??= new SentryPropagator(); Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); return tracerProviderBuilder.AddProcessor(ImplementationFactory); } + internal static BaseProcessor ImplementationFactory(IServiceProvider services) + { + List enrichers = []; + + // AspNetCoreEnricher + var userFactory = services.GetService(); + if (userFactory is not null) + { + enrichers.Add(new AspNetCoreEnricher(userFactory)); + } + + var hub = services.GetService() ?? SentrySdk.CurrentHub; + if (hub.IsEnabled) + { + return new SentrySpanProcessor(hub, enrichers); + } + + var logger = services.GetService(); + logger?.LogWarning("Sentry is disabled so no OpenTelemetry spans will be sent to Sentry."); + return DisabledSpanProcessor.Instance; + } + /// + /// /// Ensures OpenTelemetry trace information is sent to the Sentry OTLP endpoint. + /// + /// + /// Note that if you use this method to configure the trace builder, you will also need to call + /// when initialising Sentry, for Sentry to work + /// properly with OpenTelemetry. + /// /// /// The . /// The DSN for your Sentry project @@ -52,11 +92,12 @@ public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerP /// /// /// The supplied for chaining. - public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerProviderBuilder, string dsnString, TextMapPropagator? defaultTextMapPropagator = null) + public static TracerProviderBuilder AddSentryOTLP(this TracerProviderBuilder tracerProviderBuilder, string dsnString, + TextMapPropagator? defaultTextMapPropagator = null) { if (string.IsNullOrWhiteSpace(dsnString)) { - throw new ArgumentException("OTLP endpoint must be provided", nameof(dsnString)); + throw new ArgumentException("Sentry DSN must be provided for OLTP instrumentation", nameof(dsnString)); } defaultTextMapPropagator ??= new SentryPropagator(); @@ -78,28 +119,6 @@ public static TracerProviderBuilder AddSentry(this TracerProviderBuilder tracerP return client; }; }); - return tracerProviderBuilder.AddProcessor(ImplementationFactory); - } - - internal static BaseProcessor ImplementationFactory(IServiceProvider services) - { - List enrichers = new(); - - // AspNetCoreEnricher - var userFactory = services.GetService(); - if (userFactory is not null) - { - enrichers.Add(new AspNetCoreEnricher(userFactory)); - } - - var hub = services.GetService() ?? SentrySdk.CurrentHub; - if (hub.IsEnabled) - { - return new SentrySpanProcessor(hub, enrichers); - } - - var logger = services.GetService(); - logger?.LogWarning("Sentry is disabled so no OpenTelemetry spans will be sent to Sentry."); - return DisabledSpanProcessor.Instance; + return tracerProviderBuilder; } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index e7ac1e2360..ab660a78d7 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -185,10 +185,9 @@ internal ITransactionTracer StartTransaction( return NoOpTransaction.Instance; } - if (_options.Instrumenter == Instrumenter.OpenTelemetry) + if (_options.DisableSentryTracing) { - _options.LogWarning("This transaction will not be sent to Sentry. " + - "Please instrument traces using the OpenTelemetry APIs when using Sentry's OpenTelemetry integration."); + _options.LogWarning("Sentry transaction dropped because OpenTelemetry is enabled"); return NoOpTransaction.Instance; } diff --git a/src/Sentry/SentryGraphQLHttpMessageHandler.cs b/src/Sentry/SentryGraphQLHttpMessageHandler.cs index 2d2339f550..229ddafd5b 100644 --- a/src/Sentry/SentryGraphQLHttpMessageHandler.cs +++ b/src/Sentry/SentryGraphQLHttpMessageHandler.cs @@ -50,9 +50,9 @@ internal SentryGraphQLHttpMessageHandler(IHub? hub, SentryOptions? options, } request.SetFused(graphQlRequestContent); - if (_options?.Instrumenter == Instrumenter.OpenTelemetry) + if (_options?.DisableSentryTracing ?? false) { - _options.LogDebug("Skipping span creation in SentryGraphQLHttpMessageHandler because Instrumenter is set to OpenTelemetry"); + _options.LogDebug("Skipping span creation in SentryGraphQLHttpMessageHandler because OpenTelemetry is enabled"); return null; } diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index 9b2d694af2..eb9d62f8f2 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -65,9 +65,9 @@ internal SentryHttpMessageHandler(IHub? hub, SentryOptions? options, HttpMessage /// protected internal override ISpan? ProcessRequest(HttpRequestMessage request, string method, string url) { - if (_options?.Instrumenter == Instrumenter.OpenTelemetry) + if (_options?.DisableSentryTracing ?? false) { - _options.LogDebug("Skipping span creation in SentryHttpMessageHandler because Instrumenter is set to OpenTelemetry"); + _options.LogDebug("Skipping span creation in SentryHttpMessageHandler because OpenTelemetry is enabled"); return null; } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 3da1e8674c..bedcc39bf1 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -206,7 +206,7 @@ internal IEnumerable Integrations #endif #if HAS_DIAGNOSTIC_INTEGRATION - if (Instrumenter == Instrumenter.Sentry && (_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) + if (!DisableSentryTracing && (_defaultIntegrations & DefaultIntegrations.SentryDiagnosticListenerIntegration) != 0) { yield return new SentryDiagnosticListenerIntegration(); } @@ -222,7 +222,7 @@ internal IEnumerable Integrations foreach (var integration in _integrations) { - if (Instrumenter == Instrumenter.OpenTelemetry && integration is ISentryTracingIntegration) + if (DisableSentryTracing && integration is ISentryTracingIntegration) { continue; } @@ -1160,6 +1160,14 @@ public StackTraceMode StackTraceMode /// internal Instrumenter Instrumenter { get; set; } = Instrumenter.Sentry; + /// + /// During the transition period to OTLP we give SDK users the option to keep using Sentry's tracing in conjunction + /// with OTEL instrumentation. Setting this to true will disable Sentry's tracing entirely, which is the recommended + /// setting but would be a moajor change in behaviour, so we've made it opt-in for now. + /// TODO: Remove this option in a future major release and make it true / non-optional when using OTEL (i.e. implied by the Instrumenter) + /// + internal bool DisableSentryTracing { get; set; } = false; + /// /// The default factory creates SentryPropagationContext instances... this should be replaced when using OTEL /// From 6f8312dc91ac5493dc96860df356daeb162f0033 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 12 Feb 2026 17:04:00 +1300 Subject: [PATCH 6/6] Added some new tests... found some new issues --- .../Program.cs | 4 +- .../OtelPropagationContext.cs | 4 + .../SentryOptionsExtensions.cs | 14 +- .../TracerProviderBuilderExtensions.cs | 40 ++--- src/Sentry/DynamicSamplingContext.cs | 12 ++ src/Sentry/IHub.cs | 3 + src/Sentry/Internal/Hub.cs | 10 +- src/Sentry/SentryMessageHandler.cs | 9 + .../OtelPropagationContextTests.cs | 167 ++++++++++++++++++ .../TracerProviderBuilderExtensionsTests.cs | 65 +++++++ 10 files changed, 298 insertions(+), 30 deletions(-) create mode 100644 test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs diff --git a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs index dde8955089..af8f23bc21 100644 --- a/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs +++ b/samples/Sentry.Samples.OpenTelemetry.AspNetCore/Program.cs @@ -29,7 +29,7 @@ .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() // Finally, we configure OpenTelemetry over OTLP to send traces to Sentry - .AddSentryOTLP(dsn) + .AddSentryOtlp(dsn) ); builder.WebHost.UseSentry(options => @@ -43,7 +43,7 @@ options.Debug = builder.Environment.IsDevelopment(); options.SendDefaultPii = true; options.TracesSampleRate = 1.0; - options.UseOTLP(); // <-- Configure Sentry to use OpenTelemetry trace information + options.UseOtlp(); // <-- Configure Sentry to use OpenTelemetry trace information }); builder.Services diff --git a/src/Sentry.OpenTelemetry/OtelPropagationContext.cs b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs index 1d74a3f086..73839612ff 100644 --- a/src/Sentry.OpenTelemetry/OtelPropagationContext.cs +++ b/src/Sentry.OpenTelemetry/OtelPropagationContext.cs @@ -11,6 +11,10 @@ internal class OtelPropagationContext : IPropagationContext public SpanId SpanId => Activity.Current?.SpanId.AsSentrySpanId() ?? default; public SpanId? ParentSpanId => Activity.Current?.ParentSpanId.AsSentrySpanId(); + /// + /// Warning: this method may throw an exception if Activity.Current is null. + /// This method should not be used when instrumenting with OTEL. + /// public DynamicSamplingContext GetOrCreateDynamicSamplingContext(SentryOptions options, IReplaySession replaySession) { if (DynamicSamplingContext is null) diff --git a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs index 52bd50e98e..e98c39b782 100644 --- a/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs +++ b/src/Sentry.OpenTelemetry/SentryOptionsExtensions.cs @@ -30,7 +30,7 @@ public static class SentryOptionsExtensions /// /// /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. - /// We recommend you use instead. + /// We recommend you use instead. /// public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null, bool disableSentryTracing = false) @@ -55,7 +55,7 @@ public static void UseOpenTelemetry(this SentryOptions options, TracerProviderBu /// /// /// This method of initialising the Sentry OpenTelemetry integration will be depricated in a future major release. - /// We recommend you use instead. + /// We recommend you use instead. /// public static void UseOpenTelemetry(this SentryOptions options, bool disableSentryTracing = false) { @@ -88,14 +88,14 @@ public static void UseOpenTelemetry(this SentryOptions options, bool disableSent /// could wrap this in a if you needed other propagators as well. /// /// - public static void UseOTLP(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) + public static void UseOtlp(this SentryOptions options, TracerProviderBuilder builder, TextMapPropagator? textMapPropagator = null) { if (string.IsNullOrWhiteSpace(options.Dsn)) { throw new ArgumentException("Sentry DSN must be set before calling `SentryOptions.UseOTLP`", nameof(options.Dsn)); } - builder.AddSentryOTLP(options.Dsn, textMapPropagator); - options.UseOTLP(); + builder.AddSentryOtlp(options.Dsn, textMapPropagator); + options.UseOtlp(); } /// @@ -108,11 +108,11 @@ public static void UseOTLP(this SentryOptions options, TracerProviderBuilder bui /// /// /// Note: if you are using this overload to configure Sentry to work with OpenTelemetry, you will also have to call - /// , when building your + /// , when building your /// to ensure OpenTelemetry sends trace information to Sentry. /// /// The instance. - public static void UseOTLP(this SentryOptions options) + public static void UseOtlp(this SentryOptions options) { options.Instrumenter = Instrumenter.OpenTelemetry; options.DisableSentryTracing = true; diff --git a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs index 1b2cb75115..c57d23003f 100644 --- a/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Sentry.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -12,11 +12,13 @@ namespace Sentry.OpenTelemetry; /// public static class TracerProviderBuilderExtensions { + internal const string MissingDsnWarning = "Invalid DSN passed to AddSentryOTLP"; + /// /// /// Ensures OpenTelemetry trace information is sent to Sentry. OpenTelemetry spans will be converted to Sentry spans /// using a span processor. This is no longer recommended. SDK users should consider using - /// instead, which is the recommended + /// instead, which is the recommended /// way to send OpenTelemetry trace information to Sentry moving forward. /// /// @@ -74,7 +76,7 @@ internal static BaseProcessor ImplementationFactory(IServiceProvider s /// /// /// Note that if you use this method to configure the trace builder, you will also need to call - /// when initialising Sentry, for Sentry to work + /// when initialising Sentry, for Sentry to work /// properly with OpenTelemetry. /// /// @@ -92,33 +94,31 @@ internal static BaseProcessor ImplementationFactory(IServiceProvider s /// /// /// The supplied for chaining. - public static TracerProviderBuilder AddSentryOTLP(this TracerProviderBuilder tracerProviderBuilder, string dsnString, + public static TracerProviderBuilder AddSentryOtlp(this TracerProviderBuilder tracerProviderBuilder, string dsnString, TextMapPropagator? defaultTextMapPropagator = null) { - if (string.IsNullOrWhiteSpace(dsnString)) + if (Dsn.TryParse(dsnString) is not { } dsn) { - throw new ArgumentException("Sentry DSN must be provided for OLTP instrumentation", nameof(dsnString)); + throw new ArgumentException("Invalid DSN passed to AddSentryOTLP", nameof(dsnString)); } defaultTextMapPropagator ??= new SentryPropagator(); Sdk.SetDefaultTextMapPropagator(defaultTextMapPropagator); - if (Dsn.TryParse(dsnString) is not { } dsn) - { - return tracerProviderBuilder; - } + tracerProviderBuilder.AddOtlpExporter(options => OtlpConfigurationCallback(options, dsn)); + return tracerProviderBuilder; + } - tracerProviderBuilder.AddOtlpExporter(options => + // Internal helper method for testing purposes + internal static void OtlpConfigurationCallback(OtlpExporterOptions options, Dsn dsn) + { + options.Endpoint = dsn.GetOtlpTracesEndpointUri(); + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => { - options.Endpoint = dsn.GetOtlpTracesEndpointUri(); - options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.HttpClientFactory = () => - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); - return client; - }; - }); - return tracerProviderBuilder; + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Sentry-Auth", $"sentry sentry_key={dsn.PublicKey}"); + return client; + }; } } diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs index 683e809e74..3f9b82ee0e 100644 --- a/src/Sentry/DynamicSamplingContext.cs +++ b/src/Sentry/DynamicSamplingContext.cs @@ -227,6 +227,12 @@ public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTra replaySession); } + /// + /// Creates a from the given . + /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// public static DynamicSamplingContext CreateFromPropagationContext(IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) { var traceId = propagationContext.TraceId; @@ -256,6 +262,12 @@ public static DynamicSamplingContext CreateDynamicSamplingContext(this Transacti public static DynamicSamplingContext CreateDynamicSamplingContext(this UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromUnsampledTransaction(transaction, options, replaySession); + /// + /// Creates a from the given . + /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// public static DynamicSamplingContext CreateDynamicSamplingContext(this IPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession) => DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession); } diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index a5a7afd9c9..c42a1f784d 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -70,6 +70,9 @@ public ITransactionTracer StartTransaction( /// /// Gets the Sentry baggage header that allows tracing across services /// + /// Can be thrown when using OpenTelemetry instrumentation and + /// System.Diagnostics.Activity.Current is null. This method should not be used when instrumenting with OTEL. + /// public BaggageHeader? GetBaggage(); /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index ab660a78d7..def0ca82d1 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -312,6 +312,11 @@ public SentryTraceHeader GetTraceHeader() public BaggageHeader GetBaggage() { + if (_options.Instrumenter is Instrumenter.OpenTelemetry) + { + _options.LogWarning("GetBaggage should not be called when using OpenTelemetry - it may throw an exception"); + } + var span = GetSpan(); if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc) { @@ -489,7 +494,10 @@ private void ApplyTraceContextToEvent(SentryEvent evt, IPropagationContext propa evt.Contexts.Trace.TraceId = propagationContext.TraceId; evt.Contexts.Trace.SpanId = propagationContext.SpanId; evt.Contexts.Trace.ParentSpanId = propagationContext.ParentSpanId; - evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession); + if (_options.Instrumenter is Instrumenter.Sentry) + { + evt.DynamicSamplingContext = propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession); + } } public bool CaptureEnvelope(Envelope envelope) => CurrentClient.CaptureEnvelope(envelope); diff --git a/src/Sentry/SentryMessageHandler.cs b/src/Sentry/SentryMessageHandler.cs index 9c8515e93e..72c0247a08 100644 --- a/src/Sentry/SentryMessageHandler.cs +++ b/src/Sentry/SentryMessageHandler.cs @@ -133,6 +133,15 @@ private void PropagateTraceHeaders(HttpRequestMessage request, string url, ISpan } } + // We only propogate trace headers for Sentry's native intstumentation. It isn't possible to propogate + // headers when OTEL instrumentation is used since the traceId can be SentryId.Empty if there is no active + // OTEL span... which would result in an exception being thrown when trying to create the + // DynamicSamplingContext. + if (_options?.Instrumenter is Instrumenter.Sentry) + { + return; + } + if (_options?.TracePropagationTargets.MatchesSubstringOrRegex(url) is true or null) { AddSentryTraceHeader(request, parentSpan); diff --git a/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs b/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs new file mode 100644 index 0000000000..1d12319f12 --- /dev/null +++ b/test/Sentry.OpenTelemetry.Tests/OtelPropagationContextTests.cs @@ -0,0 +1,167 @@ +namespace Sentry.OpenTelemetry.Tests; + +public class OtelPropagationContextTests +{ + private class Fixture + { + public SentryId ActiveReplayId { get; } = SentryId.Create(); + public IReplaySession ActiveReplaySession { get; } + public SentryOptions SentryOptions { get; } + + public Fixture() + { + ActiveReplaySession = Substitute.For(); + ActiveReplaySession.ActiveReplayId.Returns(ActiveReplayId); + + SentryOptions = new SentryOptions { Dsn = "https://examplePublicKey@o0.ingest.sentry.io/123456" }; + } + } + + private readonly Fixture _fixture = new(); + + [Fact] + public void TraceId_NoActivityCurrent_ReturnsDefault() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var traceId = sut.TraceId; + + // Assert + traceId.Should().Be(default(SentryId)); + } + + [Fact] + public void TraceId_WithActivityCurrent_ReturnsSentryIdFromActivityTraceId() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var traceId = sut.TraceId; + + // Assert + traceId.Should().NotBe(default(SentryId)); + traceId.Should().Be(activity.TraceId.AsSentryId()); + } + + [Fact] + public void SpanId_NoActivityCurrent_ReturnsDefault() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var spanId = sut.SpanId; + + // Assert + spanId.Should().Be(default(SpanId)); + } + + [Fact] + public void SpanId_WithActivityCurrent_ReturnsSpanIdFromActivitySpanId() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var spanId = sut.SpanId; + + // Assert + spanId.Should().NotBe(default(SpanId)); + spanId.Should().Be(activity.SpanId.AsSentrySpanId()); + } + + [Fact] + public void ParentSpanId_NoActivityCurrent_ReturnsNull() + { + // Arrange + var sut = new OtelPropagationContext(); + Activity.Current = null; + + // Act + var parentSpanId = sut.ParentSpanId; + + // Assert + parentSpanId.Should().BeNull(); + } + + [Fact] + public void ParentSpanId_WithActivityCurrent_ReturnsParentSpanIdFromActivity() + { + // Arrange + using var parentActivity = new Activity("parent").Start(); + using var childActivity = new Activity("child").Start(); + var sut = new OtelPropagationContext(); + + // Act + var parentSpanId = sut.ParentSpanId; + + // Assert + parentSpanId.Should().NotBeNull(); + parentSpanId.Should().Be(parentActivity.SpanId.AsSentrySpanId()); + } + + [Fact] + public void DynamicSamplingContext_ByDefault_IsNull() + { + // Arrange & Act + var sut = new OtelPropagationContext(); + + // Assert + sut.DynamicSamplingContext.Should().BeNull(); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNull_CreatesDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + sut.DynamicSamplingContext.Should().BeNull(); + + // Act + var result = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + result.Should().NotBeNull(); + sut.DynamicSamplingContext.Should().NotBeNull(); + sut.DynamicSamplingContext.Should().BeSameAs(result); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_DynamicSamplingContextIsNotNull_ReturnsSameDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + var firstResult = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Act + var secondResult = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + firstResult.Should().BeSameAs(secondResult); + sut.DynamicSamplingContext.Should().BeSameAs(firstResult); + } + + [Fact] + public void GetOrCreateDynamicSamplingContext_WithActiveReplaySession_IncludesReplayIdInDynamicSamplingContext() + { + // Arrange + using var activity = new Activity("test").Start(); + var sut = new OtelPropagationContext(); + + // Act + var result = sut.GetOrCreateDynamicSamplingContext(_fixture.SentryOptions, _fixture.ActiveReplaySession); + + // Assert + result.Should().NotBeNull(); + result.Items.Should().Contain(kvp => kvp.Key == "replay_id" && kvp.Value == _fixture.ActiveReplayId.ToString()); + } +} diff --git a/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs b/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs index d63f64c985..89e747432c 100644 --- a/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/TracerProviderBuilderExtensionsTests.cs @@ -1,3 +1,6 @@ +using OpenTelemetry.Exporter; +using OpenTelemetry.Trace; + namespace Sentry.OpenTelemetry.Tests; public class TracerProviderBuilderExtensionsTests @@ -91,4 +94,66 @@ public void ImplementationFactory_WithDisabledHub_ReturnsDisabledSpanProcessor() // Assert result.Should().BeOfType(); // FluentAssertions } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("foo")] + public void AddSentryOltp_InvalidDsn_ThrowsArgumentException(string dsn) + { + // Arrange + var tracerProviderBuilder = Substitute.For(); + + // Act + Action act = () => tracerProviderBuilder.AddSentryOtlp(dsn); + + // Assert + act.Should().Throw() + .WithMessage($"{TracerProviderBuilderExtensions.MissingDsnWarning}*"); + } + + [Fact] + public void OtlpConfigurationCallback_SetsEndpointFromDsn() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + + // Assert + options.Endpoint.Should().Be(dsn.GetOtlpTracesEndpointUri()); + } + + [Fact] + public void OtlpConfigurationCallback_SetsProtocolToHttpProtobuf() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + + // Assert + options.Protocol.Should().Be(OtlpExportProtocol.HttpProtobuf); + } + + [Fact] + public void OtlpConfigurationCallback_HttpClientFactoryCreatesClientWithSentryAuthHeader() + { + // Arrange + var dsn = Dsn.Parse("https://examplePublicKey@o0.ingest.sentry.io/123456"); + var options = new OtlpExporterOptions(); + + // Act + TracerProviderBuilderExtensions.OtlpConfigurationCallback(options, dsn); + var client = options.HttpClientFactory!.Invoke(); + + // Assert + client.DefaultRequestHeaders.Should().Contain(h => h.Key == "X-Sentry-Auth"); + var headerValues = client.DefaultRequestHeaders.GetValues("X-Sentry-Auth"); + headerValues.Should().ContainSingle(v => v == $"sentry sentry_key={dsn.PublicKey}"); + } }