Files
Blizzard 218fba559c feat(observability): slog 日志带 trace_id,与链路双向互跳
新增 sundynix-shared/otelx/slog.go:traceHandler 包装 slog.Handler,凡 ctx 有
活跃 span 的 slog.InfoContext(ctx,...) 自动注入 trace_id/span_id;SetupSlog(服务名)
装全局 JSON slog(service 标签 + LOG_LEVEL 控级)并设默认;TraceID(ctx) 辅助取 hex。

- 三个服务 main 启动调 otelx.SetupSlog。
- gateway 访问日志 Observe() 改用 slog.InfoContext(c.Request.Context(),...)(删旧
  accessLogger)→ 每条 HTTP 日志带 trace_id。
- dispatcher orchestrator.Handle 的 received/done/error 改 ctx-aware slog → 任务
  执行日志带 trace_id。
- otelx 单测 5 例(注入/无 span 不注入/With() 后仍生效/级别解析)。
- production_readiness.md 1.1:可观测性三件套(metrics+logs带trace_id+traces)闭环。

验证:dispatcher「task done」日志 trace_id 拿去 Jaeger /api/traces/<id> 命中同一条
11 span/3 服务链路;gateway 访问日志亦带同一 trace_id。四模块 build+vet+test 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:41:33 +08:00

64 lines
2.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ""
}