← 返回资讯

API 调用日志怎么留:分三层留存,既查得到又存得少

2026-08-07

先说两个真实会撞上的场景,它们把这件事的两难摆得很清楚。

场景一:客服转来一条用户投诉,说三天前问了个问题,模型给出的回答里出现了不该有的内容。你想复现,结果发现日志里只有「某时刻某接口返回 200,耗时 1.8 秒」,请求体没存,响应体也没存。这条投诉你查不下去,只能靠猜。

场景二:安全同事问你,「用户上传的身份证号有没有可能进了模型的调用日志」。你打开日志系统一搜,发现确实进了——不仅进了主日志,还进了三个月的冷备份,以及某次告警推送的消息正文里。

这两个场景是同一枚硬币的两面。日志留得少,出事查不了;日志留得全,日志本身就成了一个新的、体量很大的敏感数据副本。而且第二个问题比第一个严重得多:查不到问题只是耽误时间,而一份全量留存的请求体,等于你在自己的可观测性系统里复制了一份用户数据,还顺带把访问它的门槛降到了「有日志系统账号就能看」。

这篇讲的就是怎么在这两头之间取平衡。我给的方案不复杂,核心就一句话:把日志按「有没有内容」拆成三层,分别用不同的采样率、不同的保留期、不同的权限管

需要先说清楚边界:本文只讲工程做法,不涉及任何法规条款,也不给合规结论。哪些字段算敏感、要留多久、能给谁看,这些是需要专业判断的问题,具体要求以贵司法务/合规同事的判断为准。工程上你能做的是把机制建好,让法务提出的任何一种要求都能被落实——这才是这篇的目标。

一、三层留存:把「体积」和「敏感度」解耦

传统做法是一条日志记到底:时间、接口、状态码、请求体、响应体,全塞进同一条记录,走同一个保留策略。问题在于这条记录里最有用的部分(用来排查、做统计)和最敏感的部分(请求体、响应体)被绑死了,你想长期保留前者,就不得不长期保留后者。

拆开就行。

第一层:指标层(全量、可长期保留)

这一层只有数字和标识,不含任何用户内容。典型字段:

字段说明
时间戳精确到毫秒
调用次数按接口/模型/团队聚合
输入、输出、缓存 token 数分列存,不要求和
首字延迟、总耗时流式接口两个都要
状态码 / 错误类型区分限速、超时、上游异常
成本归属维度团队、项目、环境、key 标识

这一层的特点是体积极小。一条记录几十到一两百字节,一天几百万次调用也就是百兆量级,而且它天然适合做时序聚合——存成分钟级或小时级的聚合数据以后还能再压一个数量级。

它没有内容,所以敏感度低,可以放心地长期保留。而长期保留恰恰是它最大的价值:你要回答「这个月的成本比半年前涨了多少、涨在哪个模型上」,靠的就是这一层。内容日志你不可能存半年,但指标可以。

第二层:元数据层(全量、中期保留)

这一层的定位是「能定位到具体一次调用,但不泄漏这次调用说了什么」。典型字段:

字段说明
请求 ID全链路唯一,见后文第三节
key 标识 / 团队标识存 key 的哈希或别名,不要存 key 明文
模型标识、路由到的上游多供应商场景下必须记
参数摘要temperature、max_tokens、是否流式、是否带工具
消息条数、各条长度只记长度,不记内容
内容哈希请求体、响应体各算一个稳定哈希
重试次数与最终结果区分「一次成功」和「重试三次才成功」

内容哈希这一项经常被忽略,但它非常好用。有了它你可以在完全不存内容的前提下回答一批问题:这个请求是不是和昨天那个一模一样(缓存该命中没命中)?某个客户端是不是在无脑重发同一个请求?两次投诉说的是不是同一个 case?

保留期上,这一层比指标层短、比内容层长。多长合适取决于你的排查窗口——用户投诉通常在多久之内到达、事故复盘通常要回溯多远,按这个定,然后以贵司法务/合规同事的判断为准做最终确认。

第三层:内容层(采样、短期、脱敏)

