Log Primer
Log Primer
Section titled “Log Primer”当你已经通过 稳定根 确认改动仍属于默认服务路径,而且问题进一步收窄到服务如何输出结构化日志时,就打开这一页:组件接受什么接口、基础 logger 如何构造,以及哪些字段助手适用。
log 拥有 StructuredLogger 接口、用 NewLogger 进行规范的基础 logger 构造,以及日志字段助手。只允许导入 stdlib — log 不了解 app bootstrap、routing 或特定功能的行为。
logger := log.NewLogger()logger.Info("server starting", log.Field("port", 8080))logger.Error("db ping failed", log.Field("err", err))什么时候从这里开始
Section titled “什么时候从这里开始”- 你正在跨模块边界接受或传递
StructuredLogger - 你正在调用
NewLogger()为应用构造基础 logger - 你正在用字段助手 API 向日志事件添加结构化字段
- 你正在实现满足
StructuredLogger的新日志后端
什么时候不该从这里开始
Section titled “什么时候不该从这里开始”- 改动关于导出管道、聚合或将日志发送到外部系统 — 那属于
x/observability - 改动添加了 CLI bootstrap 助手、默认单例构造或包级全局状态
- 工作关于请求级访问日志、字段脱敏策略或日志采样 — 那些是 middleware 关注点
- 改动是特定功能的日志语义或仓库级测试助手
当前仓库里先读哪些文件
Section titled “当前仓库里先读哪些文件”log/module.yamllog/logger.golog/fields.golog/glog.goreference/standard-service/internal/app/app.go
更具体的归属例子
Section titled “更具体的归属例子”这些工作适合留在 log | 一旦变成这些问题就应移出 |
|---|---|
StructuredLogger 接口 — 所有 logger 满足的契约 | 特定功能的 logger 适配器或应用领域的日志语义 |
NewLogger() — 返回 StructuredLogger 的规范基础构造 | CLI 助手、包级全局单例或默认 logger 注册 |
| 字段助手:任何 logger 都可接受的通用结构化字段 | 仓库级测试助手或特定功能的字段词汇表 |
| 实现满足接口的新后端(如 JSON、noop) | 导出管道 wiring、聚合或外部传输适配器 |
import "github.com/spcent/plumego/log"构造 logger
Section titled “构造 logger”// 文本输出到 stderr — 开发环境默认logger := log.NewLogger()
// JSON 输出 — 生产环境logger := log.NewLogger(log.LoggerConfig{ Format: log.LoggerFormatJSON, Level: log.INFO,})
// 丢弃所有输出 — 在测试中很有用logger := log.NewLogger(log.LoggerConfig{Format: log.LoggerFormatDiscard})LoggerFormat 选项:LoggerFormatText、LoggerFormatJSON、LoggerFormatDiscard。
logger.Info("server started", log.Fields{"addr": ":8080"})logger.Warn("config missing, using default", log.Fields{"key": "TIMEOUT"})logger.Error("database unavailable", log.Fields{"err": err.Error()})当没有结构化字段时可以省略 Fields:
logger.Info("shutdown complete")携带 context 的日志
Section titled “携带 context 的日志”logger.InfoCtx(ctx, "request processed", log.Fields{ "user_id": userID, "duration_ms": duration.Milliseconds(),})附加持久化字段
Section titled “附加持久化字段”用 WithFields 创建子 logger,使其在后续所有调用中都携带这些字段:
reqLogger := logger.WithFields(log.Fields{ "request_id": contract.RequestIDFromContext(ctx), "method": r.Method, "path": r.URL.Path,})reqLogger.Info("handler started")reqLogger.Error("handler failed", log.Fields{"err": err.Error()})With 是单字段的简写:
svcLogger := logger.With("service", "user-api")作为依赖注入
Section titled “作为依赖注入”在模块边界处接受 log.StructuredLogger,而非具体类型:
type UserRepository interface { Find(ctx context.Context, id string) (*User, error)}
type UserService struct { repo UserRepository logger log.StructuredLogger}
func NewUserService(repo UserRepository, logger log.StructuredLogger) *UserService { return &UserService{repo: repo, logger: logger}}在 app.New 中从 core app 实例获取 logger:
app := core.New(cfg.Core, core.AppDependencies{Logger: log.NewLogger()})svc := NewUserService(db, app.Logger())日志级别常量
Section titled “日志级别常量”| 常量 | 说明 |
|---|---|
log.DEBUG | 详细 — 用于开发 |
log.INFO | 正常运营消息 |
log.WARNING | 非致命问题 |
log.ERROR | 需要关注的错误 |
log.FATAL | 记录后调用 os.Exit(1) |
为什么单独写这一页
Section titled “为什么单独写这一页”日志是几乎每个组件都会接触的唯一可观测性工具。如果接口泄漏了 app-bootstrap 假设或引入了功能包,耦合就会扩散到处。log 将导入列表保持为 stdlib,专门是为了让任何稳定根或扩展都能接受 StructuredLogger 而不产生循环依赖。永远不要记录密钥 — 这是事后最难审计的唯一硬性规则。
- core API 快速参考 —
AppDependencies.Logger注入方式 - 结构化日志指南
- Metrics Primer
- Middleware Primer
- Core Primer