RequestLogging 是全库唯一真正统一的基础设施。所有走 HttpClient 的厂商都通过 .AddRequestLogging() 接入审计:一次业务调用产生一条日志,请求体在鉴权注入前捕获(不泄漏 token),异常和耗时一并记录。它解决两个核心问题——如何不阻塞业务线程地记录每一笔请求,以及如何安全地脱敏敏感数据。
处理管线
请求在 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 控制全局行为:
| 配置项 | 默认值 | 说明 |
|---|---|---|
Enabled | true | 设为 false 时 handler 直接跳过,不产生任何开销 |
SamplingRate | 1.0 | 采样率,1.0 全量、0.0 全跳过,在 handler 层完成 |
MaxLoggedBodyLength | null | 正文显式截取长度(字符数),null 表示完整记录不截断 |
MaxInMemoryBodyBytes | 64KB | 单个正文留内存的最大字节,超过则写加密临时文件 |
TemporaryBodyDirectory | null | 大正文加密临时文件目录,空则用系统临时目录子目录 |
ChannelCapacity | 10000 | Channel 有界队列容量,满时 TryWrite 立即失败释放资源 |
BatchSize | 100 | 后台批量写入的批次大小 |
SupportedMediaTypes | json/xml/text/form | 支持记录正文的媒体类型,不在集合内的记为 [Binary Data Skipped] |
SensitiveFields | password/token/secret… | 敏感字段名集合,匹配的 JSON 字符串值替换为 *** |
Enrich | null | 自定义日志增强回调,在发送前注入租户 ID 等业务字段 |
RequestLoggingOptionsValidator 实现 IValidateOptions<RequestLoggingOptions>,在启动时校验配置合法性。
脱敏与正文捕获
RequestLogSanitizer / SensitiveDataRedactor
结构化脱敏覆盖 headers、query、form 和正文(JSON / XML)多种载体。SensitiveFields 集合默认包含 password、apikey、secret、token、access_token、client_secret、authorization、appid、appkey 等常见敏感字段名,匹配时大小写不敏感,命中的 JSON 字符串值替换为 ***。
宿主可在配置回调中 Add / Remove 成员,适配厂商特定的字段命名。
HttpBodyCapture / ReplayableHttpContent / RequestLogBodyBuffer
正文捕获面临一个矛盾:HttpContent 只能读一次,但审计和真实发送都需要正文。这几个类型协作解决它:
| 类型 | 职责 |
|---|---|
ReplayableHttpContent | 包装原始内容,允许多次读取(审计 + 发送) |
HttpBodyCapture | 按 SupportedMediaTypes 决定是否捕获正文,二进制内容跳过 |
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 注册后重新添加,保证”以最后一次调用为准”,与注册顺序无关。
连接器挂载
// 命名 HttpClientservices.AddHttpClient("Alipay").AddRequestLogging("Alipay");
// typed HttpClientservices.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); }}
// ② 注册时覆盖默认 NullRequestLogStoreservices.AddRequestLogging<EfRequestLogStore>();相关
- 核心基础设施
- Rest 通用传输
- Compatibility 兼容层(
SuppressRequestAndResponseBodies) - 横切关注点