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>
76 lines
2.2 KiB
Go
76 lines
2.2 KiB
Go
package otelx
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
func newLogger(buf *bytes.Buffer) *slog.Logger {
|
|
return slog.New(traceHandler{slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo})})
|
|
}
|
|
|
|
func ctxWithSpan(traceHex, spanHex string) context.Context {
|
|
tid, _ := trace.TraceIDFromHex(traceHex)
|
|
sid, _ := trace.SpanIDFromHex(spanHex)
|
|
sc := trace.NewSpanContext(trace.SpanContextConfig{TraceID: tid, SpanID: sid})
|
|
return trace.ContextWithSpanContext(context.Background(), sc)
|
|
}
|
|
|
|
func TestTraceHandlerInjectsIDs(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := newLogger(&buf)
|
|
ctx := ctxWithSpan("0123456789abcdef0123456789abcdef", "0123456789abcdef")
|
|
|
|
logger.InfoContext(ctx, "hello")
|
|
|
|
out := buf.String()
|
|
if !strings.Contains(out, `"trace_id":"0123456789abcdef0123456789abcdef"`) {
|
|
t.Fatalf("expected trace_id in log, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, `"span_id":"0123456789abcdef"`) {
|
|
t.Fatalf("expected span_id in log, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestTraceHandlerNoSpanNoIDs(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := newLogger(&buf)
|
|
|
|
logger.InfoContext(context.Background(), "hello")
|
|
|
|
if strings.Contains(buf.String(), "trace_id") {
|
|
t.Fatalf("did not expect trace_id without a span, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestTraceHandlerPreservesWithAttrs(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := newLogger(&buf).With("service", "test-svc")
|
|
ctx := ctxWithSpan("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb")
|
|
|
|
logger.InfoContext(ctx, "hello")
|
|
|
|
out := buf.String()
|
|
// With(...) 后 trace 注入仍生效(WithAttrs 返回包装后的 handler)。
|
|
if !strings.Contains(out, `"service":"test-svc"`) || !strings.Contains(out, "trace_id") {
|
|
t.Fatalf("expected both service and trace_id after With(), got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestParseLevel(t *testing.T) {
|
|
cases := map[string]slog.Level{
|
|
"debug": slog.LevelDebug, "info": slog.LevelInfo, "warn": slog.LevelWarn,
|
|
"error": slog.LevelError, "": slog.LevelInfo, "bogus": slog.LevelInfo,
|
|
}
|
|
for in, want := range cases {
|
|
if got := parseLevel(in); got != want {
|
|
t.Errorf("parseLevel(%q)=%v want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|