3、日志级别详解(下):ERROR、CRITICAL 级别的定义、使用场景与最佳实践

好,咱们接着聊日志级别。上一节我们把 WARNING 和 INFO 聊透了,这一节来点「硬核」的——ERROR 和 CRITICAL。

这两个级别,说白了就是系统出问题时的「报警器」。用得好,你能在故障发生的第一时间定位问题;用不好,要么被海量错误日志淹没,要么漏掉真正致命的信号。

我个人习惯是:ERROR 是「还能抢救一下」,CRITICAL 是「准备后事吧」。嗯,话糙理不糙。

一、ERROR 级别:业务逻辑的「红灯」

先说说 ERROR。这个级别表示系统发生了错误,但不影响整体运行。比如用户请求参数不对、数据库连接超时、第三方接口返回异常——这些都属于 ERROR。

你想想看,一个电商网站,用户下单时库存不足,这算不算错误?算。但系统会因此崩溃吗?不会。所以它应该记成 ERROR,而不是 CRITICAL。

1. ERROR 的典型场景

  • 业务逻辑异常:比如支付失败、订单状态流转错误
  • 外部依赖故障:比如 Redis 连接超时、消息队列发送失败
  • 数据校验不通过:比如用户上传的文件格式不对
  • 资源不足:比如磁盘空间不足、连接池耗尽

我在项目中遇到过一件事:有个同事把所有数据库查询失败都记成了 CRITICAL。结果线上一天报警几百次,运维直接把他拉黑了。你想想,查询超时重试一次就好了,哪至于「致命」?

2. ERROR 的最佳实践

核心原则:ERROR 要「可恢复」

记录 ERROR 时,一定要带上足够的上下文信息,方便排查。

import logging

logger = logging.getLogger(__name__)

def process_order(order_id: int, user_id: int):
    try:
        # 假设这里调用支付接口
        result = payment_service.charge(order_id, user_id)
    except PaymentTimeoutError as e:
        # 记录 ERROR,带上关键参数
        logger.error(
            "支付超时 | order_id=%s, user_id=%s, error=%s",
            order_id, user_id, str(e)
        )
        # 这里可以重试或返回用户友好的提示
        return {"code": 500, "msg": "支付服务暂时不可用"}
    except Exception as e:
        # 未知异常,同样记 ERROR,但需要告警
        logger.error(
            "支付异常 | order_id=%s, user_id=%s, error=%s",
            order_id, user_id, str(e), exc_info=True
        )
        return {"code": 500, "msg": "系统繁忙"}

小技巧:ERROR 日志一定要包含 exc_info=Truestack_info=True,否则你拿到日志也看不出堆栈,排查起来很痛苦。

二、CRITICAL 级别:系统的「心脏骤停」

CRITICAL,顾名思义,是「致命的」。系统已经无法继续正常运行,必须立即人工介入。

我曾经在维护一个金融系统时,遇到过数据库主库宕机。当时 CRITICAL 日志瞬间刷屏,监控系统直接打电话把我从睡梦中叫醒。嗯,那种感觉,一辈子忘不了。

1. CRITICAL 的典型场景

  • 核心服务不可用:比如数据库主库挂了、消息队列彻底不可用
  • 数据完整性被破坏:比如检测到数据不一致、关键表被误删
  • 安全事件:比如检测到 SQL 注入攻击、未授权访问
  • 系统资源耗尽:比如内存溢出、磁盘写满导致无法写入日志

⚠️ 注意:CRITICAL 级别的日志,必须触发告警。短信、电话、钉钉、邮件——至少有一种方式能立刻通知到值班人员。

2. CRITICAL 的最佳实践

我个人建议:CRITICAL 日志要「少而精」。如果一天能收到几十条 CRITICAL 告警,那说明你的日志级别设置有问题。

def check_database_health():
    """数据库健康检查"""
    try:
        db.session.execute("SELECT 1")
    except OperationalError as e:
        # 数据库连接失败,这是致命的
        logger.critical(
            "数据库不可用 | host=%s, port=%s, error=%s",
            DB_HOST, DB_PORT, str(e), exc_info=True
        )
        # 发送告警
        alert_system.send_sms("数据库不可用,请立即处理!")
        # 尝试切换到备用库
        switch_to_backup_database()
    except Exception as e:
        # 其他异常,记 ERROR 即可
        logger.error(
            "数据库检查异常 | error=%s", str(e), exc_info=True
        )

避坑指南:我曾经见过一个项目,把「用户登录失败」也记成了 CRITICAL。结果每天几万条 CRITICAL 日志,真正的数据库宕机反而被淹没了。记住:CRITICAL 只留给「系统级」的致命错误

三、ERROR vs CRITICAL:到底怎么分?

很多新手会纠结:这个异常到底该记 ERROR 还是 CRITICAL?

我教你一个简单的方法:问自己一个问题——系统还能继续运行吗?

场景 级别 理由
用户请求参数错误 ERROR 只影响当前请求,系统正常
数据库连接池耗尽 CRITICAL 所有依赖数据库的功能都会受影响
第三方 API 超时 ERROR 可以重试或降级处理
主从同步延迟过大 CRITICAL 可能导致数据不一致,必须立即处理
磁盘空间不足 80% WARNING 还能撑一段时间,提前预警
磁盘空间不足 95% CRITICAL 随时可能写不进日志,系统面临崩溃

我的经验:如果你实在拿不准,就先用 ERROR。因为 CRITICAL 的告警成本很高(电话、短信、值班人员响应),误报多了大家会麻木。宁可漏报一次 CRITICAL,也不要误报一百次。

四、日志缓冲机制与 ERROR/CRITICAL 的关系

说到缓冲机制,这里有个坑我要提醒你。

很多日志框架默认会使用缓冲区,把日志先攒起来再批量写入。但 CRITICAL 级别的日志,绝对不能进缓冲区

为什么?你想想看,如果系统已经快挂了,你还把日志攒在内存里,万一还没来得及写入就崩溃了,那这条 CRITICAL 日志就永远消失了。

import logging
import logging.handlers

class CriticalImmediateHandler(logging.Handler):
    """CRITICAL 级别的日志立即写入,不经过缓冲区"""
    def emit(self, record):
        if record.levelno >= logging.CRITICAL:
            # 立即写入文件
            with open("critical.log", "a") as f:
                f.write(self.format(record) + "\n")
            # 同时发送告警
            send_alert(record)

# 配置日志
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# 普通日志走缓冲区
buffer_handler = logging.handlers.MemoryHandler(
    capacity=100,  # 攒够100条再写
    target=logging.FileHandler("app.log")
)
logger.addHandler(buffer_handler)

# CRITICAL 日志立即写入
critical_handler = CriticalImmediateHandler()
critical_handler.setLevel(logging.CRITICAL)
logger.addHandler(critical_handler)

⚠️ 重要提醒:如果你的日志系统使用了异步写入或缓冲区,请务必为 CRITICAL 级别设置「直通模式」。否则,系统崩溃时你连最后的「遗言」都看不到。

五、总结一下

好了,这一节的内容就这些。我帮你理一下重点:

  • ERROR:业务逻辑出问题,但系统还能跑。记得带上上下文和堆栈。
  • CRITICAL:系统要挂了,必须立即人工介入。少用、精用、必须触发告警。
  • 缓冲机制:CRITICAL 日志要绕过缓冲区,确保「最后一口气」能写出去。

下一节我们会聊日志的格式化与输出目标配置,到时候我会分享一些我在微服务架构中踩过的坑。嗯,敬请期待。