Bitzsoft.Integrations.RequestLogging
1.0.0-alpha.10
dotnet add package Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.10
NuGet\Install-Package Bitzsoft.Integrations.RequestLogging -Version 1.0.0-alpha.10
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.10" />
<PackageVersion Include="Bitzsoft.Integrations.RequestLogging" Version="1.0.0-alpha.10" />
<PackageReference Include="Bitzsoft.Integrations.RequestLogging" />
paket add Bitzsoft.Integrations.RequestLogging --version 1.0.0-alpha.10
#r "nuget: Bitzsoft.Integrations.RequestLogging, 1.0.0-alpha.10"
#:package Bitzsoft.Integrations.RequestLogging@1.0.0-alpha.10
#addin nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.10&prerelease
#tool nuget:?package=Bitzsoft.Integrations.RequestLogging&version=1.0.0-alpha.10&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>有界队列(满时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.0。1.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/json、application/xml、text/xml、text/plain、application/x-www-form-urlencoded。
SensitiveFields 默认:password、apikey、api_key、secret、
token、access_token、refresh_token、id_token、client_secret、
page_token、pageToken、next_page_token、nextPageToken、
continuation_token、continuationToken、client_assertion、
code_verifier、authorization、appid、appkey。
分页令牌只做字段级脱敏;同一 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(参考内部项目 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)
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判定并对正文脱敏;IRequestLogRecorder为null时整个回调为空操作,连接器无需感知宿主是否注册了日志。
核心类型一览
| 类型 | 说明 |
|---|---|
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 | 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
- Bitzsoft.Integrations.Compatibility (>= 1.0.0-alpha.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Http (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Options (>= 10.0.10)
-
net5.0
- Bitzsoft.Integrations.Compatibility (>= 1.0.0-alpha.10)
- 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
- Bitzsoft.Integrations.Compatibility (>= 1.0.0-alpha.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Http (>= 10.0.10)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Options (>= 10.0.10)
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 |