在 go-zero 中打印日志

用内置 logx 库,从「打印一行字」到「全链路可追踪」一次讲透

logx 核心库 Info / Error / Slow / Stat / Severe 结构化字段 Trace 透传 yaml 配置

为什么用 logx,而不是 zap / logrus

go-zero 自带日志库 github.com/zeromicro/go-zero/core/logx99% 的场景不需要引第三方日志库——它已经把微服务最需要的几件事内置了:

统一 JSON 格式

默认输出 JSON,字段固定(time / level / caller / content…),方便 ELK / Loki / Grafana 直接采集。

自动注入链路

WithContext(ctx) 后,日志自动带上 trace/span ID,跨服务串成一条调用链。

多通道分流

Info / Error / Slow / Stat / Severe 走不同逻辑,慢请求、统计指标可单独采集。

零额外依赖

goctl 生成的服务天然集成,svc.NewServiceContext 里一行 logx.MustSetup 就接管了。

一句话:go-zero 项目里,直接 import logx 开打;只有要对接特殊日志后端(发到 Kafka、写 ES)才需要自定义 Writer(见 ⑦)。

快速上手:打印第一行日志

最朴素的用法,和 fmt.Println 一样简单,但输出是结构化 JSON:

package main

import "github.com/zeromicro/go-zero/core/logx"

func main() {
    // 用 goctl 生成的服务会由 svc 自动 SetUp,这里手动初始化一次
    logx.MustSetup(logx.LogConf{Mode: "console", Encoding: "plain"})

    logx.Info("服务启动完成")                         // 普通信息
    logx.Infof("当前在线用户数: %d", 1024)            // 带格式化
    logx.Error(err)                                  // 记录错误
    logx.Errorf("查询用户 %d 失败: %v", uid, err)        // 带格式化
}

默认 Encoding: json 时输出(一行):

{"@timestamp":"2026-08-30T22:00:00.123+08:00","level":"info","caller":"main.go:10","content":"服务启动完成"}
每条日志都自动带 @timestamp(ISO 时间)、levelcaller(文件名:行号)、content。开发期把 Encoding 设成 plain 看人话,上线再切回 json

五个「通道」:不只是 info / error

go-zero 把日志分成几条语义不同的通道。理解它们,日志才有「可观测性」:

Info 常规业务日志 · logx.Info / Infof Slow 慢调用(超过阈值)· logx.Slow / Slowf,独立 slow 日志 Stat 统计指标(QPS/耗时)· logx.Stat / Statf,可被监控采集 Error 错误日志 · logx.Error / Errorf / ErrorStack(带堆栈) Severe 致命错误 · logx.Severe,自动附调用栈,需立即告警

Info

正常流程:启动、配置加载、关键节点。量不要太大。

Slow

耗时超过 StatSlowThreshold(默认 500ms)的慢操作,方便定位性能瓶颈。

Stat

统计类数据:请求计数、耗时分布。可直接喂监控系统。

Error

出错了但服务还能跑。ErrorStack(err) 会把堆栈一起打出来。

Severe

要命的错误(DB 连不上等)。logx.Must(err) 会直接 panic/退出。

logx.Info("订单创建成功", orderId)
logx.Slow("数据库查询过慢", cost)          // 建议配合 WithDuration
logx.Stat("http.request")                     // 统计埋点
logx.Error(err)                              // 普通错误
logx.ErrorStack(err)                        // 错误 + 完整堆栈
logx.Severe("无法连接数据库,服务不可用")
注意:Error / Slow 只会记录不会让程序崩溃。要「出错即终止」请用 logx.Must(err)——它会写 Severe 日志后 panicos.Exit(1)(由 ExitOnFatal 开关决定)。

结构化日志(带字段)

比起把变量塞进字符串,用字段能让日志被检索(比如按 user_id 过滤)。go-zero 提供两类写法:

写法 A:Infow / Errorw...LogField 参数

logx.Infow("用户登录",
    logx.Field("user_id", uid),
    logx.Field("ip", clientIP),
    logx.Field("success", ok),
)

logx.Errorw("下单失败",
    logx.Field("order_id", orderId),
    logx.Field("reason", err.Error()),
)

