埋点接口必须用HTTP POST+JSON,禁用GET和form-data;审计日志需统一结构、上下文注入、白名单脱敏、缓冲异步写入、Seq序号防时钟竞争。

埋点接口必须用 HTTP POST + JSON,别碰 query 或 form-data
用户行为数据天然带结构、有嵌套、含非 ASCII 字符(比如 UA、中文页面名),用 GET 传参会触发 URL 编码爆炸、长度截断、日志污染;application/x-www-form-urlencoded 则无法表达嵌套 Props 或空值。所有主流前端 SDK(如 Sentry、自研 JS 埋点)默认发 POST /track 带 application/json,后端就得按这个契约接。
- 接收时直接用
json.NewDecoder(r.Body).Decode(&event),别先ioutil.ReadAll再解析——大 body 会吃光内存 - 必须设
r.Header.Set("Content-Type", "application/json")校验,拒绝非 JSON 请求,防爬虫乱打 - 对
event.Props字段用json.RawMessage延迟解析,避免每次 decode 都全量反序列化无关字段 - 前端若传了
timestamp,服务端别直接信任——用time.Now().UnixMilli()覆盖,防止客户端时间错乱导致排序失效
audit.Log() 必须封装成中间件或 context-aware 函数
散落在每个 handler 里手写 log.Printf 是审计失效的起点:字段不一致、漏掉 userID、没透传 trace_id、敏感字段未脱敏。真正能落地的方案是把审计逻辑收口到一个函数,且强制从 context.Context 提取基础信息。
- 定义统一结构体:
AuditEvent{Action string, ResourceID string, Before map[string]interface{}, After map[string]interface{}, Level string} - 中间件中提取
userID(从 JWT token 解析)、ip(用r.RemoteAddr或X-Forwarded-For)、requestID(从X-Request-ID或生成),全部注入ctx - 业务 handler 里调用
audit.Log(ctx, AuditEvent{Action: "user_update_profile", ResourceID: "u_123"}),函数内部自动补全Timestamp、UserID、IP等字段 -
Before/After只在敏感变更时填充,且必须走白名单过滤(例如只保留email、phone字段),别直接json.Marshal整个 struct
异步写入不能只靠 goroutine,得加缓冲和失败兜底
同步写文件或直连数据库会拖慢接口响应,但裸起 go func(){...}() 更危险:没缓冲的 channel 会快速堆积、goroutine 泄漏、OOM。真正的生产级异步不是“丢给后台就行”,而是可控背压+本地暂存。
- 声明带缓冲 channel:
auditChan = make(chan *AuditEvent, 1000),容量根据 QPS 和平均处理耗时估算 - 消费 goroutine 按批写入:每 50 条或超时 300ms 触发一次 flush,避免小包频繁 IO
- 写入失败时,把事件 append 到本地临时文件(路径如
/var/log/app/audit_pending.jsonl),启动时先读该文件重放 - 别用
log.Printf记审计日志——它输出的是无结构文本,无法被 Loki 的| json解析,也难做字段过滤
时间戳排序失效时,用单调递增序号替代 time.Now()
高并发下多个 goroutine 同时调用 time.Now().UnixMilli(),返回值完全相同,导致两条“删除用户”日志无法分先后——审计追溯就断链。这不是精度问题,是并发时钟竞争的本质缺陷。
立即学习“go语言免费学习笔记(深入)”;
- 全局声明原子变量:
var seq uint64,写入前调用atomic.AddUint64(&seq, 1)获取唯一序号 - 审计结构体里加字段:
Seq uint64 `json:"seq"`,和Timestamp并存,排序优先用Seq - 注意:这个
Seq只用于排序,不替代Timestamp做时间分析;两者语义不同,不能合并 - 如果用了分布式部署,单机
Seq不够,需引入 Redis INCR 或 Snowflake ID,但中小系统先用原子计数器足够


















