-
-
Notifications
You must be signed in to change notification settings - Fork 231
Expand file tree
/
Copy pathSentryMiddleware.cs
More file actions
267 lines (231 loc) · 11.3 KB
/
SentryMiddleware.cs
File metadata and controls
267 lines (231 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sentry.AspNetCore.Extensions;
using Sentry.Ben.BlockingDetector;
using Sentry.Extensibility;
using Sentry.Internal;
using Sentry.Reflection;
using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IWebHostEnvironment;
namespace Sentry.AspNetCore;
/// <summary>
/// Sentry middleware for ASP.NET Core
/// </summary>
internal class SentryMiddleware : IMiddleware
{
internal static readonly object TraceHeaderItemKey = new();
internal static readonly object BaggageHeaderItemKey = new();
internal static readonly object TransactionContextItemKey = new();
private readonly Func<IHub> _getHub;
private readonly SentryAspNetCoreOptions _options;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILogger<SentryMiddleware> _logger;
private readonly IEnumerable<ISentryEventExceptionProcessor> _eventExceptionProcessors;
private readonly IEnumerable<ISentryEventProcessor> _eventProcessors;
private readonly IEnumerable<ISentryTransactionProcessor> _transactionProcessors;
internal static readonly SdkVersion NameAndVersion
= typeof(SentryMiddleware).Assembly.GetNameAndVersion();
private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name;
// Ben.BlockingDetector
private readonly BlockingMonitor? _monitor;
private readonly DetectBlockingSynchronizationContext? _detectBlockingSyncCtx;
private readonly TaskBlockingListener? _listener;
/// <summary>
/// Initializes a new instance of the <see cref="SentryMiddleware"/> class.
/// </summary>
/// <param name="getHub">The sentry Hub accessor.</param>
/// <param name="options">The options for this integration</param>
/// <param name="hostingEnvironment">The hosting environment.</param>
/// <param name="logger">Sentry logger.</param>
/// <param name="eventExceptionProcessors">Custom Event Exception Processors</param>
/// <param name="eventProcessors">Custom Event Processors</param>
/// <param name="transactionProcessors">Custom Transaction Processors</param>
/// <exception cref="ArgumentNullException">
/// next
/// or
/// sentry
/// </exception>
public SentryMiddleware(
Func<IHub> getHub,
IOptions<SentryAspNetCoreOptions> options,
IHostingEnvironment hostingEnvironment,
ILogger<SentryMiddleware> logger,
IEnumerable<ISentryEventExceptionProcessor> eventExceptionProcessors,
IEnumerable<ISentryEventProcessor> eventProcessors,
IEnumerable<ISentryTransactionProcessor> transactionProcessors)
{
ArgumentNullException.ThrowIfNull(getHub);
_getHub = getHub;
_options = options.Value;
_hostingEnvironment = hostingEnvironment;
_logger = logger;
_eventExceptionProcessors = eventExceptionProcessors;
_eventProcessors = eventProcessors;
_transactionProcessors = transactionProcessors;
if (_options.CaptureBlockingCalls)
{
_monitor = new BlockingMonitor(_getHub, _options);
_detectBlockingSyncCtx = new DetectBlockingSynchronizationContext(_monitor);
_listener = new TaskBlockingListener(_monitor);
}
}
/// <summary>
/// Handles the <see cref="HttpContext"/> while capturing any errors
/// </summary>
/// <param name="context">The context.</param>
/// <param name="next">Delegate to next middleware.</param>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var hub = _getHub();
if (!hub.IsEnabled)
{
await next(context).ConfigureAwait(false);
return;
}
using (hub.PushAndLockScope())
{
if (_options.MaxRequestBodySize != RequestSize.None)
{
context.Request.EnableBuffering();
}
if (_options.FlushOnCompletedRequest)
{
// Serverless environments flush the queue at the end of each request
context.Response.OnCompleted(() => hub.FlushAsync(_options.FlushTimeout));
}
var traceHeader = context.TryGetSentryTraceHeader(_options);
var baggageHeader = context.TryGetBaggageHeader(_options);
var transactionContext = hub.ContinueTrace(traceHeader, baggageHeader);
// Adding the headers and the TransactionContext to the context to be picked up by the Sentry tracing middleware
var didAdd = context.Items.TryAdd(TraceHeaderItemKey, traceHeader);
if (!didAdd)
{
_options.LogWarning("Sentry trace was already added. Did you initialize Sentry twice?");
}
context.Items.TryAdd(BaggageHeaderItemKey, baggageHeader);
context.Items.TryAdd(TransactionContextItemKey, transactionContext);
hub.ConfigureScope(static (scope, arg) =>
{
// At the point lots of stuff from the request are not yet filled
// Identity for example is added later on in the pipeline
// Subscribing to the event so that HTTP data is only read in case an event is going to be
// sent to Sentry. This avoid the cost on error-free requests.
// In case of event, all data made available through the HTTP Context at the time of the
// event creation will be sent to Sentry
// Important: The scope that the event is attached to is not necessarily the same one that is active
// when the event fires. Use `activeScope`, not `scope` or `hub`.
scope.OnEvaluating += (_, activeScope) =>
{
arg.middleware.SyncOptionsScope(activeScope);
arg.middleware.PopulateScope(arg.context, activeScope);
};
}, (middleware: this, context));
// Pre-create the Sentry Event ID and save it on the scope it so it's available throughout the pipeline,
// even if there's no event actually being sent to Sentry. This allows for things like a custom exception
// handler page to access the event ID, enabling user feedback, etc.
var eventId = SentryId.Create();
hub.ConfigureScope(static (scope, eventId) => scope.LastEventId = eventId, eventId);
try
{
var originalMethod = context.Request.Method;
if (_options.CaptureBlockingCalls && _monitor is not null)
{
var syncCtx = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(syncCtx == null ? _detectBlockingSyncCtx : new DetectBlockingSynchronizationContext(_monitor, syncCtx));
try
{
// For detection to work we need ConfigureAwait=true
await next(context).ConfigureAwait(true);
}
finally
{
SynchronizationContext.SetSynchronizationContext(syncCtx);
}
}
else
{
await next(context).ConfigureAwait(false);
}
if (_options.Instrumenter == Instrumenter.OpenTelemetry && Activity.Current is { } activity)
{
// The middleware pipeline finishes up before the Otel Activity.OnEnd callback is invoked so we need
// so save a copy of the scope that can be restored by our SentrySpanProcessor
hub.ConfigureScope(static (scope, activity) => activity.SetFused(scope), activity);
}
// When an exception was handled by other component (i.e: UseExceptionHandler feature).
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature?>();
if (exceptionFeature?.Error != null)
{
const string description =
"This exception was caught by an ASP.NET Core custom error handler. " +
"The web server likely returned a customized error page as a result of this exception.";
#if NET6_0_OR_GREATER
hub.ConfigureScope(static (scope, arg) =>
{
scope.ExceptionProcessors.Add(
new ExceptionHandlerFeatureProcessor(arg.originalMethod, arg.exceptionFeature)
);
}, (originalMethod, exceptionFeature));
#endif
CaptureException(exceptionFeature.Error, eventId, "IExceptionHandlerFeature", description);
}
if (_options.FlushBeforeRequestCompleted)
{
await FlushBeforeCompleted().ConfigureAwait(false);
}
}
catch (Exception e)
{
const string description =
"This exception was captured by the Sentry ASP.NET Core middleware, and then re-thrown." +
"The web server likely returned a 5xx error code as a result of this exception.";
CaptureException(e, eventId, "SentryMiddleware.UnhandledException", description);
if (_options.FlushBeforeRequestCompleted)
{
await FlushBeforeCompleted().ConfigureAwait(false);
}
ExceptionDispatchInfo.Capture(e).Throw();
}
// Some environments disables the application after sending a request,
// preventing OnCompleted flush from working.
Task FlushBeforeCompleted() => hub.FlushAsync(_options.FlushTimeout);
void CaptureException(Exception e, SentryId evtId, string mechanism, string description)
{
e.SetSentryMechanism(mechanism, description, handled: false);
var evt = new SentryEvent(e, eventId: evtId);
_logger.LogTrace("Sending event '{SentryEvent}' to Sentry.", evt);
var id = hub.CaptureEvent(evt);
_logger.LogInformation("Event '{id}' queued.", id);
}
}
}
private void SyncOptionsScope(Scope scope)
{
foreach (var callback in _options.ConfigureScopeCallbacks)
{
callback.Invoke(scope);
}
}
internal void PopulateScope(HttpContext context, Scope scope)
{
scope.AddEventProcessors(_eventProcessors.Except(scope.GetAllEventProcessors()));
scope.AddExceptionProcessors(_eventExceptionProcessors.Except(scope.GetAllExceptionProcessors()));
scope.AddTransactionProcessors(_transactionProcessors.Except(scope.GetAllTransactionProcessors()));
scope.Sdk.Name = Constants.SdkName;
scope.Sdk.Version = NameAndVersion.Version;
if (NameAndVersion.Version is { } version)
{
scope.Sdk.AddPackage(ProtocolPackageName, version);
}
if (_hostingEnvironment.WebRootPath is { } webRootPath)
{
scope.SetWebRoot(webRootPath);
}
scope.Populate(context, _options);
if (_options.IncludeActivityData && Activity.Current is not null)
{
scope.Populate(Activity.Current);
}
}
}