beego日志脱敏必须通过自定义jsonwriter实现,因其beelogger不支持字段级脱敏钩子;需在write方法中解析json、识别预设敏感字段(如id_card、mobile)并替换,再序列化输出。

Beego 日志中不能直接脱敏,必须先统一 JSON 化
Beego 的 logs.BeeLogger 不支持字段级脱敏钩子,也没有 Formatter 接口。你没法在日志写入前拦截并修改某个字段(比如把 idCard 或 phone 替换成星号)。强行在业务层手动调用 DesensitizedUtil.mobilePhone() 会污染逻辑、重复编码、漏脱敏——唯一可控的入口是自定义 Writer,且必须输出结构化 JSON。
关键点在于:脱敏动作不是“对某字段做替换”,而是“把原始日志内容 + 上下文元信息一起序列化为 JSON,并在序列化时对敏感 key 做判断处理”。
- 所有日志必须走
logs.Info()/logs.Error()等标准接口,不能直接fmt.Printf - 敏感字段名需提前约定(如
id_card、mobile、email),写死在 JSON 序列化逻辑里 - 不要试图解析
b []byte中的 JSON 字符串再改——Beego 日志默认不输出 JSON,b是纯文本,解析失败率高
自定义 JSONWriter 要重写 Write 方法并识别敏感字段
你得实现一个 JSONWriter,在 Write 方法里做两件事:一是把原始日志字符串解析成 map(假设业务侧已按 JSON 格式传入),二是对已知敏感 key 的 value 做脱敏替换,最后再 json.Marshal 输出。
示例逻辑片段:
func (j *JSONWriter) Write(b []byte) (int, error) {
var logMap map[string]interface{}
if err := json.Unmarshal(b, &logMap); err != nil {
// 非 JSON 日志,原样保留 message 字段
logMap = map[string]interface{}{"message": strings.TrimSpace(string(b))}
}
// 定义敏感字段列表
sensitiveKeys := []string{"id_card", "mobile", "email", "bank_card"}
for _, key := range sensitiveKeys {
if val, ok := logMap[key]; ok {
if strVal, isStr := val.(string); isStr && strVal != "" {
switch key {
case "mobile":
logMap[key] = desensitizeMobile(strVal)
case "id_card":
logMap[key] = desensitizeIDCard(strVal)
case "email":
logMap[key] = desensitizeEmail(strVal)
}
}
}
}
logMap["timestamp"] = time.Now().Format(time.RFC3339)
logMap["level"] = j.level // 需从日志上下文提取,实际要结合 Beego 的 level 映射
data, _ := json.Marshal(logMap)
return j.writer.Write(append(data, '\n'))
}
-
desensitizeMobile等函数可用 hutool 的DesensitizedUtil.mobilePhone(),或自己写正则(如1[3-9]\d{9}) - 注意:Beego 的
logs.SetLogger只接受logs.Logger接口,你的JSONWriter必须实现该接口 - 如果业务日志不是 JSON 格式(比如只传了字符串),就别硬解析,统一塞进
"message"字段再脱敏其内容(需更谨慎的正则提取)
Filebeat 多行合并配置必须匹配 JSON 开头
脱敏后的每条日志是一行 JSON,但 Beego 默认 panic 日志是多行堆栈,若没正确配置 Filebeat,会导致一条 panic 被切分成多条记录,ELK 里看到的是残缺 JSON。
filebeat.yml 中必须启用 multiline 并严格匹配 JSON 行首:
multiline.pattern: '^{'
multiline.negate: true
multiline.match: after
-
pattern: '^{'表示“以 { 开头的行是新日志”,这样 panic 的第二行(空格开头)就会被合并到上一行 - 不能用
^{"timestamp"——因为 timestamp 字段顺序不保证,不同 Go 版本或 marshal 库行为可能不同 - 确保 Beego 日志文件本身是追加写(
logs.SetLogger("file", ...)),且没有其他进程并发写同一文件
真正难的不是脱敏,是敏感字段的识别和一致性维护
你在 JSONWriter 里 hardcode 的 sensitiveKeys 列表,会迅速变成技术债:新增一个 emergency_contact_phone 字段,没人记得去补;前端传来的 idCard(驼峰)和后端约定的 id_card(下划线)不一致,脱敏就失效。
更可靠的做法是:让业务层主动标注,比如用结构体 tag:
type UserLog struct {
Name string `json:"name"`
IDCard string `json:"id_card" desensitize:"idcard"`
Mobile string `json:"mobile" desensitize:"mobile"`
}
然后在 JSONWriter 中用反射读取 tag,动态决定是否脱敏——但这要求所有日志都基于结构体序列化,无法兼容字符串日志。
所以现实选择往往是折中:核心接口(如登录、实名认证)强制结构体日志 + tag 脱敏;其余地方靠文档约定 + CI 检查敏感字段名拼写。这比幻想“全自动识别”更可控。











