3. 日志级别详解(DEBUG、INFO、WARN、ERROR)
日志级别这东西,说白了就是给调试信息贴标签。你贴得越细,排查问题越快。我其实见过不少同学,一上来就全用 printf,结果线上出问题,日志刷屏根本找不着重点。用对级别,就像给工具箱分了层——哪个螺丝刀干什么活,心里有数。
3.1 DEBUG 级别——开发者的显微镜
DEBUG 日志记录非常详细的信息。通常只在开发环境、单元测试或复现特定 bug 时开启。生产环境要关了它,否则性能扛不住,日志也会爆炸。
我个人习惯,在加密 SDK 的签名、验签、密钥派生这类核心函数里,埋最细的入参、中间变量和出参。举个例子:
// 加密SDK内部,伪代码
int sdk_sign(const uint8_t *data, size_t len, uint8_t *sig) {
log_debug("sdk_sign: data len=%zu, data[0..15]=%02x%02x...", len, data[0], data[1]);
int ret = internal_hmac_sha256(data, len, sig);
log_debug("sdk_sign: internal_hmac_sha256 returned %d, sig=%02x%02x...", ret, sig[0], sig[1]);
return ret;
}
我在项目里遇到过一件糗事:同一条请求,在 Windows 上签名正确,到 Linux 上就失败。开了 DEBUG 日志一看,哦,内部 hash 函数在处理传入的长度时,两个平台的 sizeof(size_t) 不同,数据截断出问题了。要是没这些细粒度的 DEBUG 日志,我可能还在怀疑网络字节序呢。
log_debug("private key hex: ...") 这种写法,果断要求删掉。真要调试密钥相关,用脱敏后的哈希值或部分摘要。
3.2 INFO 级别——日常巡检的仪表盘
INFO 日志记录关键的执行步骤、状态变更或配置加载。不追求细节,但要能串联起一次完整调用的大致流程。
我会在下面这些地方打 INFO:
- SDK 初始化完成:
log_info("SDK initialized, version 2.3.0, provider HW") - 每次加解密操作开始和结束:
log_info("Encrypt session started, seq=%u", seq_id) - 会话超时或重连事件:
log_info("Session timeout, initiating re-handshake")
你想想看,线上出问题时,运维第一反应就是看 INFO 日志。如果每条都有清晰的时间戳和操作名,定位到异常大概发生在哪个请求、哪个时间点,那就省了一半排查时间。
3.3 WARN 级别——黄灯预警,不能忽视
WARN 表示“可能出现问题”,但当前还能继续运行。比如证书即将过期、重试次数接近上限、使用了降级算法、条件分支走了非最佳路径。
我曾经吃过一次亏:线上服务突然变慢,查了半天才发现,早在三周前就有 WARN 日志 “HW module not available, fallback to SW crypto, performance may degrade”。所有人都觉得“应急降级嘛,正常”,结果积少成多,CPU 用满才暴露。嗯,这里要注意——WARN 是给开发者看的“潜在炸弹”,不能随手忽略。
常见的 WARN 场景:
- 证书有效期剩 30 天
- 硬件安全模块(HSM)返回重试错误
- 输入参数非规范但可容错处理
- 随机数源熵不足,改用软件伪随机
3.4 ERROR 级别——必须立刻响应的红灯
ERROR 指已经发生的错误,导致当前操作失败,但 SDK 整体仍可继续服务其他请求。比如签名验证失败、内存分配失败、网络发送超时、密钥不存在。
这个时候,日志必须包含足够多的上下文,让你能复现问题或快速定位故障点:
log_error("verify_signature: signature mismatch, key_id=%u, data_hash=%s, expected_sig=%s, actual_sig=%s",
key_id, hash_hex, expected_hex, actual_hex);
我自己一般会在 ERROR 日志里附加以下信息(视安全策略决定):
- 操作名称、会话 ID、请求 ID
- 错误码及其对应说明
- 关键输入摘要(非完整原文)
- 当前 SDK 状态快照(如硬件模块是否在线)
要注意避免“假报 ERROR”。比如内部重试机制第一次失败,不一定算 ERROR;等所有重试都耗尽,再记录一次 ERROR 就好。我曾经接手过一个项目,里面网络库在每次 socket 连接失败时都打 ERROR,哪怕后面重试成功了。结果监控系统天天告警,大家都麻了,真出事儿反而被淹没。
3.5 级别选择与使用原则
上面四个级别怎么选?我归纳了一张表,你存下来,下次写日志的时候扫一眼:
| 级别 | 生产环境 | 主要作用 | 典型输出频率(单次调用) |
|---|---|---|---|
| DEBUG | 关闭 | 开发/调式阶段细节追踪 | 10~50 条 |
| INFO | 开启 | 记录关键流程和里程碑 | 2~5 条 |
| WARN | 开启 | 标记非致命但需关注的异常 | 0~1 条 |
| ERROR | 开启 | 记录已发生的失败操作 | 0~1 条(重试耗尽后) |
还有一个决策小技巧:如果你不确定一条日志该用哪个级别,就反问——这条信息,正常运行时我需不需要每天看?如果答案是“需要”,INFO;如果是“可能有用但不用天天看”,WARN 或 DEBUG;如果是“永远不会出现”,那就别打日志了,直接断言或返回错误。
画个图把这四个级别的关系串起来,方便你记忆:
最后给出一个比较完整的日志范例,把四个级别都用到同一段加密操作的场景中,你感受一下:
int sdk_encrypt(const uint8_t *plaintext, size_t len, uint8_t *ciphertext) {
log_debug("enter sdk_encrypt, len=%zu, first_8_bytes=%02x%02x...", len, plaintext[0], plaintext[1]);
if (!hsm_available) {
log_warn("HSM not available, using software AES (slower)");
}
log_info("encrypt session started, algo=AES-256-GCM, key_id=%u", key_id);
int ret = crypto_aes_gcm_encrypt(plaintext, len, ciphertext);
if (ret != 0) {
log_error("encrypt failed, err=%d, plaintext_hash=%s", ret, sha256_hex(plaintext, len));
return -1;
}
log_info("encrypt succeeded, ciphertext_len=%zu", len); // GCM 不改变长度
log_debug("exit sdk_encrypt, ciphertext_first_8=%02x%02x...", ciphertext[0], ciphertext[1]);
return 0;
}
你看,每个级别各司其职:DEBUG 给开发者看内部细节,INFO 记录调用流程,WARN 标记降级事件,ERROR 在出错时带上足够上下文。这样组合下来,线上排障基本不慌。