TiyGate 请求日志:c→g→p→g→c 全链路记录规范
June 23, 2026 · View on GitHub
请求日志详情视图记录单次请求在网关内的完整四段链路。本文定义每段 记录什么、在哪里捕获、如何脱敏,确保字段齐全、客户端自定义 header 可见、且仅对敏感值脱敏。
四段链路
| 段 | 含义 | header 字段 | body 字段 | 数据来源 |
|---|---|---|---|---|
| c→g | 客户端 → 网关(ingress 请求) | redacted_headers_json | raw_envelope_json | RawEnvelope(request_logs) |
| g→p | 网关 → 供应商(egress 请求) | egress_headers_json | egress_body | ExchangeCapture(request_payloads) |
| p→g | 供应商 → 网关(upstream 响应) | upstream_resp_headers_json | upstream_resp_body | ExchangeCapture |
| g→c | 网关 → 客户端(client 响应) | client_resp_headers_json | client_resp_body | ExchangeCapture |
数据由两条记录拼成:c→g 来自 RawEnvelope,其余三段来自
ExchangeCapture,两者经 telemetry bus 异步落库后由
get_request_replay() 用 request_id LEFT JOIN 拼回,前端
RequestLogs.tsx 分四个区块展示。
字段齐全约定
记录的目标是字段完整:每个实际收发的 header 都应出现,敏感值脱敏
为 [REDACTED],但 header 名与非敏感字段必须保留。
-
c→g:
build_redacted_envelope()(crates/server/src/ingress/observability.rs) 遍历全部客户端请求 header(含x-request-id、x-debug-id等自定义 header),经Redactor脱敏后写入redacted_headers_json。自定义 header 明文保留,便于定位问题。 -
g→p:egress header 以 reqwest 实际构建的请求为准。在每个上游 发送点,请求 builder 经
inject_trace()注入traceparent、.json()/.body()写入 body 后,调用finalize_egress()(ingress/observability.rs)执行builder.build()得到 最终reqwest::Request,并从req.headers()快照完整 header 集合, 再用client.execute(req)发送。这样content-type、content-length、traceparent、authorization/x-api-key(Anthropic 还有anthropic-version)等 reqwest 在 finalize 时补齐的 header 都会被记录。历史问题:旧实现对手工构建的
upstream_headers做快照,而content-type/content-length是 reqwest 发送时才补上的、发生在快照 之后,passthrough 路径起点更是空HeaderMap,导致日志里只剩authorization与手动 push 的traceparent。改为 build-then-capture 后字段齐全。
脱敏策略
脱敏由 crates/core/src/redaction.rs 的 Redactor 完成(规则详见
redaction.md):
- header 名命中精确名单(
authorization/x-api-key/cookie等)或子串 名单(token/secret/password/credential)时,值替换为[REDACTED],header 名保留。 - JSON body 命中已知凭证键(
api_key/token/client_secret等)时递归 替换为[REDACTED],其余字段(如messages[].content)verbatim 保留。
g→p/p→g/g→c 三段在落库前由 OltpSink::capture_to_row()
(crates/store/src/log_sink/oltp.rs)统一脱敏;c→g 段在 ingress 热路径
构建 RawEnvelope 时即脱敏。捕获到的明文完整 header 只在内存中短暂存在,
落库前一定经过脱敏。
媒体剥离
- 当
raw_envelope_capture_media关闭(默认)时,c→g body 中的内联 base64 媒体被替换为字符串占位符[_media_meta mime=... size_bytes=N sha256_hex=...]。 占位符保持原始 JSON 类型(string 仍为 string),避免审计日志中出现 与真实请求结构不一致的 object 字段。 - 流式响应额外尝试把上游 SSE 合并为结构化 JSON 存入
sse_parsed_json。
覆盖的上游路径
所有 5 个协议执行函数(chat completions、anthropic messages、embeddings、
responses、gemini)的 stream 与 non-stream 分支共 9 个发送点,统一通过
finalize_egress() + client.execute() 捕获 egress header,保证规范一致。
Payload 归档到对象存储
当 TIYGATE_PAYLOAD_ARCHIVE_ENABLED=true 且 S3-Compatible 配置完整时,
服务会启动后台归档 worker,批量扫描 request_payloads 中未归档或可重试的
历史记录。worker 会将完整链路详情拆成 8 个对象:cg_req_raw/cg_req_parsed
(Client → Gateway 请求)、gp_req_raw/gp_req_parsed(Gateway → Provider 请求)、
pg_rsp_raw/pg_rsp_parsed(Provider → Gateway 响应)以及
gc_rsp_raw/gc_rsp_parsed(Gateway → Client 响应)。对象 key 规则为
{request_id}/{kind}.{txt|json},并在配置了 TIYGATE_PAYLOAD_ARCHIVE_S3_PREFIX
时追加前缀;其中 cg_req_raw 和所有 *_parsed 使用 JSON,其余 raw body 使用文本。
*_parsed 对象会包含该段已脱敏 headers,并可包含 method、path、status 或 SSE parsed body。
上传内容统一 gzip 压缩,S3 metadata 与 DB manifest 会记录 sha256、原始大小、
压缩后大小、content_type 与 content_encoding=gzip。
归档状态字段位于 request_payloads:pending、archive_ready、uploading、
uploaded、failed、expired、disabled。当前捕获行在
OltpSink::write_capture() 完成原文、脱敏正文以及流式响应的
sse_parsed_json/client_sse_parsed_json 最佳努力解析写入后,以
archive_ready 状态对归档 worker 可见;历史 pending 记录会由迁移回填为
archive_ready。归档 worker 只领取 archive_ready(以及锁超时的
uploading)记录,领取后置为 uploading。上传失败会按
TIYGATE_PAYLOAD_ARCHIVE_MAX_RETRIES 重试,未超限时回到 archive_ready,超过后
标记 failed,此时详情接口仍从 DB 原文字段读取;上传成功后才会在同一事务中
清空 request_payloads 中的详情正文与 payload headers,并清空 request_logs 中的
raw_envelope_json/redacted_headers_json,最后写入 payload_archive_manifest_json 并置为
uploaded。详情查询保持
后端代理模式:uploaded 记录由 Admin API 从对象存储读取、校验 sha256/size、
解压后再返回给前端;如果对象上传成功但 DB finalize 提交失败,记录会保持
uploading,后续归档 pass 会按 stale lock 重新领取并以相同对象 key 幂等重试。DB 日志 retention 只清理数据库记录,不删除对象存储文件,
对象生命周期应由 bucket lifecycle 或运维策略管理。
环境变量示例见 .env.example 中的 TIYGATE_PAYLOAD_ARCHIVE_* 配置项。
Header 透传策略(双向 denylist)
网关默认转发 header,只挡黑名单(denylist 模式),策略实现于
crates/core/src/header_forward.rs 的 HeaderForwardPolicy,与脱敏
Redactor 解耦:转发策略决定 header 是否真正上/下线,脱敏决定 header
值在日志里是否被掩码。
请求方向(C→G→P)
merge_client_headers()(crates/server/src/ingress.rs)在
upstream_headers 初始化之后、apply_provider_auth() 之前,把客户端请求
header 按 should_forward_request 合并进上游请求;已被 codec 设置的 header
不被覆盖,auth 注入始终最后胜出。默认不转发的请求 header:
- 凭证类(客户端对网关的凭证,绝不能泄露给 Provider,且网关注入自己的):
authorization、proxy-authorization、x-api-key、anthropic-version、cookie - 网关重算/自控类:
host、content-length、content-type、content-encoding、accept-encoding、expect - 逐跳 header(RFC 7230 §6.1):
connection、keep-alive、proxy-connection、te、trailer、transfer-encoding、upgrade - trace(网关重新注入):
traceparent、tracestate
其余 header(如 x-debug-id、x-correlation-id)默认转发给 Provider,并如实
出现在 g→p 段记录中。
响应方向(P→G→C)
forward_upstream_resp_headers() 把供应商响应 header 按
should_forward_response 转发到客户端响应(stream 与 non-stream 均覆盖)。
默认不转发的响应 header:
- 逐跳 header:
connection、keep-alive、proxy-connection、te、trailer、transfer-encoding、upgrade - 长度/编码(网关重新序列化或 reqwest 解压后失配):
content-length、content-encoding - 框架自设:
content-type(由Json/Sse决定)、date
retry-after 与 x-ratelimit-* 不在黑名单,正常转发给客户端。其余供应商
header(如 x-llm-served-by)默认转发,并如实出现在 g→c 段记录中。
例外:x-request-id 会被改写覆盖为网关为该请求生成的 request id(即
request_id,uuid::Uuid::now_v7()),覆盖供应商返回的同名值。该改写对
stream 与 non-stream 均生效,且 client_resp_headers 记录与实际下行一致。
可配置追加
在硬编码默认黑名单之上,可通过环境变量追加额外要拦截的 header(逗号分隔, 大小写不敏感):
TIYGATE_FORWARD_REQUEST_HEADER_DENY—— 追加请求方向黑名单TIYGATE_FORWARD_RESPONSE_HEADER_DENY—— 追加响应方向黑名单
例如 TIYGATE_FORWARD_REQUEST_HEADER_DENY=x-stainless-lang,x-internal 会在
默认基础上额外屏蔽这两个客户端 header。