这是唯一含请求体和响应体的一层,也是三个约束叠加最重的一层:

  • 采样:不全量。按比例采(比如很低的百分比),或者按条件采(只采报错的、只采某个正在调试的模型、只采耗时超过阈值的)。条件采样比比例采样更有用,因为你真正要看内容的场合,基本都有明确触发条件。
  • 短期:保留期以天计,而不是以月计。到期真删。
  • 脱敏:写进去之前就已经处理过,见下一节。

这样分层之后,日常工作的重心会自然地落到前两层。我的经验是,九成以上的排查靠前两层就能完成:哪个团队的调用涨了、哪个模型在报错、某个请求 ID 走了哪条链路、某次重试是不是幂等出了问题——这些都不需要看内容。真正需要翻内容的场合只有两类:模型输出质量的深度分析,和用户投诉的个案复现。前者可以用采样样本做,后者本来就是低频事件。

顺带一个副作用:一旦你把内容层单独拆出来,它的体积会小到你不需要为它的存储成本发愁,也就没有了「太贵了所以缩短保留期」这种被动决策——保留期变成一个纯粹按需要和合规要求来定的参数。关于日志与监控的采集口径,可以参考 网关可观测性怎么做 里对五类量的划分,那篇讲的是「采什么」,这篇讲的是「采到之后怎么放」。

二、脱敏必须在写日志之前做

这一节我想说得重一点,因为「先写再清洗」是我见过最多的错误做法,而且它错得很隐蔽。

典型的错误架构是这样:网关把完整请求体写进日志管道,下游挂一个清洗任务,定时扫描并把敏感字段替换掉。听起来能work,实际上有三个洞:

  1. 落盘的那一刻就已经暴露了。从写入到清洗之间有时间差,这段时间里数据是明文的。而日志管道通常有多个消费者,谁都可能在这个窗口里把它读走。
  2. 副本会逃逸。日志系统几乎一定有备份、有索引、有冷存储、有跨区复制。清洗任务改的是主存储,那些副本很难保证同步被改。你以为清干净了,其实只是清干净了你看得见的那一份。
  3. 清洗任务本身会挂。它是个旁路任务,挂了不影响主链路,所以经常挂了很久没人发现——直到某天有人去查,才发现最近两个月的日志根本没清洗过。

正确的做法是把脱敏做成写入路径上的同步步骤:数据在进入日志序列化之前就已经是脱敏后的形态,脱敏失败就不写这条内容日志(降级成只写元数据层)。这样最坏情况是丢一条内容样本,而不是漏一条明文。

哪些字段要处理

大致分四类,前三类相对好办,第四类是真正的难点。

身份标识类:各种证件号、社会保险类编号、银行账号、内部员工号、设备唯一标识。这类有明确格式,正则能覆盖大部分。

联系方式类:手机号、邮箱、详细地址。同样有格式特征。注意脱敏方式的选择——如果你还想用日志做去重或关联分析,就不能简单粗暴地全替换成星号,可以考虑保留稳定哈希(同一个手机号哈希结果相同,但无法反推)。

凭证类:API key、token、签名、Cookie、Authorization 头。这类最危险,因为一旦泄漏就是直接可用的权限。不管在哪一层,凭证都不应该以明文形式出现在日志里,包括错误堆栈和调试输出。相关的密钥管理实践可以看 网关安全加固 那篇。

自由文本类:用户输入的提示词、上传的文档片段、模型的回复正文。这是最难的一类,因为它没有格式可循——用户可能在一段闲聊里顺手报出自己的身份证号,可能把整份内部文档粘进去,也可能让模型帮忙改一封含客户信息的邮件。

自由文本的脱敏,我的判断是:不要指望靠识别解决,要靠「默认不存」解决。理由很直接——识别永远有漏网率,而在敏感数据这件事上,99% 的识别率约等于 0,因为剩下那 1% 一样能构成事故。所以自由文本的默认策略应该是不进日志;确实需要样本的时候走第三层的低采样率,并且明确告知相关方、明确保留期。至于要不要在采样样本上再叠一层识别脱敏,那是加分项,不是主线。

有一种折中做法值得考虑:内容层只存结构化的摘要而不是原文。比如记录「消息共 3 条,角色依次为 system/user/assistant,长度分别为 320/1450/890 字符,检测到疑似敏感模式 2 处」。这种记录对排查大部分格式类、长度类、结构类的问题足够用,又完全不含原文。

