feat: add tracing enrichment and prometheus exporter
This commit is contained in:
@@ -3,11 +3,12 @@ using System.Threading;
|
||||
namespace TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// 轻量级 TraceId 上下文,便于跨层访问当前请求的追踪标识。
|
||||
/// 轻量级 TraceId/SpanId 上下文,便于跨层访问当前请求的追踪标识。
|
||||
/// </summary>
|
||||
public static class TraceContext
|
||||
{
|
||||
private static readonly AsyncLocal<string?> TraceIdHolder = new();
|
||||
private static readonly AsyncLocal<string?> SpanIdHolder = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求的 TraceId。
|
||||
@@ -18,8 +19,21 @@ public static class TraceContext
|
||||
set => TraceIdHolder.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求的 SpanId。
|
||||
/// </summary>
|
||||
public static string? SpanId
|
||||
{
|
||||
get => SpanIdHolder.Value;
|
||||
set => SpanIdHolder.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理 TraceId,避免 AsyncLocal 污染其它请求。
|
||||
/// </summary>
|
||||
public static void Clear() => TraceIdHolder.Value = null;
|
||||
public static void Clear()
|
||||
{
|
||||
TraceIdHolder.Value = null;
|
||||
SpanIdHolder.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -14,23 +15,43 @@ namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||
public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger, IIdGenerator idGenerator)
|
||||
{
|
||||
private const string TraceHeader = "X-Trace-Id";
|
||||
private const string SpanHeader = "X-Span-Id";
|
||||
private const string RequestHeader = "X-Request-Id";
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var traceId = ResolveTraceId(context);
|
||||
var ownsActivity = Activity.Current is null;
|
||||
var activity = Activity.Current ?? new Activity("TakeoutSaaS.Request");
|
||||
|
||||
if (activity.Id is null)
|
||||
{
|
||||
activity.SetIdFormat(ActivityIdFormat.W3C);
|
||||
activity.Start();
|
||||
}
|
||||
|
||||
var traceId = activity.TraceId.ToString();
|
||||
var spanId = activity.SpanId.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(traceId))
|
||||
{
|
||||
traceId = ResolveTraceId(context);
|
||||
}
|
||||
|
||||
context.TraceIdentifier = traceId;
|
||||
TraceContext.TraceId = traceId;
|
||||
TraceContext.SpanId = spanId;
|
||||
|
||||
context.Response.OnStarting(() =>
|
||||
{
|
||||
context.Response.Headers[TraceHeader] = traceId;
|
||||
context.Response.Headers[SpanHeader] = spanId;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
using (logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["TraceId"] = traceId
|
||||
["TraceId"] = traceId,
|
||||
["SpanId"] = spanId
|
||||
}))
|
||||
{
|
||||
try
|
||||
@@ -40,6 +61,10 @@ public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<Correl
|
||||
finally
|
||||
{
|
||||
TraceContext.Clear();
|
||||
if (ownsActivity)
|
||||
{
|
||||
activity.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,15 @@ public sealed class RequestLoggingMiddleware(RequestDelegate next, ILogger<Reque
|
||||
{
|
||||
stopwatch.Stop();
|
||||
var traceId = TraceContext.TraceId ?? context.TraceIdentifier;
|
||||
var spanId = TraceContext.SpanId ?? Activity.Current?.SpanId.ToString() ?? string.Empty;
|
||||
logger.LogInformation(
|
||||
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}",
|
||||
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId} SpanId:{SpanId}",
|
||||
context.Request.Method,
|
||||
context.Request.Path,
|
||||
context.Response.StatusCode,
|
||||
stopwatch.Elapsed.TotalMilliseconds,
|
||||
traceId);
|
||||
traceId,
|
||||
spanId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user