日志基础:日志级别与结构化日志设计

日志这东西,说白了就是系统的「黑匣子」。我刚开始做运维那会儿,觉得日志嘛,能打出来就行。直到有一次线上故障,翻了几万行日志全是 INFO,关键错误被淹没了——嗯,那次教训让我彻底明白了日志级别的重要性。

一、日志级别的正确打开方式

先说说五个标准级别。别看它们简单,用错了会出大问题。

级别 含义 我建议的使用场景
DEBUG 调试信息 开发环境、问题排查时临时开启
INFO 正常流程 用户登录、订单创建、定时任务执行
WARN 潜在问题 磁盘使用率超过80%、重试次数超过阈值
ERROR 功能异常 数据库连接失败、API调用返回500
FATAL 致命错误 内存溢出、配置加载失败导致服务无法启动
⚠️ 我曾经踩过的坑: 把业务异常直接打成了 FATAL。结果告警系统每分钟都在报警,运维同事差点把我拉黑。FATAL 只留给「服务活不下去了」的场景,其他异常用 ERROR 就够了。

二、级别使用原则

我个人习惯遵循「金字塔原则」:

  • INFO 要克制:每个请求只打一条核心日志,别把 SQL 查询结果都打出来
  • WARN 要敏感:任何可能演变成问题的苗头,都值得记录
  • ERROR 要完整:必须包含上下文信息,比如请求ID、参数、堆栈
  • FATAL 要极少:一年出现个位数次,才算正常

你想想看,如果 ERROR 日志一天几十万条,那跟没有告警有什么区别?

三、结构化日志设计

传统日志长这样:

2024-01-15 10:30:22 ERROR 用户下单失败:null

这种日志,说白了就是废的。你根本不知道哪个用户、哪个订单、为什么失败。

结构化日志就不一样了:

{
  "timestamp": "2024-01-15T10:30:22.123Z",
  "level": "ERROR",
  "logger": "OrderService",
  "traceId": "a1b2c3d4",
  "userId": 10086,
  "orderId": "ORD20240115001",
  "message": "用户下单失败",
  "error": {
    "type": "StockInsufficient",
    "detail": "商品ID: 9527, 库存: 0, 请求: 1"
  },
  "duration": 2345
}
核心要点: 结构化日志必须包含 traceId、时间戳、级别、业务关键字段。这样你才能在 ELK 里快速聚合、过滤、分析。

四、知识体系结构图

下面这张图,是我自己总结的日志设计核心逻辑:

日志设计核心逻辑 日志级别 结构化设计 输出与存储 DEBUG / INFO / WARN ERROR / FATAL JSON 格式 traceId + 上下文 文件 / 标准输出 日志中心 / ELK 目标:可读、可查、可告警、可追溯 级别准确 + 结构清晰 = 高效排障

五、实战中的避坑指南

💡 我的经验: 日志级别一定要支持运行时动态调整。我曾经在排查问题时,需要临时开启 DEBUG 日志,但又不想重启服务。后来我们给日志框架加了个 API,通过配置中心就能实时切换级别。

还有几个细节要注意:

  • 不要打印敏感信息:密码、身份证号、银行卡号,一律脱敏
  • 控制日志大小:单条日志建议不超过 1KB,太长会影响性能
  • 统一时间格式:全系统用 ISO 8601,别有的用时间戳有的用字符串
  • ERROR 必须带堆栈:但别把整个堆栈打满,截取前 10 行就够了
⚠️ 我曾经犯过的错: 在循环里打 DEBUG 日志,结果日志量暴增 100 倍,直接把磁盘打满了。从那以后,我要求所有循环内的日志必须加频率限制。

六、代码示例

以 Java 的 Logback 为例,一个合理的配置:

<!-- logback-spring.xml -->
<configuration>
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="JSON"/>
    </root>

    <!-- 业务模块可以单独调级别 -->
    <logger name="com.example.order" level="DEBUG" additivity="false">
        <appender-ref ref="JSON"/>
    </logger>
</configuration>

代码里怎么打日志?我推荐这样:

// 好的做法
log.info("用户下单成功", 
    kv("userId", userId), 
    kv("orderId", orderId),
    kv("amount", amount));

// 不好的做法
log.info("用户" + userId + "下单成功,订单号:" + orderId);

前者是结构化日志,后者是字符串拼接。你想想看,在 ELK 里搜索 userId=10086 和搜索「用户10086下单成功」,哪个更方便?

嗯,日志级别和结构化设计,说白了就是「在正确的地方,用正确的格式,记录正确的信息」。别小看这基础功夫,我见过太多系统因为日志没设计好,排查问题要多花几倍时间。


专注资料整理