Bitzsoft.Integrations.RequestLogging
1.0.0-alpha.7
dotnet add package Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.7
NuGet\Install-Package Bitzsoft.Integrations.RequestLogging -Version 1.0.0-alpha.7
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.7" />
<PackageVersion Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.7" />
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" />
paket add Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.7
#r "nuget: Bitzsoft.Integrations.RequestLogging, 1.0.0-alpha.7"
#:package Bitzsoft.Integrations.RequestLogging@1.0.0-alpha.7
#addin nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.7&prerelease
#tool nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.7&prerelease
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.0。1.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/json、application/xml、text/xml、text/plain、application/x-www-form-urlencoded。
SensitiveFields 默认:password、apikey、api_key、secret、token、access_token、authorization、appid、appkey。
关于 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(参考内部项目 BitzOrcas 的 SysExternalRequestLogRecord)
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(参考内部项目 BitzOrcas 的 MongoExternalRequestLogger)
内部项目 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判定并对正文脱敏;IRequestLogRecorder为null时整个回调为空操作,连接器无需感知宿主是否注册了日志。
核心类型一览
| 类型 | 说明 |
|---|---|
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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Http (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
-
net5.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 5.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 5.0.0)
- Microsoft.Extensions.Http (>= 5.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 5.0.0)
- Microsoft.Extensions.Options (>= 5.0.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Http (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
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 |