package otelx import ( "context" "log/slog" "os" "strings" "go.opentelemetry.io/otel/trace" ) // traceHandler 包装一个 slog.Handler:每条带 ctx 的日志自动附上 trace_id / span_id, // 从而「日志 ↔ 链路」互跳——在 Jaeger 看到一个 span 即可拿 trace_id 过滤日志,反之亦然。 // 只有用 *Context 方法(slog.InfoContext 等)且 ctx 里有活跃 span 时才会注入。 type traceHandler struct{ slog.Handler } func (h traceHandler) Handle(ctx context.Context, r slog.Record) error { if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() { r.AddAttrs(slog.String("trace_id", sc.TraceID().String())) if sc.HasSpanID() { r.AddAttrs(slog.String("span_id", sc.SpanID().String())) } } return h.Handler.Handle(ctx, r) } func (h traceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return traceHandler{h.Handler.WithAttrs(attrs)} } func (h traceHandler) WithGroup(name string) slog.Handler { return traceHandler{h.Handler.WithGroup(name)} } // SetupSlog 安装一个全局结构化日志器(JSON → stderr),自带 service 标签且链路感知, // 并设为 slog 默认。此后任意 slog.InfoContext(ctx, ...) 都会带 trace_id(若 ctx 有 span)。 // 级别经 LOG_LEVEL 控制(debug/info/warn/error,默认 info)。 func SetupSlog(serviceName string) *slog.Logger { base := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: parseLevel(os.Getenv("LOG_LEVEL"))}) logger := slog.New(traceHandler{base}).With("service", serviceName) slog.SetDefault(logger) return logger } func parseLevel(s string) slog.Level { switch strings.ToLower(s) { case "debug": return slog.LevelDebug case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // TraceID 返回 ctx 当前 span 的 trace id(无则空)——供需要手动带 trace_id 的场景。 func TraceID(ctx context.Context) string { if sc := trace.SpanContextFromContext(ctx); sc.HasTraceID() { return sc.TraceID().String() } return "" }