Documentation
¶
Overview ¶
Package obs ("observability") is the cross-cutting layer that wires structured logging, Prometheus metrics, and the /debug snapshot view into the rest of PromptZero.
The log surface is a thin wrapper around log/slog: every REPL turn (and every --watch-triggered or workflow-invoked turn) is wrapped in WithTrace so a correlation ID threads through every downstream log line, audit entry, and Prom label. FromCtx resolves the logger the caller should use; it falls back to the global handler when the context was not derived from WithTrace.
OpenTelemetry GenAI wiring for PromptZero.
Honours the standard OTEL_* environment variables — when OTEL_EXPORTER_OTLP_ENDPOINT is unset, InitOTel returns a no-op shutdown func and all span calls become cheap routed calls through a tracer whose provider drops everything. This keeps existing deployments unchanged and lets operators opt in by setting one env.
Span attributes follow the OTel GenAI semantic conventions (gen_ai.*). The set we emit:
gen_ai.system = "anthropic" gen_ai.request.model = "claude-sonnet-4-6" (etc.) gen_ai.usage.input_tokens = int gen_ai.usage.output_tokens = int gen_ai.usage.cache_read_input_tokens = int gen_ai.usage.cache_creation_input_tokens = int gen_ai.response.finish_reasons = "end_turn" | "tool_use" | ... gen_ai.tool.name = "wifi_scan_ap" gen_ai.tool.call.id = "toolu_..."
Per the spec, tool-call spans are emitted as children of the agent turn span so a single trace shows the full request -> tools -> reply chain.
Index ¶
- func CollectRuntime() (goroutines int, heapMB, sysMB float64, lastGCAgo time.Duration, ...)
- func Default() *slog.Logger
- func FromCtx(ctx context.Context) *slog.Logger
- func RecordFinishReason(span trace.Span, reason string)
- func RecordToolResult(span trace.Span, outputLen int, errBool bool)
- func RecordUsage(span trace.Span, ...)
- func SafeGo(name string, fn func())
- func Setup(cfg LogConfig) *slog.Logger
- func SpanFromCtx(ctx context.Context) trace.Span
- func StartAgentTurn(ctx context.Context, model string, inputLen int) (context.Context, trace.Span)
- func StartToolCall(ctx context.Context, toolName, toolCallID, inputJSON string) (context.Context, trace.Span)
- func TraceID(ctx context.Context) string
- func Tracer() trace.Tracer
- func WithTrace(ctx context.Context) (context.Context, string)
- type DebugSnapshot
- type LogConfig
- type Recorder
- func (r *Recorder) Handler() http.Handler
- func (r *Recorder) LastTools() []ToolSample
- func (r *Recorder) RecordAudit(risk, level string)
- func (r *Recorder) RecordMessage(role string)
- func (r *Recorder) RecordRiskPrompt(tool, decision string)
- func (r *Recorder) RecordTokens(kind string, n int64)
- func (r *Recorder) RecordToolCall(tool, riskLevel, status string, d time.Duration)
- func (r *Recorder) RecordWebhookDelivery(name, status string)
- func (r *Recorder) RecordWorkflowRun(name, status string, d time.Duration)
- func (r *Recorder) Registry() *prometheus.Registry
- func (r *Recorder) SetAnthropicReachable(v bool)
- func (r *Recorder) SetFlipperConnected(v bool)
- func (r *Recorder) SetMarauderConnected(v bool)
- func (r *Recorder) UptimeStart() time.Time
- type ShutdownFunc
- type ToolSample
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CollectRuntime ¶
func CollectRuntime() (goroutines int, heapMB, sysMB float64, lastGCAgo time.Duration, version, plat string)
CollectRuntime pulls goroutine / heap / GC stats into a partial DebugSnapshot so callers only need to add their own Persona/Flipper/Audit state. Split out so tests can exercise the rendering layer without touching the process.
func Default ¶
Default returns the globally installed logger. Prefer FromCtx when a context is in scope — the ctx-bound logger carries trace_id for free.
func FromCtx ¶
FromCtx returns the logger bound to ctx (with trace_id attached when WithTrace was used). Falls back to the globally installed handler when ctx has no trace — callers never need to nil-check.
func RecordFinishReason ¶ added in v0.3.0
RecordFinishReason records the model's stop reason on a span. Common values: "end_turn", "tool_use", "max_tokens", "stop_sequence".
func RecordToolResult ¶ added in v0.3.0
RecordToolResult stamps a tool-call span with its outcome. errBool flips the span status to Error so trace viewers highlight failures without parsing the output payload. outputLen is logged as an attribute (not the full output) so we don't double-record content already in the audit log.
func RecordUsage ¶ added in v0.3.0
func RecordUsage(span trace.Span, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens int64)
RecordUsage stamps gen_ai.usage.* attributes onto the given span. Safe to call on a no-op span (the attribute calls are dropped).
func SafeGo ¶ added in v0.17.0
func SafeGo(name string, fn func())
SafeGo launches fn in a new goroutine wrapped with a deferred recover so a panic inside fn is caught, logged via the global logger, and does not crash the process. name identifies the goroutine in the log line so the call site is traceable; the captured stack is included so the panic site inside fn is visible without re-running with GOTRACEBACK=all.
func Setup ¶
Setup installs a slog.Logger built from cfg as the global default and returns it so tests can attach their own consumers. Levels outside the known set fall back to info with a warning. When Format is "json" the handler emits newline-delimited JSON; otherwise the text handler is used. File, when non-empty, opens (or creates) that path in append mode and mirrors every record to both stderr and the file so operators keep local tailing while still emitting to disk.
func SpanFromCtx ¶ added in v0.3.0
SpanFromCtx returns the current span bound to ctx, or a no-op span when tracing is disabled. Never returns nil so callers can unguarded call SetAttributes / End without a defensive nil check.
func StartAgentTurn ¶ added in v0.3.0
StartAgentTurn opens a span for one top-level agent turn and populates the gen_ai.request.model attribute. Returns a context that carries the span and a finish func that must be deferred.
inputLen is useful for distinguishing short intent-classification turns from long exploit-planning turns in the trace viewer — kept separate from token counts because tokens aren't known until the response comes back.
func StartToolCall ¶ added in v0.3.0
func StartToolCall(ctx context.Context, toolName, toolCallID, inputJSON string) (context.Context, trace.Span)
StartToolCall opens a child span for a single tool invocation. Captures the tool name, Anthropic tool-call id, and the input JSON as attributes so the trace viewer can surface "what did the agent call with what args" without cross-referencing the audit log.
The input JSON is passed as a string — JSON encoding on the caller side is cheap and keeps this helper allocation-free for no-op spans.
func TraceID ¶
TraceID returns the trace attached to ctx, or "" when none was set. Callers that need to round-trip the trace ID into a downstream system (audit row, webhook payload, Prom label) read it here.
func Tracer ¶ added in v0.3.0
Tracer returns the global PromptZero tracer. Always safe to call — when OTel is disabled this returns a no-op tracer whose spans are free. Callers should propagate the returned context through the operation they're measuring.
func WithTrace ¶
WithTrace returns a context carrying a fresh 16-hex trace ID and a slog.Logger bound to that ID. If ctx already carries a trace the existing value is preserved — this makes WithTrace safe to call at nested boundaries (workflow phases, validator gate, rules dispatch) without silently discarding the turn's correlation ID.
Types ¶
type DebugSnapshot ¶
type DebugSnapshot struct {
BuildVersion string
GoVersion string
Platform string
Uptime time.Duration
TraceID string
PersonaName string
PersonaTools int
PersonaAllow int
FlipperPort string
FlipperUp bool
FlipperModel string
MarauderPort string
MarauderUp bool
AuditDBPath string
AuditRows int64
SessionID string
Goroutines int
HeapMB float64
SysMB float64
LastGCAgo time.Duration
LastTools []ToolSample
OfflineMode bool
}
DebugSnapshot is the state bag /debug renders. Each field is optional — callers fill what they can from their local surface. The Render method turns this into the boxed multi-line text the REPL prints.
func (DebugSnapshot) Render ¶
func (s DebugSnapshot) Render(w io.Writer, width int)
Render draws the snapshot as a box of rows. Width is the inner box width in columns; 68 is a sensible default that fits most terminals without wrapping. Uses only ASCII box-drawing characters so NO_COLOR environments still render.
type LogConfig ¶
type LogConfig struct {
Level string `yaml:"log_level,omitempty"`
Format string `yaml:"log_format,omitempty"`
File string `yaml:"log_file,omitempty"`
}
LogConfig is the user-visible slice of observability configuration that affects log output. YAML tags match the on-disk shape under `observability:` — see internal/config for the round-trip.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder owns a Prometheus registry and the full PromptZero metric surface. It is constructed once per process and handed to every subsystem that emits events (audit observer, webhook dispatcher, agent, workflows). Each Record* method is concurrency-safe and cheap — callers should never conditionally branch on "metrics enabled"; instead, pass a nil Recorder to disable (every method is a nil-receiver no-op).
The Recorder uses its own *prometheus.Registry (not the default global) so tests and the Go runtime's process metrics don't bleed into PromptZero's scrape surface. Handler() returns an http.Handler that exposes the registry in the standard Prom text format at whatever route the web server mounts.
func NewRecorder ¶
func NewRecorder() *Recorder
NewRecorder builds a Recorder backed by a fresh Prometheus registry. The native histogram buckets below target PromptZero's observed latencies (sub-second reads up through multi-minute brute-forces).
func (*Recorder) Handler ¶
Handler returns the standard Prometheus text-format HTTP handler. Mount it at whatever path config.Observability.MetricsPath resolves to.
func (*Recorder) LastTools ¶
func (r *Recorder) LastTools() []ToolSample
LastTools returns a chronological copy of the recent tool calls, oldest first. Used by the /debug snapshot.
func (*Recorder) RecordAudit ¶
RecordAudit bumps the audit counter. Call once per audit.Record from the audit observer hook.
func (*Recorder) RecordMessage ¶
RecordMessage tracks message-history churn so dashboards can see how chatty a session is. Role is "user", "assistant", or "tool".
func (*Recorder) RecordRiskPrompt ¶
RecordRiskPrompt records an operator decision at the risk gate. Decision is "approve", "deny", or "approve_all".
func (*Recorder) RecordTokens ¶
RecordTokens pushes token consumption into the Prom counter. Kind is "input" or "output".
func (*Recorder) RecordToolCall ¶
RecordToolCall bumps the tool invocation counter and timing histogram. Status is one of "ok", "error", "denied" so Grafana dashboards can split the three outcomes without parsing labels. A nil recorder is a no-op — pass nil freely when metrics are disabled.
func (*Recorder) RecordWebhookDelivery ¶
RecordWebhookDelivery counts an outbound webhook attempt. Status is "ok", "error", or a numeric 4xx/5xx class the dispatcher cares to split on.
func (*Recorder) RecordWorkflowRun ¶
RecordWorkflowRun increments the workflow counter and records the wall-clock duration. Name matches the advertised tool name (e.g. "workflow_nfc_badge_pipeline"), status is "ok" or "error".
func (*Recorder) Registry ¶
func (r *Recorder) Registry() *prometheus.Registry
Registry returns the underlying registry so tests can scrape it directly without standing up an HTTP server.
func (*Recorder) SetAnthropicReachable ¶
SetAnthropicReachable toggles the offline-mode gauge. See internal/cost for the detection logic that drives this.
func (*Recorder) SetFlipperConnected ¶
SetFlipperConnected flips the connected gauge for the Flipper serial transport. Called by the flipper connect/reconnect hooks.
func (*Recorder) SetMarauderConnected ¶
SetMarauderConnected flips the Marauder gauge. Only toggled when the operator is actively using --wifi; otherwise stays at the default 0.
func (*Recorder) UptimeStart ¶
UptimeStart reports when NewRecorder was called. /debug reads this to render an uptime string.
type ShutdownFunc ¶ added in v0.3.0
ShutdownFunc flushes pending spans and releases the exporter's resources. Safe to call multiple times; subsequent calls are no-ops.
func InitOTel ¶ added in v0.3.0
func InitOTel(ctx context.Context) (ShutdownFunc, error)
InitOTel wires up an OTel tracer provider against the OTLP HTTP exporter. When OTEL_EXPORTER_OTLP_ENDPOINT is empty, the function installs a no-op tracer and returns a no-op shutdown — so callers can always invoke the returned shutdown in a defer without branching.
The service name defaults to "promptzero" but can be overridden via OTEL_SERVICE_NAME (standard semconv).