Bitzsoft.Integrations.RequestLogging 1.0.0-alpha.10

This is a prerelease version of Bitzsoft.Integrations.RequestLogging.
dotnet add package Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.10
                    
NuGet\Install-Package Bitzsoft.Integrations.RequestLogging -Version 1.0.0-alpha.10
                    
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.10" />
                    
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.10" />
                    
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.10
                    
#r "nuget: Bitzsoft.Integrations.RequestLogging, 1.0.0-alpha.10"
                    
#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.10
                    
#: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.10&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.10&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> 有界队列(满时 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 (70)

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

Package Downloads
Bitzsoft.Integrations.Sms

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

Bitzsoft.Integrations.AzureAD

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

Bitzsoft.Integrations.SsoRedirect

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

Bitzsoft.Integrations.LegalDatabase

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

Bitzsoft.Integrations.Beisen

北森 HR 系统集成客户端 — REST API 封装、OAuth Token 管理、多租户配置

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-alpha.10 117 7/26/2026
1.0.0-alpha.9 673 7/12/2026
1.0.0-alpha.8 656 7/1/2026
1.0.0-alpha.7 679 6/16/2026
1.0.0-alpha.6 424 6/16/2026