released software 应保留多少 traces 才能调试多数问题?
结论
没有通用标准规定发布版本必须保留多少条 traces,也没有可靠的 100 / 70 / 30 / 10 分配比例。
日志是否有价值,要看事件频率、单条日志大小、诊断用途和运行环境,不能只看代码里有多少条 log statements。生产版本一般需要保留能够反映系统状态、异常和关键业务结果的 INFO、WARN、ERROR 日志,并支持按 component、用户或请求临时开启 DEBUG/TRACE。实际输出量应根据性能和容量预算确定。
软件成熟后,也不建议直接把全局日志级别从 INFO 永久提高到 WARN 或 ERROR,否则可能丢失启动、配置变更、版本切换、任务完成等正常但重要的运行记录。
为什么不能按条数分配
假设两个 component 都定义了 100 个日志点:
- Component A 的某条
INFO只在每次启动时执行一次。 - Component B 的某条
INFO位于高频消息循环中,每秒可能执行数万次。
两者的日志点数量相同,实际产生的数据量以及 CPU、I/O 开销可能相差几个数量级。字段数量、字符串格式化成本、调用栈和同步写盘方式也会影响单条日志的性能开销。
更适合衡量日志规模的指标是:
日志带宽 = 平均事件频率 × 平均单条日志大小
每日容量 = 日志带宽 × 86400
同时还要测量日志对 CPU、内存、磁盘 I/O 和请求延迟的影响。
各 log level 应表达什么
log level 应反映事件的性质和严重程度,不能用来控制某个阶段要保留多少条日志。
TRACE
用于记录很细的执行过程,如状态机迁移、协议字段和函数内部步骤。默认关闭,只在有限范围内短时间启用。
DEBUG
用于记录开发和深入诊断所需的信息,如分支选择、缓存命中、重试过程和内部状态摘要。生产环境可以保留这些日志点,但通常不默认输出。
INFO
用于记录低频且对运维有用的正常事件,例如:
- component 启动、停止和版本信息;
- 配置加载或变更;
- 后台任务开始、完成和结果摘要;
- 外部依赖连接状态变化;
- 重要业务操作成功,但不包含敏感内容。
INFO 不适合记录循环的每次迭代、每个消息包或每次函数调用。
WARN
表示发生了异常,但系统已经恢复、进入降级状态或仍可继续工作。例如,超时后重试成功、切换到备用服务,或者队列接近容量上限。
如果值班人员看到某条 WARN 后不需要采取行动,也无法借此诊断问题,这条日志可能更适合放在 INFO 或 DEBUG 级别。
ERROR
表示某项操作失败、数据可能丢失,或者问题需要人工关注。ERROR 应提供足够的上下文,但不要在异常向上传播的每一层重复记录同一个错误。
捕获异常后,如果当前代码只补充上下文并继续向上抛出,通常应由最终处理异常的边界统一记录一次。
建立可执行的日志规范
1. 先定义诊断场景
先列出发布版本中常见且影响较大的问题,例如:
- component 无法启动;
- 跨 component 请求超时;
- 消息丢失或重复;
- 状态不一致;
- 外部依赖不可用;
- 资源耗尽;
- 某个用户请求执行失败。
再检查现有日志能否回答这些问题:
发生了什么?
发生在什么时候?
涉及哪个 component、实例和版本?
哪个请求或任务受到影响?
请求经过了哪些 components?
失败原因和最终处理结果是什么?
如果重复日志无法帮助回答这些问题,数量再多也没有太大用处。
2. 为跨 component 调用传递关联标识
在多 component 系统中,能否关联不同日志,往往比日志数量更重要。每个请求至少应携带稳定的 trace_id,必要时还可以加入 span_id、request_id 或 job_id。
public Result handle(Request request) {
String traceId = request.getTraceId();
logger.info(
"task_started trace_id={} task_id={} component={}",
traceId,
request.getTaskId(),
"scheduler"
);
try {
Result result = worker.execute(request);
logger.info(
"task_completed trace_id={} task_id={} status={}",
traceId,
request.getTaskId(),
result.status()
);
return result;
} catch (TimeoutException e) {
logger.error(
"task_failed trace_id={} task_id={} dependency={} reason=timeout",
traceId,
request.getTaskId(),
"worker-service",
e
);
throw e;
}
}
所有 components 应统一字段名,避免一个 component 使用 requestId,另一个却使用 req_no。
3. 控制高频路径
循环、消息处理和重试路径很容易引发日志洪泛。可以通过聚合、采样或限速控制输出,同时保留必要的诊断信息。
long failedCount = 0;
long lastReportTime = System.currentTimeMillis();
for (Message message : messages) {
try {
process(message);
} catch (RecoverableException e) {
failedCount++;
logger.debug(
"message_processing_failed message_id={} reason={}",
message.id(),
e.getMessage()
);
}
long now = System.currentTimeMillis();
if (now - lastReportTime >= 60_000) {
logger.warn(
"message_failures_summary failed_count={} window_seconds=60",
failedCount
);
failedCount = 0;
lastReportTime = now;
}
}
这样设置后,默认级别只记录一分钟内的失败摘要。需要调查具体问题时,再临时开启 DEBUG 查看单个失败事件。
审计记录、计费结果、安全事件以及无法从其他数据中恢复的失败信息,不应随意采样。
4. 避免关闭级别后仍执行昂贵计算
有些日志框架支持延迟格式化参数,但昂贵的对象转换仍可能在日志方法调用前执行。必要时应先检查对应级别是否启用:
if (logger.isDebugEnabled()) {
logger.debug(
"scheduler_state task_id={} state={}",
taskId,
buildDetailedStateSnapshot()
);
}
优先使用参数化日志:
logger.debug("queue_depth component={} depth={}", componentName, queueDepth);
避免提前拼接字符串:
logger.debug("queue_depth component=" + componentName + " depth=" + queueDepth);
具体开销取决于使用的 logging framework,应针对实际版本和配置进行基准测试。
5. 设定资源预算
日志规范应规定资源预算,不应规定各级别所占的百分比。例如:
logging_budget:
steady_state:
max_bytes_per_second: <根据容量测试确定>
max_cpu_overhead_percent: <根据性能目标确定>
burst:
max_duration_seconds: <根据存储和告警能力确定>
retention:
production_days: <根据业务、合规和成本要求确定>
尖括号中的数值必须通过实际负载测试确定,不能直接照搬其他系统的数据。测试需要覆盖正常流量、峰值流量、依赖故障和重试风暴。
推荐的发布策略
可以采用以下发布策略:
- 在代码中保留必要的
TRACE和DEBUG日志点。 - 发布版本默认输出低频
INFO以及全部有效的WARN、ERROR。 - 支持按 component 动态调整日志级别,并尽量避免因调整级别而重启。
- 允许按
trace_id、用户、任务或实例限时开启诊断。 - 使用内存 ring buffer 保存最近一段时间的详细事件,在发生错误时导出,容量按内存预算确定。
- 对重复错误进行限速和聚合,保留首次出现时间、累计次数和最后一次出现时间。
- 设置日志轮转、压缩、保留期限和磁盘上限。
- 将日志开销纳入持续性能测试,防止新增日志点逐步突破预算。
需要特别注意的问题
日志中不要记录密码、访问令牌、私钥、完整 session、支付数据或不必要的个人信息。用户输入、协议报文和对象序列化结果也要设置长度上限,避免单个异常请求产生大量日志。
还应避免以下问题:
- 在多个调用层重复记录同一个异常;
- 将预期的输入错误记为
ERROR; WARN长期大量出现,却没有对应的告警或处理动作;- 只记录“操作失败”,没有 component、操作对象、错误原因和关联标识;
- 为减少日志数量而删除关键上下文,导致剩余日志无法关联;
- 使用日志记录所有指标。吞吐量、延迟、错误率和资源使用更适合交给 metrics 系统统计;
- 使用日志代替审计记录。审计数据通常需要独立的完整性保障、访问控制和保留策略。
日志点应足以重建关键事件链,再通过级别、动态开关、关联标识、采样、聚合和容量限制控制实际输出。日志是否足够,应由故障演练和性能测试验证,不能根据固定比例推导。
备注:内容仅供参考。
