mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
7.4 KiB
7.4 KiB
日志模式
关于 slog 设置、handler 配置、测试、HTTP 中间件以及从旧版 log 包迁移的详细模式。
设置 slog
基本配置
package main
import (
"log/slog"
"os"
)
func main() {
// JSON handler 用于生产(机器可解析)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("server started", "addr", ":8080")
// 输出:{"time":"...","level":"INFO","msg":"server started","addr":":8080"}
}
用于开发的 Text Handler
// 本地开发的人类可读输出
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
slog.SetDefault(logger)
// 输出:time=... level=DEBUG msg="cache lookup" key=user:42 hit=true
动态级别控制
使用 slog.LevelVar 在运行时更改最低级别(例如通过管理端点或信号处理器):
var programLevel = new(slog.LevelVar) // 默认 Info
func init() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: programLevel,
}))
slog.SetDefault(logger)
}
// 从管理端点或信号处理器调用
func enableDebug() {
programLevel.Set(slog.LevelDebug)
}
自定义 Handler 模式
添加源位置
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelInfo,
}))
// 输出包含:"source":{"function":"main.handleRequest","file":"server.go","line":42}
使用默认属性包装 Handler
使用 slog.Handler 中间件向每条日志记录注入字段:
type contextHandler struct {
inner slog.Handler
attrs []slog.Attr
}
func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.inner.Enabled(ctx, level)
}
func (h *contextHandler) Handle(ctx context.Context, r slog.Record) error {
r.AddAttrs(h.attrs...)
return h.inner.Handle(ctx, r)
}
func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &contextHandler{inner: h.inner.WithAttrs(attrs), attrs: h.attrs}
}
func (h *contextHandler) WithGroup(name string) slog.Handler {
return &contextHandler{inner: h.inner.WithGroup(name), attrs: h.attrs}
}
多 Handler(扇出)
写入多个目标(例如 stdout + 文件):
type multiHandler struct {
handlers []slog.Handler
}
func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, h := range m.handlers {
if h.Enabled(ctx, level) {
return true
}
}
return false
}
func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
var errs []error
for _, h := range m.handlers {
if h.Enabled(ctx, r.Level) {
if err := h.Handle(ctx, r); err != nil {
errs = append(errs, err)
}
}
}
return errors.Join(errs...)
}
func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
handlers[i] = h.WithAttrs(attrs)
}
return &multiHandler{handlers: handlers}
}
func (m *multiHandler) WithGroup(name string) slog.Handler {
handlers := make([]slog.Handler, len(m.handlers))
for i, h := range m.handlers {
handlers[i] = h.WithGroup(name)
}
return &multiHandler{handlers: handlers}
}
使用 slogtest 测试
Go 1.22+ 提供了 testing/slogtest 来验证 handler 实现:
package myhandler_test
import (
"testing"
"testing/slogtest"
)
func TestHandler(t *testing.T) {
// newHandler 返回你的自定义 slog.Handler 和一个
// 将输出解析为 []map[string]any 的函数用于验证。
results := func(t *testing.T) map[string]any {
// 在此解析你的 handler 输出
}
h := NewMyHandler(buf, nil)
slogtest.Run(t, func(t *testing.T) slog.Handler { return h }, results)
}
在测试中捕获日志
对于断言日志输出的单元测试,写入 buffer:
func TestOrderProcessing(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, nil))
processOrder(logger, order)
if !strings.Contains(buf.String(), `"order_id"`) {
t.Error("expected order_id in log output")
}
}
HTTP 请求日志中间件
一个完整的中间件,记录每个请求的计时、状态和请求作用域字段:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.NewString()
}
logger := slog.With(
"request_id", reqID,
"method", r.Method,
"path", r.URL.Path,
)
// 包装 response writer 以捕获状态码
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
// 将日志器存入 context 供下游 handler 使用
ctx := context.WithValue(r.Context(), loggerKey, logger)
next.ServeHTTP(rw, r.WithContext(ctx))
logger.Info("request completed",
"status", rw.status,
"elapsed_ms", time.Since(start).Milliseconds(),
)
})
}
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
从 Context 获取日志器
type ctxKey struct{}
var loggerKey = ctxKey{}
func loggerFromCtx(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return l
}
return slog.Default()
}
从 log.Printf 迁移到 slog
第 1 步:替换直接调用
// 迁移前
log.Printf("user %s logged in from %s", userID, ip)
// 迁移后
slog.Info("user logged in", "user_id", userID, "ip", ip)
第 2 步:替换 main() 中的 log.Fatalf
// 迁移前
log.Fatalf("failed to connect: %v", err)
// 迁移后——slog 没有 Fatal;在 main 中使用 slog + os.Exit
slog.Error("failed to connect", "err", err)
os.Exit(1)
第 3 步:桥接旧代码
如果逐步迁移,将标准 log 包的输出通过 slog 重定向:
// 在 main() 中,设置 slog 之后:
slog.SetDefault(logger)
// 标准 log 包现在通过 slog 的默认 handler 写入。
// 这是因为 slog.SetDefault 也会更新 log.Default()。
第 4 步:替换日志器参数
// 迁移前:传递 *log.Logger
func NewServer(addr string, logger *log.Logger) *Server
// 迁移后:显式传递 *slog.Logger
func NewServer(addr string, logger *slog.Logger) *Server
// 或从 handler 中的 context 派生
func (s *Server) handleRequest(ctx context.Context) {
logger := loggerFromCtx(ctx)
logger.Info("handling request")
}
迁移清单
| 步骤 | 更改什么 | 验证 |
|---|---|---|
| 1 | log.Printf → slog.Info/Warn/Error |
rg 'log\.Printf' 返回 0 个匹配 |
| 2 | log.Fatalf → slog.Error + os.Exit(1) 在 main 中 |
仅在 main() 中 |
| 3 | 在 main 中尽早设置 slog.SetDefault |
旧版 log 调用通过 slog 路由 |
| 4 | *log.Logger 参数 → *slog.Logger |
所有构造函数已更新 |
| 5 | 移除已替换处的 "log" 导入 |
goimports 会自动处理 |