zap必须禁用ConsoleEncoder改用JSONEncoder,因默认输出含ANSI色码和换行,非合法JSON,导致otlp/ES/Loki解析失败;gRPC客户端需封装重连与内存缓冲队列,避免连接中断日志丢失及阻塞业务。

zap 必须禁用 ConsoleEncoder,改用 JSONEncoder
常见错误现象:zap.NewProduction() 在容器或日志中心里解析失败,报 invalid character '' looking for beginning of value 或字段全乱码。这是因为默认的 ConsoleEncoder 输出带 ANSI 颜色、缩进和换行符,根本不是合法 JSON。
使用场景:所有要被 otelcol、Loki 或 ES 消费的日志,必须是纯 JSON 行(每行一个对象,无换行嵌套)。
实操建议:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 初始化 logger 时显式用
zapcore.NewJSONEncoder,并禁用颜色、缩进、时间格式化(让客户端自己控制时间精度) - 示例配置片段:
encoderCfg := zap.NewProductionEncoderConfig() encoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder encoderCfg.EncodeLevel = zapcore.LowercaseLevelEncoder encoderCfg.ConsoleSeparator = "" // 防止意外插入空格 core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), writer, level)
- 别依赖
zap.NewProduction()的默认行为——它适合终端看,不适合机器消费
grpc 客户端必须封装重连 + 内存缓冲队列
常见错误现象:日志服务重启后,应用连续 5 分钟无日志上报;网络抖动时大量日志静默丢失;context.DeadlineExceeded 错误反向污染 HTTP handler。
立即学习“go语言免费学习笔记(深入)”;
为什么这样做:gRPC 连接断开后不会自动重连,otlpgrpc.NewClient() 默认只做一次连接尝试;同步发送日志会阻塞业务 goroutine。
实操建议:
- 用
grpc.WithTransportCredentials(insecure.NewCredentials())(开发)或credentials.NewTLS(...)(生产)初始化 client - 连接失败时启用指数退避:从
100ms开始,上限5s,避免雪崩式重试 - 日志写入先入内存 channel(容量建议
1024),由独立 goroutine 消费并调用ExportLogs;channel 满时用select { case logChan 丢弃旧日志,不 panic、不阻塞
otelcol 配置里 protocols.grpc 必须显式开启
常见错误现象:Go 客户端没报错,但 otelcol 日志里完全收不到任何日志;otelcol 启动后监听的是 :4317,但 netstat 查不到监听状态。
参数差异:otelcol 默认只启用 OTLP/HTTP 接收器,protocols.grpc 是 opt-in 配置项,漏掉就等于关掉了整个 gRPC 入口。
实操建议:
- 确认
receivers.otlp.protocols.grpc存在且未被注释,例如:receivers: otlp: protocols: grpc: - 检查 otelcol 版本是否 ≥ v0.100.0(OTLP/gRPC 日志支持稳定落地的分水岭)
- 用
grpcurl -plaintext localhost:4317 list验证服务是否暴露了opentelemetry.proto.collector.logs.v1.LogsService
trace_id 必须由客户端注入,不能靠 otelcol 补充
常见错误现象:Kibana 或 Grafana 中能查到日志,但按 trace_id 筛选时结果为空;Loki 中 label {trace_id="xxx"} 查不到对应日志。
原因很直接:otelcol 默认把 trace_id 放在 attributes.trace_id 字段下,而 ES/Loki 查询时认的是顶层 trace_id 字段;补了也串不起来,因为服务端生成的 trace_id 和请求链路无关。
实操建议:
- Go 客户端打日志时,必须从 context 提取并显式传入:
ctx.Value("trace_id") // 或 opentelemetry-go 的 trace.SpanFromContext(ctx).SpanContext().TraceID() - otelcol 配置中加
processors.attributes提升字段:processors: attributes/traceid: actions: - key: trace_id from_attribute: attributes.trace_id action: insert - 验证方式:curl otelcol 的
/debug/pprof/goroutine?debug=2或看其日志是否输出trace_id extracted to root类提示
真正卡住人的地方不在代码量,而在字段层级和配置开关——attributes.trace_id 和 trace_id 差一个点,protocols.grpc 少一行,日志就进不了 pipeline。


















