Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

RequestLogging 请求审计

通用 HTTP 审计基础设施——DelegatingHandler 拦截、后台批量写入、结构化脱敏与正文捕获。

Last updated

RequestLogging 是全库唯一真正统一的基础设施。所有走 HttpClient 的厂商都通过 .AddRequestLogging() 接入审计:一次业务调用产生一条日志,请求体在鉴权注入前捕获(不泄漏 token),异常和耗时一并记录。它解决两个核心问题——如何不阻塞业务线程地记录每一笔请求,以及如何安全地脱敏敏感数据。

处理管线

业务请求

RequestLogHandler
DelegatingHandler

正文捕获 · 脱敏
RequestLogSanitizer

HttpClient 发送

第三方 API

响应/异常/耗时

Channel 非阻塞入队

RequestLogProcessor
后台单线程

IRequestLogStore
批量写入

请求在 RequestLogHandler 拦截后捕获正文并脱敏,发送完成后把 RequestLogEntry 非阻塞写入 Channel。后台 RequestLogProcessor 单线程读取并批量调用 IRequestLogStore.WriteBatchAsync。业务线程全程不等存储。

核心组件

RequestLogHandler

DelegatingHandler 实现,挂在命名 HttpClient 的 handler 链最外层。它拦截请求、响应、异常和耗时四类信号,构建 RequestLogEntry 后入队。

// ① 命名客户端挂载审计 Handler(最外层,先于鉴权/重试 handler)
services.AddHttpClient("Alipay")
.AddRequestLogging("Alipay");
// ② typed 客户端挂载,providerName 为空时取类型名去掉 HttpClient 后缀
services.AddHttpClient<AlipayHttpClient>()
.AddRequestLogging<AlipayHttpClient>();

RequestLogProcessor

后台批处理服务,注册为 Singleton 同时暴露为 IHostedService。它从有界 Channel 读取 RequestLogEntry,积累到 BatchSize 条或队列无更多数据时触发一次 WriteBatchAsync。因为是后台单线程调用,IRequestLogStore 实现内部无需自行加锁。

IRequestLogStore

日志存储抽象,由宿主实现。核心包不绑定任何 ORM:

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

默认实现 NullRequestLogStore 直接丢弃所有日志——不接入存储时零开销。宿主接入真实存储后才真正写入。

IRequestLogRecorder

供非 HttpClient 管道的 SDK 连接器回调写入审计日志。某些厂商 SDK(如腾讯云 SDK)自带 HTTP 管道,不经过 IHttpClientFactory,无法挂 RequestLogHandler。这类连接器在调用前后自行构建 RequestLogEntry,通过 IRequestLogRecorder.Record 入队,复用同一套采样、脱敏和后台批量写入逻辑。

public interface IRequestLogRecorder
{
void Record(RequestLogEntry entry);
}

RequestLogProcessor 同一单例会暴露为 IRequestLogRecorder,因此 HttpClient 管道和 SDK 管道写入的是同一个队列。

RequestLogEntry 与 RequestLoggingOptions

RequestLogEntry 是单笔请求的完整审计记录,包含方法、URI、状态码、耗时、请求/响应正文(已脱敏)、异常信息和 ExtensionData 扩展字段。

RequestLoggingOptions 控制全局行为:

配置项默认值说明
Enabledtrue设为 false 时 handler 直接跳过,不产生任何开销
SamplingRate1.0采样率,1.0 全量、0.0 全跳过,在 handler 层完成
MaxLoggedBodyLengthnull正文显式截取长度(字符数),null 表示完整记录不截断
MaxInMemoryBodyBytes64KB单个正文留内存的最大字节,超过则写加密临时文件
TemporaryBodyDirectorynull大正文加密临时文件目录,空则用系统临时目录子目录
ChannelCapacity10000Channel 有界队列容量,满时 TryWrite 立即失败释放资源
BatchSize100后台批量写入的批次大小
SupportedMediaTypesjson/xml/text/form支持记录正文的媒体类型,不在集合内的记为 [Binary Data Skipped]
SensitiveFieldspassword/token/secret…敏感字段名集合,匹配的 JSON 字符串值替换为 ***
Enrichnull自定义日志增强回调,在发送前注入租户 ID 等业务字段

RequestLoggingOptionsValidator 实现 IValidateOptions<RequestLoggingOptions>,在启动时校验配置合法性。

脱敏与正文捕获

RequestLogSanitizer / SensitiveDataRedactor