写法 B:链式 WithFields(先攒字段,多次复用)

logger := logx.WithContext(ctx).WithFields(
    logx.Field("trace_id", traceID),
    logx.Field("biz", "payment"),
)
logger.Info("开始扣款")
logger.Error(chargeErr)   // 这条也会带上上面两个字段
logx.Field(key, value) 返回一个 LogField不会马上打印,要交给 Infow/Errorw 的变参,或 WithFields 链式调用才会输出。直接 logx.Field(...) 是无效代码。

在服务里「串联上下文」——最关键的一步

在 go-zero 的 handler / logic 里,你拿到的 ctx 里已经带着 trace / span ID(来自 OTEL 或内部 trace 中间件)。用 logx.WithContext(ctx) 打日志,这些链路 ID 会自动出现在每条日志里,跨服务一眼就能追到同一次请求。

API 网关 trace: abc123 User RPC span: u-77 Order RPC span: o-12 { level:info, trace:abc123, span:u-77, caller:user/logic.go:42, content:"查询用户 1001" } { level:info, trace:abc123, span:o-12, caller:order/logic.go:88, content:"创建订单 OK" } { level:error, trace:abc123, span:o-12, caller:order/logic.go:90, content:"扣款失败: timeout" }

在 logic 中这样用(l.ctx 是 go-zero 注入的 context):

func (l *OrderLogic) CreateOrder(req *types.Req) (*types.Resp, error) {
    // 关键:把 ctx 传进去,日志自动带 trace / span
    logx.WithContext(l.ctx).Infof("创建订单 user=%d", req.UserId)

    order, err := l.svc.OrderModel.Insert(l.ctx, req)
    if err != nil {
        // 错误日志同样带链路 ID,方便全链路定位
        logx.WithContext(l.ctx).Errorw("插入订单失败",
            logx.Field("user_id", req.UserId),
            logx.Field("err", err.Error()),
        )
        return nil, err
    }
    return &order, nil
}
坑:只写 logx.Info(...)不带 trace 的。务必养成 logx.WithContext(ctx).Info(...) 的习惯,否则日志在 ELK 里就追不到是哪次请求了。

配置详解:用 yaml 控制日志行为

go-zero 服务里,config.yamlLog 段会由 svc.NewServiceContext 自动 logx.MustSetup,不用手写。常见配置如下:

Log:
  ServiceName: order-api        # volume 模式会在文件名前缀加服务名+主机名
  Mode: console                 # console(终端) | file(文件) | volume(k8s)
  Encoding: json                # json(机器读) | plain(人读, 开发用)
  Level: info                   # debug | info | error | severe
  Path: logs                    # 日志目录,默认 logs
  TimeFormat: "2006-01-02T15:04:05.000Z07:00"
  Rotation: daily               # daily(按天) | size(按大小)
  KeepDays: 7                   # 保留天数
  MaxSize: 512                  # size 模式下单文件上限 MB
  MaxBackups: 10                # size 模式备份数
  Compress: false               # 是否压缩旧日志
  Stat: true                    # 是否写统计日志
字段取值默认值说明
Modeconsole / file / volumeconsole输出到终端 / 文件 / k8s 带主机名
Encodingjson / plainjsonJSON 便于采集,plain 便于肉眼
Leveldebug / info / error / severeinfo低于该级别不输出
Path字符串logs文件模式下的目录
ServiceName字符串volume 模式必需,否则报错
Rotationdaily / sizedaily按天滚动 / 按大小滚动
KeepDays整数0(全留)保留天数,到期删除
MaxSize整数 MB0(不限)仅 size 模式生效
Compresstrue / falsefalse压缩历史日志
Stattrue / falsetrue是否输出 Stat 统计日志
MaxContentLength整数0(不限)单条日志内容截断长度
Level: error 时,Info / Slow / Stat 都会被屏蔽,只打 Error 和 Severe。调试时临时调成 debug 能看到更多(前提是代码里用了 logx.Debug)。

进阶:耗时、全局字段、自定义输出

带耗时:WithDuration

start := time.Now()
// ... 一段业务 ...
logx.WithContext(ctx).WithDuration(time.Since(start)).Slow("慢查询")
// 输出会多一个 duration 字段,配合 Slow 通道做性能监控

