Skip to content

Telemetry and lifecycle

source consumes crucible/telemetry, the suite’s vendor-neutral tracing and metrics interface. It defines no observability abstraction of its own and pulls in no telemetry vendor. You pass one shared Tracer and Meter, the same ones the rest of your service and the state kernel use.

h := source.NewHopper(
source.WithLogger(logger), // *slog.Logger
source.WithTracer(tracer), // telemetry.Tracer
source.WithMeter(meter), // telemetry.Meter
)

Every seam defaults to a no-op: a discarding slog handler, telemetry.NopTracer(), telemetry.NopMeter(). An un-instrumented Hopper allocates no backend and does no IO on the hot path. Observability is opt-in, never a required dependency.

A consume is one span with a child span per stage, so a trace tells the whole story of a message: decode, route, fire, persist, ack.

InstrumentKindMeaning
source.consumespanone message, with decode/route/fire/persist/ack children
source.laggaugeconsumer lag
source.ack · source.nakcounteracked / redelivered messages
source.retry · source.dlqcounterretried / dead-lettered messages
source.transition_appliedcountertransitions applied by the state-machine bridge
source.transition_rejectedcounterguard rejections (invalid-for-state)

The per-message span is started on the context the engine threads through every stage, so each stage’s own spans nest underneath it, and when a transition fans back out through a sink Manifold the emit span nests under the fire stage. One trace correlates consume, transition, and the resulting writes.

Lifecycle is bounded and predictable. On ctx cancel the Hopper stops fetching, finishes in-flight work, commits, and closes, so there is no goroutine leak and no commit-before-process. A readiness signal reports when the consumer is live, a gap the Delivery broker had. A wedged backend cannot hang teardown past the deadline you set.