218fba559c
新增 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>
64 lines
2.0 KiB
Go
64 lines
2.0 KiB
Go
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 ""
|
||
}
|