Observer Pattern¶
See also:
statspackage on pkg.go.dev · Guide: Using the Observer Pattern · http-trace-span-propagation exampleRunnable demos:
examples/stats-observer·examples/adapters-nethttp·examples/adapters-mqtt·examples/flat-key-patch
The observer pattern is go-codex's unified observability layer across all four layers of the library: codecs, adapters, formats (files), and forge. It provides structured hooks for three observability signals — metrics, logging, and distributed tracing — with no library dependency.
The user decides which signals to use (any, all, or none) and implements the corresponding interfaces. Implementations are fully swappable between development stubs, Prometheus counters, OTel tracing, or any other backend.
The six observer interfaces¶
| Interface | Signal | Methods | Layer |
|---|---|---|---|
stats.ValidationObserver |
metrics + logging | RecordValidationError(loc, constraint, field) |
Codecs — direct codec calls |
stats.Observer |
metrics + logging | embeds ValidationObserver + RecordRequest, RecordSubscribe, RecordPublish |
Adapters — HTTP, MQTT, MCP |
stats.PipelineObserver |
metrics + logging | RecordApply(name, version, success, dur) |
Forge — computation pipelines |
stats.FileObserver |
metrics + logging | RecordFileRead · RecordFileWrite |
Formats — file I/O |
stats.SecurityObserver |
metrics + logging | RecordSecurityRejection(location, scheme) |
Adapters — security rejection events |
stats.TraceObserver |
distributed tracing | StartSpan(ctx, operation, name) ctx · EndSpan(ctx, err) |
Adapters, forge, file — spans |
Every interface is optional. Implement only what you need.
Built-in types¶
| Type | Implements | Purpose |
|---|---|---|
stats.NoopObserver |
all six | Zero-cost default — every Option field defaults to this |
stats.LoggingObserver |
all five except TraceObserver | Logs every event via slog — configure handler for dev/prod/OTel |
stats.NewFanout(observers...) |
all six | Fans out to multiple observers — compose metrics + logging + tracing |
Composition¶
Pass a single stats.NewFanout value to every layer. No type assertions needed at call sites:
obs := stats.NewFanout(metrics, stats.NewLoggingObserver(logger), tracer)
// Same value — works on every layer:
stats.ReportErrors(obs, "config", err) // codec
nethttp.Register(mux, route, handler, nethttp.Options{Observer: obs}) // adapter
configFile.Read(nil, format.FileOptions{Observer: obs}) // format/file
forge.NewRegistry("P", "1.0.0").WithObserver(obs) // forge
Context propagation for trace spans¶
TraceObserver.StartSpan returns a context.Context that carries the active span. Adapters pass this context to the application handler, enabling parent-child span relationships:
HTTP client: nethttp.Call(ctx, ...) → traceparent header
Downstream: handler(ctx, req) → ctx carries incoming span
├─ forge: ApplyContext(ctx, in) → child of HTTP span
└─ file: FileOptions{Context: ctx} → child of HTTP span
Server adapters (nethttp, chi, mqtt) always pass the incoming ctx to StartSpan. They do not detect whether a parent span exists — the TraceObserver implementation decides:
- When a parent span is present (e.g. extracted from an HTTP
traceparentheader by OTel middleware) → child span. - When no parent is present (
context.Background(), direct test call) → root span.
// OTel — parent decision handled automatically by the SDK:
func (t *OTelTracer) StartSpan(ctx context.Context, op, name string) context.Context {
ctx, span := otel.Tracer("go-codex").Start(ctx, op, // parent or nil from ctx
otel.WithAttributes(attribute.String("name", name)),
)
return ctx
}
The library provides the hook; the user's implementation controls span parenting.
All adapter entry points accept context.Context:
- nethttp.Call(ctx, ...) — HTTP client, propagates downstream
- nethttp.Handler — HTTP server, ctx from *http.Request.Context()
- mqtt.Publish(ctx, ...) — MQTT publish
- mqtt.SubscribeHandler(ctx, ...) — MQTT subscribe, ctx flows to handler
To propagate into forge and file:
- Use forge.Function.ApplyContext(ctx, in) instead of Apply(in)
- Set format.FileOptions.Context to the handler's context
Per-layer behavior¶
| Layer | How the observer is injected | Events emitted |
|---|---|---|
| Codec | stats.ReportErrors(obs, location, err) |
RecordValidationError per failing field |
| Adapter (HTTP/MQTT/MCP) | Options{Observer: obs} |
RecordRequest/RecordSubscribe/RecordPublish, validation errors, security rejections, trace spans |
| Format (file I/O) | FileOptions{Observer: obs} |
RecordFileRead/RecordFileWrite, validation errors, trace spans |
| Forge | Registry.WithObserver(obs) |
RecordApply per function call, trace spans |
For per-adapter code examples, OpenTelemetry tracing, Prometheus wiring, location values, and a full end-to-end walk-through, see the Guide: Using the Observer Pattern.