三、请求 ID 要贯穿全链路

前面反复提到「请求 ID」,这一节说说它怎么落地。

它要解决的问题是:一次用户投诉,怎么串起从前端点击到最终响应之间的所有环节。没有这条线索,你在网关日志里找到的是一堆时间相近的调用,根本分不清哪条是这位用户的。

实现要点:

在业务侧生成,不要在网关生成。 网关生成的 ID 无法关联到网关之前的环节——用户在哪个页面点的、走了哪个业务接口、命中了哪条业务逻辑分支,这些全丢了。正确的做法是在最靠近用户的那一层生成,然后一路往下传。

用足够长的随机值。 别用自增 ID,也别用可预测的拼接(比如「用户ID + 时间戳」),前者在分布式场景下会撞、后者本身就泄漏信息。UUID 或等长的随机串都可以。

通过请求头透传。 业务侧把它放进调用网关的请求头,网关记录它、并在调用上游时继续带上(如果上游支持自定义头透传的话;不支持就至少在自己这一层记全)。响应里也应该把它带回去,这样前端能拿到,出问题时用户或客服可以直接提供这个 ID。

三层日志都要带。 这是关键。指标层带上它,你能从聚合数据下钻到单次调用;元数据层带上它,你能定位链路;内容层带上它,采样到的样本才能和链路对上。三层用同一个 ID 关联,等于用很低的成本换来了一个可下钻的日志体系。

重试要区分「同一次业务请求」和「同一次上游调用」。 建议两个 ID:业务请求 ID 在重试之间保持不变,上游调用 ID 每次重试都新生成。这样你既能看到「这次业务请求总共重试了三次」,又能分别看每次重试的具体情况。这一点和幂等设计是配套的,展开可以看 接入层幂等设计

别把敏感信息编进 ID。 见过有人为了方便,把租户名、用户手机号后四位拼进请求 ID。这等于把敏感信息塞进了一个会出现在 URL、日志、错误消息、甚至第三方系统里的字段,非常不划算。

四、谁能看:权限要按层分级

三层日志的敏感度不一样,权限就不该一样。一个可以参考的分级:

建议的可见范围说明
指标层较宽。研发、运维、需要看成本的业务负责人不含内容,风险低,开放能减少大量沟通成本
元数据层中等。研发与运维能定位问题,但看不到内容
内容层严格。明确的少数人,最好走申请制含用户数据,按最小必要原则给

几点补充:

内容层的访问最好是「有理由的」而不是「有权限的」。也就是说,即使一个人在名单里,每次访问也应该关联到一个具体事由(某个工单号、某次事故编号)。这不是形式主义——它让「谁在什么时候因为什么看了什么」变得可追溯,而这恰恰是内容层最需要的保护。

审计日志本身也要有访问审计。这句话听起来像绕口令,但它是真问题:如果查看内容日志这个动作本身不被记录,那么整套权限控制在事后是无法验证的。谁看了、看了哪条、什么时候看的,这些记录应该单独存,并且存到一个和主日志不同的地方、由不同的人管。让日志管理员能随意修改自己的访问记录,等于没有审计。

离职和转岗要有回收流程。权限最容易失效的地方不是授予环节,是回收环节。这个和日志系统本身无关,但它会让你前面所有的分级设计归零。

导出要单独管。很多日志系统的查询界面有「导出 CSV」按钮,一按数据就离开了你的权限体系,进了某个人的电脑。内容层应该禁用导出,或者至少让导出成为一个需要额外审批、且被单独记录的动作。

五、保留期与删除:难的是「真的删掉」

设定保留期是容易的事,一行配置。难的是确保到期后数据真的没了。

最常见的问题是「忘了删」,而且往往不是主存储忘了删,是这些地方忘了:

  • 备份。主库配了 30 天 TTL,但每周全量备份保留一年——那这份数据实际上活了一年。
  • 索引。搜索引擎的索引可能和原始数据分开存储、分开配置生命周期。
  • 归档层。为了省钱转到冷存储的那部分,经常在转移时丢掉了原来的 TTL 设置。
  • 下游消费者。日志被同步到数仓、BI 系统、算法团队的特征库之后,那些副本的生命周期是各管各的。
  • 本地导出与临时文件。某次排查时导出的样本文件,躺在某个人的下载目录里。

