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); });
}