Bitzsoft.Integrations.RequestLogging 1.0.0-alpha.7

This is a prerelease version of Bitzsoft.Integrations.RequestLogging.
dotnet add package Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.7
                    
NuGet\Install-Package Bitzsoft.Integrations.RequestLogging -Version 1.0.0-alpha.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.7" />
                    
Directory.Packages.props
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.7
                    
#r "nuget: Bitzsoft.Integrations.RequestLogging, 1.0.0-alpha.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Bitzsoft.Integrations.RequestLogging@1.0.0-alpha.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.7&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.7&prerelease
                    
Install as a Cake Tool

Bitzsoft.Integrations.RequestLogging

第三方请求日志(Request Logging)横切组件,通过 DelegatingHandler 无侵入拦截所有 HttpClient 出站请求,经无锁 Channel<T> 异步队列批量持久化,宿主通过 IRequestLogStore 控制写入目标。核心包不绑定任何 ORM(EF Core / SqlSugar / Dapper / MongoDB 均由宿主实现)。

适用范围:所有走 IHttpClientFactory 的连接器(经 DelegatingHandler 拦截)+ 走厂商 SDK 自有 HTTP 管道的连接器(经 IRequestLogRecorder 回调)。

功能

  • 零侵入拦截RequestLogHandler 作为 DelegatingHandler 挂到 HttpClient 管道,自动捕获每次调用的方法/端点/请求头/请求体/状态码/响应体/异常/耗时,连接器业务代码无需改动。
  • 非阻塞异步队列:日志条目经 Channel<T> 有界队列(满即 DropWrite 丢弃最新,绝不阻塞业务线程)由后台 BackgroundService 批量消费写入存储。
  • 流安全读取:仅对白名单文本媒体类型(JSON/XML/text/form)读取正文,并通过 LoadIntoBufferAsync 缓冲,读取后请求仍可正常发送、响应仍可由调用方读取;二进制流(图片/PDF/文件等)记为 [Binary Data Skipped]
  • OOM 防护:已知 Content-Length 超过 MaxBodyLength 时跳过缓冲,避免大文件下载拖垮内存(MaxBodyLength <= 0 时关闭此防护,见下文)。
  • 敏感脱敏:自动识别 password/token/secret/authorization 等字段(含数值/布尔)替换为 ***,防止密钥泄漏到日志;脱敏先于截断,避免跨截断边界泄漏。
  • 采样SamplingRate 控制记录比例(0.0~1.0),被跳过的请求不进入队列。
  • Enricher 扩展RequestLoggingOptions.Enrich 回调允许宿主向日志条目注入租户 ID、用户 ID、环境标记等业务字段。
  • SDK 回调钩子IRequestLogRecorder.Record 供非 HttpClient 管道的 SDK 连接器(阿里云 OSS / 腾讯云 / AWS / Azure 等 SDK 自带 HTTP 管道)回调写入同一管道。
  • 控制反转(IoC):核心包零 ORM 绑定,IRequestLogStore 由宿主实现(SqlSugar / Dapper / EF Core / MongoDB …)。

安装

dotnet add package Bitzsoft.Integrations.RequestLogging

快速开始

① 宿主注册基础设施(仅一次)

using Bitzsoft.Integrations.RequestLogging;
using Microsoft.Extensions.DependencyInjection;

// 持久化:注册管道 + 指定存储实现(MyRequestLogStore 为宿主自行实现 IRequestLogStore)
services.AddRequestLogging<MyRequestLogStore>(options =>
{
    options.MaxBodyLength = 8192;
    options.SensitiveFields.Add("private_key");
});

// 或:仅启用管道、不持久化(回落到 NullRequestLogStore,日志丢弃)
services.AddRequestLogging();

② 连接器挂载(每个 HttpClient 客户端)

// 泛型:providerName 自动取 typeof(TClient).Name(去除 HttpClient 后缀)
services.AddHttpClient<OrderClient>()
        .AddRequestLogging<OrderClient>();

// 命名客户端:显式指定 providerName
services.AddHttpClient("Aliyun")
        .AddRequestLogging("Aliyun");

