mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 23:06:36 +08:00
315 lines
7.4 KiB
Markdown
315 lines
7.4 KiB
Markdown
# 日志模式
|
|
|
|
关于 slog 设置、handler 配置、测试、HTTP 中间件以及从旧版 `log` 包迁移的详细模式。
|
|
|
|
## 设置 slog
|
|
|
|
### 基本配置
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 本地开发的人类可读输出
|
|
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` 在运行时更改最低级别(例如通过管理端点或信号处理器):
|
|
|
|
```go
|
|
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 模式
|
|
|
|
### 添加源位置
|
|
|
|
```go
|
|
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` 中间件向每条日志记录注入字段:
|
|
|
|
```go
|
|
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 + 文件):
|
|
|
|
```go
|
|
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 实现:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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 请求日志中间件
|
|
|
|
一个完整的中间件,记录每个请求的计时、状态和请求作用域字段:
|
|
|
|
```go
|
|
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 获取日志器
|
|
|
|
```go
|
|
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 步:替换直接调用
|
|
|
|
```go
|
|
// 迁移前
|
|
log.Printf("user %s logged in from %s", userID, ip)
|
|
|
|
// 迁移后
|
|
slog.Info("user logged in", "user_id", userID, "ip", ip)
|
|
```
|
|
|
|
### 第 2 步:替换 main() 中的 log.Fatalf
|
|
|
|
```go
|
|
// 迁移前
|
|
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 重定向:
|
|
|
|
```go
|
|
// 在 main() 中,设置 slog 之后:
|
|
slog.SetDefault(logger)
|
|
|
|
// 标准 log 包现在通过 slog 的默认 handler 写入。
|
|
// 这是因为 slog.SetDefault 也会更新 log.Default()。
|
|
```
|
|
|
|
### 第 4 步:替换日志器参数
|
|
|
|
```go
|
|
// 迁移前:传递 *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` 会自动处理 |
|