全局字段:AddGlobalFields

之后所有日志统一加上环境/版本等字段,省得每条都写:

logx.AddGlobalFields(
    logx.Field("env", "prod"),
    logx.Field("version", "v1.3.0"),
)

自定义输出:SetWriter / AddWriter

要把日志同时发到 Kafka、ES 或远程收集器,实现 logx.Writer 接口(Info/Error/Slow/.../Close 几个方法),再 SetWriter 接管,或 AddWriter 叠加到现有输出之上:

type kafkaWriter struct{ prod *kafka.Producer }
func (w *kafkaWriter) Info(v any, fields ...logx.LogField)  { w.send("info", v, fields) }
func (w *kafkaWriter) Error(v any, fields ...logx.LogField) { w.send("error", v, fields) }
// ... 实现 Slow/Stat/Severe/Stack/Close/Alert ...

logx.AddWriter(&kafkaWriter{prod: prod})  // 在原有文件/控制台基础上叠加
多数可观测性方案(如接入 Prometheus + Loki + Tracing)用 file 模式把 JSON 日志落盘,再由 Filebeat / Promtail 采集即可,不必自己写 Writer。

完整可运行示例

下面是一段可直接放进 internal/logic/*.go 的综合示例,覆盖:配置注入、链路透传、结构化字段、耗时统计、错误分级。

package logic

import (
    "context"
    "time"

    "github.com/zeromicro/go-zero/core/logx"
)

type GreetLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewGreetLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GreetLogic {
    return &GreetLogic{ctx: ctx, svcCtx: svcCtx}
}

func (l *GreetLogic) Greet(name string) (string, error) {
    start := time.Now()
    // 1) 常规信息,自动带 trace/span(因为传了 l.ctx)
    logx.WithContext(l.ctx).Infow("收到问候请求",
        logx.Field("name", name),
    )

    if name == "" {
        // 2) 参数错误:用 Error + 字段,方便检索
        logx.WithContext(l.ctx).Errorw("name 为空",
            logx.Field("name", name),
        )
        return "", errors.New("name required")
    }

    // 3) 模拟一次下游调用
    msg, err := l.svcCtx.UserRpc.SayHello(l.ctx, name)
    if err != nil {
        // 4) 下游失败:带堆栈 + 字段,且带链路 ID
        logx.WithContext(l.ctx).Errorw("调用 UserRpc 失败",
            logx.Field("name", name),
            logx.Field("err", err.Error()),
        )
        return "", err
    }

    // 5) 统计耗时,慢了进 Slow 通道,正常进 Stat
    cost := time.Since(start)
    logger := logx.WithContext(l.ctx).WithDuration(cost)
    if cost > 500*time.Millisecond {
        logger.Slow("问候处理偏慢")
    } else {
        logger.Info("问候处理完成")
    }

    return msg, nil
}

常见坑 & 速查

配置没生效

自己写的 main 里忘了 logx.MustSetup;goctl 生成的服务会在 svc.NewServiceContext 自动调用,前提是 config 里嵌了 logx.LogConf

日志没 trace

直接 logx.Info 不带链路。永远logx.WithContext(ctx).Info(...)

Level 调高丢日志

Level: error 会把 Info/Slow/Stat 全屏蔽,排查时记得调低。

Field 不打印

logx.Field 只是构造器,必须交给 Infow/ErrorwWithFields 才会输出。

Error ≠ panic

logx.Error 不会中断程序;要「出错即停」用 logx.Must(err)

plain vs json

开发用 plain 看人话,生产用 json 便于采集。

速查表:
logx.Info / Infof 普通信息 · logx.Error / Errorf / ErrorStack 错误 · logx.Slow / Slowf 慢操作 · logx.Stat / Statf 统计 · logx.Severe 致命 · logx.WithContext(ctx) 链路透传 · logx.Field(k, v) 结构化字段 · logx.WithDuration(d) 耗时 · logx.MustSetup(c) 初始化。

API 以当前 go-zero 版本(master / latest)为准;logx.WithFields(ctx, ...) 旧写法已被 ContextWithFields 取代,新代码请用 WithContext(ctx).WithFields(...) 链式。