内置连接器(TeamWork / Finance / FileStorage / OutboundCall / Sms / …)的 Add* 扩展方法已自动挂载,宿主无需再调用 .AddRequestLogging<TClient>(),只需按 ① 注册一次基础设施即可。

工作原理

业务代码 → HttpClient 请求
            ↓
  ┌─ DelegatingHandler 管道(RequestLogHandler 为最外层,先于鉴权/重试)─┐
  │  采样判定 → 构建 RequestLogEntry → Enrich 注入 → 流安全读取请求体      │
  │  → base.SendAsync → 读响应体 → (异常路径记 Exception)              │
  └──────────────────────────────────────────────────────────────────────┘
            ↓ TryWrite(满即丢,纳秒级,不阻塞)
      Channel<RequestLogEntry>(有界队列,默认容量 10000)
            ↓ 后台批量消费
      RequestLogProcessor(BackgroundService)
            ↓ 控制反转
      IRequestLogStore.WriteBatchAsync(batch)   ← 宿主实现

SDK 类连接器(不走 HttpClient)则在其调用包装处调用 IRequestLogRecorder.Record(entry),汇入同一个 Channel。

配置项详解(RequestLoggingOptions

通过 services.AddRequestLogging<TStore>(o => { ... }) 回调配置。所有集合为引用类型实例,回调中可 Add/Remove

属性 类型 默认值 说明
Enabled bool true 总开关。设 false 时 handler 直接透传,零开销。
SamplingRate double 1.0 采样率 0.0~1.01.0=全量记录;0.0=全部跳过;0.1≈10% 请求被记录。被跳过的请求不进入队列。
MaxBodyLength int 4096 请求/响应正文最大截取长度(字符数)。超出部分以 …[截断] 标记。0 或负数 = 不限制(见下文专述)。
ChannelCapacity int 10000 Channel<T> 有界队列容量。队列满时 DropWrite 丢弃最新条目,永不阻塞业务线程。调小省内存、调大抗流量峰值。
BatchSize int 100 后台批量写入每批条数。每积累 BatchSize 条或队列暂无更多数据时触发一次 WriteBatchAsync
SupportedMediaTypes HashSet<string> 见下 允许读取正文的媒体类型白名单(大小写不敏感)。不在此集合的类型记为 [Binary Data Skipped]
SensitiveFields HashSet<string> 见下 敏感字段名集合(大小写不敏感)。正文中匹配的 JSON 字段值(字符串/数值/布尔/null)统一替换为 ***
Enrich Action<HttpRequestMessage, RequestLogEntry>? null 自定义增强回调,在发送前向 entry.ExtensionData 注入业务字段(租户/用户等)。须快进非阻塞(运行在 HTTP 管道线程)。

SupportedMediaTypes 默认application/jsonapplication/xmltext/xmltext/plainapplication/x-www-form-urlencoded

SensitiveFields 默认passwordapikeyapi_keysecrettokenaccess_tokenauthorizationappidappkey

关于 MaxBodyLength(重要)

  • 默认 4096:请求/响应正文超过 4096 字符会被截断,并在末尾标注 …[截断];同时已知 Content-Length 超过该值时直接跳过缓冲(返回 [Body Too Large Skipped]),避免大文件下载把整段响应缓冲进内存导致 OOM。
  • 0 或负数 = 不限制:既不截断、也不因 Content-Length 超限跳过,完整保留请求/响应正文。适用于某些第三方接口(如长报表、大列表、AI 流式聚合)输出很长且需要完整留存排查的场景。
services.AddRequestLogging<MyRequestLogStore>(o =>
{
    o.MaxBodyLength = 0; // 不限制,完整保留所有正文
});

不限制时的内存权衡:大响应体会被完整缓冲进内存,高并发下需评估内存压力。若希望"尽量长但有上限",可设一个较大的值(如内部项目 BitzOrcas 的实践是上限 2,000,000 字符、硬上限 3,000,000),既覆盖绝大多数长响应又防止失控。示例见下方 SqlSugar/MongoDB Store。

持久化:自定义 IRequestLogStore 实现

核心包只定义抽象,宿主按所用 ORM/存储自行实现:

public interface IRequestLogStore
{
    Task WriteBatchAsync(IReadOnlyList<RequestLogEntry> entries, CancellationToken cancellationToken);
}

RequestLogEntry 标准字段:Id / TraceId / Provider / Endpoint / Method / ApiName / RequestHeaders / RequestBody / StatusCode / ResponseBody / Exception / BeginTime / DurationMs / IsSuccess,以及扩展字典 ExtensionData(Tags 模式,存自定义业务字段)。

示例 1:SqlSugar(参考内部项目 BitzOrcasSysExternalRequestLogRecord

using Bitzsoft.Integrations.RequestLogging;
using SqlSugar;

// 日志表实体(长文本字段用 Length = int.MaxValue)
[SugarTable("sys_external_request_log")]
public class SysExternalRequestLog
{
    [SugarColumn(IsPrimaryKey = true, ColumnName = "id")]
    public string Id { get; set; } = "";

    [SugarColumn(Length = 64)]  public string Provider { get; set; } = "";
    [SugarColumn(Length = 512)] public string? Endpoint { get; set; }
    [SugarColumn(Length = 32)]  public string? Method { get; set; }
    [SugarColumn(Length = 256)] public string? ApiName { get; set; }
    [SugarColumn(Length = int.MaxValue)] public string? RequestHeaders { get; set; }
    [SugarColumn(Length = int.MaxValue)] public string? RequestBody { get; set; }
    [SugarColumn(Length = int.MaxValue)] public string? ResponseBody { get; set; }
    [SugarColumn(Length = 16)]  public int? StatusCode { get; set; }
    [SugarColumn(Length = int.MaxValue)] public string? Exception { get; set; }
    public DateTime BeginTime { get; set; }
    public int DurationMs { get; set; }
    [SugarColumn(Length = 64)] public string? TraceId { get; set; }
    [SugarColumn(Length = 64)] public string? TenantId { get; set; } // 取自 ExtensionData
    public DateTime CreateTime { get; set; }
}

public class SqlSugarRequestLogStore : IRequestLogStore
{
    private readonly ISqlSugarClient _db;
    public SqlSugarRequestLogStore(ISqlSugarClient db) => _db = db;

    public async Task WriteBatchAsync(IReadOnlyList<RequestLogEntry> entries, CancellationToken ct)
    {
        if (entries.Count == 0) return;

        var rows = entries.Select(e => new SysExternalRequestLog
        {
            Id = e.Id,
            Provider = e.Provider,
            Endpoint = e.Endpoint,
            Method = e.Method,
            ApiName = e.ApiName,
            RequestHeaders = e.RequestHeaders,
            RequestBody = e.RequestBody,
            ResponseBody = e.ResponseBody,
            StatusCode = e.StatusCode,
            Exception = e.Exception,
            BeginTime = e.BeginTime,
            DurationMs = e.DurationMs,
            TraceId = e.TraceId,
            TenantId = e.ExtensionData?.GetValueOrDefault("TenantId")?.ToString(),
            CreateTime = DateTime.Now,
        }).ToList();

        // 批量高性能写入
        await _db.Insertable(rows).ExecuteCommandAsync(ct);
    }
}

示例 2:Dapper

using System.Data;
using Bitzsoft.Integrations.RequestLogging;
using Dapper;

public class DapperRequestLogStore : IRequestLogStore
{
    private readonly IDbConnection _conn;
    public DapperRequestLogStore(IDbConnection conn) => _conn = conn;

    private const string Sql = @"
INSERT INTO sys_external_request_log
  (id, provider, endpoint, method, api_name, request_headers, request_body,
   status_code, response_body, exception, begin_time, duration_ms, trace_id, tenant_id, create_time)
VALUES
  (@Id,@Provider,@Endpoint,@Method,@ApiName,@RequestHeaders,@RequestBody,
   @StatusCode,@ResponseBody,@Exception,@BeginTime,@DurationMs,@TraceId,@TenantId,@CreateTime);";

    public async Task WriteBatchAsync(IReadOnlyList<RequestLogEntry> entries, CancellationToken ct)
    {
        if (entries.Count == 0) return;
        var rows = entries.Select(e => new
        {
            e.Id, e.Provider, e.Endpoint, e.Method, e.ApiName,
            e.RequestHeaders, e.RequestBody, e.StatusCode, e.ResponseBody,
            e.Exception, e.BeginTime, e.DurationMs, e.TraceId,
            TenantId = e.ExtensionData?.GetValueOrDefault("TenantId")?.ToString(),
            CreateTime = DateTime.Now,
        });
        await _conn.ExecuteAsync(new CommandDefinition(Sql, rows, cancellationToken: ct));
    }
}

示例 3:MongoDB(参考内部项目 BitzOrcasMongoExternalRequestLogger

内部项目 BitzOrcas 的实践是:每字段做 SafeSubstring(0, maxLen)(默认上限 2,000,000、硬上限 3,000,000),入队后由后台服务批量写 MongoDB。等价于本组件的 IRequestLogStore 实现:

using Bitzsoft.Integrations.RequestLogging;
using MongoDB.Driver;

public class MongoRequestLogStore : IRequestLogStore
{
    private readonly IMongoCollection<BsonDocument> _col;
    public MongoRequestLogStore(IMongoDatabase db)
        => _col = db.GetCollection<BsonDocument>("sys_external_request_log");

    public async Task WriteBatchAsync(IReadOnlyList<RequestLogEntry> entries, CancellationToken ct)
    {
        if (entries.Count == 0) return;

        var docs = entries.Select(e => new BsonDocument
        {
            { "_id", e.Id },
            { "provider", e.Provider },
            { "endpoint", e.Endpoint },
            { "method", e.Method },
            { "apiName", e.ApiName },
            { "requestHeaders", e.RequestHeaders },
            { "requestBody", e.RequestBody },
            { "statusCode", e.StatusCode },
            { "responseBody", e.ResponseBody },
            { "exception", e.Exception },
            { "beginTime", e.BeginTime },
            { "durationMs", e.DurationMs },
            { "traceId", e.TraceId },
            { "tenantId", e.ExtensionData?.GetValueOrDefault("TenantId")?.ToString() },
            { "createTime", DateTime.Now },
        });

        await _col.InsertManyAsync(docs, cancellationToken: ct);
    }
}

正文字段长度由 MaxBodyLength 在入队前统一截断/脱敏,Store 实现一般无需再二次截断;若仍想兜底(如 Mongo 单文档 16MB 上限),可在 Store 内对超长字段再做一次 Substring

Enricher:注入业务上下文

Enrich 在发送前调用,可向 entry.ExtensionData 注入租户/用户等字段(禁止注入 Scoped 服务到 Handler 构造,Handler 是池化的;用 AsyncLocal 或在此回调内解析):

services.AddRequestLogging<MyRequestLogStore>(o =>
{
    o.Enrich = (request, entry) =>
    {
        entry.ExtensionData["TenantId"] = CurrentContext.TenantId;
        entry.ExtensionData["UserId"]   = CurrentContext.UserId;
    };
});

SDK 连接器:IRequestLogRecorder 回调

非 HttpClient 管道的 SDK(阿里云 OSS / 腾讯云 / AWS / Azure / SMS 等)无法用 DelegatingHandler 拦截,改在其调用包装处注入 IRequestLogRecorder 并调用 Record,汇入同一管道:

public class MySdkClient
{
    private readonly IRequestLogRecorder? _recorder;
    public MySdkClient(IRequestLogRecorder? recorder = null) => _recorder = recorder;

    public async Task<string> CallAsync(object req, CancellationToken ct)
    {
        var sw = Stopwatch.StartNew();
        var entry = new RequestLogEntry
        {
            Provider = "MySdk", Endpoint = "api.example.com",
            Method = "SDK", ApiName = "Call", BeginTime = DateTime.UtcNow,
            RequestBody = JsonSerializer.Serialize(req),
        };
        try
        {
            var resp = await SdkInvoke(req, ct);
            entry.StatusCode = 200;
            entry.ResponseBody = JsonSerializer.Serialize(resp);
            return resp;
        }
        catch (Exception ex) { entry.Exception = ex.ToString(); throw; }
        finally { entry.DurationMs = (int)sw.ElapsedMilliseconds; _recorder?.Record(entry); }
    }
}

Record 内部会按 Enabled/SamplingRate 判定并对正文脱敏;IRequestLogRecordernull 时整个回调为空操作,连接器无需感知宿主是否注册了日志。

核心类型一览

类型 说明
IRequestLogStore 存储抽象,宿主实现 WriteBatchAsync
IRequestLogRecorder SDK 连接器的回调钩子,调用 Record 汇入同一管道
NullRequestLogStore 默认空实现,未注册存储时使用
RequestLogEntry 单条请求日志数据模型(含 ExtensionData 扩展字典)
RequestLoggingOptions 配置项(开关/采样/正文上限/队列/批次/媒体类型/敏感字段/Enricher)
RequestLogHandler DelegatingHandler 拦截器(internal,最外层)
RequestLogProcessor Channel 队列 + 后台批量消费者,同时实现 IRequestLogRecorder(internal)
SensitiveDataRedactor 正则字段名脱敏 + 截断(internal)

设计要点 / 避坑

  • 永不阻塞业务:队列满 DropWrite 丢弃最新条目,写入失败仅记日志、不影响业务请求。
  • Handler 池化DelegatingHandler 由工厂池化、按 HandlerLifetime 轮换;不要在 Handler 构造里注入 Scoped 服务,租户/用户上下文经 Enrich 回调或 AsyncLocal 在执行期解析。
  • 最外层语义RequestLogHandler 挂在最外层(先于鉴权/重试),一次业务调用一条日志、请求体为鉴权注入前(不泄漏 token)、耗时含全链路。
  • 流安全:仅白名单文本类型读正文;LoadIntoBufferAsync 缓冲后请求仍可发送、响应仍可被调用方读取。
  • OOM 防护:已知 Content-Length 超过 MaxBodyLength 时跳过缓冲;MaxBodyLength <= 0 关闭此防护(完整保留,注意内存)。
  • 关停排空:宿主优雅关停时后台服务尽量排空队列剩余条目。

依赖

用途
Microsoft.Extensions.Http IHttpClientFactory / DelegatingHandler 管道
Microsoft.Extensions.DependencyInjection.Abstractions DI 扩展方法
Microsoft.Extensions.Options 强类型配置
Microsoft.Extensions.Logging.Abstractions 日志
Microsoft.Extensions.Hosting.Abstractions BackgroundService

目标框架:net5.0;net8.0;net10.0

相关包

说明
Bitzsoft.Integrations.*(全部连接器) 均已内置挂载,宿主仅需 AddRequestLogging<TStore>() 一次
Product Compatible and additional computed target framework versions.
.NET net5.0 is compatible.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (68)

Showing the top 5 NuGet packages that depend on Bitzsoft.Integrations.RequestLogging:

Package Downloads
Bitzsoft.Integrations.SsoRedirect

SSO 跳转共享层 — URL 模板、AES 加密、HMAC 签名与 API 重定向管道

Bitzsoft.Integrations.LegalDatabase

法规案例库抽象层 — 统一接口定义、基础模型与 SSO 跳转管道

Bitzsoft.Integrations.AI

AI 服务集成 — 基于 Microsoft.Extensions.AI IChatClient 的轻量 OpenAI 兼容客户端

Bitzsoft.Integrations.AgentFramework

AI Agent 框架 — 基于 Microsoft Semantic Kernel 的多轮对话与流式响应

Bitzsoft.Integrations.FileStorage.Aws

AWS S3 文件存储实现

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-alpha.7 406 6/16/2026
1.0.0-alpha.6 408 6/16/2026