3. 日志级别设计:DEBUG、INFO、WARN、ERROR、FATAL 的定义与使用场景、级别动态调整

日志级别这东西,说白了就是给日志信息贴个标签。

告诉系统:这条消息有多重要?该不该记?该不该报警?

我见过不少团队,一开始图省事,所有日志全用 INFO。结果线上出问题,日志文件几分钟就撑爆磁盘。嗯,这就是典型的「级别设计没做好」。

3.1 五大日志级别的定义

咱们从最轻到最重,一个一个说。

级别 数值 含义 典型颜色
DEBUG 0 调试信息,开发阶段用 灰色
INFO 1 正常运行的状态记录 绿色
WARN 2 潜在问题,不影响运行 黄色
ERROR 3 功能异常,需要关注 红色
FATAL 4 致命错误,进程可能退出 深红/紫红
数值越小,优先级越低。 设置日志级别为 WARN 时,只会输出 WARN 及以上(WARN、ERROR、FATAL)的日志。

3.2 各级别的使用场景

DEBUG

我个人习惯,DEBUG 日志只出现在开发环境和测试环境。

比如:

  • 函数的入参和返回值
  • 循环中的中间变量
  • 条件分支的走向

线上环境开 DEBUG?我劝你别这么干。我曾经在一个高并发服务上误开了 DEBUG 级别,结果日志量暴涨 100 倍,磁盘 IO 直接打满。那叫一个惨。

INFO

INFO 是「正常业务轨迹」的记录。

比如:

  • 用户登录成功
  • 订单创建完成
  • 定时任务执行结束

你想想看,如果线上出了故障,你第一件事就是翻 INFO 日志,看请求走到哪一步了。所以 INFO 要写得「可读性强」,别整一堆机器码。

WARN

WARN 是个容易被忽略的级别。

它表示「系统还能跑,但有点不对劲」。

举个例子:

  • 接口响应时间超过 1 秒(但没超时)
  • 重试了 2 次才成功
  • 配置项使用了默认值

我建议把 WARN 当成「预警信号」。别等到 ERROR 才慌,WARN 就是给你提前打预防针的。

ERROR

ERROR 意味着「某个功能挂了」。

比如:

  • 数据库连接失败
  • 第三方接口返回 500
  • 参数校验不通过

这里有个坑:ERROR 日志一定要带上上下文。光写一句「查询失败」有什么用?你得告诉我:查的哪个表?参数是什么?异常堆栈呢?

ERROR 日志的黄金法则: 让看到日志的人,不需要翻代码就能定位问题。

FATAL

FATAL 是最严重的级别。系统已经无法继续运行了。

比如:

  • 内存溢出
  • 配置文件加载失败
  • 核心依赖服务不可用

FATAL 日志通常伴随着进程退出。所以,不要滥用 FATAL。我见过有人把「用户输入错误」也记成 FATAL,这明显不合适。

3.3 级别动态调整

为什么需要动态调整?

你想想看:线上跑得好好的,突然某个接口报错了。你想看 DEBUG 日志,但当时没开。重启改配置?太慢了。等下次复现?不知道什么时候。

所以,运行时动态调整日志级别 是运维的必备能力。

实现方式

常见的有三种:

  1. API 接口:暴露一个 HTTP 端点,比如 POST /loglevel?level=DEBUG
  2. 配置中心:通过 Apollo、Nacos 等动态推送
  3. 信号机制:比如给进程发 SIGUSR1 信号切换级别

我个人最推荐 API 方式。简单、直接、可控。

代码示例(Python)

import logging
from flask import Flask, request

app = Flask(__name__)
logger = logging.getLogger("my_app")

@app.route("/loglevel", methods=["POST"])
def set_log_level():
    level = request.json.get("level", "").upper()
    if level in ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]:
        logger.setLevel(getattr(logging, level))
        return {"status": "ok", "level": level}
    return {"status": "error", "msg": "invalid level"}, 400

if __name__ == "__main__":
    app.run(port=8080)

代码示例(Go)

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strings"
)

var logLevel = "INFO"

func setLogLevel(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Level string `json:"level"`
    }
    json.NewDecoder(r.Body).Decode(&req)
    level := strings.ToUpper(req.Level)
    switch level {
    case "DEBUG", "INFO", "WARN", "ERROR", "FATAL":
        logLevel = level
        w.Write([]byte(`{"status":"ok"}`))
    default:
        w.WriteHeader(400)
        w.Write([]byte(`{"status":"error"}`))
    }
}

func main() {
    http.HandleFunc("/loglevel", setLogLevel)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
注意: 动态调整级别后,要记录一条日志,说明「谁在什么时间把级别改成了什么」。不然排查问题时,你都不知道级别被人动过。

3.4 级别设计的避坑指南

  • 别把敏感信息写进日志。密码、身份证号、银行卡号,这些不能出现在任何级别里。
  • 别在循环里打 DEBUG。尤其是 for 循环,一次请求可能打几万条日志。
  • ERROR 和 FATAL 要配报警。光记日志没人看,等于白记。
  • 级别要分层。不同模块可以设置不同的级别。比如核心交易模块用 INFO,非核心模块用 WARN。

我曾经接手过一个项目,所有日志全是 ERROR。你猜怎么着?大家看多了,反而对 ERROR 麻木了。真正出问题的时候,没人当回事。

所以,级别不是越严重越好,而是越准确越好。

3.5 小结

日志级别设计,其实就三句话:

  1. DEBUG 给开发看,INFO 给运维看,WARN 给预警看,ERROR 给人看,FATAL 给机器看。
  2. 级别要能动态调,别让重启成为唯一手段。
  3. 别滥用,别乱用,别不分类。

下一章,咱们聊聊日志格式的设计。嗯,那也是个容易踩坑的地方。