-
-
Notifications
You must be signed in to change notification settings - Fork 213
/
Copy pathSentryFunctionsWorkerMiddleware.cs
128 lines (106 loc) · 5.22 KB
/
SentryFunctionsWorkerMiddleware.cs
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
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Sentry.Extensibility;
using Sentry.Internal;
namespace Sentry.Azure.Functions.Worker;
internal class SentryFunctionsWorkerMiddleware : IFunctionsWorkerMiddleware
{
private const string Operation = "function";
private readonly IHub _hub;
private readonly IDiagnosticLogger? _logger;
private static readonly ConcurrentDictionary<string, string> TransactionNameCache = new();
public SentryFunctionsWorkerMiddleware(IHub hub)
{
_hub = hub;
_logger = hub.GetSentryOptions()?.DiagnosticLogger;
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
var transactionContext = await StartOrContinueTraceAsync(context);
var transaction = _hub.StartTransaction(transactionContext);
Exception? unhandledException = null;
try
{
_hub.ConfigureScope(scope =>
{
scope.Transaction = transaction;
scope.Contexts["function"] = new Dictionary<string, string>
{
{ "name", context.FunctionDefinition.Name },
{ "entryPoint", context.FunctionDefinition.EntryPoint },
{ "invocationId", context.InvocationId }
};
});
context.CancellationToken.ThrowIfCancellationRequested();
await next(context);
}
catch (Exception exception)
{
exception.SetSentryMechanism(nameof(SentryFunctionsWorkerMiddleware),
"This exception was caught by the Sentry Functions middleware. " +
"The Function has thrown an exception that was not handled by the user code.",
handled: false);
unhandledException = exception;
throw;
}
finally
{
if (unhandledException is not null)
{
transaction.Finish(unhandledException);
}
else
{
var statusCode = context.GetHttpResponseData()?.StatusCode;
// For HTTP triggered function, finish transaction with the returned HTTP status code
if (statusCode is not null)
{
var status = SpanStatusConverter.FromHttpStatusCode(statusCode.Value);
transaction.Finish(status);
}
else
{
transaction.Finish();
}
}
}
}
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = AotHelper.SuppressionJustification)]
private async Task<TransactionContext> StartOrContinueTraceAsync(FunctionContext context)
{
var transactionName = context.FunctionDefinition.Name;
// Get the HTTP request data
var requestData = await context.GetHttpRequestDataAsync();
if (requestData is null)
{
// not an HTTP trigger
return SentrySdk.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation);
}
var httpMethod = requestData.Method.ToUpperInvariant();
var transactionNameKey = $"{context.FunctionDefinition.EntryPoint}-{httpMethod}";
// Note that, when Trimming is enabled, we can't use reflection to read route data from the HttpTrigger
// attribute. In that case the route name will always be /api/<FUNCTION_NAME>
// If this is ever a problem for customers, we can potentially see if there are alternate ways to get this info
// from route tables or something. We're not even sure if anyone will use this functionality for now though.
if (!AotHelper.IsNativeAot && !TransactionNameCache.TryGetValue(transactionNameKey, out transactionName))
{
// Find the HTTP Trigger attribute via reflection
var assembly = Assembly.LoadFrom(context.FunctionDefinition.PathToAssembly);
var entryPointName = context.FunctionDefinition.EntryPoint;
var typeName = entryPointName[..entryPointName.LastIndexOf('.')];
var methodName = entryPointName[(typeName.Length + 1)..];
var attribute = assembly.GetType(typeName)?.GetMethod(methodName)?.GetParameters()
.Select(p => p.GetCustomAttribute<HttpTriggerAttribute>())
.FirstOrDefault(a => a is not null);
transactionName = attribute?.Route is { } route
// Compose the transaction name from the method and route
? $"{httpMethod} /{route.TrimStart('/')}"
// There's no route provided, so use the absolute path of the URL
: $"{httpMethod} {requestData.Url.AbsolutePath}";
TransactionNameCache.TryAdd(transactionNameKey, transactionName);
}
var traceHeader = requestData.TryGetSentryTraceHeader(_logger);
var baggageHeader = requestData.TryGetBaggageHeader(_logger);
return SentrySdk.ContinueTrace(traceHeader, baggageHeader, transactionName, Operation);
}
}