From 3034d431f1cc0b565e5afdbc780a6bb2fe333a8b Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 2 Oct 2026 17:22:41 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Rename=20obsolete=5Fv1?= =?UTF-8?q?2.cs=20to=20obsoletes-v11.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the root obsoletes-v10.cs convention where obsoletes-vN.cs holds code that is obsolete in vN and removed in vN+1. The ExceptionRecordingMode environment variable scaffold is removed in v12, so it belongs in obsoletes-v11.cs. --- .../{OpenTelemetry/Tracing/obsolete_v12.cs => obsoletes-v11.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/NServiceBus.Core/{OpenTelemetry/Tracing/obsolete_v12.cs => obsoletes-v11.cs} (100%) 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 From 4ad89b13557ecc85c918c73b472d763ba72494f5 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Fri, 2 Oct 2026 17:22:41 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20Single=20AppContext=20switch=20?= =?UTF-8?q?for=20the=20OpenTelemetry=20v11=20behaviors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several OpenTelemetry behaviors become the only behavior in v11 with no option left to opt in or out. Until then they are gated behind one AppContext switch, NServiceBus.Core.OpenTelemetry.UseV11Behavior, so an endpoint adopts the v11 behavior as a whole instead of per feature: - DistributedContextPropagator-based trace context and baggage propagation - handler spans from the NServiceBus.Core.Handler ActivitySource - ambient transport SDK receive span as parent of the incoming span - destination and message type in span names - no Start/Finished dispatching events on the incoming span - no execution.result metric tag - no legacy otel.status_code/otel.status_description tags and no exception.escaped attribute Replaces the UseDistributedContextPropagator, UseHandlerActivitySource and UseTransportActivityAsParent switches and removes the unreleased UseMessageTypeNamesInSpanNames, EmitMessageDispatchingEvents and Meters.EmitExecutionResultTags options. PublishTraceMode keeps its public option in v11 and is not governed by the switch. All v11 scaffolding now lives in the root obsoletes-v10.cs, replacing OpenTelemetry/Tracing/obsolete_v11.cs. --- ...ide-trace-state-and-baggage-propagation.md | 2 +- .../OpenTelemetryAcceptanceTest.cs | 2 +- .../When_outgoing_activity_has_baggage.cs | 2 +- .../When_processing_incoming_message.cs | 21 +- ...g_message_with_default_activity_sources.cs | 2 +- .../Traces/When_publishing_messages.cs | 20 +- .../When_recoverability_action_occurs.cs | 9 +- .../Traces/When_sending_messages.cs | 21 +- .../Traces/When_sending_replies.cs | 24 +- .../NServiceBus.AcceptanceTests.csproj | 1 + .../obsoletes-v10.cs | 40 +++ ...IApprovals.ApproveNServiceBus.approved.txt | 8 - .../Envelopes/EnvelopeUnwrapperTests.cs | 2 +- .../OpenTelemetry/ActivityFactoryTests.cs | 23 +- .../ContextPropagationCompatibilityTests.cs | 15 +- .../ContextPropagationDefaultBehaviorTests.cs | 11 +- .../HandlerActivitySourceTests.cs | 27 +- .../InstrumentationOptionsTests.cs | 2 +- .../OpenTelemetry/MeterTests.cs | 2 +- .../PipelineMetricsExecutionResultTagTests.cs | 43 +++ ...TransportParentSpanDefaultBehaviorTests.cs | 7 +- .../Incoming/InvokeHandlerTerminatorTest.cs | 2 +- .../SerializeMessageConnectorTests.cs | 2 +- .../Pipeline/MainPipelineExecutorTests.cs | 2 +- .../Pipeline/PipelineMetricTagsTests.cs | 2 +- ...tReceiveToPhysicalMessageConnectorTests.cs | 27 +- .../Unicast/LoadHandlersConnectorTests.cs | 8 +- src/NServiceBus.Core.Tests/obsoletes-v10.cs | 29 ++ src/NServiceBus.Core/EndpointCreator.cs | 2 +- .../OpenTelemetry/InstrumentationOptions.cs | 4 +- .../Tracing/ActivityDisplayNames.cs | 2 +- .../OpenTelemetry/Tracing/ActivityFactory.cs | 21 +- .../Tracing/ContextPropagation.cs | 12 +- .../OpenTelemetry/Tracing/obsolete_v11.cs | 328 ------------------ .../Pipeline/Incoming/PipelineMetrics.cs | 4 +- ...nsportReceiveToPhysicalMessageConnector.cs | 3 +- .../Outgoing/RoutingToDispatchConnector.cs | 2 +- .../Pipeline/PipelineComponent.cs | 7 +- .../Receiving/ReceiveComponent.cs | 3 +- .../Unicast/MessageOperations.cs | 12 +- src/NServiceBus.Core/obsoletes-v10.cs | 206 +++++++++++ 41 files changed, 436 insertions(+), 526 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/obsoletes-v10.cs create mode 100644 src/NServiceBus.Core.Tests/OpenTelemetry/PipelineMetricsExecutionResultTagTests.cs create mode 100644 src/NServiceBus.Core.Tests/obsoletes-v10.cs delete mode 100644 src/NServiceBus.Core/OpenTelemetry/Tracing/obsolete_v11.cs 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 64dcd34a22c..6a3b0d9253d 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 a2748753077..f67d0ab06bd 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 b6a2310ad25..33cfd28674b 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 86fc87529a4..32d60e7e036 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 a233aa178b1..4b6c631b89b 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 39b8e57885d..c0359fd053f 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 6e24936ab53..09e3b64de80 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 a56a3caac7e..61499ddee54 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 7e79d6582c5..8077fbb2027 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 2f335d4f9e5..82995eed81c 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 00000000000..1b6fd71d6eb --- /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 5a9feef8a4a..25091c74f63 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 b7136605694..d5bd75d9546 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 b2c983d4278..6c10d823757 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 ae5dad4ba34..950a847047c 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 4f496a5e660..af9b6897042 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 8af728a9c8a..2d87714a419 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 b01433e755d..2b1847401bb 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 77eb1764ca4..14bd2817b1f 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 00000000000..0c676641ebd --- /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 0064600331e..81bad803712 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 5948f081060..2b74cf4a74d 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 dacfb4e7447..9aeddeaec52 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 9e56b4ee174..f78b9f7261b 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 b316643e361..aa3353a319c 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 1f5eae8ffc0..82d939f1946 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 291c25dac83..7a4f512495c 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 00000000000..f1b823e10e3 --- /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 9cbd8239550..8ba13b52343 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 c1ebb7ad1ab..33080ad7218 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 ec5f69f29fd..418698e2754 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 687dcd15067..5e0cbfabe30 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 ffa1b15091f..083b3961bc8 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 4836a5834db..00000000000 --- 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 84c8aa2c7b0..04a9a284d9e 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 8ba0e81163b..d9d45700d2a 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 cedb2d2ea7a..75e01b3a046 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 f25328126dc..887d5ff1494 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 c3a8e40d784..1bd1af04ca1 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 208b3c2cade..d5042055337 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