直接用 Go 标准库 log 不行,因其仅支持单机同步写文件或 stdout,缺乏网络传输、缓冲队列、多实例去重/聚合能力,无法应对 Kubernetes 多 Pod 或集群部署下的日志分散、链路追踪与 trace_id 聚合需求。

为什么直接用 log 包不行
Go 标准库 log 是单机同步写文件或 stdout 的,没有网络传输、无缓冲队列、不支持多实例去重/聚合,更无法应对节点宕机或网络抖动。一旦服务部署在 Kubernetes 多个 Pod 或物理机集群里,日志就散落各处,tail -f 查不到完整链路,也做不到按 trace_id 聚合。
用 gRPC + Protocol Buffers 做日志转发最可控
自己实现一个轻量日志 agent(每台机器跑一个),统一把本地 log 输出转成 LogEntry 结构体,通过 gRPC 流式发送到中心 collector。比 HTTP 更低延迟、更省序列化开销,还能天然支持 TLS 和流控。
-
LogEntry必须包含timestamp(纳秒级)、service_name、host、level、message、trace_id(可选)字段 - agent 端要用
grpc.WithBlock()+ 重试逻辑(比如指数退避),否则 collector 重启时日志直接丢 - collector 端建议用
grpc.Server配合sync.Pool复用LogEntry实例,避免高频 GC - 别把日志直接写入 MySQL —— 写入吞吐扛不住,改用
Loki或ClickHouse这类时序/列存专用系统
用 zap + lumberjack 做本地日志落地再上报
线上服务不能依赖网络稳定,必须先落盘再异步发。Zap 性能高、结构化好,搭配 lumberjack.Logger 可自动轮转,避免磁盘打满。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 配置
lumberjack时设MaxSize: 100(MB)、MaxBackups: 7、Compress: true - 用
zapcore.AddSync(&yourGRPCWriter{})把 zap 输出同时写到本地文件和 gRPC 流,不要用 goroutine 单独拉日志文件——会丢行、乱序 - 如果用
filebeat替代自研 agent,确保它启用了multiline.pattern合并 stacktrace,否则 panic 日志被切碎
trace_id 关联日志的关键不是加字段,而是上下文透传
很多团队在日志里硬塞 trace_id 字符串,但没打通 HTTP/gRPC 请求生命周期。结果查到的日志还是断的。
- HTTP 中间件里从
req.Header.Get("X-Request-ID")或req.Context().Value("trace_id")提取,注入到 zaplogger.With(zap.String("trace_id", tid)) - gRPC server 端用
grpc.UnaryInterceptor从metadata.FromIncomingContext(ctx)拿trace-idkey - 下游调用时,必须显式把
trace_id放进新 context:ctx = metadata.AppendToOutgoingContext(ctx, "trace-id", tid) - 别用全局变量存
trace_id—— 并发下会串,Go 的context是唯一安全载体
真正难的不是发日志,是让每条日志在分布式调用中始终带着同一个 trace_id,且不因中间件、协程、定时任务而丢失上下文。漏掉任意一环,排查时就又回到 grep 大法。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










