< Summary

Information
Class: ArturRios.Util.WebApi.Middleware.TraceActivityMiddleware
Assembly: ArturRios.Util.WebApi
File(s): /home/runner/work/dotnet-webapi-util/dotnet-webapi-util/src/Middleware/TraceActivityMiddleware.cs
Line coverage
100%
Covered lines: 35
Uncovered lines: 0
Coverable lines: 35
Total lines: 104
Line coverage: 100%
Branch coverage
95%
Covered branches: 23
Total branches: 24
Branch coverage: 95.8%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%44100%
.cctor()100%11100%
InvokeAsync()94.44%1818100%
StartActivity(...)100%22100%

File(s)

/home/runner/work/dotnet-webapi-util/dotnet-webapi-util/src/Middleware/TraceActivityMiddleware.cs

#LineLine coverage
 1using System.Diagnostics;
 2using ArturRios.Util.WebApi.Extensions;
 3using Microsoft.AspNetCore.Http;
 4using Microsoft.Extensions.Logging;
 5using Microsoft.Extensions.Options;
 6
 7namespace 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>
 3122public class TraceActivityMiddleware(
 3123    RequestDelegate next,
 3124    ILogger<TraceActivityMiddleware> logger,
 3125    IOptions<TraceActivityOptions>? options = null) : WebApiMiddleware
 26{
 3127    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    {
 138        Activity.DefaultIdFormat = ActivityIdFormat.W3C;
 139        Activity.ForceDefaultIdFormat = true;
 140    }
 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    {
 3246        ArgumentNullException.ThrowIfNull(context);
 47
 3248        var activity = Activity.Current;
 3249        var createdActivity = activity is null;
 50
 3251        activity ??= StartActivity(context.Request);
 52
 3253        var traceId = activity.TraceId.ToString();
 54
 3255        context.TraceIdentifier = traceId;
 3256        context.Items[TraceIdItemKey] = traceId;
 3257        context.Response.Headers[TraceParentHeader] = activity.ToTraceParent();
 58
 3259        var clientIp = _options.LogClientIp || _options.TagClientAddress ? context.GetClientIpAddress() : null;
 60
 3261        if (_options.TagClientAddress && clientIp is not null)
 62        {
 563            activity.SetTag(ClientAddressTag, clientIp);
 64        }
 65
 3266        if (_options.LogClientIp)
 67        {
 3168            logger.LogInformation("Started request with TraceId {TraceId} from {ClientIp}", traceId,
 3169                clientIp ?? UnknownClientIp);
 70        }
 71        else
 72        {
 173            logger.LogInformation("Started request with TraceId {TraceId}", traceId);
 74        }
 75
 76        try
 77        {
 3278            await next(context);
 3279        }
 80        finally
 81        {
 3282            logger.LogTrace("Ending request with TraceId {TraceId}", traceId);
 83
 3284            if (createdActivity && Activity.Current == activity)
 85            {
 1686                activity.Stop();
 87            }
 88        }
 3289    }
 90
 91    private static Activity StartActivity(HttpRequest request)
 92    {
 1693        var activity = new Activity(ActivityName).SetIdFormat(ActivityIdFormat.W3C);
 94
 1695        if (ActivityContext.TryParse(request.Headers[TraceParentHeader], request.Headers[TraceStateHeader],
 1696                out var parent))
 97        {
 198            activity.SetParentId(parent.TraceId, parent.SpanId, parent.TraceFlags);
 199            activity.TraceStateString = parent.TraceState;
 100        }
 101
 16102        return activity.Start();
 103    }
 104}