所以「设定保留期」这件事的正确形态不是配一个参数,而是画一张数据流向图,标出每一个副本落点,逐个确认它的生命周期配置。这张图应该在架构变更时更新——每接入一个新的日志消费者,图上就多一个点。

验证删除比配置删除更重要。 建议做一个定期的抽查动作:随机挑几条超过保留期的记录标识(用请求 ID 就行,它不敏感),在所有落点上查一遍,确认查不到。这个动作可以自动化,跑成周期任务。没有验证的删除策略,本质上只是一个良好的愿望。

至于具体保留多少天/多少月,这里不给数字,也不做任何合规上的判断——具体年限以贵司法务/合规同事的判断为准。工程上你要保证的是:法务说多少天,系统就能真做到多少天,并且能证明做到了。

六、一个常被绕过的路径:错误上报与告警

这是我最想强调的一点,因为它几乎总是被漏掉。

你花了很大力气设计三层日志、设计脱敏、设计权限。然后某天服务报了个异常,异常处理代码里写了这么一行:把请求内容拼进错误消息,方便定位。于是:

  • 这条错误消息进了错误追踪平台(那是另一套系统,有自己的权限模型和保留期,通常比你的日志系统宽松);
  • 它触发了告警,告警内容被推送到了群聊或邮件(群里有多少人?这些消息保留多久?有人会截图吗?);
  • 它可能还进了服务的标准错误输出,被容器平台收集走,进了又一个日志系统。

这三条路径全都绕过了你在网关里精心设计的脱敏逻辑,因为脱敏是在日志写入路径上做的,而异常上报走的是另一条路。

要检查的清单:

  • 异常消息模板。搜一遍代码里构造异常消息的地方,看有没有直接把请求体、消息内容、参数值拼进去的。
  • 未捕获异常的堆栈。有些框架会在堆栈里带上函数参数值,那等于把请求内容整个打出来了。
  • 告警规则的消息体。告警模板里引用的字段,是不是只引用了元数据层的字段。
  • 调试日志。开发环境常见的「打印完整请求」,要确保它在生产环境真的关掉了,而不是靠一个可能被误配的开关。
  • 上游返回的错误。有些上游在报错时会把你发过去的内容原样回显在错误消息里,你如果直接记录上游错误全文,就间接记录了请求内容。
  • 第三方 SDK 的内建日志。有些客户端库自带调试日志,级别调高时会打印完整请求响应。

处理原则和主链路一致:错误消息里只放请求 ID 和元数据层的字段,需要看内容时凭请求 ID 去内容层查(如果这条恰好被采样到)。这样错误上报路径就变成了「不含内容」的路径,前面所有的设计才真正闭合。

应用侧的可观测性设计里也有类似的路径问题,可以对照 应用层可观测性 一起看。

七、上线前检查清单

  • 三层日志已经分开存储,指标层、元数据层、内容层各自有独立的保留期配置,而不是共用一套。
  • 脱敏在写入路径上同步执行,不存在「先落盘后清洗」的环节;脱敏失败时降级为不写内容层,而不是写明文。
  • 全链路请求 ID 从业务侧生成、透传到网关与上游、并在响应中回传;三层日志都带这个 ID。
  • 任何一层日志里都没有 API key、token、签名、Authorization 头的明文——包括错误堆栈和调试输出。
  • 内容层的访问权限独立于其他两层,走名单或申请制;访问动作本身被单独记录,且记录存放在日志管理员改不了的地方。
  • 画出了完整的数据副本流向图(主存储、备份、索引、归档、数仓、下游消费者),每个落点的生命周期都已确认;有周期性的删除验证任务。
  • 检查过错误上报与告警路径:异常消息、未捕获堆栈、告警模板、第三方 SDK 调试日志里都不含请求内容。
  • 保留期与脱敏范围的最终口径已经与法务/合规同事确认过——工程上只负责让口径能被准确落实。

最后一句:这套东西的价值不在于「日志做得漂亮」,而在于当有人问你「三个月前那次调用到底发生了什么」和「用户数据有没有在日志里躺着」时,你两个问题都能给出有依据的回答。这两个回答不冲突,前提是你在设计的时候就把它们当成两件事来处理。

相关阅读