默认 log 包应停用,因其无级别控制、不支持结构化输出且无法动态调整目标,难以满足线上排查与监控需求;推荐 zap 或 zerolog,并通过 context 传递带上下文的 logger。

为什么默认 log 包在项目启动后就该停用
Go 标准库的 log 包没有级别控制、不支持结构化输出、无法动态调整输出目标,一旦项目需要排查线上问题或对接监控系统,它就会成为日志链路的断点。尤其在 HTTP 服务中,你没法用 log.Printf 区分 info / warn / error,也不能把请求 ID、耗时、状态码自动注入每条日志。
- 替换方案首选
zap(性能高、结构化强)或zerolog(零分配、API 简洁),二者都支持With()追加字段、LevelEnabler动态开关级别 - 避免直接在 handler 里调用全局 logger 实例,应通过
context.WithValue(ctx, key, logger)传递带请求上下文的 logger - 别把敏感字段(如
user_token、password)直接打到日志里——用logger.With(zap.String("user_id", uid)).Info("login success")替代拼接字符串
HTTP 中间件如何统一记录请求生命周期
手动在每个 handler 开头打 start、结尾打 end 日志极易遗漏且难以对齐字段。用中间件封装是唯一靠谱做法,关键在于:时间戳必须在 next.ServeHTTP 前后用 time.Now() 精确采集,且所有字段要一次性写入,避免多次 IO。
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &responseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(lw, r)
duration := time.Since(start)
logger.Info("http request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.Int("status", lw.statusCode),
zap.Duration("duration", duration),
zap.String("ip", getClientIP(r)),
zap.String("user_agent", r.UserAgent()),
)
})
}
-
responseWriter必须实现WriteHeader(int)方法才能捕获真实 status code,否则默认是 200 - 不要在中间件里记录 request body —— 它可能已被读取过一次(如 JSON 解析),再次读会阻塞或 panic
- 如果用了 Gin/Echo,优先用它们自带的
gin.LoggerWithConfig或echo.MiddlewareLogger,避免重复造轮子
怎么让 Prometheus 抓取到自定义指标而不爆内存
用 prometheus.NewCounter 或 prometheus.NewHistogram 注册指标后,若不控制 label 维度,比如把 user_id 当作 label 值,会导致指标数量无限膨胀,进程 OOM。
- 只对有限枚举值使用 label:如
handler("login" / "order_create")、status_code("200" / "404" / "500") - 耗时类指标一律用
HistogramVec,并设置合理 buckets:prometheus.ExponentialBuckets(0.01, 2, 10)覆盖 10ms–5s 区间 - 暴露指标的 HTTP handler 必须挂载到
/metrics,且确保路径不被其他中间件拦截(例如 auth 中间件不该作用于该路径)
var (
httpDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duration of HTTP requests.",
Buckets: prometheus.ExponentialBuckets(0.01, 2, 10),
},
[]string{"handler", "status_code"},
)
)
func init() {
prometheus.MustRegister(httpDuration)
}
本地调试和生产环境日志行为为何必须不同
开发时你要看到颜色、行号、函数名;上线后这些全是冗余开销,还会干扰日志聚合系统(如 Loki)的解析。硬编码判断 os.Getenv("ENV") == "prod" 不可靠,应由构建阶段或配置驱动。
立即学习“go语言免费学习笔记(深入)”;
- 用
zap.Config分离配置:dev 模式启用Development: true,prod 模式设Encoding: "json"+Level: zapcore.ErrorLevel - 日志输出目标不能写死为
os.Stdout—— 生产环境应重定向到文件,并配合lumberjack.Logger实现轮转 - 监控探针(如 healthz、metrics)的日志级别建议固定为
Warn或更高,避免被高频请求刷屏
最常被忽略的一点:日志采样。高频低价值日志(如“cache miss”)不做采样,几秒内就能打满磁盘。用 zap.Sampling(&zap.SamplingConfig{Initial: 100, Thereafter: 100}) 控制单位时间内的最大输出条数。










