vojo/apps/ai-bot/trace.go

66 lines
2.7 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
)
// trace.go threads a per-request correlation id (and the small request facts the logger
// and the body-logging gate need) through context — the userver / OpenTelemetry idiom:
// mint once at the top of a request, and every log line below it (down to the HTTP call
// to the model) carries the same trace_id without passing a logger by hand. ctx is
// already plumbed through the whole request path (handleEvent → respond → generate →
// LLMClient.Complete → the transport), so a value placed here surfaces everywhere.
//
// The id is 16 random bytes rendered as 32 hex chars — the W3C Trace-Context / OTel
// trace-id shape — so the trace_id field maps straight onto an OpenTelemetry trace id if
// an exporter is added later (no log/field rename). Today this is a correlation key, not
// a full SpanContext: real distributed tracing would still add a span_id and traceparent
// propagation across services.
type ctxKey int
const reqInfoKey ctxKey = iota
// reqInfo is the per-request data carried in context: the trace id stamped on every log
// line, the sender (so the body-log lines stay filterable by user), and verbose —
// whether this sender is on the LOG_BODIES_USERS allowlist. verbose is decided once, at
// admission, so the deep transport never re-checks the allowlist; it just reads the flag.
type reqInfo struct {
traceID string
sender string
verbose bool
}
// withRequestTrace stamps the request's trace id + sender + body-logging decision onto
// ctx. Call it once per handled event; the value flows down through the per-room
// goroutine, the per-request deadline ctx (WithTimeout preserves values), and into the
// model transport.
func withRequestTrace(ctx context.Context, traceID, sender string, verbose bool) context.Context {
return context.WithValue(ctx, reqInfoKey, reqInfo{traceID: traceID, sender: sender, verbose: verbose})
}
func reqInfoFromContext(ctx context.Context) (reqInfo, bool) {
ri, ok := ctx.Value(reqInfoKey).(reqInfo)
return ri, ok
}
// traceFromContext returns the request trace id, or "" when ctx carries none (startup,
// the appservice transaction handler) — the slog handler then simply omits trace_id.
func traceFromContext(ctx context.Context) string {
if ri, ok := reqInfoFromContext(ctx); ok {
return ri.traceID
}
return ""
}
// newTraceID mints a random 16-byte id as 32 hex chars (the OTel trace-id shape).
// crypto/rand.Read never returns an error and always fills the buffer (Go 1.24+: on an
// entropy failure it crashes the process rather than returning a short read), so ignoring
// the error is safe — the id is always fully random.
func newTraceID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}