日志配置必须在 kratos.New() 之前完成,否则将 fallback 到默认空实现且不可逆;需在 main() 开头调用 log.SetLogger(),使用 log.NewHelper() 获取带 caller 和 traceID 的结构化日志。

日志配置必须在 kratos.New() 之前完成
Kratos 的日志组件(log.Logger)是全局单例,且初始化时机极早——kratos.New() 内部会立即调用 log.GetLogger() 获取实例。如果此时还没配置好日志,就会 fallback 到默认的空实现(不输出、无格式、无法设置 level),后续再调用 log.SetLogger() 也无效。
正确做法是在 main() 开头就完成日志初始化:
func main() {
// 必须最先做:构造并设置 logger
logger := log.With(log.NewStdLogger(os.Stdout),
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"service.id", "user-service",
)
log.SetLogger(logger)
<pre class="brush:php;toolbar:false;">app := kratos.New(
kratos.Name("user-service"),
// ... 其他选项
)
// ...}
- 漏掉
log.SetLogger()是最常见错误,现象是控制台完全没日志,或只有 Kratos 启动 banner 但业务日志消失 -
log.With()的字段名(如"ts")必须是字符串字面量,不能拼接或变量;否则 runtime 会 panic - 若用
log.NewJSONLogger(),需确保os.Stdout或文件句柄支持并发写(标准os.File是线程安全的,但自定义io.Writer要自行加锁)
在 Service/Handler 中获取 logger 要用 log.NewHelper()
直接在 handler 函数里调用 log.Info() 看似可行,但会导致所有日志都标记为 log.go:123(即 log 包内部行号),丢失真实调用位置。Kratos 推荐用 log.Helper 封装,它会自动注入 caller 信息,并支持结构化字段追加。
典型用法:
func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.CreateUserResponse, error) {
h := log.NewHelper(log.With(log.GetLogger(), "method", "CreateUser"))
<pre class="brush:php;toolbar:false;">h.InfoContext(ctx, "create user start", "user_id", req.UserId)
// ... 业务逻辑
h.InfoContext(ctx, "create user success", "user_id", req.UserId)
return &v1.CreateUserResponse{UserId: req.UserId}, nil}
-
log.NewHelper()不创建新 logger,只是对已有 logger 的轻量封装,开销可忽略 -
InfoContext()/ErrorContext()会自动提取ctx中的 traceID(如果已集成 OpenTelemetry 或 Kratos 的 tracing 中间件) - 避免在循环内反复调用
log.NewHelper()—— 可提至函数开头一次初始化
日志 level 控制依赖 log.SetLevel(),而非环境变量
Kratos 默认日志 level 是 log.LevelInfo,不会输出 Debug 级别日志。很多人误以为改 LOG_LEVEL=debug 环境变量就能生效,实际 Kratos 日志层完全不读环境变量,必须显式调用 log.SetLevel()。
推荐结合 flag 或配置中心动态调整:
var logLevel = flag.String("log.level", "info", "log level: debug, info, warn, error")
func main() {
flag.Parse()
level, _ := log.ParseLevel(*logLevel)
log.SetLevel(level)
<pre class="brush:php;toolbar:false;">logger := log.With(log.NewStdLogger(os.Stdout), /* ... */)
log.SetLogger(logger)
// ...}
-
log.ParseLevel()支持"debug"/"INFO"/"warn"等大小写不敏感字符串 - 线上环境务必设为
log.LevelWarn或更高,避免Debug日志刷爆磁盘或影响性能 - 若用 viper 加载配置,注意
viper.GetString("log.level")返回空字符串时要 fallback,默认 info
结构化日志字段不能嵌套,复杂数据需序列化
Kratos 日志底层使用 go-kit/log,其 log.With() 和 Helper 方法只接受扁平 key-value 对。传入 struct、map 或 slice 会触发 fmt.Sprint(),导致日志可读性差、无法被 ELK 等系统解析。
正确方式是手动展开或 JSON 序列化:
// ❌ 错误:直接传 struct
h.Info("user", user) // 输出 "user"="&{1 john ...}"
<p>// ✅ 正确:展开关键字段
h.Info("user_id", user.ID, "user_name", user.Name, "status", user.Status)</p><p>// ✅ 或序列化(适合调试,但注意性能)
data, _ := json.Marshal(user)
h.Debug("user_detail", "json", string(data))- HTTP 请求参数、错误详情等动态内容,建议统一用
json.Marshal()后作为"detail"字段输出 - 避免在高频路径(如中间件、gRPC 拦截器)中无节制打结构化日志,尤其是含大对象时
- 字段名不要带空格或点号(如
"user.name"),部分日志采集器(如 filebeat)解析会出错
日志的 caller 追踪和结构化字段处理是容易被跳过的细节,一旦出问题很难回溯——比如线上报错只显示 “failed”,却找不到是哪个 service 哪个 method 报的。把 log.NewHelper() 当成和 context.Context 一样随调用链传递,比事后 grep 日志省力得多。


















