不能用 log.printf + stdout 做分布式日志代理,因其无缓冲、无重试、无结构化字段、无法注入 trace_id、pod 重启即丢日志、字符串拼接导致不可索引、格式不可控、无失败反馈、无法限流/背压。

zap 必须和本地落盘、网络传输解耦,否则一断网就丢日志;grpc 比 http 更适合日志上报,但必须配 WithBlock(false) 和重试;OpenTelemetry Collector 是接收端唯一合理选择,硬写 http.HandleFunc 等于主动放弃可靠性。
为什么不能用 log.Printf + stdout 做分布式日志代理
它不解决任何分布式问题:没有缓冲、没有重试、没有序列化控制、无法注入 trace_id 或 host_id。Kubernetes 里一个 Pod 重启,stdout 就断一次,之前未 flush 的日志全丢。更致命的是,log.Printf 输出是字符串拼接,字段不可索引,后续在 Loki 或 ES 里根本没法按 service_name 或 level 过滤。
- 日志格式不可控:无法保证每条日志含
time、level、service、trace_id四个必填字段 - 无失败反馈:写到 stdout 成功 ≠ 日志送达中心,网络抖动时静默丢失
- 无法限流/背压:突发日志洪峰直接打爆接收端内存或连接数
zapcore.Core + grpc.ClientStreaming 是最可控的上报路径
核心不是“用 zap”,而是用 zapcore.Core 接口拦截每条日志,做结构化序列化 + 异步发送。不要用 zap.String("msg", fmt.Sprintf(...)),而要用 zap.String("user_id", uid) 这种字段式写法。
- 客户端必须带本地内存缓冲(如
chan *LogEntry+sync.Pool复用),容量设为 1024~4096,超限时 drop old 或 block write -
grpc.Dial必须传grpc.WithBlock(false),否则 collector 重启时整个服务卡住;改用grpc.FailOnNonTempDialError(true)+ 主动轮询连接状态 - 流式发送用
ClientStreaming,每次发一批(例如 50 条),比 unary RPC 减少 90% 的连接开销 - 每条
LogEntry必须含time.UnixNano()(非time.Now().String()),且字段名与 OTLP 协议对齐,方便后续对接otelcol
接收端别自己写 handler,直接用 OpenTelemetry Collector
写一个 http.HandleFunc("/log", ...) 看似简单,实际会暴露三个致命缺陷:无 ACK、无路由策略、无采样能力。所有日志都挤进一个 endpoint,磁盘满或解析失败时整批丢弃,且无法按 service_name 分发到不同 Kafka Topic 或 Loki Stream。
- 启动
otelcol时启用otlpgrpcreceiver 和至少两个 exporter(如loki+kafka),配置文件里定义 routing rule: -
service_pods_prod→loki;service_pods_staging→kafka;level == "ERROR"→ 同时发slackwebhook - collector 自带 buffer(
memory_limiter)、retry(max_elapsed_time可设 5m)、compression(gzip)和 TLS 终止,不用你重复造轮子 - Go 客户端只需引入
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc,调ExportSpans即可——日志走 trace exporter 是 hack,但目前最省事
tail 文件采集场景下 rotation 和 offset 持久化是最大坑点
用 github.com/hpcloud/tail 监控 /var/log/myapp/*.log 时,logrotate 切文件瞬间若没正确处理 IN_MOVED_FROM 事件,就会漏掉最后几百行。更麻烦的是 agent 重启后,得从上次位置继续读,否则重复或跳过。
- 不要依赖
tail默认的SeekInfo,它只存 offset 不存 inode;应配合fsnotify监听rename事件,识别 rotation 并触发Reopen - offset 必须落盘(如写入
/var/lib/log-agent/offsets.json),每次 read 后异步刷盘,避免 crash 丢位点 - 多文件采集时,每个 file 对应独立 goroutine + channel,别用单个
tail.Tail实例喂多个文件,否则 rotation 事件互相干扰 - 如果日志含 stacktrace(多行),必须开启
multiline.pattern: '^[[:space:]]+at '类规则,否则 panic 被切成 10 条孤立日志
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











