69 lines
2.4 KiB
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[:])
|
|
}
|