Documentation
¶
Overview ¶
Package stats defines the Observer interface for codec and adapter lifecycle events.
go-codex exposes two levels of observability:
Codec-level (ValidationObserver) ¶
Use ValidationObserver when you call codecs directly without an adapter — for example, validating config files, parsing binary protocols, or any non-HTTP/MQTT use case. Implement just ValidationObserver.RecordValidationError and call ReportErrors after each codex.Codec.Decode:
val, err := appConfigCodec.Decode(rawData) stats.ReportErrors(obs, "config", err)
Adapter-level (Observer) ¶
Use the full Observer interface when wiring to an adapter. It embeds ValidationObserver and adds transport-specific hooks for HTTP and MQTT:
nethttp.Register(mux, route, handler, nethttp.Options{Observer: obs})
adaptermqtt.SubscribeHandler(ctx, ch, fn, adaptermqtt.SubscribeOptions{Observer: obs})
Composing metrics and logging ¶
Use NewLoggingObserver and NewFanout to separate the metrics concern from the logging concern — no mixing of slog and counters in one struct:
metrics := &MyMetricsObserver{} // pure counters — swap for Prometheus
obs := stats.NewFanout(
metrics,
stats.NewLoggingObserver(slog.Default().With("component", "api")),
)
nethttp.Register(mux, route, handler, nethttp.Options{Observer: obs})
LoggingObserver implements all five observer interfaces and logs every event via slog. Configure the logger's handler for your environment:
- slog.NewTextHandler for development
- slog.NewJSONHandler for log aggregation
- An OpenTelemetry slog bridge for distributed traces
NewFanout fans out to all provided observers and also implements the optional FileObserver, SecurityObserver, and PipelineObserver interfaces — delegating each to the inner observers that satisfy those interfaces.
NoopObserver satisfies all five interfaces at zero cost; it is the default when no observer is configured.
Index ¶
- func ConstraintName(err error) string
- func ReportErrors(obs ValidationObserver, location string, err error)
- func WithObserver(ctx context.Context, obs Observer) context.Context
- type CacheObserver
- type FileObserver
- type LoggingObserver
- func (o *LoggingObserver) RecordApply(name, version string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordCacheHit(key string, d time.Duration)
- func (o *LoggingObserver) RecordCacheMiss(key string, d time.Duration)
- func (o *LoggingObserver) RecordCacheWrite(key, op string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordFileRead(path string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordFileWrite(path string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordMigration(op, name string, version int64, d time.Duration, err error)
- func (o *LoggingObserver) RecordPublish(topic string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordRequest(method, path string, statusCode int, d time.Duration)
- func (o *LoggingObserver) RecordSecurityRejection(location, scheme string)
- func (o *LoggingObserver) RecordStreamItem(function string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordSubscribe(topic string, success bool, d time.Duration)
- func (o *LoggingObserver) RecordValidation(table, op string, d time.Duration, err error)
- func (o *LoggingObserver) RecordValidationError(location, constraint, field string)
- type NoopObserver
- func (NoopObserver) EndSpan(_ context.Context, _ error)
- func (NoopObserver) RecordApply(_, _ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordCacheHit(_ string, _ time.Duration)
- func (NoopObserver) RecordCacheMiss(_ string, _ time.Duration)
- func (NoopObserver) RecordCacheWrite(_, _ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordFileRead(_ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordFileWrite(_ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordMigration(_, _ string, _ int64, _ time.Duration, _ error)
- func (NoopObserver) RecordPublish(_ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordRequest(_, _ string, _ int, _ time.Duration)
- func (NoopObserver) RecordSecurityRejection(_, _ string)
- func (NoopObserver) RecordStreamItem(_ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordSubscribe(_ string, _ bool, _ time.Duration)
- func (NoopObserver) RecordValidation(_, _ string, _ time.Duration, _ error)
- func (NoopObserver) RecordValidationError(_, _, _ string)
- func (NoopObserver) StartSpan(ctx context.Context, _, _ string) context.Context
- type Observer
- type PipelineObserver
- type SQLObserver
- type SecurityObserver
- type StreamObserver
- type TraceObserver
- type ValidationObserver
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ConstraintName ¶
ConstraintName extracts the constraint identifier from a field-level error:
- codex.ConstraintError.Name when the error is a constraint failure
- "type-mismatch" for codex.TypeMismatchError
- "required" for codex.ErrMissingField
- "" for any other error type
func ReportErrors ¶
func ReportErrors(obs ValidationObserver, location string, err error)
ReportErrors walks err and calls obs.RecordValidationError for every codec validation failure it finds. location identifies the data source (e.g. "body", "query", "payload", "config"). A no-op if err is nil or contains no recognisable validation errors.
Handled error types:
- codex.ValidationErrors — each entry reports its field and constraint.
- codex.KeyError — reports the failing map key as the field.
- codex.ElementError — reports the slice index as the field (e.g. "[2]").
For all three, the walker recurses into the wrapped cause so nested errors (e.g. a KeyError whose cause is a ValidationErrors) are fully reported. Any other wrapped error is unwrapped and recursed into silently.
Example ¶
package main
import (
"fmt"
"github.com/DaniDeer/go-codex/codex"
"github.com/DaniDeer/go-codex/stats"
"github.com/DaniDeer/go-codex/validate"
)
func main() {
// ValidationObserver: implement to receive per-field validation events.
type simpleObserver struct{ count int }
obs := &simpleObserver{}
recordFn := func(location, constraintName, field string) {
obs.count++
}
// Wrap in a ValidationObserver for ReportErrors.
type wrapObs struct{ fn func(string, string, string) }
w := &wrapObs{fn: recordFn}
// Example: decode a struct with two failing fields.
type Config struct {
Port int
Level string
}
portCodec := codex.Int().Refine(validate.RangeInt(1, 65535))
levelCodec := codex.String().Refine(validate.OneOf("debug", "info", "warn", "error"))
configCodec := codex.Struct[Config](
codex.RequiredField("port", portCodec,
func(c Config) int { return c.Port },
func(c *Config, v int) { c.Port = v },
),
codex.OptionalField("level", levelCodec,
func(c Config) string { return c.Level },
func(c *Config, v string) { c.Level = v },
),
)
_, err := configCodec.Decode(map[string]any{"port": float64(99999), "level": "verbose"})
_ = w // suppress unused warning
stats.ReportErrors(stats.NoopObserver{}, "config", err)
// err is a structured codex.ValidationErrors — all fields collected at once.
fmt.Println(err != nil)
}
Output: true
func WithObserver ¶ added in v0.11.0
WithObserver returns a copy of ctx carrying obs as the default observer. Adapters, stream bridges, and [ports.File] operations that receive this context will use obs automatically when no explicit Observer is set in their options struct — without passing Observer: obs on every call site.
Use WithObserver at application startup to wire one observer everywhere:
obs := stats.NewFanout(metricsObserver, stats.NewLoggingObserver(slog.Default()))
ctx := stats.WithObserver(context.Background(), obs)
// All adapters use obs when Options.Observer is nil:
nethttp.Handler(handle, fn, nethttp.Options{})
mqtt.Subscribe(ctx, client, handle, 1, fn, mqtt.SubscribeOptions{})
stream.Apply(ctx, s, fn, stream.ApplyOptions{})
The context-provided observer has **lower** priority than an explicitly set opts.Observer — explicit always wins:
nethttp.Handler(handle, fn, nethttp.Options{Observer: auditObs}) // auditObs used, ctx obs ignored
The observer is scoped to the context: a child context from context.WithValue inherits the parent's observer unless overridden. Use WithObserver again on a child context to swap the observer for a sub-tree.
Example ¶
package main
import (
"context"
"github.com/DaniDeer/go-codex/stats"
)
func main() {
obs := stats.NewLoggingObserver(nil) // nil logger is replaced by slog.Default() in real code
ctx := stats.WithObserver(context.Background(), obs)
// Retrieve anywhere downstream — adapters do this internally.
retrieved := stats.ObserverFromContext(ctx)
_ = retrieved
}
Output:
Types ¶
type CacheObserver ¶ added in v0.12.0
type CacheObserver interface {
// RecordCacheHit is called for every cache lookup that found and decoded
// a value. key is the expanded cache key (e.g. "user:42").
RecordCacheHit(key string, duration time.Duration)
// RecordCacheMiss is called for every cache lookup that found no value.
RecordCacheMiss(key string, duration time.Duration)
// RecordCacheWrite is called for every cache write or delete, success or
// failure. op is "set" or "del".
RecordCacheWrite(key, op string, success bool, duration time.Duration)
}
CacheObserver is an optional extension to Observer for cache adapter lifecycle events (adapters/redis). Cache adapters type-assert the configured Observer to CacheObserver before calling its methods — existing Observer implementations need not change.
type MyObserver struct{ ... }
func (o *MyObserver) RecordCacheHit(key string, d time.Duration) { hits.Inc() }
func (o *MyObserver) RecordCacheMiss(key string, d time.Duration) { misses.Inc() }
func (o *MyObserver) RecordCacheWrite(key, op string, success bool, d time.Duration) { ... }
type FileObserver ¶ added in v0.11.0
type FileObserver interface {
// RecordFileRead is called after every [ports.File.Read], [ports.File.Update],
// or [ports.File.Patch] attempt (read phase). path is the concrete file path
// (after template substitution), success is false on any error including
// decode/validation failures.
RecordFileRead(path string, success bool, duration time.Duration)
// RecordFileWrite is called after every [ports.File.Write], [ports.File.Update],
// or [ports.File.Patch] attempt (write phase). success is false on any
// encode or filesystem error.
RecordFileWrite(path string, success bool, duration time.Duration)
}
FileObserver is an optional extension to Observer for file I/O lifecycle events. [ports.File] type-asserts the configured observer to FileObserver before calling its methods, so implementing this interface is purely additive — existing Observer implementations need not change.
type MyObserver struct{ ... }
func (o *MyObserver) RecordFileRead(path string, success bool, d time.Duration) {
// increment a Prometheus counter, emit a log line, etc.
}
func (o *MyObserver) RecordFileWrite(path string, success bool, d time.Duration) { ... }
type LoggingObserver ¶ added in v0.11.0
type LoggingObserver struct {
// contains filtered or unexported fields
}
LoggingObserver logs every observer event as a structured slog message. It implements all observer interfaces except TraceObserver: Observer (embeds ValidationObserver), PipelineObserver, SecurityObserver, FileObserver, SQLObserver, StreamObserver, and CacheObserver.
TraceObserver is intentionally not implemented — slog has no concept of distributed trace spans. Use stats.NewFanout to combine a LoggingObserver with a separate TraceObserver implementation (e.g. OpenTelemetry).
Configure the logger's handler for your environment:
- slog.NewTextHandler for development
- slog.NewJSONHandler for structured log aggregation
- An OpenTelemetry slog bridge for distributed traces
Combine with a metrics observer via NewFanout for both metrics and logging:
obs := stats.NewFanout(
metricsObserver,
stats.NewLoggingObserver(slog.Default().With("component", "api")),
)
func NewLoggingObserver ¶ added in v0.11.0
func NewLoggingObserver(logger *slog.Logger) *LoggingObserver
NewLoggingObserver returns a LoggingObserver backed by logger.
func (*LoggingObserver) RecordApply ¶ added in v0.11.0
func (o *LoggingObserver) RecordApply(name, version string, success bool, d time.Duration)
func (*LoggingObserver) RecordCacheHit ¶ added in v0.12.0
func (o *LoggingObserver) RecordCacheHit(key string, d time.Duration)
func (*LoggingObserver) RecordCacheMiss ¶ added in v0.12.0
func (o *LoggingObserver) RecordCacheMiss(key string, d time.Duration)
func (*LoggingObserver) RecordCacheWrite ¶ added in v0.12.0
func (o *LoggingObserver) RecordCacheWrite(key, op string, success bool, d time.Duration)
func (*LoggingObserver) RecordFileRead ¶ added in v0.11.0
func (o *LoggingObserver) RecordFileRead(path string, success bool, d time.Duration)
func (*LoggingObserver) RecordFileWrite ¶ added in v0.11.0
func (o *LoggingObserver) RecordFileWrite(path string, success bool, d time.Duration)
func (*LoggingObserver) RecordMigration ¶ added in v0.11.0
func (*LoggingObserver) RecordPublish ¶ added in v0.11.0
func (o *LoggingObserver) RecordPublish(topic string, success bool, d time.Duration)
func (*LoggingObserver) RecordRequest ¶ added in v0.11.0
func (o *LoggingObserver) RecordRequest(method, path string, statusCode int, d time.Duration)
func (*LoggingObserver) RecordSecurityRejection ¶ added in v0.11.0
func (o *LoggingObserver) RecordSecurityRejection(location, scheme string)
func (*LoggingObserver) RecordStreamItem ¶ added in v0.11.0
func (o *LoggingObserver) RecordStreamItem(function string, success bool, d time.Duration)
func (*LoggingObserver) RecordSubscribe ¶ added in v0.11.0
func (o *LoggingObserver) RecordSubscribe(topic string, success bool, d time.Duration)
func (*LoggingObserver) RecordValidation ¶ added in v0.11.0
func (o *LoggingObserver) RecordValidation(table, op string, d time.Duration, err error)
func (*LoggingObserver) RecordValidationError ¶ added in v0.11.0
func (o *LoggingObserver) RecordValidationError(location, constraint, field string)
type NoopObserver ¶
type NoopObserver struct{}
NoopObserver discards all events. It satisfies all observer interfaces — Observer (embeds ValidationObserver), PipelineObserver, SecurityObserver, FileObserver, SQLObserver, StreamObserver, CacheObserver, and TraceObserver — and is the zero-cost default used when no observer is configured.
func (NoopObserver) EndSpan ¶ added in v0.11.0
func (NoopObserver) EndSpan(_ context.Context, _ error)
func (NoopObserver) RecordApply ¶
func (NoopObserver) RecordApply(_, _ string, _ bool, _ time.Duration)
func (NoopObserver) RecordCacheHit ¶ added in v0.12.0
func (NoopObserver) RecordCacheHit(_ string, _ time.Duration)
func (NoopObserver) RecordCacheMiss ¶ added in v0.12.0
func (NoopObserver) RecordCacheMiss(_ string, _ time.Duration)
func (NoopObserver) RecordCacheWrite ¶ added in v0.12.0
func (NoopObserver) RecordCacheWrite(_, _ string, _ bool, _ time.Duration)
func (NoopObserver) RecordFileRead ¶ added in v0.11.0
func (NoopObserver) RecordFileRead(_ string, _ bool, _ time.Duration)
func (NoopObserver) RecordFileWrite ¶ added in v0.11.0
func (NoopObserver) RecordFileWrite(_ string, _ bool, _ time.Duration)
func (NoopObserver) RecordMigration ¶ added in v0.11.0
func (NoopObserver) RecordPublish ¶
func (NoopObserver) RecordPublish(_ string, _ bool, _ time.Duration)
func (NoopObserver) RecordRequest ¶
func (NoopObserver) RecordRequest(_, _ string, _ int, _ time.Duration)
func (NoopObserver) RecordSecurityRejection ¶
func (NoopObserver) RecordSecurityRejection(_, _ string)
func (NoopObserver) RecordStreamItem ¶ added in v0.11.0
func (NoopObserver) RecordStreamItem(_ string, _ bool, _ time.Duration)
func (NoopObserver) RecordSubscribe ¶
func (NoopObserver) RecordSubscribe(_ string, _ bool, _ time.Duration)
func (NoopObserver) RecordValidation ¶ added in v0.11.0
func (NoopObserver) RecordValidation(_, _ string, _ time.Duration, _ error)
func (NoopObserver) RecordValidationError ¶
func (NoopObserver) RecordValidationError(_, _, _ string)
type Observer ¶
type Observer interface {
ValidationObserver
// RecordRequest is called after every request/response cycle completes,
// regardless of transport. method describes the transport and operation;
// values vary by adapter:
// - HTTP adapters (nethttp, chi): uppercase HTTP method ("GET", "POST", …)
// - ZeroMQ adapters: "ZMQ-REP", "ZMQ-REQ", "ZMQ-ROUTER", "ZMQ-DEALER"
// - MCP adapter (mcpgo): "tool", "resource", "prompt"
// - MQTT5 request/reply: "MQTT5-REQ" (caller), "MQTT5-REP" (server)
//
// path is the route pattern or topic template (e.g. "/users/{id}",
// "sensors/{sensorID}/data"), not the concrete URL or resolved topic.
// statusCode follows HTTP conventions: 200 success, 400 client error,
// 500 server/encode error; 0 means no request reached the transport (e.g.
// pre-flight validation failure or context already cancelled).
// duration is the total round-trip time including encode/decode.
RecordRequest(method, path string, statusCode int, duration time.Duration)
// RecordSubscribe is called after every inbound message or event is fully
// processed. topic is the concrete incoming value (not a template).
// success is false when decode or the application handler failed.
// duration is the total processing time.
//
// Used by:
// - MQTT adapters (mqtt, mqtt5): called per incoming message
// - SSE stream bridges (nethttp, chi): called per emitted SSE event
// (success=true on send, success=false on write error or stream error)
RecordSubscribe(topic string, success bool, duration time.Duration)
// RecordPublish is called after every outbound message is sent.
// topic is the resolved publish topic (after template substitution).
// success is false when encode failed, the broker returned an error, or
// the context was cancelled. duration covers encode + broker acknowledgement.
//
// Note: RecordPublish is not called when topic template substitution itself
// fails — the message never reached the encode/publish stage in that case.
//
// Used by: MQTT adapters (mqtt, mqtt5), ZeroMQ Publish.
RecordPublish(topic string, success bool, duration time.Duration)
}
Observer receives lifecycle events emitted by codec adapters. It embeds ValidationObserver for per-field validation errors and adds transport hooks for request/response cycles, subscriptions, and publishes. All methods must be safe for concurrent use.
func NewFanout ¶ added in v0.11.0
NewFanout returns an Observer that fans out all calls to each provided observer. The returned value also implements FileObserver, SecurityObserver, PipelineObserver, SQLObserver, StreamObserver, CacheObserver, and TraceObserver — delegating each to the inner observers that satisfy those interfaces, so composing a metrics-only observer with a LoggingObserver works without any type-assertion boilerplate.
obs := stats.NewFanout(
metricsObserver,
stats.NewLoggingObserver(slog.Default()),
)
func ObserverFromContext ¶ added in v0.11.0
ObserverFromContext retrieves the observer stored by WithObserver. Returns NoopObserver{} when no observer has been stored in ctx — the same zero-cost default used when opts.Observer is nil and no context observer exists.
type PipelineObserver ¶
type PipelineObserver interface {
// RecordApply is called after every forge.Function*.Apply completes.
// name is the function name, version is its version string, success is false
// when any validation or computation step returned an error, duration is the
// total Apply time (input validation + computation + output validation).
RecordApply(name, version string, success bool, duration time.Duration)
}
PipelineObserver receives lifecycle events from forge.Function*.Apply calls. It is a separate interface from Observer because forge is a domain computation layer, not a transport adapter. Implement alongside Observer when you want telemetry for both transport and KPI computation in the same process.
type SQLObserver ¶ added in v0.11.0
type SQLObserver interface {
// RecordValidation is called after every [adapters/sql.Validate] call,
// success or failure. table and op mirror ValidateOptions.Table/Op.
// err is nil on success.
RecordValidation(table, op string, duration time.Duration, err error)
// RecordMigration is called once per applied or rolled-back migration file
// during Migrator.Up or Migrator.Down. op is "up" or "down".
RecordMigration(op, name string, version int64, duration time.Duration, err error)
}
SQLObserver is an optional extension to Observer for SQL adapter lifecycle events. adapters/sql.Validate and adapters/sql.Migrator type-assert the configured Observer to SQLObserver before calling its methods — existing Observer implementations need not change.
type MyObserver struct{ ... }
func (o *MyObserver) RecordValidation(table, op string, d time.Duration, err error) {
// record metrics, emit a log line, etc.
}
func (o *MyObserver) RecordMigration(op, name string, version int64, d time.Duration, err error) { ... }
type SecurityObserver ¶
type SecurityObserver interface {
// RecordSecurityRejection is called when a security check rejects a request
// or message. location is the route path (HTTP) or topic (MQTT). scheme is
// the first declared security scheme name for the operation.
RecordSecurityRejection(location, scheme string)
}
SecurityObserver is an optional extension to Observer for security rejection events. Adapters type-assert the configured Observer to SecurityObserver before calling RecordSecurityRejection, so implementing this interface is purely additive — existing Observer implementations need not change.
type MyObserver struct{ ... }
func (o *MyObserver) RecordSecurityRejection(location, scheme string) {
// increment a Prometheus counter, emit a log line, etc.
}
type StreamObserver ¶ added in v0.11.0
type StreamObserver interface {
// RecordStreamItem is called for every item that passes through [stream.Apply],
// success or failure. function is the forge function name.
// success is false when forge.Function.Apply returned an error.
RecordStreamItem(function string, success bool, duration time.Duration)
}
StreamObserver is an optional extension to Observer for stream-level throughput metrics. [stream.Apply] type-asserts the configured Observer to StreamObserver before calling its methods — existing Observer implementations need not change.
type MyObserver struct{ ... }
func (o *MyObserver) RecordStreamItem(function string, success bool, d time.Duration) {
// record per-item throughput, latency, etc.
}
type TraceObserver ¶ added in v0.11.0
type TraceObserver interface {
// StartSpan starts a new trace span for the named operation and returns
// a context.Context containing the new span for child propagation.
// The returned context should be passed to the application's handler
// function so that business logic can create child spans.
StartSpan(ctx context.Context, operation, name string) context.Context
// EndSpan ends the span associated with ctx, recording err as an error
// event when non-nil.
EndSpan(ctx context.Context, err error)
}
TraceObserver is an optional extension to Observer for distributed tracing. Adapters type-assert the configured Observer to TraceObserver before calling StartSpan/EndSpan, so implementing this interface is purely additive — existing Observer implementations need not change.
type MyTracer struct{ ... }
func (t *MyTracer) StartSpan(ctx context.Context, operation, name string) context.Context {
span, ctx := otel.Tracer("go-codex").Start(ctx, operation,
otel.WithAttributes(attribute.String("name", name)),
)
return ctx
}
func (t *MyTracer) EndSpan(ctx context.Context, err error) {
span := trace.SpanFromContext(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
span.End()
}
The pattern at each adapter call site mirrors the SecurityObserver guard:
if to, ok := obs.(TraceObserver); ok {
ctx = to.StartSpan(ctx, op, name)
defer func() { to.EndSpan(ctx, err) }()
}
Where err is the eventual operation error (nil on success).
operation values follow a convention: "http.request", "mqtt.subscribe", "mqtt.publish", "forge.apply", "file.read", "file.write", "mcp.tool", "mcp.resource", "mcp.prompt". name is the concrete identifier (route path template, topic, function name, file path).
type ValidationObserver ¶
type ValidationObserver interface {
// RecordValidationError is called for each field that fails codec
// validation. location identifies the data source (e.g. "body", "query",
// "payload", "config"), constraintName is the constraint identifier (e.g.
// "minLen(3)", "non-negative-int", "email", "type-mismatch", "required"),
// field is the field or parameter name from the structured error.
RecordValidationError(location, constraintName, field string)
}
ValidationObserver is the codec-level observability hook. Implement this interface when using codecs directly (without an adapter) to receive per-field validation error events. Use ReportErrors to extract codex.ValidationErrors from a decode error and call [RecordValidationError] for each failing field.
Observer embeds ValidationObserver for use with adapters.