结构化脱敏覆盖 headers、query、form 和正文(JSON / XML)多种载体。SensitiveFields 集合默认包含 passwordapikeysecrettokenaccess_tokenclient_secretauthorizationappidappkey 等常见敏感字段名,匹配时大小写不敏感,命中的 JSON 字符串值替换为 ***

宿主可在配置回调中 Add / Remove 成员,适配厂商特定的字段命名。

HttpBodyCapture / ReplayableHttpContent / RequestLogBodyBuffer

正文捕获面临一个矛盾:HttpContent 只能读一次,但审计和真实发送都需要正文。这几个类型协作解决它:

类型职责
ReplayableHttpContent包装原始内容,允许多次读取(审计 + 发送)
HttpBodyCaptureSupportedMediaTypes 决定是否捕获正文,二进制内容跳过
RequestLogBodyBuffer正文缓冲,超过 MaxInMemoryBodyBytes 时切换到加密临时文件

大正文不会撑爆内存:超过阈值后自动落盘到加密临时文件,后台持久化时再逐条读取。这条路径不影响日志完整性,只控制内存占用。

DI 注册

RequestLogging 的 DI 扩展分为宿主基础设施和连接器挂载两组,全部位于 Microsoft.Extensions.DependencyInjection 命名空间。

宿主注册

// ① 接入真实存储 + 配置
services.AddRequestLogging<MyDbRequestLogStore>(options =>
{
options.SamplingRate = 0.5; // 采样 50%
options.MaxLoggedBodyLength = 4096; // 正文最多记录 4096 字符
options.SensitiveFields.Add("sign"); // 追加厂商特定敏感字段
});
// ② 不接入存储(开发期,用 NullRequestLogStore)
services.AddRequestLogging();

AddRequestLogging<TStore> 会移除既有 IRequestLogStore 注册后重新添加,保证”以最后一次调用为准”,与注册顺序无关。

连接器挂载

// 命名 HttpClient
services.AddHttpClient("Alipay").AddRequestLogging("Alipay");
// typed HttpClient
services.AddHttpClient<JiraCloudHttpClient>().AddRequestLogging<JiraCloudHttpClient>();

连接器调用 .AddRequestLogging() 时会幂等注册核心服务(通过 RequestLoggingCoreMarker 哨兵保证全进程仅注册一次),宿主未显式接入存储时自动使用 NullRequestLogStore。这意味着连接器包和宿主可以安全地各自调用,不会重复注册。

幂等机制

EnsureCoreServices 通过私有哨兵类型 RequestLoggingCoreMarker 判断核心服务是否已注册:

// ③ 标记已注册则核心服务已就绪,直接返回(幂等)
foreach (var descriptor in services)
{
if (descriptor.ServiceType == typeof(RequestLoggingCoreMarker))
return;
}
services.AddSingleton<RequestLoggingCoreMarker>();
services.AddOptions();
services.AddLogging();
services.TryAddEnumerable(/* RequestLoggingOptionsValidator */);
services.TryAddSingleton<IRequestLogStore, NullRequestLogStore>();
services.AddSingleton<RequestLogProcessor>();
services.AddHostedService(sp => sp.GetRequiredService<RequestLogProcessor>());
services.TryAddSingleton<IRequestLogRecorder>(
sp => sp.GetRequiredService<RequestLogProcessor>()); // 同一单例

自定义存储实现

实现 IRequestLogStore 即可接入任意存储(EF Core / Dapper / MongoDB / Elasticsearch 等)。因为 WriteBatchAsync 由后台单线程调用,实现内无需自行加锁:

public sealed class EfRequestLogStore(AppDbContext db) : IRequestLogStore
{
public async Task WriteBatchAsync(
IReadOnlyList<RequestLogEntry> entries,
CancellationToken cancellationToken)
{
// ① 批量映射为实体,一次 SaveChanges
var rows = entries.Select(e => new RequestLogRow
{
ProviderName = e.ProviderName,
Method = e.Method,
RequestUri = e.RequestUri,
StatusCode = e.StatusCode,
ElapsedMs = e.ElapsedMs,
RequestBody = e.RequestBody,
ResponseBody = e.ResponseBody,
Error = e.Error,
OccurredAt = e.OccurredAt,
}).ToList();
await db.RequestLogs.AddRangeAsync(rows, cancellationToken);
await db.SaveChangesAsync(cancellationToken);
}
}
// ② 注册时覆盖默认 NullRequestLogStore
services.AddRequestLogging<EfRequestLogStore>();

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%