You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现HttpClient请求与响应的可选日志记录?

动态控制HttpClient异常请求日志的实现方案

针对你的需求,核心思路是通过自定义DelegatingHandler拦截请求,结合appsettings.json配置动态决定是否记录日志,无需修改业务代码,也不用重新编译应用。

1. 配置设计:用灵活标识替代完整URL

完整URL过于死板,推荐用以下几种标识匹配目标请求,配置示例如下:

"HttpClientLoggingSettings": {
  "Enabled": true, // 全局开关
  "LogTargets": [
    // 匹配HTTP方法+路径模板(如GET /api/users/{id})
    {
      "RequestMethod": "GET",
      "PathPattern": "/api/users/{id}",
      "LogOnErrorOnly": true // 仅服务器返回异常时记录
    },
    // 匹配自定义请求头(精准控制单个业务方法)
    {
      "RequestHeaderKey": "X-Request-Tag",
      "RequestHeaderValue": "Order_Create",
      "LogOnErrorOnly": true
    },
    // 匹配命名HttpClient(按业务客户端分组控制)
    {
      "NamedClient": "PaymentClient",
      "LogOnErrorOnly": false // 可选:不管成败都记录
    }
  ]
}

2. 实现自定义委托处理器

创建LoggingDelegatingHandler,注入配置和日志组件,拦截请求与响应并判断是否需要记录:

public class LoggingDelegatingHandler : DelegatingHandler
{
    private readonly IConfiguration _config;
    private readonly ILogger<LoggingDelegatingHandler> _logger;

    public LoggingDelegatingHandler(IConfiguration config, ILogger<LoggingDelegatingHandler> logger)
    {
        _config = config;
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var loggingSettings = _config.GetSection("HttpClientLoggingSettings").Get<HttpClientLoggingSettings>();
        if (!loggingSettings.Enabled)
        {
            return await base.SendAsync(request, cancellationToken);
        }

        // 预存请求信息
        var requestContent = await request.Content?.ReadAsStringAsync(cancellationToken) ?? string.Empty;
        var requestDetails = new
        {
            Method = request.Method.Method,
            Url = request.RequestUri?.ToString(),
            Headers = request.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
            Content = requestContent
        };

        // 发送请求获取响应
        var response = await base.SendAsync(request, cancellationToken);

        // 判断是否需要记录日志
        bool needLog = false;
        foreach (var target in loggingSettings.LogTargets)
        {
            // 匹配路径模板(用RouteTemplate处理占位符)
            if (!string.IsNullOrWhiteSpace(target.PathPattern) && 
                request.Method.Method.Equals(target.RequestMethod, StringComparison.OrdinalIgnoreCase))
            {
                var template = new RouteTemplate(target.PathPattern);
                var matcher = new TemplateMatcher(template, new RouteValueDictionary());
                if (matcher.TryMatch(request.RequestUri?.AbsolutePath ?? string.Empty, new RouteValueDictionary()))
                {
                    needLog = target.LogOnErrorOnly ? !response.IsSuccessStatusCode : true;
                    break;
                }
            }
            // 匹配自定义请求头
            else if (!string.IsNullOrWhiteSpace(target.RequestHeaderKey) && 
                request.Headers.TryGetValues(target.RequestHeaderKey, out var values) && 
                values.Contains(target.RequestHeaderValue))
            {
                needLog = target.LogOnErrorOnly ? !response.IsSuccessStatusCode : true;
                break;
            }
            // 匹配命名HttpClient(需在注册时传递名称到请求属性)
            else if (!string.IsNullOrWhiteSpace(target.NamedClient) && 
                request.Properties.TryGetValue("HttpClientName", out var clientName) && 
                clientName.ToString() == target.NamedClient)
            {
                needLog = target.LogOnErrorOnly ? !response.IsSuccessStatusCode : true;
                break;
            }
        }

        if (needLog)
        {
            // 读取响应内容
            var responseContent = await response.Content?.ReadAsStringAsync(cancellationToken) ?? string.Empty;
            var responseDetails = new
            {
                StatusCode = (int)response.StatusCode,
                ReasonPhrase = response.ReasonPhrase,
                Headers = response.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value)),
                Content = responseContent
            };

            _logger.LogError("HttpClient异常请求\n请求详情: {Request}\n响应详情: {Response}",
                JsonSerializer.Serialize(requestDetails), JsonSerializer.Serialize(responseDetails));
        }

        return response;
    }
}

// 对应配置的实体类
public class HttpClientLoggingSettings
{
    public bool Enabled { get; set; }
    public List<LogTarget> LogTargets { get; set; } = new();
}

public class LogTarget
{
    public string? RequestMethod { get; set; }
    public string? PathPattern { get; set; }
    public string? RequestHeaderKey { get; set; }
    public string? RequestHeaderValue { get; set; }
    public string? NamedClient { get; set; }
    public bool LogOnErrorOnly { get; set; } = true;
}

3. 注册处理器与HttpClient

在Program.cs中注册处理器,并关联到目标HttpClient:

// 注册委托处理器
builder.Services.AddTransient<LoggingDelegatingHandler>();

// 给命名HttpClient添加处理器
builder.Services.AddHttpClient("PaymentClient")
    .AddHttpMessageHandler<LoggingDelegatingHandler>()
    // 传递命名HttpClient名称到请求属性,用于匹配配置
    .ConfigureHttpClient((sp, client) => 
    {
        client.DefaultRequestHeaders.Add("X-HttpClient-Name", "PaymentClient");
    });

// 全局给所有HttpClient添加处理器(可选)
builder.Services.AddHttpClient()
    .AddHttpMessageHandler<LoggingDelegatingHandler>();

4. 动态开关与热重载

.NET 6+默认支持appsettings.json热重载,修改配置后无需重启应用:

  • 修改HttpClientLoggingSettings.Enabled为false可全局关闭日志
  • 调整LogTargets可新增/移除需要记录的请求类型

5. 请求标识选择建议

  • 路径模板:适合RESTful接口,能匹配一类请求(比如所有用户详情查询)
  • 自定义请求头:适合精准控制单个业务方法,在封装HttpClient的方法中添加request.Headers.Add("X-Request-Tag", "User_GetById");即可
  • 命名HttpClient:适合按业务线分组(比如支付、用户中心)批量控制日志

内容的提问来源于stack exchange,提问作者TryHard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 15:01:01