textmachine/backend/internal/obs/trace.go

69 lines
2.4 KiB
Go

// Package obs is the observability seam: trace ids threaded through context,
// the slog handler that stamps them on every log line, gated LLM-exchange
// logging, and recovered goroutines. Port of vojo trace.go/logging.go +
// safego lifted out of bot.go; the request axis was swapped from (sender,
// verbose) to (book, chapter, chunk, stage, role) — the translation domain.
package obs
import (
"context"
"crypto/rand"
"encoding/hex"
)
// trace.go threads a per-run correlation id and the small request facts the
// logger needs through context — the userver / OpenTelemetry idiom: mint once
// at the top of a unit of work, 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.
//
// The id is 16 random bytes rendered as 32 hex chars — the W3C Trace-Context /
// OTel trace-id shape — so the field maps straight onto an OTel exporter later.
type ctxKey int
const reqInfoKey ctxKey = iota
// ReqInfo is the per-call data carried in context. Zero-value fields are
// simply omitted from logs.
type ReqInfo struct {
TraceID string
Book string
Chapter int
Chunk int
Stage string // pipeline stage: draft | edit | ...
Role string // agent role: translator | editor | ...
// LogBodies gates raw LLM request/response body logging (privacy by
// default; decided once at admission so the transport just reads the flag).
LogBodies bool
}
// WithReqInfo stamps the call's info onto ctx. WithTimeout/WithCancel preserve
// values, so it flows down into the model transport.
func WithReqInfo(ctx context.Context, ri ReqInfo) context.Context {
return context.WithValue(ctx, reqInfoKey, ri)
}
// ReqInfoFromContext returns the call info, if any.
func ReqInfoFromContext(ctx context.Context) (ReqInfo, bool) {
ri, ok := ctx.Value(reqInfoKey).(ReqInfo)
return ri, ok
}
// TraceFromContext returns the trace id, or "" when ctx carries none (startup
// paths) — 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 on modern Go (an entropy
// failure crashes the process rather than returning a short read).
func NewTraceID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}