Telemetry design

Stario’s telemetry layer is separate from routing but first-class: every request gets a span; bootstrap and shutdown get spans; you can plug TTY, JSON, SQLite, or a custom tracer that implements the Tracer protocol (Telemetry reference).

In handlers, c is the per-request Context and c.span is the request span created during dispatch (Handlers, context, and the writer).

Mental model

Think of telemetry as structured narration of what the code did:

ToolUse it for
AttributesDimensions you will filter or group in dashboards: user.id, http.route, db.shard, build.id.
EventsInstant facts that are not a duration: “validation failed”, “cache hit”, “relay publish”.
Child spans / span.stepIntervals inside a request: “load rows”, “render template”, “external HTTP call”.

Request span vs “user-visible latency”

The default request span covers the whole request exchange in App.__call__ (handler work plus w.end() in finally)—not “CPU inside the handler coroutine” alone. If you:

  • send HTML early and then do more CPU work, or

  • return 204 and continue broadcasting,

…the span may end while the user still perceives activity—or vice versa for slow flushes.

If you care about a specific phase, record it:

  • with c.span.step("flush_sse"):

  • c.span.event("bytes_sent", {"n": n})

  • Child intervals under the request: c.span.step(...) (and with c.span.step(...)) create spans with parent_id set to the request span. Span.create(...) on the tracer or c.span.create(...) creates a detached root span (no parent)—use that for background work or separate traces, not as a synonym for “nest under the request.” All tracers record the tree; export detail depends on the sink.

Where telemetry is not a substitute for logs

Text logs are still fine for local printf debugging. Spans shine when you need traces across requests and consistent key/value fields in production sinks.

Built-in tracers

TracerTypical use
TTYTracerLocal dev—human-readable span tree on a TTY
JsonTracerPipes, systemd, log aggregators—one JSON object per span line
SqliteTracerLocal inspection / small dashboards—SqliteTracer

stario serve / watch default to auto: TTY tracer when stdout is a TTY, JSON otherwise.

Custom tracer

Implement the Tracer protocol (see stario.telemetry.core and existing tracers), then point the CLI at it:

bash
uv run stario watch main:bootstrap --tracer mypkg.telemetry:make_tracer

The symbol must be a zero-argument callable that returns a tracer instance (see resolve_tracer_factory in the CLI). Use this to ship spans to OpenTelemetry, Honeycomb, internal gRPC, etc., without changing application handlers.

Startup attributes on the bootstrap span are especially valuable in production—which binary and config actually booted—see Application lifecycle.


Reference: Telemetry · Bootstrap spans: Application lifecycle