日志从字符串变成结构
传统日志经常这样写:
log.Printf("user %d login failed: %v", userID, err)
它能看,但机器不太好解析。后来很多服务会输出 JSON 日志,字段里有 level、time、user_id、path、duration_ms。这样日志平台可以按字段搜索、聚合和告警。Go 标准库里的 log/slog 就是为了结构化日志提供统一接口。
学习 slog 的重点不是记住所有 API,而是理解日志应该记录什么:动作、关键字段、错误、耗时、请求范围信息。好的日志能帮助你在服务出问题时快速定位;糟糕的日志只是大量无意义字符串。
这篇文章从最小用法讲起,再写一个 HTTP 请求日志中间件。
最小 slog
logger := slog.Default()
logger.Info("server starting")
带字段:
logger.Info("server starting",
"addr", ":8080",
"env", "development",
)
错误日志:
if err != nil {
logger.Error("load config failed", "err", err)
}
字段是成对出现的 key/value。key 通常使用短小稳定的英文,比如 user_id、path、method、duration_ms。不要把整段中文描述塞进 key,中文可以放在 message,但字段名最好稳定,方便查询。
文本日志和 JSON 日志
创建文本 handler:
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello", "name", "Go")
创建 JSON handler:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello", "name", "Go")
本地开发用文本日志比较舒服,生产环境常用 JSON 日志,方便日志平台解析。你可以通过配置决定:
func NewLogger(format string) *slog.Logger {
switch format {
case "json":
return slog.New(slog.NewJSONHandler(os.Stdout, nil))
default:
return slog.New(slog.NewTextHandler(os.Stdout, nil))
}
}
使用:
logger := NewLogger(os.Getenv("LOG_FORMAT"))
日志格式属于运行时配置,不应该写死在业务函数里。
With 添加公共字段
服务启动时可以创建带公共字段的 logger:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
With("service", "user-api").
With("version", "v1.2.0")
之后每条日志都会带上这些字段。
请求级别也可以创建子 logger:
requestLogger := logger.With(
"request_id", requestID,
"method", r.Method,
"path", r.URL.Path,
)
requestLogger.Info("request started")
这比每次手动写重复字段更清楚。With 不会修改原 logger,而是返回一个带额外字段的新 logger。
HTTP 请求日志中间件
先写状态码 recorder:
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(status int) {
r.status = status
r.ResponseWriter.WriteHeader(status)
}
中间件:
func RequestLogger(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{
ResponseWriter: w,
status: http.StatusOK,
}
next.ServeHTTP(rec, r)
logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
使用:
mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthHandler)
server := &http.Server{
Addr: ":8080",
Handler: RequestLogger(logger, mux),
}
这条日志比普通字符串更容易搜索。你可以查询 status >= 500,也可以按 path 聚合耗时。
不要记录敏感信息
结构化日志更容易被系统收集、转发和长期保存,所以敏感信息更要谨慎。不要记录密码、token、完整身份证号、银行卡号、私密请求体。
错误示例:
logger.Info("login request",
"email", req.Email,
"password", req.Password,
)
正确做法:
logger.Info("login failed",
"email", req.Email,
"reason", "invalid_credentials",
)
即使是 email,也要看业务合规要求。日志不是数据库,不应该成为另一个敏感数据仓库。
小结
log/slog 让 Go 标准库拥有了结构化日志能力。你可以选择文本或 JSON handler,用 key/value 字段记录上下文,用 With 添加公共字段,用中间件记录 HTTP 请求。
日志的价值在于排查问题。每条关键日志都应该回答:发生了什么,和哪个用户或请求有关,在哪个路径,结果如何,耗时多久,错误是什么。字段稳定、信息克制、敏感数据不进日志,是结构化日志的基本要求。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。