为什么需要结构化日志

结构化日志以键值对输出,便于机器检索与聚合分析。良好的日志体系应满足:一致的字段命名、可关联的 trace_id/span_id、合理的级别划分(debug/info/warn/error)、可观测性平台(ELK/Loki/ClickHouse)无缝接入。

Logrus 快速上手与工程化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import (
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"os"
)

func init() {
log.SetFormatter(&log.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z07:00",
})
log.SetOutput(&lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 100, // MB
MaxBackups: 7,
MaxAge: 14, // days
Compress: true,
})
log.SetLevel(log.InfoLevel)
}

func main() {
log.WithFields(log.Fields{
"trace_id": "t-001", "user_id": 1001,
}).Info("order created")
}

Hook 机制将日志同步推送到外部系统:

1
2
3
4
5
6
type KafkaHook struct{ prod *kafka.Writer }
func (h *KafkaHook) Levels() []log.Level { return log.AllLevels }
func (h *KafkaHook) Fire(e *log.Entry) error {
b, _ := e.Bytes()
return h.prod.WriteMessages(context.Background(), kafka.Message{Value: b})
}

优势:生态丰富、易上手;劣势:相对分配较多,极端高并发下开销偏大。

Zerolog 高性能实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
logger := zerolog.New(os.Stdout).With().
Timestamp().
Str("service", "order").
Logger()
logger.Info().Str("trace_id", "t-001").Str("user_id", "1001").Msg("order created")
}

分样与级别控制:

1
2
3
4
logger := logger.Sample(&zerolog.BurstSampler{
Burst: 10, Period: time.Second,
})
log.Logger = logger.Level(zerolog.InfoLevel)

优势:零分配、极致性能;劣势:生态与 Hook 较少,需要扩展代码管理输出目标。

字段规范与追踪关联

建议统一字段:

  • trace_id、span_id:分布式追踪关联
  • user_id、tenant、env、version:上下文与多租户标识
  • path、method、status、latency_ms:HTTP 关键指标

在中间件中注入追踪信息:

1
2
3
4
5
6
7
8
func WithTrace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tid := r.Header.Get("X-Request-Id")
if tid == "" { tid = uuid.NewString() }
ctx := log.With().Str("trace_id", tid).Logger().WithContext(r.Context())
next.ServeHTTP(w, r.WithContext(ctx))
})
}

输出管道与后端存储

  • 本地文件 + 日志轮转(lumberjack)
  • STDOUT -> Docker/K8s -> Fluent Bit -> Loki/ES/ClickHouse
  • 直接写 Kafka/Pulsar 进行异步解耦(注意失败重试与阻塞)

迁移策略与混合方案

  • 开发环境 Logrus TextFormatter 可读性更好;生产统一 JSON
  • 混合:业务低频模块用 Logrus,热点路径改用 Zerolog
  • 渐进式迁移:封装统一 Logger 接口适配两种实现

踩坑与优化

  • 大字段/二进制直写日志导致索引膨胀:过滤或脱敏
  • 高并发同步写文件阻塞:使用缓冲、异步队列
  • 过度使用 Debug:生产环境降级或采样
  • 时间戳格式混乱:统一 ISO8601 或毫秒 Epoch

FAQ

  • 如何输出调用栈?Logrus 可配合 WithField("stack", stack());Zerolog 提供 Stack() 选项
  • JSON 与 Text 兼容?开发 Text、生产 JSON;或提供两个输出
  • 如何在 K8s 中采集?推荐 STDOUT,Sidecar/DaemonSet 收集器统一转发

总结

选型并非非黑即白。Logrus 生态与可读性强,Zerolog 极致性能。建议:统一字段规范和输出格式、在链路中注入 trace、为热点路径采用 Zerolog 或采样策略,最终接入集中化平台实现检索与告警。