✨ feat(server): 添加静态状态页与调试入口
- 将状态页、调试页改为 `wwwroot` 静态资源 - 补充调试配置接口与前端脚本 - 为兼容层、规划层和运行时补充日志 - 更新集成测试覆盖新入口
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Flyshot.Server.Host.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 请求与响应日志中间件。
|
||||
/// 记录每个 HTTP 请求的进入时间、方法、路径、查询串、请求体,
|
||||
/// 以及响应的状态码、耗时和响应体(调试级别)。
|
||||
/// </summary>
|
||||
public sealed class RequestResponseLoggingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 请求体最大日志长度,超出则截断并附加省略标记。
|
||||
/// </summary>
|
||||
private const int MaxBodyLogLength = 4096;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化请求响应日志中间件。
|
||||
/// </summary>
|
||||
/// <param name="next">下一个中间件委托。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 HTTP 请求并记录输入输出。
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP 上下文。</param>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var request = context.Request;
|
||||
var requestId = Activity.Current?.Id ?? context.TraceIdentifier;
|
||||
|
||||
// 记录请求进入信息(Info 级别:方法、路径、查询参数)。
|
||||
_logger.LogInformation(
|
||||
"[HTTP-REQ] [{RequestId}] {Method} {Path}{QueryString} — 客户端 {RemoteIp}",
|
||||
requestId,
|
||||
request.Method,
|
||||
request.Path,
|
||||
request.QueryString.HasValue ? request.QueryString.Value : string.Empty,
|
||||
context.Connection.RemoteIpAddress);
|
||||
|
||||
// 读取并记录请求体(Debug 级别)。
|
||||
string? requestBody = null;
|
||||
if (request.ContentLength > 0 && request.Body.CanRead)
|
||||
{
|
||||
request.EnableBuffering();
|
||||
requestBody = await ReadBodyAsync(request.Body, context.RequestAborted).ConfigureAwait(false);
|
||||
request.Body.Position = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(requestBody))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"[HTTP-REQ-BODY] [{RequestId}] {Body}",
|
||||
requestId,
|
||||
TruncateBody(requestBody));
|
||||
}
|
||||
}
|
||||
|
||||
// 拦截响应流以便读取响应体。
|
||||
var originalResponseBody = context.Response.Body;
|
||||
using var responseBodyStream = new MemoryStream();
|
||||
context.Response.Body = responseBodyStream;
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(
|
||||
exception,
|
||||
"[HTTP-ERR] [{RequestId}] {Method} {Path} 处理过程中发生未捕获异常",
|
||||
requestId,
|
||||
request.Method,
|
||||
request.Path);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stopwatch.Stop();
|
||||
responseBodyStream.Position = 0;
|
||||
var responseBody = await ReadBodyAsync(responseBodyStream, context.RequestAborted).ConfigureAwait(false);
|
||||
responseBodyStream.Position = 0;
|
||||
await responseBodyStream.CopyToAsync(originalResponseBody, context.RequestAborted).ConfigureAwait(false);
|
||||
context.Response.Body = originalResponseBody;
|
||||
|
||||
var statusCode = context.Response.StatusCode;
|
||||
var level = statusCode >= 500 ? LogLevel.Error : statusCode >= 400 ? LogLevel.Warning : LogLevel.Information;
|
||||
|
||||
// 记录响应概要(Info/Warning/Error 级别)。
|
||||
_logger.Log(
|
||||
level,
|
||||
"[HTTP-RES] [{RequestId}] {Method} {Path} => {StatusCode} ({ElapsedMs}ms)",
|
||||
requestId,
|
||||
request.Method,
|
||||
request.Path,
|
||||
statusCode,
|
||||
stopwatch.ElapsedMilliseconds);
|
||||
|
||||
// 记录响应体(Debug 级别)。
|
||||
if (!string.IsNullOrEmpty(responseBody))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"[HTTP-RES-BODY] [{RequestId}] {Body}",
|
||||
requestId,
|
||||
TruncateBody(responseBody));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从流中读取文本内容。
|
||||
/// </summary>
|
||||
private static async Task<string> ReadBodyAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!stream.CanRead)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
var body = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
return body;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 截断过长内容,避免日志膨胀。
|
||||
/// </summary>
|
||||
private static string TruncateBody(string body)
|
||||
{
|
||||
if (body.Length <= MaxBodyLogLength)
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
return body[..MaxBodyLogLength] + " ... [截断,总长度=" + body.Length + "]";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user