Bitzsoft.Integrations.RequestLogging 1.0.1

dotnet add package Bitzsoft.Integrations.RequestLogging --version 1.0.1
                    
NuGet\Install-Package Bitzsoft.Integrations.RequestLogging -Version 1.0.1
                    
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.1" />
                    
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.1" />
                    
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.1
                    
#r "nuget: Bitzsoft.Integrations.RequestLogging, 1.0.1"
                    
#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.1
                    
#: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.1
                    
Install as a Cake Addin
#tool nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.1
                    
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> 有界队列(满时 TryWrite 明确拒绝并释放临时资源,绝不等待业务线程)由后台 BackgroundService 消费写入存储。
  • 完整正文 + 有界内存:文本正文默认完整保留;超过 MaxInMemoryBodyBytes 后自动写入 AES 加密临时文件,持久化时逐条物化,不以跳过正文换取内存安全。
  • 流安全读取:请求和响应正文替换为可重放 HttpContent,内部重试和调用方仍可完整读取;二进制流(图片/PDF/文件等)记为 [Binary Data Skipped]
  • 敏感脱敏:自动识别 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.MaxInMemoryBodyBytes = 64 * 1024; // 只控制内存/磁盘切换
    // options.MaxLoggedBodyLength = 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% 请求被记录。被跳过的请求不进入队列。
MaxLoggedBodyLength int? null 最终日志正文上限(字符数)。默认完整记录;仅显式设置正数时截断。
MaxInMemoryBodyBytes int 65536 单正文内存阈值(字节数)。超出后切换到加密临时文件,不改变最终正文。
TemporaryBodyDirectory string? null 临时正文目录;默认使用系统临时目录下的组件专用子目录。
MaxBodyLength int 0 旧版兼容别名;正数映射到 MaxLoggedBodyLength,0/负数表示不截断。
ChannelCapacity int 10000 Channel<T> 有界队列容量。队列满时立即拒绝最新条目并释放其临时资源,永不阻塞业务线程
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_tokenrefresh_tokenid_tokenclient_secretpage_tokenpageTokennext_page_tokennextPageTokencontinuation_tokencontinuationTokenclient_assertioncode_verifierauthorizationappidappkey

分页令牌只做字段级脱敏;同一 JSON 中的查询、过滤条件和业务正文仍完整 记录,也不会因此启用正文长度限制。

整段正文均为凭据时(例如 OAuth Token 请求/响应),连接器可以显式跳过正文:

using Bitzsoft.Integrations.RequestLogging;

using var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
    Content = tokenForm
};
request.SuppressRequestAndResponseBodiesFromLogging();

日志仍保留方法、脱敏后的端点、状态码和耗时,请求与响应正文记录为 [Sensitive Data Skipped]。这属于单次请求的显式安全分类,不会截断或限制 其他请求;普通正文仍遵循 MaxLoggedBodyLength = null 的默认完整记录策略。 通用 REST transport 的 client-credentials 与 JWT Bearer token 交换,以及 AzureAD 授权码交换,均已使用该标记。

正文完整度与内存阈值(重要)

  • MaxLoggedBodyLength = null 是默认值:完整保留长报表、大列表和兆级第三方响应。
  • MaxInMemoryBodyBytes 仅是内存阈值。超过阈值的原始正文以进程内随机密钥加密后落入临时文件;队列丢弃、持久化完成、请求/响应释放时自动清理。
  • 若业务或目标存储确有字段限制,可显式设置 MaxLoggedBodyLength;这是业务保留策略,不是内存保护策略。
services.AddRequestLogging<MyRequestLogStore>(o =>
{
    o.MaxInMemoryBodyBytes = 128 * 1024;
    o.MaxLoggedBodyLength = null; // 默认值;完整记录
});

兼容接口 MaxBodyLength 仍可使用:正数代表显式截断,0 或负数代表不截断。新代码应使用语义更清楚的 MaxLoggedBodyLength

持久化:自定义 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

MongoDB 单文档存在平台自身的大小约束;如完整正文可能超过目标存储能力,应在 Store 层使用 GridFS/对象存储外置正文并保存引用,而不是由核心组件静默截断:

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);
    }
}

核心组件默认把完整、已脱敏正文交给 Store。目标数据库有硬限制时,Store 应明确采用大字段、GridFS 或对象存储引用;若业务接受截断,则由宿主显式设置 MaxLoggedBodyLength

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)

设计要点 / 避坑

  • 永不等待业务线程:队列满时 TryWrite 返回 false,立即释放该条目的临时正文并记录告警。
  • Handler 池化DelegatingHandler 由工厂池化、按 HandlerLifetime 轮换;不要在 Handler 构造里注入 Scoped 服务,租户/用户上下文经 Enrich 回调或 AsyncLocal 在执行期解析。
  • 最外层语义RequestLogHandler 挂在最外层(先于鉴权/重试),一次业务调用一条日志、请求体为鉴权注入前(不泄漏 token)、耗时含全链路。
  • 流安全:仅白名单文本类型读正文;可重放内容保证请求可重试、响应仍可被调用方读取。
  • 完整但有界:正文默认不截断;内存阈值外使用加密临时文件,后台对大正文逐条物化后写入。
  • 关停排空:宿主优雅关停时后台服务尽量排空队列剩余条目。

依赖

用途
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 (109)

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.Sms

短信多供应商集成 — 阿里云/腾讯云/华为云/云片 + 国际 Twilio/Vonage,统一消息对象 API

Bitzsoft.Integrations.AzureAD

Azure AD / Microsoft Entra ID 集成客户端 — OAuth 2.0 PKCE 流程、OIDC Token 验证、Graph API 组查询、用户自动供应

Bitzsoft.Integrations.FileStorage.Azure

Azure Blob Storage 文件存储实现

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 4,054 8/3/2026
1.0.0 4,018 8/2/2026
1.0.0-alpha.10 537 7/26/2026
1.0.0-alpha.9 766 7/12/2026
1.0.0-alpha.8 752 7/1/2026
1.0.0-alpha.7 771 6/16/2026
1.0.0-alpha.6 427 6/16/2026