Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OpenTelemetry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion build/Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>low</NuGetAuditLevel>
<!-- Suppress warnings for repo code using experimental features -->
<NoWarn>$(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004</NoWarn>
<NoWarn>$(NoWarn);OTEL1000;OTEL1001;OTEL1002;OTEL1004;OTEL1005</NoWarn>
<AnalysisLevel>latest-All</AnalysisLevel>
</PropertyGroup>

Expand Down
31 changes: 31 additions & 0 deletions docs/diagnostics/experimental-apis/OTEL1005.md
Original file line number Diff line number Diff line change
@@ -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`
6 changes: 6 additions & 0 deletions docs/diagnostics/experimental-apis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ OpenTelemetry.Metrics.MetricStreamConfiguration.ExemplarReservoirFactory.set ->
[OTEL1000]static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.UseOpenTelemetry(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, System.Action<OpenTelemetry.Logs.LoggerProviderBuilder!>? configureBuilder, System.Action<OpenTelemetry.Logs.OpenTelemetryLoggerOptions!>? configureOptions) -> Microsoft.Extensions.Logging.ILoggingBuilder!
[OTEL1001]static OpenTelemetry.Sdk.CreateLoggerProviderBuilder() -> OpenTelemetry.Logs.LoggerProviderBuilder!
[OTEL1004]virtual OpenTelemetry.Metrics.FixedSizeExemplarReservoir.OnCollected() -> void
[OTEL1005]OpenTelemetry.ExtendedBaseProcessor<T>
[OTEL1005]OpenTelemetry.ExtendedBaseProcessor<T>.ExtendedBaseProcessor() -> void
[OTEL1005]virtual OpenTelemetry.ExtendedBaseProcessor<T>.OnEnding(T data) -> void
[OTEL1005]override OpenTelemetry.CompositeProcessor<T>.OnEnding(T data) -> void
3 changes: 3 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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
Expand Down
22 changes: 22 additions & 0 deletions src/OpenTelemetry/CompositeProcessor.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -10,7 +13,11 @@ namespace OpenTelemetry;
/// Represents a chain of <see cref="BaseProcessor{T}"/>s.
/// </summary>
/// <typeparam name="T">The type of object to be processed.</typeparam>
#if EXPOSE_EXPERIMENTAL_FEATURES
public class CompositeProcessor<T> : ExtendedBaseProcessor<T>
#else
public class CompositeProcessor<T> : BaseProcessor<T>
#endif
{
internal readonly DoublyLinkedListNode Head;
private DoublyLinkedListNode tail;
Expand Down Expand Up @@ -69,6 +76,21 @@ public override void OnEnd(T data)
}
}

#if EXPOSE_EXPERIMENTAL_FEATURES
/// <inheritdoc/>
[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<T>).IsAssignableFrom(cur.Value.GetType()))
{
((ExtendedBaseProcessor<T>)cur.Value).OnEnding(data);
}
}
}
#endif

/// <inheritdoc/>
public override void OnStart(T data)
{
Expand Down
34 changes: 34 additions & 0 deletions src/OpenTelemetry/ExtendedBaseProcessor.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extended base processor base class.
/// </summary>
/// <typeparam name="T">The type of object to be processed.</typeparam>
[Experimental(DiagnosticDefinitions.ExtendedBaseProcessorExperimentalApi, UrlFormat = DiagnosticDefinitions.ExperimentalApiUrlFormat)]
#pragma warning disable CA1012 // Abstract types should not have public constructors
public abstract class ExtendedBaseProcessor<T> : BaseProcessor<T>
#pragma warning restore CA1012 // Abstract types should not have public constructors
{
/// <summary>
/// Called synchronously before a telemetry object ends.
/// </summary>
/// <param name="data">
/// The started telemetry object.
/// </param>
/// <remarks>
/// 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.
/// </remarks>
public virtual void OnEnding(T data)
{
}
}
#endif
10 changes: 10 additions & 0 deletions src/OpenTelemetry/Trace/TracerProviderSdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ internal TracerProviderSdk(

if (SuppressInstrumentationScope.DecrementIfTriggered() == 0)
{
if (typeof(ExtendedBaseProcessor<Activity>).IsAssignableFrom(this.processor?.GetType()))
{
(this.processor as ExtendedBaseProcessor<Activity>)?.OnEnding(activity);
}

this.processor?.OnEnd(activity);
}
};
Expand Down Expand Up @@ -224,6 +229,11 @@ internal TracerProviderSdk(

if (SuppressInstrumentationScope.DecrementIfTriggered() == 0)
{
if (typeof(ExtendedBaseProcessor<Activity>).IsAssignableFrom(this.processor?.GetType()))
{
(this.processor as ExtendedBaseProcessor<Activity>)?.OnEnding(activity);
}

this.processor?.OnEnd(activity);
}
};
Expand Down
1 change: 1 addition & 0 deletions src/Shared/DiagnosticDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
15 changes: 14 additions & 1 deletion test/OpenTelemetry.Tests/Shared/TestActivityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@

namespace OpenTelemetry.Tests;

internal sealed class TestActivityProcessor : BaseProcessor<Activity>
internal sealed class TestActivityProcessor : ExtendedBaseProcessor<Activity>
{
public Action<Activity>? StartAction;
public Action<Activity>? EndingAction;
public Action<Activity>? EndAction;

public TestActivityProcessor()
Expand All @@ -20,6 +21,13 @@ public TestActivityProcessor(Action<Activity>? onStart, Action<Activity>? onEnd)
this.EndAction = onEnd;
}

public TestActivityProcessor(Action<Activity>? onStart, Action<Activity>? onEnding, Action<Activity>? onEnd)
{
this.StartAction = onStart;
this.EndingAction = onEnding;
this.EndAction = onEnd;
}

public bool ShutdownCalled { get; private set; }

public bool ForceFlushCalled { get; private set; }
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,38 @@ 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<Activity>([p1, p2]))
{
processor.OnStart(activity);
processor.OnEnding(activity);
processor.OnEnd(activity);
}

Assert.Equal("1234", result);
Assert.Equal("start1start2ending2end1end2", result);
}

[Fact]
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<Activity>([p1]);
Assert.Throws<InvalidOperationException>(() => { processor.OnStart(activity); });
Assert.Throws<InvalidOperationException>(() => { processor.OnEnding(activity); });
Assert.Throws<InvalidOperationException>(() => { processor.OnEnd(activity); });
}

Expand Down