日志格式化规范:JSON格式标准、关键字段定义、日志脱敏规则
聊到日志格式化,很多同学第一反应是「不就是把日志打成JSON吗?」。嗯,这话对了一半。JSON格式确实是目前业界最通用的标准,但真正落地时,你会发现坑比想象中多得多。
我个人习惯把日志格式化拆成三个层次来看:结构标准、字段定义、安全合规。这三层缺一不可。今天我们就一个一个掰开揉碎了讲。
为什么非要用JSON?
你想想看,风控日志每天少则几百万条,多则上亿条。如果还是用那种「2024-01-01 12:00:00 | INFO | 用户登录成功」的纯文本格式,到了ELK或者ClickHouse里,你拿什么做结构化查询?
JSON的好处说白了就三点:
- 自描述:字段名和值在一起,不用猜
- 易解析:几乎所有日志平台都原生支持
- 可扩展:加字段不影响老数据
我在项目中遇到过最典型的反面教材——某团队用「|」分隔符拼日志,结果业务字段里偶尔也出现「|」,解析直接崩了。从那以后,我定了一条铁律:风控日志必须用JSON。
JSON格式标准:别小看这些细节
先看一个我推荐的日志格式模板:
{
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"userId": "user_10086",
"timestamp": 1704067200000,
"level": "INFO",
"logger": "com.risk.engine.RuleExecutor",
"message": "规则命中:高风险交易拦截",
"riskScore": 85.5,
"ruleId": "RULE_001",
"deviceInfo": {
"ip": "192.168.1.1",
"ua": "Mozilla/5.0 ..."
},
"extra": {}
}
这里有几个容易踩的坑:
另外,字段命名统一用小驼峰。别问我为什么不用下划线——因为Java和JavaScript的序列化库默认都是小驼峰,保持一致能省去很多字段映射的麻烦。
关键字段定义:这三个字段是命根子
风控日志里字段可能上百个,但真正核心的就三个:requestId、userId、timestamp。我称之为「风控日志三要素」。
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
| requestId | UUID字符串 | 全局唯一请求标识,用于全链路追踪 | a1b2c3d4-e5f6-7890-abcd-ef1234567890 |
| userId | 字符串 | 用户唯一标识,脱敏后存储 | user_10086 |
| timestamp | 长整型 | 事件发生时间,Unix毫秒级 | 1704067200000 |
requestId 这东西,说白了就是日志界的「身份证号」。一次请求从网关进来,经过风控引擎、规则计算、结果回调,所有环节的日志都带上同一个requestId。出了问题,直接grep这个ID,整条链路一目了然。
我记得有一次线上出了个误杀案例,用户正常交易被拦截。我通过requestId把从入参到规则计算到决策输出的所有日志拉出来,发现是规则引擎里一个缓存没刷新的bug。如果没有requestId,这种跨服务的问题定位,基本等于大海捞针。
userId 要注意的是脱敏。直接存手机号或身份证号?那是给自己挖坑。我建议统一用内部映射ID,比如「user_10086」这种格式。既保留了可追溯性,又规避了敏感数据泄露的风险。
timestamp 为什么强调毫秒级?因为风控场景下,同一秒内可能有多次操作。比如用户在一秒内连续点击了三次支付按钮,如果时间戳只精确到秒,你根本分不清先后顺序。毫秒级精度才能支撑这种高频场景的时序分析。
日志脱敏规则:安全合规是底线
这块内容,我建议你把它当成「红线」来看。合规部门查起来,可不是闹着玩的。
常见的脱敏场景分三类:
- 个人身份信息:手机号、身份证、邮箱
- 金融敏感信息:银行卡号、CVV、密码
- 设备指纹信息:IMEI、MAC地址
脱敏策略我一般用这几种:
- 掩码:保留前后几位,中间用星号代替。比如手机号「138****1234」
- 截断:只保留部分信息。比如身份证只保留后四位
- 替换:用固定值替代。比如密码统一替换为「***」
- 加密:对敏感字段做不可逆哈希
下面是一个脱敏工具的Python示例:
import re
import hashlib
def mask_phone(phone: str) -> str:
"""手机号脱敏:保留前3后4"""
return re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', phone)
def mask_id_card(id_card: str) -> str:
"""身份证脱敏:保留前6后4"""
return re.sub(r'(\d{6})\d{8}(\d{4})', r'\1********\2', id_card)
def hash_sensitive(value: str) -> str:
"""敏感字段哈希"""
return hashlib.sha256(value.encode()).hexdigest()
# 使用示例
log_data = {
"userId": "user_10086",
"phone": mask_phone("13812345678"),
"idCard": mask_id_card("110101199001011234"),
"passwordHash": hash_sensitive("mypassword123")
}
知识体系结构图
下面这张图把日志格式化的核心脉络梳理了一下,你可以对照着看看自己团队目前做到了哪一层:
看到这张图你应该能感受到,日志格式化不是简单地把字段拼成JSON就完事了。它背后是一整套从结构规范到字段定义再到安全合规的体系。我见过太多团队在第一步「JSON格式」上就翻车了,更别提后面的脱敏和字段定义了。
最后说一句,日志格式化这件事,前期多花点心思,后期能省下无数排查问题的时间。你想想看,线上出了故障,日志格式乱七八糟,连requestId都找不到,那场面……嗯,经历过的人都懂。