结构化日志
本指南展示如何使用 log.NewLogger 和 log.StructuredLogger 输出结构化日志、为子系统附加持久化字段,以及将 logger 显式传入 handler 和 middleware。
边界原理请参见 Log Primer。
- 用
log.NewLogger构造基础 logger - 用
WithFields和With附加持久化字段 - 在 handler 日志调用中使用
Fields - 通过 handler 结构体传递 logger
第一步 — 构造基础 logger
Section titled “第一步 — 构造基础 logger”参考服务在 app.New 中构造一次 logger,并将其传入 core.New:
import plumelog "github.com/spcent/plumego/log"
logger := plumelog.NewLogger()
app := core.New(cfg.Core, core.AppDependencies{ Logger: logger,})NewLogger 返回 log.StructuredLogger,默认输出人类可读的文本。需要 JSON 输出时,将 LoggerConfig 传给同一个构造函数:
logger := plumelog.NewLogger(plumelog.LoggerConfig{ Format: plumelog.LoggerFormatJSON,})第二步 — 用 WithFields 将 logger 限定到子系统
Section titled “第二步 — 用 WithFields 将 logger 限定到子系统”为子系统创建带有固定字段的子 logger。原始 logger 不会被修改:
dbLogger := logger.WithFields(log.Fields{ "component": "database", "dsn_host": cfg.DBHost,})
dbLogger.Info("connection pool ready", log.Fields{"max_conns": 10})// 输出:component=database dsn_host=db.example.com msg="connection pool ready" max_conns=10单个字段时使用 With:
handlerLogger := logger.With("handler", "items")第三步 — 将 logger 注入 handler 结构体
Section titled “第三步 — 将 logger 注入 handler 结构体”不要使用包级 logger 变量。通过 handler 结构体注入 logger:
type ItemHandler struct { DB *sql.DB Logger log.StructuredLogger}
func (h *ItemHandler) Get(w http.ResponseWriter, r *http.Request) { id := r.URL.Query().Get("id") h.Logger.Info("handling get item", log.Fields{"id": id})
item, err := h.DB.QueryRowContext(r.Context(), "SELECT id, name FROM items WHERE id=$1", id) if err != nil { h.Logger.Error("db query failed", log.Fields{"id": id, "error": err.Error()}) _ = contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeInternal). Message("could not load item"). Build()) return } // ...}第四步 — 注册时使用限定作用域的 logger
Section titled “第四步 — 注册时使用限定作用域的 logger”在 RegisterRoutes 中,注入前创建 handler 级别的子 logger:
func (a *App) RegisterRoutes() error { items := &handler.ItemHandler{ DB: a.DB, Logger: a.Core.Logger().With("handler", "items"), } return a.Core.Get("/api/items", http.HandlerFunc(items.Get))}ItemHandler 的每行日志都会携带 handler=items,无需 handler 手动添加。
这种模式带来什么
Section titled “这种模式带来什么”WithFields和With返回新 logger — 子系统之间无共享状态。- 不存在包级 logger 全局变量;每个 logger 都可以从构造函数调用处追溯。
accesslogmiddleware 已经记录了请求级字段;handler 日志在此基础上添加业务上下文。- 永远不要记录密钥、token 或私钥 —
StructuredLogger使用无类型字段,便于事后审计。