From 92fa740092cdf6690c6a761bc73124e2f836399c Mon Sep 17 00:00:00 2001 From: majanjua-amzn Date: Thu, 16 Oct 2025 15:20:00 -0700 Subject: [PATCH] feat: add on ending span processor functionality This change creates a new ExtendedBaseProcessor that allows users to implement onEnding functionality to their processor, as per the spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#onending --- OpenTelemetry.sln | 1 + build/Common.props | 2 +- .../diagnostics/experimental-apis/OTEL1005.md | 31 +++++++++++++++++ docs/diagnostics/experimental-apis/README.md | 6 ++++ .../Experimental/PublicAPI.Unshipped.txt | 4 +++ src/OpenTelemetry/CHANGELOG.md | 3 ++ src/OpenTelemetry/CompositeProcessor.cs | 22 ++++++++++++ src/OpenTelemetry/ExtendedBaseProcessor.cs | 34 +++++++++++++++++++ src/OpenTelemetry/Trace/TracerProviderSdk.cs | 13 +++++++ src/Shared/DiagnosticDefinitions.cs | 1 + .../Shared/TestActivityProcessor.cs | 15 +++++++- .../Trace/CompositeActivityProcessorTests.cs | 14 +++++--- 12 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 docs/diagnostics/experimental-apis/OTEL1005.md create mode 100644 src/OpenTelemetry/ExtendedBaseProcessor.cs diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index 8272cc9e787..7e6689e6945 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -308,6 +308,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "experimental-apis", "experi docs\diagnostics\experimental-apis\OTEL1000.md = docs\diagnostics\experimental-apis\OTEL1000.md docs\diagnostics\experimental-apis\OTEL1001.md = docs\diagnostics\experimental-apis\OTEL1001.md docs\diagnostics\experimental-apis\OTEL1004.md = docs\diagnostics\experimental-apis\OTEL1004.md + docs\diagnostics\experimental-apis\OTEL1005.md = docs\diagnostics\experimental-apis\OTEL1005.md docs\diagnostics\experimental-apis\README.md = docs\diagnostics\experimental-apis\README.md EndProjectSection EndProject diff --git a/build/Common.props b/build/Common.props index 44c697e393c..ff61b5f5bd0 100644 --- a/build/Common.props +++ b/build/Common.props @@ -12,7 +12,7 @@ all low - $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004 + $(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004;OTEL1005 latest-All diff --git a/docs/diagnostics/experimental-apis/OTEL1005.md b/docs/diagnostics/experimental-apis/OTEL1005.md new file mode 100644 index 00000000000..96a8775a51f --- /dev/null +++ b/docs/diagnostics/experimental-apis/OTEL1005.md @@ -0,0 +1,31 @@ +# OpenTelemetry .NET Diagnostic: OTEL1004 + +## Overview + +This is an experimental API for modifying spans before they end/close + +### Details + +#### ExtendedBaseProcessor + +The abstract class `ExtendedBaseProcessor` provides an extension of the +`BaseProcessor` that allows spans to be modified before they end as per the +OpenTelemetry specification. It provides the `OnEnding` function that is called +during the span `End()` operation. The end timestamp MUST have been computed +(the `OnEnding` method duration is not included in the span duration). The Span +object MUST still be mutable (i.e., `SetAttribute`, `AddLink`, `AddEvent` can be +called) while `OnEnding` is called. This method MUST be called synchronously +within the [`Span.End()` API](api.md#end), therefore it should not block or +throw an exception. If multiple `SpanProcessors` are registered, their +`OnEnding` callbacks are invoked in the order they have been registered. The +SDK MUST guarantee that the span can no longer be modified by any other thread +before invoking `OnEnding` of the first `SpanProcessor`. From that point on, +modifications are only allowed synchronously from within the invoked `OnEnding` +callbacks. All registered SpanProcessor `OnEnding` callbacks are executed before +any SpanProcessor's `OnEnd` callback is invoked. + +**Parameters:** + +* `span` - a read/write span object for the span which is about to be ended. + +**Returns:** `Void` diff --git a/docs/diagnostics/experimental-apis/README.md b/docs/diagnostics/experimental-apis/README.md index daa80d34b38..4b341c881a5 100644 --- a/docs/diagnostics/experimental-apis/README.md +++ b/docs/diagnostics/experimental-apis/README.md @@ -33,6 +33,12 @@ Description: ExemplarReservoir Support Details: [OTEL1004](./OTEL1004.md) +### OTEL1005 + +Description: OnEnding Implementation + +Details: [OTEL1005](./OTEL1005.md) + ## Inactive Experimental APIs which have been released stable or removed: diff --git a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt index b9507b58b38..3499a317a9a 100644 --- a/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -25,3 +25,7 @@ OpenTelemetry.Metrics.MetricStreamConfiguration.ExemplarReservoirFactory.set -> [OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action? configureBuilder, System.Action? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder! [OTEL1001]static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder! [OTEL1004]virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void +[OTEL1005]OpenTelemetry.ExtendedBaseProcessor +[OTEL1005]OpenTelemetry.ExtendedBaseProcessor.ExtendedBaseProcessor() -> void +[OTEL1005]virtual OpenTelemetry.ExtendedBaseProcessor.OnEnding(T data) -> void +[OTEL1005]override OpenTelemetry.CompositeProcessor.OnEnding(T data) -> void diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 367f9b2d7e2..dcb886b0bba 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -37,6 +37,9 @@ Released 2025-Oct-21 * Add support for .NET 10.0. ([#6307](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6307)) +* feat: add on ending span processor functionality + ([#6617](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6617)) + ## 1.13.1 Released 2025-Oct-09 diff --git a/src/OpenTelemetry/CompositeProcessor.cs b/src/OpenTelemetry/CompositeProcessor.cs index df484950e55..c2a682d1389 100644 --- a/src/OpenTelemetry/CompositeProcessor.cs +++ b/src/OpenTelemetry/CompositeProcessor.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +#endif using System.Diagnostics; using OpenTelemetry.Internal; @@ -10,7 +13,11 @@ namespace OpenTelemetry; /// Represents a chain of s. /// /// The type of object to be processed. +#if EXPOSE_EXPERIMENTAL_FEATURES +public class CompositeProcessor : ExtendedBaseProcessor +#else public class CompositeProcessor : BaseProcessor +#endif { internal readonly DoublyLinkedListNode Head; private DoublyLinkedListNode tail; @@ -69,6 +76,21 @@ public override void OnEnd(T data) } } +#if EXPOSE_EXPERIMENTAL_FEATURES + /// + [Experimental(DiagnosticDefinitions.ExtendedBaseProcessorExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] + public override void OnEnding(T data) + { + for (var cur = this.Head; cur != null; cur = cur.Next) + { + if (typeof(ExtendedBaseProcessor).IsAssignableFrom(cur.Value.GetType())) + { + ((ExtendedBaseProcessor)cur.Value).OnEnding(data); + } + } + } +#endif + /// public override void OnStart(T data) { diff --git a/src/OpenTelemetry/ExtendedBaseProcessor.cs b/src/OpenTelemetry/ExtendedBaseProcessor.cs new file mode 100644 index 00000000000..9f8d897378b --- /dev/null +++ b/src/OpenTelemetry/ExtendedBaseProcessor.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if EXPOSE_EXPERIMENTAL_FEATURES +using System.Diagnostics.CodeAnalysis; +using OpenTelemetry.Internal; + +namespace OpenTelemetry; + +/// +/// Extended base processor base class. +/// +/// The type of object to be processed. +[Experimental(DiagnosticDefinitions.ExtendedBaseProcessorExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)] +#pragma warning disable CA1012 // Abstract types should not have public constructors +public abstract class ExtendedBaseProcessor : BaseProcessor +#pragma warning restore CA1012 // Abstract types should not have public constructors +{ + /// + /// Called synchronously before a telemetry object ends. + /// + /// + /// The started telemetry object. + /// + /// + /// This function is called synchronously on the thread which ended + /// the telemetry object. This function should be thread-safe, and + /// should not block indefinitely or throw exceptions. + /// + public virtual void OnEnding(T data) + { + } +} +#endif diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index d79fe0b051c..324b7f43707 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -189,6 +189,12 @@ internal TracerProviderSdk( if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) { +#if EXPOSE_EXPERIMENTAL_FEATURES + if (typeof(ExtendedBaseProcessor).IsAssignableFrom(this.processor?.GetType())) + { + (this.processor as ExtendedBaseProcessor)?.OnEnding(activity); + } +#endif this.processor?.OnEnd(activity); } }; @@ -224,6 +230,13 @@ internal TracerProviderSdk( if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) { +#if EXPOSE_EXPERIMENTAL_FEATURES + if (typeof(ExtendedBaseProcessor).IsAssignableFrom(this.processor?.GetType())) + { + (this.processor as ExtendedBaseProcessor)?.OnEnding(activity); + } +#endif + this.processor?.OnEnd(activity); } }; diff --git a/src/Shared/DiagnosticDefinitions.cs b/src/Shared/DiagnosticDefinitions.cs index f6b449352ff..1a04bab5576 100644 --- a/src/Shared/DiagnosticDefinitions.cs +++ b/src/Shared/DiagnosticDefinitions.cs @@ -10,6 +10,7 @@ internal static class DiagnosticDefinitions public const string LoggerProviderExperimentalApi = "OTEL1000"; public const string LogsBridgeExperimentalApi = "OTEL1001"; public const string ExemplarReservoirExperimentalApi = "OTEL1004"; + public const string ExtendedBaseProcessorExperimentalApi = "OTEL1005"; /* Definitions which have been released stable: public const string ExemplarExperimentalApi = "OTEL1002"; diff --git a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs index 2ca908126d2..833a194d78d 100644 --- a/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs +++ b/test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs @@ -5,9 +5,10 @@ namespace OpenTelemetry.Tests; -internal sealed class TestActivityProcessor : BaseProcessor +internal sealed class TestActivityProcessor : ExtendedBaseProcessor { public Action? StartAction; + public Action? EndingAction; public Action? EndAction; public TestActivityProcessor() @@ -20,6 +21,13 @@ public TestActivityProcessor(Action? onStart, Action? onEnd) this.EndAction = onEnd; } + public TestActivityProcessor(Action? onStart, Action? onEnding, Action? onEnd) + { + this.StartAction = onStart; + this.EndingAction = onEnding; + this.EndAction = onEnd; + } + public bool ShutdownCalled { get; private set; } public bool ForceFlushCalled { get; private set; } @@ -31,6 +39,11 @@ public override void OnStart(Activity span) this.StartAction?.Invoke(span); } + public override void OnEnding(Activity span) + { + this.EndingAction?.Invoke(span); + } + public override void OnEnd(Activity span) { this.EndAction?.Invoke(span); diff --git a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs index a287f897714..3c9897705d0 100644 --- a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs @@ -26,21 +26,23 @@ public void CompositeActivityProcessor_CallsAllProcessorSequentially() var result = string.Empty; using var p1 = new TestActivityProcessor( - activity => { result += "1"; }, - activity => { result += "3"; }); + activity => { result += "start1"; }, + activity => { result += "end1"; }); using var p2 = new TestActivityProcessor( - activity => { result += "2"; }, - activity => { result += "4"; }); + activity => { result += "start2"; }, + activity => { result += "ending2"; }, + activity => { result += "end2"; }); using var activity = new Activity("test"); using (var processor = new CompositeProcessor([p1, p2])) { processor.OnStart(activity); + processor.OnEnding(activity); processor.OnEnd(activity); } - Assert.Equal("1234", result); + Assert.Equal("start1start2ending2end1end2", result); } [Fact] @@ -48,12 +50,14 @@ public void CompositeActivityProcessor_ProcessorThrows() { using var p1 = new TestActivityProcessor( _ => throw new InvalidOperationException("Start exception"), + _ => throw new InvalidOperationException("Ending exception"), _ => throw new InvalidOperationException("End exception")); using var activity = new Activity("test"); using var processor = new CompositeProcessor([p1]); Assert.Throws(() => { processor.OnStart(activity); }); + Assert.Throws(() => { processor.OnEnding(activity); }); Assert.Throws(() => { processor.OnEnd(activity); }); }