Skip to content

Commit 89a50ab

Browse files
Fix: Scope not being applied to OpenTelemetry spans in ASP.NET Core (#2690)
1 parent ed6d161 commit 89a50ab

File tree

10 files changed

+105
-22
lines changed

10 files changed

+105
-22
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
- Release of Azure Functions (Isolated Worker/Out-of-Process) support ([#2686](https://github.com/getsentry/sentry-dotnet/pull/2686))
1313

14+
### Fixes
15+
- Scope is now correctly applied to Transactions when using OpenTelemetry on ASP.NET Core ([#2690](https://github.com/getsentry/sentry-dotnet/pull/2690))
16+
1417
### Dependencies
1518

1619
- Bump CLI from v2.20.7 to v2.21.2 ([#2645](https://github.com/getsentry/sentry-dotnet/pull/2645), [#2647](https://github.com/getsentry/sentry-dotnet/pull/2647), [#2698](https://github.com/getsentry/sentry-dotnet/pull/2698))

src/Sentry.AspNetCore/SentryMiddleware.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.Extensions.Options;
1010
using Sentry.AspNetCore.Extensions;
1111
using Sentry.Extensibility;
12+
using Sentry.Internal;
1213
using Sentry.Reflection;
1314

1415
namespace Sentry.AspNetCore;
@@ -132,6 +133,12 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
132133
{
133134
var originalMethod = context.Request.Method;
134135
await next(context).ConfigureAwait(false);
136+
if (_options.Instrumenter == Instrumenter.OpenTelemetry && Activity.Current is {} activity)
137+
{
138+
// The middleware pipeline finishes up before the Otel Activity.OnEnd callback is invoked so we need
139+
// so save a copy of the scope that can be restored by our SentrySpanProcessor
140+
hub.ConfigureScope(scope => activity.SetFused(scope));
141+
}
135142

136143
// When an exception was handled by other component (i.e: UseExceptionHandler feature).
137144
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature?>();

src/Sentry.OpenTelemetry/SentrySpanProcessor.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using OpenTelemetry;
22
using Sentry.Extensibility;
3+
using Sentry.Internal;
34
using Sentry.Internal.Extensions;
45
using Sentry.Internal.OpenTelemetry;
56

@@ -156,6 +157,12 @@ public override void OnEnd(Activity data)
156157

157158
// Transactions set otel attributes (and resource attributes) as context.
158159
transaction.Contexts["otel"] = GetOtelContext(attributes);
160+
// Events are received/processed in a different AsyncLocal context. Restoring the scope that started it.
161+
var activityScope = data.GetFused<Scope>();
162+
if (activityScope is { } savedScope && _hub is Hub hub)
163+
{
164+
hub.RestoreScope(savedScope);
165+
}
159166
}
160167
else
161168
{

src/Sentry/Internal/Hub.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Sentry.Extensibility;
22
using Sentry.Infrastructure;
33
using Sentry.Integrations;
4+
using Sentry.Internal.ScopeStack;
45

56
namespace Sentry.Internal;
67

@@ -97,6 +98,8 @@ public async Task ConfigureScopeAsync(Func<Scope, Task> configureScope)
9798

9899
public IDisposable PushScope<TState>(TState state) => ScopeManager.PushScope(state);
99100

101+
public void RestoreScope(Scope savedScope) => ScopeManager.RestoreScope(savedScope);
102+
100103
[Obsolete]
101104
public void WithScope(Action<Scope> scopeCallback) => ScopeManager.WithScope(scopeCallback);
102105

@@ -486,8 +489,7 @@ public void CaptureTransaction(Transaction transaction, Hint? hint)
486489
try
487490
{
488491
// Apply scope data
489-
var currentScopeAndClient = ScopeManager.GetCurrent();
490-
var scope = currentScopeAndClient.Key;
492+
var (scope, client) = ScopeManager.GetCurrent();
491493
scope.Evaluate();
492494
scope.Apply(transaction);
493495

@@ -513,7 +515,6 @@ public void CaptureTransaction(Transaction transaction, Hint? hint)
513515
}
514516
}
515517

516-
var client = currentScopeAndClient.Value;
517518
client.CaptureTransaction(processedTransaction, hint);
518519
}
519520
catch (Exception e)
@@ -543,8 +544,8 @@ public async Task FlushAsync(TimeSpan timeout)
543544
{
544545
try
545546
{
546-
var currentScope = ScopeManager.GetCurrent();
547-
await currentScope.Value.FlushAsync(timeout).ConfigureAwait(false);
547+
var (_, client) = ScopeManager.GetCurrent();
548+
await client.FlushAsync(timeout).ConfigureAwait(false);
548549
}
549550
catch (Exception e)
550551
{

src/Sentry/Internal/IHubEx.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace Sentry.Internal;
33
internal interface IHubEx : IHub
44
{
55
SentryId CaptureEventInternal(SentryEvent evt, Hint? hint, Scope? scope = null);
6+
67
T? WithScope<T>(Func<Scope, T?> scopeCallback);
78
Task WithScopeAsync(Func<Scope, Task> scopeCallback);
89
Task<T?> WithScopeAsync<T>(Func<Scope, Task<T?>> scopeCallback);

src/Sentry/Internal/IInternalScopeManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ namespace Sentry.Internal;
55
internal interface IInternalScopeManager : ISentryScopeManager, IDisposable
66
{
77
KeyValuePair<Scope, ISentryClient> GetCurrent();
8+
void RestoreScope(Scope savedScope);
9+
810
IScopeStackContainer ScopeStackContainer { get; }
911

1012
// TODO: Move The following to ISentryScopeManager in a future major version.

src/Sentry/Internal/SentryScopeManager.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,18 @@ public SentryScopeManager(SentryOptions options, ISentryClient rootClient)
3030
NewStack = () => new[] { new KeyValuePair<Scope, ISentryClient>(new Scope(options), rootClient) };
3131
}
3232

33-
public KeyValuePair<Scope, ISentryClient> GetCurrent()
34-
{
35-
var current = ScopeAndClientStack;
36-
return current[^1];
37-
}
33+
public KeyValuePair<Scope, ISentryClient> GetCurrent() => ScopeAndClientStack[^1];
3834

3935
public void ConfigureScope(Action<Scope>? configureScope)
4036
{
41-
var scope = GetCurrent();
42-
configureScope?.Invoke(scope.Key);
37+
var (scope, _) = GetCurrent();
38+
configureScope?.Invoke(scope);
4339
}
4440

4541
public Task ConfigureScopeAsync(Func<Scope, Task>? configureScope)
4642
{
47-
var scope = GetCurrent();
48-
return configureScope?.Invoke(scope.Key) ?? Task.CompletedTask;
43+
var (scope, _) = GetCurrent();
44+
return configureScope?.Invoke(scope) ?? Task.CompletedTask;
4945
}
5046

5147
public IDisposable PushScope() => PushScope<object>(null);
@@ -92,39 +88,58 @@ public IDisposable PushScope<TState>(TState? state)
9288
return scopeSnapshot;
9389
}
9490

91+
public void RestoreScope(Scope savedScope)
92+
{
93+
if (IsGlobalMode)
94+
{
95+
_options.LogWarning("RestoreScope called in global mode, returning.");
96+
return;
97+
}
98+
99+
var currentScopeAndClientStack = ScopeAndClientStack;
100+
var (previousScope, client) = currentScopeAndClientStack[^1];
101+
102+
_options.LogDebug("Scope restored");
103+
var newScopeAndClientStack = new KeyValuePair<Scope, ISentryClient>[currentScopeAndClientStack.Length + 1];
104+
Array.Copy(currentScopeAndClientStack, newScopeAndClientStack, currentScopeAndClientStack.Length);
105+
newScopeAndClientStack[^1] = new KeyValuePair<Scope, ISentryClient>(savedScope, client);
106+
107+
ScopeAndClientStack = newScopeAndClientStack;
108+
}
109+
95110
public void WithScope(Action<Scope> scopeCallback)
96111
{
97112
using (PushScope())
98113
{
99-
var scope = GetCurrent();
100-
scopeCallback.Invoke(scope.Key);
114+
var (scope, _) = GetCurrent();
115+
scopeCallback.Invoke(scope);
101116
}
102117
}
103118

104119
public T? WithScope<T>(Func<Scope, T?> scopeCallback)
105120
{
106121
using (PushScope())
107122
{
108-
var scope = GetCurrent();
109-
return scopeCallback.Invoke(scope.Key);
123+
var (scope, _) = GetCurrent();
124+
return scopeCallback.Invoke(scope);
110125
}
111126
}
112127

113128
public async Task WithScopeAsync(Func<Scope, Task> scopeCallback)
114129
{
115130
using (PushScope())
116131
{
117-
var scope = GetCurrent();
118-
await scopeCallback.Invoke(scope.Key).ConfigureAwait(false);
132+
var (scope, _) = GetCurrent();
133+
await scopeCallback.Invoke(scope).ConfigureAwait(false);
119134
}
120135
}
121136

122137
public async Task<T?> WithScopeAsync<T>(Func<Scope, Task<T?>> scopeCallback)
123138
{
124139
using (PushScope())
125140
{
126-
var scope = GetCurrent();
127-
return await scopeCallback.Invoke(scope.Key).ConfigureAwait(false);
141+
var (scope, _) = GetCurrent();
142+
return await scopeCallback.Invoke(scope).ConfigureAwait(false);
128143
}
129144
}
130145

src/Sentry/TransactionTracer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Sentry.Extensibility;
22
using Sentry.Internal;
3+
using Sentry.Internal.ScopeStack;
34
using Sentry.Protocol;
45

56
namespace Sentry;

test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,30 @@ public async Task InvokeAsync_SetsEventIdOnEvent()
659659
_ = _fixture.Hub.Received().CaptureEvent(Arg.Is<SentryEvent>(e => e.EventId.Equals(eventId)));
660660
}
661661

662+
[Fact]
663+
public async Task InvokeAsync_InstrumenterOpenTelemetry_SavesScope()
664+
{
665+
// Arrange
666+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
667+
var scope = new Scope();
668+
_fixture.Hub.ConfigureScope(Arg.Do<Action<Scope>>(action => action.Invoke(scope)));
669+
var sut = _fixture.GetSut();
670+
var activity = new Activity("test").Start();
671+
672+
try
673+
{
674+
// Act
675+
await sut.InvokeAsync(_fixture.HttpContext, _fixture.RequestDelegate);
676+
677+
// Assert
678+
activity.GetFused<Scope>().Should().Be(scope);
679+
}
680+
finally
681+
{
682+
activity.Stop();
683+
}
684+
}
685+
662686
[Fact]
663687
public async Task InvokeAsync_RequestContainsSentryHeaders_ContinuesTrace()
664688
{

test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,28 @@ public void OnEnd_FinishesSpan()
263263
}
264264
}
265265

266+
[Fact]
267+
public void OnEnd_RestoresSavedScope()
268+
{
269+
// Arrange
270+
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
271+
_fixture.ScopeManager = Substitute.For<IInternalScopeManager>();
272+
var sut = _fixture.GetSut();
273+
274+
var scope = new Scope();
275+
var data = Tracer.StartActivity("transaction")!;
276+
data.SetFused(scope);
277+
sut.OnStart(data);
278+
279+
sut._map.TryGetValue(data.SpanId, out var span);
280+
281+
// Act
282+
sut.OnEnd(data);
283+
284+
// Assert
285+
_fixture.ScopeManager.Received(1).RestoreScope(scope);
286+
}
287+
266288
[Fact]
267289
public void OnEnd_SpansEnriched()
268290
{

0 commit comments

Comments
 (0)