| | | 1 | | using System.Diagnostics; |
| | | 2 | | using ArturRios.Util.WebApi.Extensions; |
| | | 3 | | using Microsoft.AspNetCore.Http; |
| | | 4 | | using Microsoft.Extensions.Logging; |
| | | 5 | | using Microsoft.Extensions.Options; |
| | | 6 | | |
| | | 7 | | namespace ArturRios.Util.WebApi.Middleware; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Ensures every request is associated with a W3C-format <see cref="Activity"/>, propagating or creating one as |
| | | 11 | | /// needed, and exposes its trace id on <see cref="HttpContext.TraceIdentifier"/>, <c>HttpContext.Items["TraceId"]</c> |
| | | 12 | | /// and the response's <c>traceparent</c> header. When it has to create the activity itself, an incoming |
| | | 13 | | /// <c>traceparent</c>/<c>tracestate</c> pair becomes its parent, so the caller's trace continues. Logs the start of |
| | | 14 | | /// each request at <see cref="LogLevel.Information"/> with its trace id and the client's IP address (see |
| | | 15 | | /// <see cref="HttpContextClientExtensions.GetClientIpAddress"/>), which is also tagged on the activity as |
| | | 16 | | /// <c>client.address</c>. Both uses of the IP address can be turned off through <see cref="TraceActivityOptions"/>. |
| | | 17 | | /// </summary> |
| | | 18 | | /// <param name="next">The next middleware in the pipeline.</param> |
| | | 19 | | /// <param name="logger">Used to log the start and end of each request's trace.</param> |
| | | 20 | | /// <param name="options">Controls whether the client's IP address is logged and tagged; when <see langword="null"/>, |
| | | 21 | | /// the <see cref="TraceActivityOptions"/> defaults apply.</param> |
| | 31 | 22 | | public class TraceActivityMiddleware( |
| | 31 | 23 | | RequestDelegate next, |
| | 31 | 24 | | ILogger<TraceActivityMiddleware> logger, |
| | 31 | 25 | | IOptions<TraceActivityOptions>? options = null) : WebApiMiddleware |
| | | 26 | | { |
| | 31 | 27 | | private readonly TraceActivityOptions _options = options?.Value ?? new TraceActivityOptions(); |
| | | 28 | | |
| | | 29 | | private const string ActivityName = "ServerReceive"; |
| | | 30 | | private const string ClientAddressTag = "client.address"; |
| | | 31 | | private const string UnknownClientIp = "unknown"; |
| | | 32 | | private const string TraceIdItemKey = "TraceId"; |
| | | 33 | | private const string TraceParentHeader = "traceparent"; |
| | | 34 | | private const string TraceStateHeader = "tracestate"; |
| | | 35 | | |
| | | 36 | | static TraceActivityMiddleware() |
| | | 37 | | { |
| | 1 | 38 | | Activity.DefaultIdFormat = ActivityIdFormat.W3C; |
| | 1 | 39 | | Activity.ForceDefaultIdFormat = true; |
| | 1 | 40 | | } |
| | | 41 | | |
| | | 42 | | /// <summary>Attaches the current (or a newly created) trace activity to the context and response, then invokes the |
| | | 43 | | /// <param name="context">The current HTTP context.</param> |
| | | 44 | | public async Task InvokeAsync(HttpContext context) |
| | | 45 | | { |
| | 32 | 46 | | ArgumentNullException.ThrowIfNull(context); |
| | | 47 | | |
| | 32 | 48 | | var activity = Activity.Current; |
| | 32 | 49 | | var createdActivity = activity is null; |
| | | 50 | | |
| | 32 | 51 | | activity ??= StartActivity(context.Request); |
| | | 52 | | |
| | 32 | 53 | | var traceId = activity.TraceId.ToString(); |
| | | 54 | | |
| | 32 | 55 | | context.TraceIdentifier = traceId; |
| | 32 | 56 | | context.Items[TraceIdItemKey] = traceId; |
| | 32 | 57 | | context.Response.Headers[TraceParentHeader] = activity.ToTraceParent(); |
| | | 58 | | |
| | 32 | 59 | | var clientIp = _options.LogClientIp || _options.TagClientAddress ? context.GetClientIpAddress() : null; |
| | | 60 | | |
| | 32 | 61 | | if (_options.TagClientAddress && clientIp is not null) |
| | | 62 | | { |
| | 5 | 63 | | activity.SetTag(ClientAddressTag, clientIp); |
| | | 64 | | } |
| | | 65 | | |
| | 32 | 66 | | if (_options.LogClientIp) |
| | | 67 | | { |
| | 31 | 68 | | logger.LogInformation("Started request with TraceId {TraceId} from {ClientIp}", traceId, |
| | 31 | 69 | | clientIp ?? UnknownClientIp); |
| | | 70 | | } |
| | | 71 | | else |
| | | 72 | | { |
| | 1 | 73 | | logger.LogInformation("Started request with TraceId {TraceId}", traceId); |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | try |
| | | 77 | | { |
| | 32 | 78 | | await next(context); |
| | 32 | 79 | | } |
| | | 80 | | finally |
| | | 81 | | { |
| | 32 | 82 | | logger.LogTrace("Ending request with TraceId {TraceId}", traceId); |
| | | 83 | | |
| | 32 | 84 | | if (createdActivity && Activity.Current == activity) |
| | | 85 | | { |
| | 16 | 86 | | activity.Stop(); |
| | | 87 | | } |
| | | 88 | | } |
| | 32 | 89 | | } |
| | | 90 | | |
| | | 91 | | private static Activity StartActivity(HttpRequest request) |
| | | 92 | | { |
| | 16 | 93 | | var activity = new Activity(ActivityName).SetIdFormat(ActivityIdFormat.W3C); |
| | | 94 | | |
| | 16 | 95 | | if (ActivityContext.TryParse(request.Headers[TraceParentHeader], request.Headers[TraceStateHeader], |
| | 16 | 96 | | out var parent)) |
| | | 97 | | { |
| | 1 | 98 | | activity.SetParentId(parent.TraceId, parent.SpanId, parent.TraceFlags); |
| | 1 | 99 | | activity.TraceStateString = parent.TraceState; |
| | | 100 | | } |
| | | 101 | | |
| | 16 | 102 | | return activity.Start(); |
| | | 103 | | } |
| | | 104 | | } |