diff --git a/docs/decisions/2026-10-01-receive-side-trace-state-and-baggage-propagation.md b/docs/decisions/2026-10-01-receive-side-trace-state-and-baggage-propagation.md index 64dcd34a22..6a3b0d9253 100644 --- a/docs/decisions/2026-10-01-receive-side-trace-state-and-baggage-propagation.md +++ b/docs/decisions/2026-10-01-receive-side-trace-state-and-baggage-propagation.md @@ -112,7 +112,7 @@ context by hand again. Mechanically, `ActivityFactory` creates the incoming activity, forces the W3C id format, adds the tags and starts it. Only then does it read the headers. `ContextPropagation.PropagateContextFromHeaders` is split into `PropagateTraceStateFromHeaders(activity, headers)` and `PropagateBaggageFromHeaders(activity, -headers)`. The split applies to both propagator paths, including the legacy path in `obsolete_v11.cs`. +headers)`. The split applies to both propagator paths, including the legacy path in `obsoletes-v10.cs`. Both methods expect a started activity. The combined method stays for callers that need both. ## Consequences diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/OpenTelemetryAcceptanceTest.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/OpenTelemetryAcceptanceTest.cs index a274875307..f67d0ab06b 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/OpenTelemetryAcceptanceTest.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/OpenTelemetryAcceptanceTest.cs @@ -9,7 +9,7 @@ public abstract class OpenTelemetryAcceptanceTest : NServiceBusAcceptanceTest protected TestingActivityListener NServiceBusActivityListener { get; private set; } [SetUp] - public void Setup() => NServiceBusActivityListener = TestingActivityListener.SetupDiagnosticListener("NServiceBus.Core", "NServiceBus.Core.Recoverability"); + public void Setup() => NServiceBusActivityListener = TestingActivityListener.SetupDiagnosticListener("NServiceBus.Core", "NServiceBus.Core.Handler", "NServiceBus.Core.Recoverability"); [TearDown] public void Cleanup() diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_outgoing_activity_has_baggage.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_outgoing_activity_has_baggage.cs index b6a2310ad2..33cfd28674 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_outgoing_activity_has_baggage.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_outgoing_activity_has_baggage.cs @@ -35,7 +35,7 @@ public async Task Should_propagate_baggage_to_headers() // Default (backwards-compatible) propagation produces the legacy comma-separated, percent-encoded format. // The W3C OWS format ("key3 = , key2 = value2, key1 = value1") is produced only when the - // NServiceBus.Core.OpenTelemetry.UseDistributedContextPropagator AppContext switch is enabled (default in v11). + // NServiceBus.Core.OpenTelemetry.UseV11Behavior AppContext switch is enabled (the only behavior in v11). Assert.That(context.BaggageHeader, Is.EqualTo("key3=,key2=value2,key1=value1")); } diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_incoming_message.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_incoming_message.cs index 86fc87529a..32d60e7e03 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_incoming_message.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_incoming_message.cs @@ -81,10 +81,11 @@ public Task Handle(IncomingMessage message, IMessageHandlerContext context) } [Test] - public async Task Should_use_receive_address_in_span_name_when_opted_in() + [OpenTelemetryV11Defaults] + public async Task Should_include_receive_address_in_span_name() { await Scenario.Define() - .WithEndpoint(e => e + .WithEndpoint(e => e .When(s => s.SendLocal(new IncomingMessage()))) .Run(); @@ -96,21 +97,5 @@ await Scenario.Define() Assert.That(incomingActivity.DisplayName, Is.Not.EqualTo("process message")); } - public class ReceivingEndpointWithDestinationNaming : EndpointConfigurationBuilder - { - public ReceivingEndpointWithDestinationNaming() => - EndpointSetup(b => b.Tracing().UseMessageTypeNamesInSpanNames = true); - - [Handler] - public class MessageHandler(Context testContext) : IHandleMessages - { - public Task Handle(IncomingMessage message, IMessageHandlerContext context) - { - testContext.MarkAsCompleted(); - return Task.CompletedTask; - } - } - } - public class IncomingMessage : IMessage; } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_default_activity_sources.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_default_activity_sources.cs index a233aa178b..4b6c631b89 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_default_activity_sources.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_processing_message_with_default_activity_sources.cs @@ -10,7 +10,7 @@ public class When_processing_message_with_default_activity_sources : OpenTelemet { // Until v11, handler spans are emitted from the "NServiceBus.Core" ActivitySource by default // for backwards compatibility. The dedicated "NServiceBus.Core.Handler" source is opt-in via - // the NServiceBus.Core.OpenTelemetry.UseHandlerActivitySource AppContext switch (default in v11). + // the NServiceBus.Core.OpenTelemetry.UseV11Behavior AppContext switch (the only behavior in v11). [Test] public async Task Should_emit_handler_span_from_main_source() { diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_publishing_messages.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_publishing_messages.cs index 39b8e57885..c0359fd053 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_publishing_messages.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_publishing_messages.cs @@ -180,12 +180,13 @@ public Task Handle(ThisIsAnEvent @event, IMessageHandlerContext context) } [Test] - public async Task Should_use_event_type_in_span_name_when_opted_in() + [OpenTelemetryV11Defaults] + public async Task Should_include_event_type_in_span_name() { await Scenario.Define() - .WithEndpoint(b => b + .WithEndpoint(b => b .When(ctx => ctx.SomeEventSubscribed, s => s.Publish())) - .WithEndpoint(b => b.When((session, ctx) => + .WithEndpoint(b => b.When((session, ctx) => { if (ctx.HasNativePubSubSupport) { @@ -203,15 +204,14 @@ await Scenario.Define() Assert.That(publishedMessage.DisplayName, Is.EqualTo("publish ThisIsAnEvent")); } - public class PublisherWithDestinationNaming : EndpointConfigurationBuilder + public class PublisherForV11Naming : EndpointConfigurationBuilder { - public PublisherWithDestinationNaming() => + public PublisherForV11Naming() => EndpointSetup(b => { - b.Tracing().UseMessageTypeNamesInSpanNames = true; b.OnEndpointSubscribed((s, context) => { - if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(SubscriberForPublisherWithDestinationNaming)))) + if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(SubscriberForPublisherForV11Naming)))) { if (s.MessageType == typeof(ThisIsAnEvent).AssemblyQualifiedName) { @@ -222,13 +222,13 @@ public PublisherWithDestinationNaming() => }); } - public class SubscriberForPublisherWithDestinationNaming : EndpointConfigurationBuilder + public class SubscriberForPublisherForV11Naming : EndpointConfigurationBuilder { - public SubscriberForPublisherWithDestinationNaming() => + public SubscriberForPublisherForV11Naming() => EndpointSetup(c => { }, metadata => { - metadata.RegisterPublisherFor(); + metadata.RegisterPublisherFor(); }); [Handler] diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_recoverability_action_occurs.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_recoverability_action_occurs.cs index 6e24936ab5..09e3b64de8 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_recoverability_action_occurs.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_recoverability_action_occurs.cs @@ -52,15 +52,12 @@ await Scenario.Define() } [Test] - public async Task Should_include_destination_in_display_name_when_opted_in() + [OpenTelemetryV11Defaults] + public async Task Should_include_destination_in_display_name() { await Scenario.Define() .WithEndpoint(b => b - .CustomConfig(c => - { - c.Recoverability().Immediate(i => i.NumberOfRetries(1)).Delayed(i => i.NumberOfRetries(0)); - c.Tracing().UseMessageTypeNamesInSpanNames = true; - }) + .CustomConfig(c => c.Recoverability().Immediate(i => i.NumberOfRetries(1)).Delayed(i => i.NumberOfRetries(0))) .DoNotFailOnErrorMessages() .When(s => s.SendLocal(new FailingMessage()))) .Done(_ => ActionTags().Contains("move_to_error")) diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_messages.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_messages.cs index a56a3caac7..61499ddee5 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_messages.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_messages.cs @@ -132,10 +132,11 @@ public Task Handle(OutgoingMessage message, IMessageHandlerContext context) } [Test] - public async Task Should_use_destination_in_send_span_name_when_opted_in() + [OpenTelemetryV11Defaults] + public async Task Should_include_destination_in_send_span_name() { await Scenario.Define() - .WithEndpoint(b => b + .WithEndpoint(b => b .When(s => s.SendLocal(new OutgoingMessage()))) .Run(); @@ -147,22 +148,6 @@ await Scenario.Define() Assert.That(sentMessage.DisplayName, Is.Not.EqualTo("send message")); } - public class TestEndpointWithDestinationNaming : EndpointConfigurationBuilder - { - public TestEndpointWithDestinationNaming() => - EndpointSetup(b => b.Tracing().UseMessageTypeNamesInSpanNames = true); - - [Handler] - public class MessageHandler(Context testContext) : IHandleMessages - { - public Task Handle(OutgoingMessage message, IMessageHandlerContext context) - { - testContext.MarkAsCompleted(); - return Task.CompletedTask; - } - } - } - [Test] public async Task Should_create_new_linked_trace_on_receive_when_endpoint_defaults_to_span_link() { diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_replies.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_replies.cs index 7e79d6582c..8077fbb202 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_replies.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_sending_replies.cs @@ -56,10 +56,11 @@ public Task Handle(OutgoingReply message, IMessageHandlerContext context) } [Test] - public async Task Should_use_destination_in_reply_span_name_when_opted_in() + [OpenTelemetryV11Defaults] + public async Task Should_include_destination_in_reply_span_name() { await Scenario.Define() - .WithEndpoint(b => b + .WithEndpoint(b => b .When(s => s.SendLocal(new IncomingMessage()))) .Run(); @@ -70,25 +71,6 @@ await Scenario.Define() Assert.That(replyMessage.DisplayName, Does.StartWith("reply ")); } - public class TestEndpointWithDestinationNaming : EndpointConfigurationBuilder - { - public TestEndpointWithDestinationNaming() => - EndpointSetup(b => b.Tracing().UseMessageTypeNamesInSpanNames = true); - - [Handler] - public class MessageHandler(Context testContext) : IHandleMessages, - IHandleMessages - { - public Task Handle(IncomingMessage message, IMessageHandlerContext context) => context.Reply(new OutgoingReply()); - - public Task Handle(OutgoingReply message, IMessageHandlerContext context) - { - testContext.MarkAsCompleted(); - return Task.CompletedTask; - } - } - } - public class IncomingMessage : IMessage; public class OutgoingReply : IMessage; diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 2f335d4f9e..82995eed81 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -48,6 +48,7 @@ + diff --git a/src/NServiceBus.AcceptanceTests/obsoletes-v10.cs b/src/NServiceBus.AcceptanceTests/obsoletes-v10.cs new file mode 100644 index 0000000000..1b6fd71d6e --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/obsoletes-v10.cs @@ -0,0 +1,40 @@ +namespace NServiceBus.AcceptanceTests; + +using System; +using System.Reflection; +using NUnit.Framework; +using NUnit.Framework.Interfaces; + +// ============================================================================= +// EVERYTHING IN THIS FILE IS TEMPORARY AND WILL BE REMOVED IN v11, together with +// the V11BehaviorSwitch block in NServiceBus.Core/obsoletes-v10.cs. The file is +// excluded from the shipped acceptance test sources in the project file. +// ============================================================================= + +// Runs the test with the NServiceBus.Core.OpenTelemetry.UseV11Behavior AppContext switch enabled, i.e. with +// the OpenTelemetry defaults of v11. The switch value is cached process-wide, so the cache is reset through +// the internal V11BehaviorSwitch.ResetUseV11Behavior method. Only safe in [NonParallelizable] fixtures such +// as OpenTelemetryAcceptanceTest. In v11 these defaults are the only behavior: delete this attribute and +// remove it from the tests that use it. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true)] +public sealed class OpenTelemetryV11DefaultsAttribute : Attribute, ITestAction +{ + const string SwitchName = "NServiceBus.Core.OpenTelemetry.UseV11Behavior"; + + static readonly MethodInfo ResetSwitch = typeof(EndpointConfiguration).Assembly + .GetType("NServiceBus.V11BehaviorSwitch", throwOnError: true)! + .GetMethod("ResetUseV11Behavior", BindingFlags.Static | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("NServiceBus.V11BehaviorSwitch.ResetUseV11Behavior not found"); + + public ActionTargets Targets => ActionTargets.Test; + + public void BeforeTest(ITest test) => Set(true); + + public void AfterTest(ITest test) => Set(false); + + static void Set(bool enabled) + { + AppContext.SetSwitch(SwitchName, enabled); + ResetSwitch.Invoke(null, null); + } +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 5a9feef8a4..25091c74f6 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -691,13 +691,10 @@ namespace NServiceBus { public InstrumentationOptions() { } public NServiceBus.DelayedDeliveryInstrumentationOptions DelayedDelivery { get; } - public bool EmitMessageDispatchingEvents { get; set; } public NServiceBus.ExceptionRecordingMode ExceptionRecordingMode { get; set; } - public NServiceBus.MetersOptions Meters { get; } public NServiceBus.TraceMode PublishTraceMode { get; set; } public NServiceBus.RecoverabilityInstrumentationOptions Recoverability { get; } public NServiceBus.TraceMode SendTraceMode { get; set; } - public bool UseMessageTypeNamesInSpanNames { get; set; } } public sealed class KeyedServiceKey { @@ -857,11 +854,6 @@ namespace NServiceBus [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(@"AddMessageType relies on an NServiceBus source-generated interceptor to register the statically known message hierarchy without reflection when trimming is enabled. If this warning is reported, the interceptor was not used for this call; enable or restore the NServiceBus analyzer/source-generator tooling and use a statically known message type.")] public static void AddMessageType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TMessage>(this NServiceBus.EndpointConfiguration config) { } } - public class MetersOptions - { - public MetersOptions() { } - public bool EmitExecutionResultTags { get; set; } - } public static class MetricTagsExtensions { extension(NServiceBus.Pipeline.IBehaviorContext context) diff --git a/src/NServiceBus.Core.Tests/Envelopes/EnvelopeUnwrapperTests.cs b/src/NServiceBus.Core.Tests/Envelopes/EnvelopeUnwrapperTests.cs index b713660569..d5bd75d954 100644 --- a/src/NServiceBus.Core.Tests/Envelopes/EnvelopeUnwrapperTests.cs +++ b/src/NServiceBus.Core.Tests/Envelopes/EnvelopeUnwrapperTests.cs @@ -29,7 +29,7 @@ public void Setup() originalBody = "payload"u8.ToArray().AsMemory(); messageContext = new MessageContext(nativeId, originalHeaders, originalBody, new TransportTransaction(), "receiveAddress", new ContextBag()); meterFactory = new TestMeterFactory(); - pipelineMetrics = new PipelineMetrics(meterFactory, "queue", "disc", new MetersOptions()); + pipelineMetrics = new PipelineMetrics(meterFactory, "queue", "disc"); } [TearDown] diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityFactoryTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityFactoryTests.cs index b2c983d427..6c10d82375 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityFactoryTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityFactoryTests.cs @@ -49,26 +49,13 @@ static MessageContext CreateMessageContext() => new(Guid.NewGuid().ToString(), [], Array.Empty(), new TransportTransaction(), "receiver", new ContextBag()); } + // Until v11 the "transport span as parent" behavior is opt-in. This fixture runs with the v11 + // defaults because that is what the tests below describe; the pre-v11 default is covered by + // TransportParentSpanDefaultBehaviorTests. In v11, remove the attribute together with the + // V11BehaviorSwitch block in obsoletes-v10.cs. + [OpenTelemetryV11Defaults] class StartIncomingActivity : ActivityFactoryTests { - // Until v11 the "transport span as parent" behavior is opt-in. This fixture runs with it - // enabled because that is the v11 default the tests below describe; the pre-v11 default - // is covered by TransportParentSpanDefaultBehaviorTests. In v11, delete this SetUp/TearDown - // pair together with obsolete_v11.cs. - [SetUp] - public void OptInToTransportSpanAsParent() - { - AppContext.SetSwitch(TransportParentActivitySwitch.UseTransportActivityAsParentSwitchName, true); - TransportParentActivitySwitch.ResetUseTransportActivityAsParent(); - } - - [TearDown] - public void ResetTransportSpanSwitch() - { - AppContext.SetSwitch(TransportParentActivitySwitch.UseTransportActivityAsParentSwitchName, false); - TransportParentActivitySwitch.ResetUseTransportActivityAsParent(); - } - [TestCase(Headers.NServiceBusDiagnosticsTraceParent)] [TestCase(Headers.DiagnosticsTraceParent)] // for backwards compatibility public void Should_attach_to_header_trace_when_available_and_no_ambient_activity(string headerName) diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationCompatibilityTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationCompatibilityTests.cs index ae5dad4ba3..950a847047 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationCompatibilityTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationCompatibilityTests.cs @@ -7,22 +7,9 @@ using NUnit.Framework; [TestFixture] +[OpenTelemetryV11Defaults] public class ContextPropagationCompatibilityTests { - [SetUp] - public void EnableDistributedContextPropagator() - { - AppContext.SetSwitch(LegacyContextPropagation.UseDistributedContextPropagatorSwitchName, true); - LegacyContextPropagation.ResetUseDistributedContextPropagator(); - } - - [TearDown] - public void ResetDistributedContextPropagator() - { - AppContext.SetSwitch(LegacyContextPropagation.UseDistributedContextPropagatorSwitchName, false); - LegacyContextPropagation.ResetUseDistributedContextPropagator(); - } - delegate void Writer(Activity activity, Dictionary headers); delegate void Reader(Activity activity, IDictionary headers); diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationDefaultBehaviorTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationDefaultBehaviorTests.cs index 4f496a5e66..af9b689704 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationDefaultBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/ContextPropagationDefaultBehaviorTests.cs @@ -9,15 +9,8 @@ [TestFixture] public class ContextPropagationDefaultBehaviorTests { - // Without the opt-in switch, the endpoint default must remain the backwards-compatible - // legacy propagator (percent-encoded, comma-separated, whitespace preserved). - [SetUp] - public void EnsureDefault() - { - AppContext.SetSwitch(LegacyContextPropagation.UseDistributedContextPropagatorSwitchName, false); - LegacyContextPropagation.ResetUseDistributedContextPropagator(); - } - + // Without the OpenTelemetryV11Defaults attribute, the endpoint default must remain the + // backwards-compatible legacy propagator (percent-encoded, comma-separated, whitespace preserved). [Test] public void Default_uses_legacy_percent_encoded_baggage_format() { diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/HandlerActivitySourceTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/HandlerActivitySourceTests.cs index 8af728a9c8..2d87714a41 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/HandlerActivitySourceTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/HandlerActivitySourceTests.cs @@ -21,21 +21,10 @@ public class HandlerActivitySourceTests public void SetUp() => mainListener = TestingActivityListener.SetupNServiceBusDiagnosticListener(); [TearDown] - public void TearDown() - { - mainListener.Dispose(); - AppContext.SetSwitch(HandlerActivitySourceSwitch.UseHandlerActivitySourceSwitchName, false); - HandlerActivitySourceSwitch.ResetUseHandlerActivitySource(); - } - - static void OptIn() - { - AppContext.SetSwitch(HandlerActivitySourceSwitch.UseHandlerActivitySourceSwitchName, true); - HandlerActivitySourceSwitch.ResetUseHandlerActivitySource(); - } + public void TearDown() => mainListener.Dispose(); [Test] - public void Default_emits_handler_activity_from_main_source() + public void Should_emit_handler_activity_from_main_source() { using var ambientActivity = new Activity("ambient activity"); ambientActivity.Start(); @@ -47,9 +36,9 @@ public void Default_emits_handler_activity_from_main_source() } [Test] - public void Opt_in_emits_handler_activity_from_handler_source() + [OpenTelemetryV11Defaults] + public void Should_emit_handler_activity_from_handler_source() { - OptIn(); using var handlerListener = TestingActivityListener.SetupDiagnosticListener("NServiceBus.Core.Handler"); using var ambientActivity = new Activity("ambient activity"); @@ -62,9 +51,9 @@ public void Opt_in_emits_handler_activity_from_handler_source() } [Test] - public void Opt_in_preserves_display_name_and_handler_type_tag() + [OpenTelemetryV11Defaults] + public void Should_preserve_display_name_and_handler_type_tag() { - OptIn(); using var handlerListener = TestingActivityListener.SetupDiagnosticListener("NServiceBus.Core.Handler"); using var ambientActivity = new Activity("ambient activity"); @@ -80,9 +69,9 @@ public void Opt_in_preserves_display_name_and_handler_type_tag() } [Test] - public void Opt_in_without_handler_source_listener_does_not_create_handler_activity() + [OpenTelemetryV11Defaults] + public void Should_not_create_handler_activity_without_handler_source_listener() { - OptIn(); using var ambientActivity = new Activity("ambient activity"); ambientActivity.Start(); diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/InstrumentationOptionsTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/InstrumentationOptionsTests.cs index b01433e755..2b1847401b 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/InstrumentationOptionsTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/InstrumentationOptionsTests.cs @@ -13,7 +13,7 @@ public void Should_default_trace_connectors_to_current_behavior() using (Assert.EnterMultipleScope()) { Assert.That(options.SendTraceMode, Is.EqualTo(TraceMode.ContinueExisting), "sends continue the trace by default"); - // Set by ApplyPreV11Defaults in obsolete_v11.cs. When that file is deleted in v11 this + // Set by ApplyPreV11Defaults in obsoletes-v10.cs. When that file is deleted in v11 this // assertion fails: flip it to TraceMode.ContinueExisting, which is the initializer's value. Assert.That(options.PublishTraceMode, Is.EqualTo(TraceMode.StartNew), "publishes start a new linked trace by default until v11"); } diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/MeterTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/MeterTests.cs index 77eb1764ca..14bd2817b1 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/MeterTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/MeterTests.cs @@ -22,7 +22,7 @@ public void Verify_MeterAPI() using var meterFactory = new TestMeterFactory(); //The IncomingPipelineMeter constructor creates the meters, therefore a new instance before collecting the metrics. #pragma warning disable CA1806 - new PipelineMetrics(meterFactory, "queue", "disc", new MetersOptions()); + new PipelineMetrics(meterFactory, "queue", "disc"); #pragma warning restore CA1806 using var metricsListener = TestingMetricListener.SetupNServiceBusMetricsListener(); diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/PipelineMetricsExecutionResultTagTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/PipelineMetricsExecutionResultTagTests.cs new file mode 100644 index 0000000000..0c676641eb --- /dev/null +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/PipelineMetricsExecutionResultTagTests.cs @@ -0,0 +1,43 @@ +namespace NServiceBus.Core.Tests.OpenTelemetry; + +using System; +using System.Collections.Generic; +using System.Linq; +using AcceptanceTests.Core.OpenTelemetry.Metrics; +using NServiceBus.Pipeline; +using NUnit.Framework; + +// In v11 the execution.result tag is gone: delete this file together with the V11BehaviorSwitch block in +// obsoletes-v10.cs. +[TestFixture] +public class PipelineMetricsExecutionResultTagTests +{ + [Test] + public void Should_emit_execution_result_tag() + { + var tags = RecordFailure(); + + Assert.That(tags.Select(t => t.Key), Does.Contain("execution.result")); + } + + [Test] + [OpenTelemetryV11Defaults] + public void Should_not_emit_execution_result_tag() + { + var tags = RecordFailure(); + + Assert.That(tags.Select(t => t.Key), Does.Not.Contain("execution.result")); + } + + static KeyValuePair[] RecordFailure() + { + using var meterFactory = new TestMeterFactory(); + using var listener = TestingMetricListener.SetupNServiceBusMetricsListener(); + + var metrics = new PipelineMetrics(meterFactory, "queue", "disc"); + metrics.RecordMessageProcessingFailure(new PipelineMetricTags(), new InvalidOperationException("boom")); + + Assert.That(listener.Tags, Has.Count.EqualTo(1), "exactly one metric is expected to be recorded"); + return listener.Tags.Values.Single(); + } +} diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/TransportParentSpanDefaultBehaviorTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/TransportParentSpanDefaultBehaviorTests.cs index 0064600331..81bad80371 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/TransportParentSpanDefaultBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/TransportParentSpanDefaultBehaviorTests.cs @@ -11,9 +11,9 @@ namespace NServiceBus.Core.Tests.OpenTelemetry; using NServiceBus.Transport; using NUnit.Framework; -// Covers the pre-v11 default of TransportParentActivitySwitch: without the opt-in switch the incoming +// Covers the pre-v11 default of V11BehaviorSwitch: without the OpenTelemetryV11Defaults attribute the incoming // message span stays a child of the NServiceBus sender span even when a transport SDK span is -// ambient. In v11 that default is gone, so delete this file together with obsolete_v11.cs. +// ambient. In v11 that default is gone, so delete this file together with the V11BehaviorSwitch block in obsoletes-v10.cs. [TestFixture] public class TransportParentSpanDefaultBehaviorTests { @@ -25,15 +25,12 @@ public class TransportParentSpanDefaultBehaviorTests public void SetUp() { nsbActivityListener = TestingActivityListener.SetupNServiceBusDiagnosticListener(); - AppContext.SetSwitch(TransportParentActivitySwitch.UseTransportActivityAsParentSwitchName, false); - TransportParentActivitySwitch.ResetUseTransportActivityAsParent(); } [TearDown] public void TearDown() { nsbActivityListener.Dispose(); - TransportParentActivitySwitch.ResetUseTransportActivityAsParent(); } [Test] diff --git a/src/NServiceBus.Core.Tests/Pipeline/Incoming/InvokeHandlerTerminatorTest.cs b/src/NServiceBus.Core.Tests/Pipeline/Incoming/InvokeHandlerTerminatorTest.cs index 5948f08106..2b74cf4a74 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/Incoming/InvokeHandlerTerminatorTest.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/Incoming/InvokeHandlerTerminatorTest.cs @@ -11,7 +11,7 @@ [TestFixture] public class InvokeHandlerTerminatorTest { - readonly InvokeHandlerTerminator terminator = new(new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + readonly InvokeHandlerTerminator terminator = new(new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); [Test] public async Task When_saga_found_and_handler_is_saga_should_invoke_handler() diff --git a/src/NServiceBus.Core.Tests/Pipeline/Incoming/SerializeMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Incoming/SerializeMessageConnectorTests.cs index dacfb4e744..9aeddeaec5 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/Incoming/SerializeMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/Incoming/SerializeMessageConnectorTests.cs @@ -59,7 +59,7 @@ static Task InvokeSerializer(TestableOutgoingLogicalMessageContext context, stri var behavior = new SerializeMessageConnector( new FakeSerializer("myContentType"), registry, - new PipelineMetrics(new TestMeterFactory(), queueName, discriminator, new MetersOptions())); + new PipelineMetrics(new TestMeterFactory(), queueName, discriminator)); return behavior.Invoke(context, _ => Task.CompletedTask); } diff --git a/src/NServiceBus.Core.Tests/Pipeline/MainPipelineExecutorTests.cs b/src/NServiceBus.Core.Tests/Pipeline/MainPipelineExecutorTests.cs index 9e56b4ee17..f78b9f7261 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/MainPipelineExecutorTests.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/MainPipelineExecutorTests.cs @@ -123,7 +123,7 @@ static MessageContext CreateMessageContext() => static MainPipelineExecutor CreateMainPipelineExecutor(ServiceProvider serviceProvider, IPipeline receivePipeline) { - var incomingPipelineMetrics = new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions()); + var incomingPipelineMetrics = new PipelineMetrics(new TestMeterFactory(), "queue", "disc"); var executor = new MainPipelineExecutor( serviceProvider, new PipelineCache(serviceProvider, new PipelineModifications()), diff --git a/src/NServiceBus.Core.Tests/Pipeline/PipelineMetricTagsTests.cs b/src/NServiceBus.Core.Tests/Pipeline/PipelineMetricTagsTests.cs index b316643e36..aa3353a319 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/PipelineMetricTagsTests.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/PipelineMetricTagsTests.cs @@ -36,7 +36,7 @@ public void Should_not_fail_when_handling_more_than_one_logical_message() }; var messageMapper = new MessageMapper(); - var behavior = new DeserializeMessageConnector(new MessageDeserializerResolver(new FakeSerializer(), []), new LogicalMessageFactory(registry, messageMapper), registry, messageMapper, false, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + var behavior = new DeserializeMessageConnector(new MessageDeserializerResolver(new FakeSerializer(), []), new LogicalMessageFactory(registry, messageMapper), registry, messageMapper, false, new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); Assert.DoesNotThrowAsync(async () => await behavior.Invoke(context, c => { diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs index 1f5eae8ffc..82d939f194 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs @@ -172,6 +172,29 @@ await Invoke(context, c => Assert.That(pipelineActivity.Events.Count(e => e.Name == "Finished dispatching"), Is.EqualTo(1)); } + // In v11 the dispatching events are gone: delete this test and + // Should_add_batch_dispatch_events_when_sending_batched_messages together with the + // V11BehaviorSwitch block in obsoletes-v10.cs. + [Test] + [OpenTelemetryV11Defaults] + public async Task Should_not_add_batch_dispatch_events_when_sending_batched_messages() + { + var context = CreateContext(fakeBatchPipeline, Guid.NewGuid().ToString()); + + using var pipelineActivity = new Activity("test activity"); + pipelineActivity.Start(); + context.Extensions.SetIncomingPipelineActivity(pipelineActivity); + + await Invoke(context, c => + { + var batchedSends = c.Extensions.Get(); + batchedSends.Add(new TransportOperation(new OutgoingMessage(Guid.NewGuid().ToString(), [], Array.Empty()), new UnicastAddressTag("destination"))); + return Task.CompletedTask; + }); + + Assert.That(pipelineActivity.Events, Is.Empty); + } + [Test] public async Task Should_not_add_batch_dispatch_events_when_no_batched_messages() { @@ -216,7 +239,7 @@ await Invoke(context, c => public async Task Should_still_dispatch_when_outbox_is_disabled() { var noOpBehavior = new TransportReceiveToPhysicalMessageConnector( - new NoOpOutboxStorage(), new PipelineMetrics(fakeMeterFactory, "queue", "disc", new MetersOptions()), new InstrumentationOptions(), NullLogger.Instance); + new NoOpOutboxStorage(), new PipelineMetrics(fakeMeterFactory, "queue", "disc"), NullLogger.Instance); var context = CreateContext(fakeBatchPipeline, "id"); @@ -267,7 +290,7 @@ public void SetUp() fakeBatchPipeline = new FakeBatchPipeline(); fakeMeterFactory = new TestMeterFactory(); - behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new PipelineMetrics(fakeMeterFactory, "queue", "disc", new MetersOptions()), new InstrumentationOptions(), NullLogger.Instance); + behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new PipelineMetrics(fakeMeterFactory, "queue", "disc"), NullLogger.Instance); } [TearDown] diff --git a/src/NServiceBus.Core.Tests/Unicast/LoadHandlersConnectorTests.cs b/src/NServiceBus.Core.Tests/Unicast/LoadHandlersConnectorTests.cs index 291c25dac8..7a4f512495 100644 --- a/src/NServiceBus.Core.Tests/Unicast/LoadHandlersConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Unicast/LoadHandlersConnectorTests.cs @@ -17,7 +17,7 @@ public class LoadHandlersConnectorTests [Test] public void Should_throw_when_there_are_no_registered_message_handlers() { - var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); var context = new TestableIncomingLogicalMessageContext(); @@ -30,7 +30,7 @@ public void Should_throw_when_there_are_no_registered_message_handlers() [Test] public void Should_throw_if_ambient_transaction_is_different_from_scope_used_by_transport() { - var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); var context = new TestableIncomingLogicalMessageContext(); @@ -50,7 +50,7 @@ public void Should_throw_if_ambient_transaction_is_different_from_scope_used_by_ [Test] public void Should_throw_if_ambient_transaction_suppressed_when_transport_uses_a_scope() { - var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + var behavior = new LoadHandlersConnector(new MessageHandlerRegistry(), NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); var context = new TestableIncomingLogicalMessageContext(); @@ -78,7 +78,7 @@ public void Should_not_throw_if_ambient_scope_is_same_as_transport_scope() context.Services.AddSingleton(); context.Extensions.Set(new NoOpOutboxTransaction()); - var behavior = new LoadHandlersConnector(messageHandlerRegistry, NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions())); + var behavior = new LoadHandlersConnector(messageHandlerRegistry, NoOpActivityFactory.Instance, new PipelineMetrics(new TestMeterFactory(), "queue", "disc")); using (new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { diff --git a/src/NServiceBus.Core.Tests/obsoletes-v10.cs b/src/NServiceBus.Core.Tests/obsoletes-v10.cs new file mode 100644 index 0000000000..f1b823e10e --- /dev/null +++ b/src/NServiceBus.Core.Tests/obsoletes-v10.cs @@ -0,0 +1,29 @@ +namespace NServiceBus.Core.Tests; + +using System; +using NUnit.Framework; +using NUnit.Framework.Interfaces; + +// ============================================================================= +// EVERYTHING IN THIS FILE IS TEMPORARY AND WILL BE REMOVED IN v11, together with +// the V11BehaviorSwitch block in NServiceBus.Core/obsoletes-v10.cs. +// ============================================================================= + +// Runs the test with the NServiceBus.Core.OpenTelemetry.UseV11Behavior AppContext switch enabled, i.e. with +// the OpenTelemetry defaults of v11. In v11 these defaults are the only behavior: delete this attribute and +// remove it from the tests that use it. +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true)] +public sealed class OpenTelemetryV11DefaultsAttribute : Attribute, ITestAction +{ + public ActionTargets Targets => ActionTargets.Test; + + public void BeforeTest(ITest test) => Set(true); + + public void AfterTest(ITest test) => Set(false); + + static void Set(bool enabled) + { + AppContext.SetSwitch(V11BehaviorSwitch.UseV11BehaviorSwitchName, enabled); + V11BehaviorSwitch.ResetUseV11Behavior(); + } +} diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 9cbd823955..8ba13b5234 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -156,7 +156,7 @@ void Configure() pipelineSettings); receiveComponent.AddManifest(hostingConfiguration, settings); - pipelineComponent = PipelineComponent.Initialize(pipelineSettings, hostingConfiguration, receiveConfiguration, hostingConfiguration.ActivityFactory.Options.Meters); + pipelineComponent = PipelineComponent.Initialize(pipelineSettings, hostingConfiguration, receiveConfiguration); // The settings can only be locked after initializing the feature component since it uses the settings to store & share feature state. // As well as all the other components have been initialized diff --git a/src/NServiceBus.Core/OpenTelemetry/InstrumentationOptions.cs b/src/NServiceBus.Core/OpenTelemetry/InstrumentationOptions.cs index c1ebb7ad1a..33080ad721 100644 --- a/src/NServiceBus.Core/OpenTelemetry/InstrumentationOptions.cs +++ b/src/NServiceBus.Core/OpenTelemetry/InstrumentationOptions.cs @@ -13,7 +13,7 @@ public partial class InstrumentationOptions /// public InstrumentationOptions() => ApplyPreV11Defaults(); - // Implemented in obsolete_v11.cs. A partial void method without an implementation is removed + // Implemented in obsoletes-v10.cs. A partial void method without an implementation is removed // by the compiler, so deleting that file makes the initializers below the final defaults. partial void ApplyPreV11Defaults(); @@ -41,7 +41,7 @@ public partial class InstrumentationOptions /// /// Controls how the receive-side processing span relates to the publish span for events published by this endpoint. /// Defaults to : receivers continue the trace. - /// Until v11 the default is for backward compatibility, see obsolete_v11.cs. + /// Until v11 the default is for backward compatibility, see obsoletes-v10.cs. /// Can be overridden per message via /// or . /// diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityDisplayNames.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityDisplayNames.cs index ec5f69f29f..418698e275 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityDisplayNames.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityDisplayNames.cs @@ -12,7 +12,7 @@ static class ActivityDisplayNames public const string ReplyMessage = "reply"; public const string Recoverability = "recover"; - // Operation-only prefixes used when UseMessageTypeNamesInSpanNames is enabled + // Operation-only prefixes used when V11BehaviorSwitch.UseV11Behavior is enabled (the only naming in v11, see obsoletes-v10.cs) internal const string ProcessOperation = "process"; internal const string PublishOperation = "publish"; internal const string SendOperation = "send"; diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs index 687dcd1506..5e0cbfabe3 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs @@ -42,7 +42,7 @@ sealed partial class ActivityFactory(InstrumentationOptions options) : IActivity Activity.Current = null; activity = activitySource.CreateActivity(activityName, ActivityKind.Consumer, parentContext: default, links: [new ActivityLink(senderContext)]); } - else if (TransportParentActivitySwitch.UseTransportActivityAsParent && Activity.Current != null) // remove the switch check in v11, see obsolete_v11.cs + else if (V11BehaviorSwitch.UseV11Behavior && Activity.Current != null) // remove the switch check in v11, see obsoletes-v10.cs { // A transport SDK receive span is ambient: make it the parent (an activity without // a parent context adopts Activity.Current when it starts) and link to the NSB sender span. @@ -136,7 +136,7 @@ static bool TryParseSenderContext(Dictionary headers, out Activi return activity; } - activity.DisplayName = Options.UseMessageTypeNamesInSpanNames + activity.DisplayName = V11BehaviorSwitch.UseV11Behavior ? $"{ActivityDisplayNames.ProcessOperation} {context.ReceiveAddress}" : ActivityDisplayNames.ProcessMessage; @@ -170,7 +170,8 @@ static bool TryParseSenderContext(Dictionary headers, out Activi // Until v11 the dedicated handler source is opt-in; existing configurations only // subscribe to the main source and must keep receiving handler spans from it. - var source = HandlerActivitySourceSwitch.UseHandlerActivitySource + // Remove the switch check in v11, see obsoletes-v10.cs. + var source = V11BehaviorSwitch.UseV11Behavior ? ActivitySources.Handler : ActivitySources.Main; @@ -211,7 +212,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi activity.AddTag(ActivityTags.RecoverabilityAction, "immediate_retry"); activity.DisplayName = ActivityDisplayNames.ImmediateRetryOperation; - if (Options.UseMessageTypeNamesInSpanNames) + if (V11BehaviorSwitch.UseV11Behavior) { activity.DisplayName += $" {receiveAddress}"; } @@ -221,7 +222,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi activity.AddTag(ActivityTags.RecoverabilityAction, "delayed_retry"); activity.DisplayName = ActivityDisplayNames.DelayedRetryOperation; - if (Options.UseMessageTypeNamesInSpanNames) + if (V11BehaviorSwitch.UseV11Behavior) { activity.DisplayName += $" {receiveAddress}"; } @@ -230,7 +231,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi { activity.AddTag(ActivityTags.RecoverabilityAction, "move_to_error"); - activity.DisplayName = Options.UseMessageTypeNamesInSpanNames + activity.DisplayName = V11BehaviorSwitch.UseV11Behavior ? $"{ActivityDisplayNames.MoveToErrorOperation} {moveToError.ErrorQueue}" : $"{ActivityDisplayNames.MoveToErrorOperation} error"; } @@ -251,7 +252,11 @@ public void RecordError(Activity? activity, Exception exception, IServiceProvide activity.SetStatus(ActivityStatusCode.Error, exception.Message); activity.SetTag(ActivityTags.ErrorType, exception.GetType().FullName); - LegacyExceptionTags.SetLegacyStatusTags(activity, exception); + // Removed in v11, see obsoletes-v10.cs + if (!V11BehaviorSwitch.UseV11Behavior) + { + LegacyExceptionTags.SetLegacyStatusTags(activity, exception); + } if (!exception.Data.Contains(ExceptionRecordedFlag)) { @@ -263,7 +268,7 @@ public void RecordError(Activity? activity, Exception exception, IServiceProvide } else { - activity.AddException(exception, LegacyExceptionTags.EscapedTagList); + activity.AddException(exception, V11BehaviorSwitch.UseV11Behavior ? default : LegacyExceptionTags.EscapedTagList); // drop the tag list in v11, see obsoletes-v10.cs } exception.Data[ExceptionRecordedFlag] = true; diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ContextPropagation.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ContextPropagation.cs index ffa1b15091..083b3961bc 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ContextPropagation.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ContextPropagation.cs @@ -22,8 +22,8 @@ public static void PropagateContextToHeaders(Activity? activity, Dictionary headers) { - // Removed in v11, see obsolete_v11.cs - if (!LegacyContextPropagation.UseDistributedContextPropagator) + // Removed in v11, see obsoletes-v10.cs + if (!V11BehaviorSwitch.UseV11Behavior) { LegacyContextPropagation.PropagateTraceStateFromHeaders(activity, headers); return; @@ -69,8 +69,8 @@ public static void PropagateTraceStateFromHeaders(Activity activity, IDictionary // the headers. Otherwise every hop would put each key on the wire twice. public static void PropagateBaggageFromHeaders(Activity activity, IDictionary headers) { - // Removed in v11, see obsolete_v11.cs - if (!LegacyContextPropagation.UseDistributedContextPropagator) + // Removed in v11, see obsoletes-v10.cs + if (!V11BehaviorSwitch.UseV11Behavior) { LegacyContextPropagation.PropagateBaggageFromHeaders(activity, headers, activity.Parent); return; diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v11.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v11.cs deleted file mode 100644 index 4836a5834d..0000000000 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v11.cs +++ /dev/null @@ -1,328 +0,0 @@ -#nullable enable - -namespace NServiceBus; - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Particular.Obsoletes; - -// ============================================================================= -// EVERYTHING IN THIS FILE IS TEMPORARY AND WILL BE REMOVED IN v11. -// -// In v10.3, switching to System.Diagnostics.DistributedContextPropagator for -// OpenTelemetry trace-context/baggage propagation changes the baggage wire -// format (W3C OWS encoding + whitespace trimming) and is therefore breaking on -// rolling upgrades. To stay backwards compatible it is opt-in via an AppContext -// switch and the legacy propagator below remains the default. -// -// In v11 the new propagator becomes the default: delete this entire file and -// remove the `if (!LegacyContextPropagation.UseDistributedContextPropagator)` -// delegation blocks in ContextPropagation.cs. -// ============================================================================= -static class LegacyContextPropagation -{ - enum SwitchState : byte - { - Unchecked = 0, - Enabled = 1, - Disabled = 2 - } - - static SwitchState cachedUseDistributedContextPropagator; - - [PreObsolete("https://github.com/Particular/NServiceBus/issues/7825", - Note = "In v11, DistributedContextPropagator-based context propagation becomes the default and this switch will be removed together with the legacy propagator in obsolete_v11.cs.", - ReplacementTypeOrMember = "ContextPropagation")] - public const string UseDistributedContextPropagatorSwitchName = "NServiceBus.Core.OpenTelemetry.UseDistributedContextPropagator"; - - [PreObsolete("https://github.com/Particular/NServiceBus/issues/7825", - Note = "In v11, DistributedContextPropagator-based context propagation becomes the default and this switch will be removed together with the legacy propagator in obsolete_v11.cs.", - ReplacementTypeOrMember = "ContextPropagation")] - public static bool UseDistributedContextPropagator - { - get - { - var state = cachedUseDistributedContextPropagator; - if (state != SwitchState.Unchecked) - { - return state == SwitchState.Enabled; - } - - state = AppContext.TryGetSwitch(UseDistributedContextPropagatorSwitchName, out var isEnabled) && isEnabled - ? SwitchState.Enabled - : SwitchState.Disabled; - cachedUseDistributedContextPropagator = state; - - return state == SwitchState.Enabled; - } - } - - internal static void ResetUseDistributedContextPropagator() => cachedUseDistributedContextPropagator = SwitchState.Unchecked; - - public static void PropagateContextToHeaders(Activity? activity, Dictionary headers) - { - if (activity is null) - { - return; - } - - if (activity.Id is not null) - { - headers[Headers.DiagnosticsTraceParent] = activity.Id; - } - - if (activity.TraceStateString is not null) - { - headers[Headers.DiagnosticsTraceState] = activity.TraceStateString; - } - - var baggage = string.Join(",", activity.Baggage.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value ?? string.Empty)}")); - if (!string.IsNullOrEmpty(baggage)) - { - headers[Headers.DiagnosticsBaggage] = baggage; - } - } - - public static void PropagateContextFromHeaders(Activity? activity, IDictionary headers) - { - if (activity is null) - { - return; - } - - PropagateTraceStateFromHeaders(activity, headers); - PropagateBaggageFromHeaders(activity, headers); - } - - public static void PropagateTraceStateFromHeaders(Activity activity, IDictionary headers) - { - if (headers.TryGetValue(Headers.DiagnosticsTraceState, out var traceState)) - { - activity.TraceStateString = traceState; - } - } - - // parent: the activity whose baggage chain is checked so keys it already carries are not added again, - // see ContextPropagation.PropagateBaggageFromHeaders. - public static void PropagateBaggageFromHeaders(Activity activity, IDictionary headers, Activity? parent = null) - { - if (!headers.TryGetValue(Headers.DiagnosticsBaggage, out var baggageValue)) - { - return; - } - - var baggageSpan = baggageValue.AsSpan(); - // HINT: Iterate in reverse order because Activity baggage is LIFO - while (!baggageSpan.IsEmpty) - { - var lastComma = baggageSpan.LastIndexOf(','); - ReadOnlySpan baggageItem; - - if (lastComma >= 0) - { - baggageItem = baggageSpan[(lastComma + 1)..]; - baggageSpan = baggageSpan[..lastComma]; - } - else - { - baggageItem = baggageSpan; - baggageSpan = []; - } - - var firstEquals = baggageItem.IndexOf('='); - if (firstEquals < 0 || firstEquals >= baggageItem.Length) - { - continue; - } - - var key = baggageItem[..firstEquals].Trim().ToString(); - if (parent?.GetBaggageItem(key) is not null) - { - continue; - } - - var value = baggageItem[(firstEquals + 1)..]; - activity.AddBaggage(key, Uri.UnescapeDataString(value)); - } - } -} - -// Handler spans move from the "NServiceBus.Core" ActivitySource to the dedicated -// "NServiceBus.Core.Handler" source so they can be filtered/sampled independently -// (https://github.com/Particular/NServiceBus/issues/7284). Existing OpenTelemetry -// configurations only subscribe to "NServiceBus.Core" and would silently lose handler -// spans, so the new source is opt-in via an AppContext switch until v11. -// -// In v11 the dedicated source becomes the default: delete this class and remove the -// source selection in ActivityFactory.StartHandlerActivity so it always uses -// ActivitySources.Handler. -static class HandlerActivitySourceSwitch -{ - enum SwitchState : byte - { - Unchecked = 0, - Enabled = 1, - Disabled = 2 - } - - static SwitchState cachedUseHandlerActivitySource; - - [PreObsolete("https://github.com/Particular/NServiceBus/issues/7284", - Note = "In v11, handler spans are always emitted from the NServiceBus.Core.Handler ActivitySource and this switch will be removed.", - ReplacementTypeOrMember = "ActivitySources.Handler")] - public const string UseHandlerActivitySourceSwitchName = "NServiceBus.Core.OpenTelemetry.UseHandlerActivitySource"; - - [PreObsolete("https://github.com/Particular/NServiceBus/issues/7284", - Note = "In v11, handler spans are always emitted from the NServiceBus.Core.Handler ActivitySource and this switch will be removed.", - ReplacementTypeOrMember = "ActivitySources.Handler")] - public static bool UseHandlerActivitySource - { - get - { - var state = cachedUseHandlerActivitySource; - if (state != SwitchState.Unchecked) - { - return state == SwitchState.Enabled; - } - - state = AppContext.TryGetSwitch(UseHandlerActivitySourceSwitchName, out var isEnabled) && isEnabled - ? SwitchState.Enabled - : SwitchState.Disabled; - cachedUseHandlerActivitySource = state; - - return state == SwitchState.Enabled; - } - } - - internal static void ResetUseHandlerActivitySource() => cachedUseHandlerActivitySource = SwitchState.Unchecked; -} - - -// When a transport SDK (Azure Service Bus, RabbitMQ, SQS, ...) has its own OpenTelemetry -// instrumentation, an SDK "receive" span is the ambient Activity.Current while the incoming -// pipeline starts. The incoming message span should then be a child of that SDK span and only -// link to the NServiceBus sender span. Until v11 the sender span stays the parent by default so -// existing trace shapes don't change on a minor upgrade; the new shape is opt-in via an -// AppContext switch. -// -// In v11 the SDK span becomes the parent unconditionally: delete this class, remove the -// `TransportParentActivitySwitch.UseTransportSpanAsParent` check in -// ActivityFactory.CreateActivityFromIncomingMessage (keeping only the branch that links to the -// sender span), delete TransportParentSpanDefaultBehaviorTests.cs and remove the opt-in -// SetUp/TearDown pair in ActivityFactoryTests.StartIncomingActivity. -static class TransportParentActivitySwitch -{ - enum SwitchState : byte - { - Unchecked = 0, - Enabled = 1, - Disabled = 2 - } - - static SwitchState cachedUseTransportActivityAsParent; - - public const string UseTransportActivityAsParentSwitchName = "NServiceBus.Core.OpenTelemetry.UseTransportActivityAsParent"; - - public static bool UseTransportActivityAsParent - { - get - { - var state = cachedUseTransportActivityAsParent; - if (state != SwitchState.Unchecked) - { - return state == SwitchState.Enabled; - } - - state = AppContext.TryGetSwitch(UseTransportActivityAsParentSwitchName, out var isEnabled) && isEnabled - ? SwitchState.Enabled - : SwitchState.Disabled; - cachedUseTransportActivityAsParent = state; - - return state == SwitchState.Enabled; - } - } - - internal static void ResetUseTransportActivityAsParent() => cachedUseTransportActivityAsParent = SwitchState.Unchecked; -} - -// This class bridges two independent legacy exception-tagging behaviors, both -// scheduled for removal in v11: -// -// - SetLegacyStatusTags sets the "otel.status_code"/"otel.status_description" -// tags, which predate native support for Activity.SetStatus/Activity.Status -// and are now redundant with it. Kept only for consumers still reading the -// tags directly instead of Activity.Status. -// - EscapedTagList carries "exception.escaped", an attribute the OTel semantic -// conventions have marked Deprecated: -// https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-logs/ -// It's added to the exception event for backward compatibility with -// existing consumers of that attribute. -// -// In v11, delete this entire class, remove the -// `LegacyExceptionTags.SetLegacyStatusTags(activity, exception);` call in -// ActivityFactory.RecordError, and stop passing EscapedTagList to -// activity.AddException in the same method. -static class LegacyExceptionTags -{ - public static void SetLegacyStatusTags(Activity activity, Exception exception) - { - activity.SetTag("otel.status_code", "ERROR"); - activity.SetTag("otel.status_description", exception.Message); - } - - public static TagList EscapedTagList { get; } = new() { { "exception.escaped", true } }; -} - -public partial class InstrumentationOptions -{ - // Publishes start a new linked trace by default in v10 for backward compatibility. In v11, - // delete this method: PublishTraceMode then defaults to TraceMode.ContinueExisting through - // its initializer in InstrumentationOptions.cs, and the empty partial declaration plus the - // constructor call there can be removed. - partial void ApplyPreV11Defaults() => PublishTraceMode = TraceMode.StartNew; - - /// - /// Appends the destination to span names following the OTel messaging convention - /// {messaging.operation.name} {destination}, e.g. "process orders" or "send payments". - /// Disabled by default for backward compatibility. - /// - /// - /// In v11, this property will be dropped and "true" will be the only behavior. - /// - public bool UseMessageTypeNamesInSpanNames { get; set; } - - /// - /// Controls whether the "Start dispatching" and "Finished dispatching" activity events - /// are added to the incoming message span when outgoing messages are dispatched. - /// Enabled by default for backward compatibility. Disable to avoid the ingestion cost - /// of these events when they add no diagnostic value. - /// - /// - /// In v11, this property will be dropped and "false" will be the only behavior. - /// - public bool EmitMessageDispatchingEvents { get; set; } = true; - - /// - /// Controls meter instruments behaviors. - /// - public MetersOptions Meters { get; } = new(); -} - -/// -/// Controls opt-in meter instruments behaviors. -/// Accessed via endpointConfiguration.Tracing().Meters. -/// -public class MetersOptions -{ - /// - /// Emits the legacy execution.result tag with values "success" or "failure" - /// on handler time, processing time, saga fetch time, deserialize time, and serialize time metrics. - /// Enabled by default for backwards compatibility. Disable to reduce tag cardinality. - /// - /// - /// In v11, this property will be dropped and "false" will be the only behavior. - /// - public bool EmitExecutionResultTags { get; set; } = true; -} diff --git a/src/NServiceBus.Core/Pipeline/Incoming/PipelineMetrics.cs b/src/NServiceBus.Core/Pipeline/Incoming/PipelineMetrics.cs index 84c8aa2c7b..04a9a284d9 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/PipelineMetrics.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/PipelineMetrics.cs @@ -30,9 +30,9 @@ class PipelineMetrics const string CommitTime = "nservicebus.persistence.commit_time"; // queueName and discriminator are null for send-only endpoints, which have no receive queue to report. - public PipelineMetrics(IMeterFactory meterFactory, string? queueName, string? discriminator, MetersOptions metersOptions) + public PipelineMetrics(IMeterFactory meterFactory, string? queueName, string? discriminator) { - emitExecutionResultTags = metersOptions.EmitExecutionResultTags; + emitExecutionResultTags = !V11BehaviorSwitch.UseV11Behavior; // removed in v11 together with the tag, see obsoletes-v10.cs var meter = meterFactory.Create("NServiceBus.Core.Pipeline.Incoming", "0.4.0"); totalProcessedSuccessfully = meter.CreateCounter(TotalProcessedSuccessfully, description: "Total number of messages processed successfully by the endpoint."); diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 8ba0e81163..d9d45700d2 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -17,7 +17,6 @@ namespace NServiceBus; partial class TransportReceiveToPhysicalMessageConnector( IOutboxStorage outboxStorage, PipelineMetrics pipelineMetrics, - InstrumentationOptions instrumentationOptions, ILogger logger) : IStageForkConnector { @@ -134,7 +133,7 @@ async Task InvokeWithoutOutbox(ITransportReceiveContext context, Func 0 && operations[0].AddressTag is UnicastAddressTag unicastTag && outgoingMessage.Headers.TryGetValue(Headers.MessageIntent, out var intentStr) diff --git a/src/NServiceBus.Core/Pipeline/PipelineComponent.cs b/src/NServiceBus.Core/Pipeline/PipelineComponent.cs index cedb2d2ea7..75e01b3a04 100644 --- a/src/NServiceBus.Core/Pipeline/PipelineComponent.cs +++ b/src/NServiceBus.Core/Pipeline/PipelineComponent.cs @@ -12,8 +12,7 @@ sealed class PipelineComponent PipelineComponent(PipelineModifications modifications) => this.modifications = modifications; public static PipelineComponent Initialize(PipelineSettings settings, - HostingComponent.Configuration hostingConfiguration, ReceiveComponent.Configuration receiveConfiguration, - MetersOptions metersOptions) + HostingComponent.Configuration hostingConfiguration, ReceiveComponent.Configuration receiveConfiguration) { // make the PipelineMetrics available to the Pipeline hostingConfiguration.Services.AddSingleton(sp => @@ -22,11 +21,11 @@ public static PipelineComponent Initialize(PipelineSettings settings, if (receiveConfiguration.IsSendOnlyEndpoint) { - return new PipelineMetrics(meterFactory, null, null, metersOptions); + return new PipelineMetrics(meterFactory, null, null); } string discriminator = receiveConfiguration.InstanceSpecificQueueAddress?.Discriminator ?? ""; - return new PipelineMetrics(meterFactory, receiveConfiguration.LocalQueueAddress.BaseAddress, discriminator, metersOptions); + return new PipelineMetrics(meterFactory, receiveConfiguration.LocalQueueAddress.BaseAddress, discriminator); }); return new PipelineComponent(settings.modifications); diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index f25328126d..887d5ff149 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -66,13 +66,12 @@ public static ReceiveComponent Configure( return transport.Receivers[MainReceiverId].Subscriptions; }); - pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", b => + pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", static b => { var storage = b.GetService() ?? new NoOpOutboxStorage(); return new TransportReceiveToPhysicalMessageConnector( storage, b.GetRequiredService(), - hostingConfiguration.ActivityFactory.Options, b.GetRequiredService>() ); }, "Allows to abort processing the message"); diff --git a/src/NServiceBus.Core/Unicast/MessageOperations.cs b/src/NServiceBus.Core/Unicast/MessageOperations.cs index c3a8e40d78..1bd1af04ca 100644 --- a/src/NServiceBus.Core/Unicast/MessageOperations.cs +++ b/src/NServiceBus.Core/Unicast/MessageOperations.cs @@ -25,7 +25,7 @@ class MessageOperations( protected readonly IPipeline replyPipeline = replyPipeline; protected readonly IPipeline subscribePipeline = subscribePipeline; protected readonly IPipeline unsubscribePipeline = unsubscribePipeline; - readonly bool UseMessageTypeNamesInSpanNames = activityFactory.Options.UseMessageTypeNamesInSpanNames; + readonly bool useMessageTypeNamesInSpanNames = V11BehaviorSwitch.UseV11Behavior; // removed in v11, see obsoletes-v10.cs public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, PublishOptions options) @@ -69,7 +69,7 @@ async Task PublishMessage(IBehaviorContext context, [DynamicallyAccessedMembers( MergeDispatchProperties(publishContext, options.DispatchProperties); - var displayName = UseMessageTypeNamesInSpanNames + var displayName = useMessageTypeNamesInSpanNames ? $"{ActivityDisplayNames.PublishOperation} {messageType.Name}" : ActivityDisplayNames.PublishEvent; @@ -103,7 +103,7 @@ public async Task Subscribe(IBehaviorContext context, Type[] eventTypes, Subscri MergeDispatchProperties(subscribeContext, options.DispatchProperties); - var displayName = UseMessageTypeNamesInSpanNames + var displayName = useMessageTypeNamesInSpanNames ? $"{ActivityDisplayNames.SubscribeEvent} {string.Join(' ', eventTypes.Select(x => x.Name))}" : ActivityDisplayNames.SubscribeEvent; @@ -132,7 +132,7 @@ public async Task Unsubscribe(IBehaviorContext context, Type eventType, Unsubscr MergeDispatchProperties(unsubscribeContext, options.DispatchProperties); - var displayName = UseMessageTypeNamesInSpanNames + var displayName = useMessageTypeNamesInSpanNames ? $"{ActivityDisplayNames.UnsubscribeEvent} {eventType.Name}" : ActivityDisplayNames.UnsubscribeEvent; @@ -193,7 +193,7 @@ async Task SendMessage(IBehaviorContext context, [DynamicallyAccessedMembers(Dyn MergeDispatchProperties(outgoingContext, options.DispatchProperties); - var displayName = UseMessageTypeNamesInSpanNames + var displayName = useMessageTypeNamesInSpanNames ? $"{ActivityDisplayNames.SendMessage} {messageType.Name}" : ActivityDisplayNames.SendMessage; @@ -253,7 +253,7 @@ async Task ReplyMessage(IBehaviorContext context, [DynamicallyAccessedMembers(Dy MergeDispatchProperties(outgoingContext, options.DispatchProperties); - var displayName = UseMessageTypeNamesInSpanNames + var displayName = useMessageTypeNamesInSpanNames ? $"{ActivityDisplayNames.ReplyMessage} {messageType.Name}" : ActivityDisplayNames.ReplyMessage; diff --git a/src/NServiceBus.Core/obsoletes-v10.cs b/src/NServiceBus.Core/obsoletes-v10.cs index 208b3c2cad..d504205533 100644 --- a/src/NServiceBus.Core/obsoletes-v10.cs +++ b/src/NServiceBus.Core/obsoletes-v10.cs @@ -621,5 +621,211 @@ public MessageMetadataRegistry(Func isMessageType, bool allowDynamic } } +namespace NServiceBus +{ +#nullable enable + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + + // ============================================================================= + // OPENTELEMETRY V11 BEHAVIOR OPT-IN. EVERYTHING IN THIS BLOCK IS REMOVED IN v11. + // + // Several OpenTelemetry behaviors become the one and only behavior in v11, with no + // option left to opt in or out. Until then an endpoint can adopt them as a whole on + // v10 through a single AppContext switch: + // + // AppContext.SetSwitch("NServiceBus.Core.OpenTelemetry.UseV11Behavior", true); + // + // or in runtimeconfig.template.json / the project file: + // + // + // + // + // + // With the switch enabled: + // - Trace context and baggage are propagated through System.Diagnostics.DistributedContextPropagator + // (W3C baggage encoding) instead of LegacyContextPropagation below. The baggage wire format changes + // (W3C OWS encoding + whitespace trimming), which is breaking on rolling upgrades. + // https://github.com/Particular/NServiceBus/issues/7825 + // - Handler spans are emitted from the dedicated "NServiceBus.Core.Handler" ActivitySource instead of + // "NServiceBus.Core" so they can be filtered/sampled independently. Existing OpenTelemetry + // configurations that only subscribe to "NServiceBus.Core" would silently lose handler spans. + // https://github.com/Particular/NServiceBus/issues/7284 + // - When a transport SDK (Azure Service Bus, RabbitMQ, SQS, ...) has its own OpenTelemetry + // instrumentation, its ambient "receive" span becomes the parent of the incoming message span and + // the NServiceBus sender span is linked instead of being the parent. This changes trace shapes. + // - Span names follow the OTel messaging convention `{messaging.operation.name} {destination}`, + // e.g. "process orders", "send Payment", "publish OrderPlaced", "immediate retry orders". + // - The "Start dispatching"/"Finished dispatching" events are no longer added to the incoming span + // when outgoing messages are dispatched. + // - The legacy `execution.result` tag ("success"/"failure") is no longer emitted on the handler time, + // processing time, saga fetch time, deserialize time, and serialize time metrics. + // - The legacy `otel.status_code`/`otel.status_description` tags (redundant with Activity.Status) and + // the deprecated `exception.escaped` exception event attribute are no longer set on failures. + // https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-logs/ + // + // In v11: delete this entire namespace block, search the code base for `V11BehaviorSwitch` and keep + // only the branch each check guards for the enabled case. ActivityFactory, ContextPropagation, + // MessageOperations, RoutingToDispatchConnector, TransportReceiveToPhysicalMessageConnector and + // PipelineMetrics are the production call sites. Delete the pre-v11 default tests + // (ContextPropagationDefaultBehaviorTests, LegacyContextPropagationTests, TransportParentSpanDefaultBehaviorTests, + // the "Default_..." tests in HandlerActivitySourceTests) and the switch SetUp/TearDown pairs in the + // remaining unit and acceptance tests, together with the OpenTelemetryV11Defaults attribute in the + // obsoletes-v10.cs of NServiceBus.Core.Tests and NServiceBus.AcceptanceTests. + // ============================================================================= + static class V11BehaviorSwitch + { + enum SwitchState : byte + { + Unchecked = 0, + Enabled = 1, + Disabled = 2 + } + + static SwitchState cachedUseV11Behavior; + + public const string UseV11BehaviorSwitchName = "NServiceBus.Core.OpenTelemetry.UseV11Behavior"; + + public static bool UseV11Behavior + { + get + { + var state = cachedUseV11Behavior; + if (state != SwitchState.Unchecked) + { + return state == SwitchState.Enabled; + } + + state = AppContext.TryGetSwitch(UseV11BehaviorSwitchName, out var isEnabled) && isEnabled + ? SwitchState.Enabled + : SwitchState.Disabled; + cachedUseV11Behavior = state; + + return state == SwitchState.Enabled; + } + } + + internal static void ResetUseV11Behavior() => cachedUseV11Behavior = SwitchState.Unchecked; + } + + // The pre-v11 trace-context/baggage propagator. ContextPropagation.cs delegates here while + // V11BehaviorSwitch.UseV11Behavior is off. + static class LegacyContextPropagation + { + public static void PropagateContextToHeaders(Activity? activity, Dictionary headers) + { + if (activity is null) + { + return; + } + + if (activity.Id is not null) + { + headers[Headers.DiagnosticsTraceParent] = activity.Id; + } + + if (activity.TraceStateString is not null) + { + headers[Headers.DiagnosticsTraceState] = activity.TraceStateString; + } + + var baggage = string.Join(",", activity.Baggage.Select(item => $"{item.Key}={Uri.EscapeDataString(item.Value ?? string.Empty)}")); + if (!string.IsNullOrEmpty(baggage)) + { + headers[Headers.DiagnosticsBaggage] = baggage; + } + } + + public static void PropagateContextFromHeaders(Activity? activity, IDictionary headers) + { + if (activity is null) + { + return; + } + + PropagateTraceStateFromHeaders(activity, headers); + PropagateBaggageFromHeaders(activity, headers); + } + + public static void PropagateTraceStateFromHeaders(Activity activity, IDictionary headers) + { + if (headers.TryGetValue(Headers.DiagnosticsTraceState, out var traceState)) + { + activity.TraceStateString = traceState; + } + } + + // parent: the activity whose baggage chain is checked so keys it already carries are not added again, + // see ContextPropagation.PropagateBaggageFromHeaders. + public static void PropagateBaggageFromHeaders(Activity activity, IDictionary headers, Activity? parent = null) + { + if (!headers.TryGetValue(Headers.DiagnosticsBaggage, out var baggageValue)) + { + return; + } + + var baggageSpan = baggageValue.AsSpan(); + // HINT: Iterate in reverse order because Activity baggage is LIFO + while (!baggageSpan.IsEmpty) + { + var lastComma = baggageSpan.LastIndexOf(','); + ReadOnlySpan baggageItem; + + if (lastComma >= 0) + { + baggageItem = baggageSpan[(lastComma + 1)..]; + baggageSpan = baggageSpan[..lastComma]; + } + else + { + baggageItem = baggageSpan; + baggageSpan = []; + } + + var firstEquals = baggageItem.IndexOf('='); + if (firstEquals < 0 || firstEquals >= baggageItem.Length) + { + continue; + } + + var key = baggageItem[..firstEquals].Trim().ToString(); + if (parent?.GetBaggageItem(key) is not null) + { + continue; + } + + var value = baggageItem[(firstEquals + 1)..]; + activity.AddBaggage(key, Uri.UnescapeDataString(value)); + } + } + } + + // The pre-v11 exception tagging. ActivityFactory.RecordError applies these while + // V11BehaviorSwitch.UseV11Behavior is off. + static class LegacyExceptionTags + { + public static void SetLegacyStatusTags(Activity activity, Exception exception) + { + activity.SetTag("otel.status_code", "ERROR"); + activity.SetTag("otel.status_description", exception.Message); + } + + public static TagList EscapedTagList { get; } = new() { { "exception.escaped", true } }; + } + + public partial class InstrumentationOptions + { + // Publishes start a new linked trace by default in v10 for backward compatibility. This one keeps a + // public option (PublishTraceMode) in v11 and is therefore not governed by V11BehaviorSwitch. In v11, + // delete this method: PublishTraceMode then defaults to TraceMode.ContinueExisting through its + // initializer in InstrumentationOptions.cs, and the empty partial declaration plus the constructor + // call there can be removed. + partial void ApplyPreV11Defaults() => PublishTraceMode = TraceMode.StartNew; + } +#nullable restore +} + #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v12.cs b/src/NServiceBus.Core/obsoletes-v11.cs similarity index 100% rename from src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v12.cs rename to src/NServiceBus.Core/obsoletes-v11.cs