2. 日志基础

日志这东西,说白了就是系统的「黑匣子」。API 出问题的时候,你第一个翻的就是它。我见过太多团队,日志写得乱七八糟,出了事故根本没法追溯。今天咱们就把日志基础打扎实。

日志级别:从 DEBUG 到 FATAL

日志级别不是拍脑袋定的。每个级别都有明确的语义,用错了反而添乱。我个人习惯把这五个级别分成三类:调试类、告警类、灾难类。

级别 含义 典型场景 生产环境是否开启
DEBUG 调试信息 变量值、函数入口/出口、SQL 语句 ❌ 关闭
INFO 常规信息 请求开始/结束、用户登录、配置加载 ✅ 开启
WARN 警告 重试、降级、配置缺失但使用默认值 ✅ 开启
ERROR 错误 请求失败、数据库连接超时、业务异常 ✅ 开启
FATAL 致命错误 OOM、磁盘写满、配置严重错误 ✅ 开启

核心原则:生产环境只开 INFO 及以上级别。DEBUG 日志量太大,会拖垮磁盘 IO。我曾经接手过一个项目,生产环境开着 DEBUG 级别,一天产生 200GB 日志,结果磁盘三天就满了——嗯,那场面挺狼狈的。

这里有个容易踩的坑:WARN 和 ERROR 的边界。我见过有人把「请求超时重试」写成 ERROR,其实这应该是 WARN。ERROR 意味着当前请求已经失败,需要人工介入;WARN 只是告诉你「有点不对劲,但系统自己处理了」。你想想看,如果所有异常都打 ERROR,告警系统会把你烦死的。

日志格式标准:别让解析器崩溃

日志格式不统一,是 API 审计的噩梦。我建议所有日志都采用 结构化格式,比如 JSON。为什么?因为机器好解析,人也好读。

一个标准的 API 日志条目,至少包含以下字段:

  • timestamp:ISO 8601 格式,带时区
  • level:日志级别
  • logger:产生日志的类或模块名
  • trace_id:请求追踪 ID,用于串联上下游
  • message:日志正文
  • context:上下文信息(用户 ID、请求路径、耗时等)

举个例子,这是我常用的格式:

{
  "timestamp": "2025-03-21T14:32:10.123+08:00",
  "level": "ERROR",
  "logger": "com.api.gateway.AuthFilter",
  "trace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "message": " 验证失败",
  "context": {
    "user_id": "u_10086",
    "request_path": "/api/v2/orders",
    "method": "POST",
    "duration_ms": 45,
    "error_detail": "JWT 签名过期"
  }
}

我的习惯:所有日志都带上 trace_id。这样你从网关到微服务,再到数据库,一条链路全串起来。排查问题的时候,直接 grep trace_id 就能拿到完整上下文,不用东翻西找。

为什么强调 JSON?因为非结构化日志(比如纯文本)解析起来太痛苦了。我曾经遇到过一个系统,日志格式是 [时间] 级别 - 消息,结果时间格式不统一,有的带毫秒有的不带,解析脚本写了一百多行——后来我直接让开发改成了 JSON。

日志轮转策略:别让磁盘爆炸

日志不轮转,磁盘迟早会满。这不是危言耸听。我见过一个线上事故:日志文件涨到 50GB,直接把磁盘撑爆了,导致整个服务不可用。所以轮转策略必须提前规划好。

常用的轮转策略有三种:

策略 触发条件 适用场景 注意事项
基于大小 文件达到指定大小(如 100MB) 日志量稳定的系统 避免设置过小,频繁轮转影响性能
基于时间 每天/每小时轮转一次 日志量波动大的系统 配合保留天数使用
混合策略 大小或时间任一条件满足 大多数生产系统 推荐使用

以 Java 的 Logback 为例,一个典型的配置长这样:

<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>/var/log/api-gateway.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <fileNamePattern>/var/log/api-gateway.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
    <maxFileSize>100MB</maxFileSize>
    <maxHistory>30</maxHistory>
    <totalSizeCap>10GB</totalSizeCap>
  </rollingPolicy>
</appender>

这个配置的意思是:每天一个日志文件,单个文件超过 100MB 就自动切分,保留最近 30 天,总大小不超过 10GB。你想想看,这样既不会丢日志,也不会撑爆磁盘。

避坑指南:我曾经遇到过一个问题——日志轮转后,旧日志文件被删除了,但进程还持有文件句柄,导致磁盘空间没释放。这是因为 Linux 下删除文件只是解除链接,文件句柄还在。解决方案是:轮转后通知进程重新打开日志文件,或者使用 logrotate 的 copytruncate 模式。

知识体系总览

下面这张图把日志基础的核心逻辑串起来了。从日志产生到最终归档,每一步都有讲究。

日志基础核心流程 日志级别 日志格式 日志轮转 日志存储 级别细分 DEBUG / INFO / WARN ERROR / FATAL 生产环境只开 INFO+ 格式标准 timestamp / level trace_id / message context 上下文 轮转策略 基于大小 / 基于时间 混合策略(推荐) 保留天数 + 总大小上限 存储与归档 本地磁盘 / 对象存储 冷热分层 定期清理过期日志 核心目标:可追溯、可解析、不丢数据、不撑磁盘 日志级别控制噪音,格式标准保证可读,轮转策略保障存储安全

实战建议:从第一天就规范起来

日志这东西,前期偷懒,后期加倍还债。我建议你从项目第一天就定好规范:

  1. 统一日志框架:整个团队用同一个日志库,别各写各的
  2. 日志级别评审:每次代码评审都看一眼日志级别用得对不对
  3. 轮转策略自动化:用配置中心管理,别手动改服务器配置
  4. 定期检查磁盘:写个脚本每天检查日志目录大小,超过阈值就告警

一个小技巧:我习惯在日志里加一个 @version 字段。每次改日志格式就升版本号。这样下游的日志解析系统可以根据版本号自动适配,不会因为格式变更导致解析失败。

好了,日志基础就聊到这儿。记住一句话:日志是系统的第一道防线。级别、格式、轮转,这三件事做好了,后面做审计和分析才能事半功倍。