优先选 zerolog,因其轻量且 json 字段名(如 level、ts)与 elk/loki 兼容;zap 默认 lvl 字段名不匹配 loki 解析规则。

日志该用 zerolog 还是 zap?别只看性能,先看结构化需求
Go 生态里日志库选型不是比谁快,而是看你的日志是否要被 ELK 或 Loki 摄取。如果必须输出 JSON 且字段名固定(比如 level、ts、service),zerolog 更轻量、API 更直白;zap 功能更全(支持采样、多写入器),但默认字段名是 level 而非 lvl,和 Grafana Loki 的默认解析规则不一致,容易导致 level 字段查不到。
实操建议:
- 用
zerolog.New(os.Stderr).With().Timestamp().Str("service", "api").Logger()初始化,避免每次写日志都重复加公共字段 - 禁止在日志中拼接字符串:
log.Info().Str("user_id", u.ID).Msg("user login")✅,而不是log.Info().Msg("user login: " + u.ID)❌ - 如果用了 Kubernetes,把
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix,避免时区混乱导致日志时间戳错位
prometheus/client_golang 的 Counter 和 Histogram 别混用
常见错误是把请求耗时用 Counter 统计——它只能累加,没法算 P95。反过来,用 Histogram 记录“请求成功数”也浪费:它会为每个 bucket 单独打点,而成功/失败这种布尔态更适合 Counter 配合标签。
实操建议:
- 请求总量、错误总数、队列积压数 → 用
prometheus.NewCounterVec,标签如method、status_code - HTTP 延迟、DB 查询耗时 → 用
prometheus.NewHistogramVec,显式设置Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5},别依赖默认值 - 暴露指标前务必调用
prometheus.MustRegister(metric),否则/metrics端点返回空,且无任何报错提示
OpenTelemetry Go SDK 的 Tracer 必须复用,不能每次 HTTP 请求都 new
新手常把 otel.Tracer("http") 放在 handler 里,结果每秒创建成百上千个 tracer 实例,内存暴涨、GC 频繁,追踪数据反而大量丢失。Go 的 Tracer 是线程安全的,全局单例即可。
实操建议:
- 在
main()初始化一次:tracer := otel.Tracer("my-service"),然后注入到 HTTP handler 或数据库中间件 - 手动创建 span 时,用
ctx, span := tracer.Start(r.Context(), "db.query"),别漏掉把ctx传下去,否则下游 span 无法链路串联 - 若用 Gin,直接集成
ginotel.Middleware;若用 net/http,用otelhttp.NewHandler包裹 handler,别自己从头实现 trace 注入逻辑
三者联动的关键:上下文传递和字段对齐
日志里看不到 trace_id?指标里分不清是哪个请求的延迟?根本原因往往是上下文没串起来,或者字段命名不统一。OpenTelemetry 的 trace ID 默认是 16 字节十六进制字符串,但很多日志系统(如 Loki)期望的是 32 位小写 hex,少一位就匹配不上。
实操建议:
- 在日志中显式注入 trace ID:
log.With().Str("trace_id", trace.SpanContext().TraceID().String()).Logger() - 所有服务共用同一套 service 名称(通过
service.nameresource 属性设置),否则 Prometheus 的rate(http_request_duration_seconds_sum[5m])无法跨服务聚合 - 不要依赖自动注入的 span 名称,HTTP handler 的 span 名默认是
GET /api/users,但 Prometheus metric 的http_route标签需要是/api/users,二者需手动对齐,否则告警规则失效
最易被忽略的点:本地开发时 trace 数据发给 Jaeger,上线后切到 OTLP endpoint,但忘记改环境变量 OTEL_EXPORTER_OTLP_ENDPOINT,结果所有 trace 静默丢弃,连错误日志都不打。











