Telemetry
At a glance
Default CLI tracer — unset
STARIO_TRACERisauto(TTY when stdout is a TTY, otherwise JSON).Request span —
Appsetsrequest.method,request.path, andresponse.status_code; add your own dimensions withc.span.attr/attrs.Per request —
c.spanis the root span. Call methods on the span handle:attr,attrs,event,step(),new_trace(),link().Child work —
with c.span.step("db.query") as child:for synchronous nested spans. For work that outlives the handler,child = c.span.step(...),child.start(),c.app.create_task(...)(not bareasyncio.create_task),child.end()in the task body.Enter the tracer with
with tracer:beforecreate()so background writers and sinks are active.Tracers — pick at process start (
STARIO_TRACER,Server, or a custom factory). Handlers only seeSpanhandles.Bundled sinks — import from submodules (
stario.telemetry.json.JsonTracer,stario.telemetry.sqlite.SqliteTracer,stario.telemetry.tty.TTYTracer,stario.telemetry.noop.NoOpTracer).Health —
tracer.stats()returnsTelemetryStats(dropped spans, serialization errors, writer errors).
Stario records what happened, how long it took, and what went wrong next to the code that did the work.
Span lifecycle
Tracer.create(name, attributes?, *, parent=) returns a Span handle. The clock does not start until start() or with span. end() exports the finished record via tracer.on_end(span) and freezes further mutations.
with tracer: with tracer.create("request") as span: span.attrs({"request.method": "GET"}) with span.step("db.load") as db: db.attr("rows", 1)Uncaught exceptions inside with span record an exception event, call fail(str(exc)), then end().
span.event(name, attributes?, body=) adds a timestamped note. Event body is for readable diagnostics (str, BaseException, or None); structured data belongs in attributes.
span.fail(message) marks the span failed without requiring an exception. It does not stop request handling.
span.step(name) creates a child span while the parent is in progress. span.new_trace(name) starts a detached root in the same tracer (no parent). span.link(name, span_id, attributes?) records a reference to another span without a parent/child edge.
Finished spans ignore attr, attrs, event, link, and fail after end().
Example: request span and child steps
import asyncio from stario import Context, Writer async def report(c: Context, w: Writer) -> None: c.span.attrs( { "request.id": c.req.headers.get("x-request-id", ""), "user.id": c.state.get("user_id", ""), } ) with c.span.step("db.load_profile", {"table": "profiles"}) as db_span: await asyncio.sleep(0) # stand-in for await pool.fetch(...) db_span.attr("rows", 1) with c.span.step("http.billing") as api_span: await asyncio.sleep(0) api_span.event("billing.payload", {"bytes": 2048}) w.respond(b"ok", b"text/plain")Bootstrap receives the same Span API on the span passed into bootstrap(app, span).
Bundled tracers
Import tracer backends from their modules:
from stario.telemetry.json import JsonTracerfrom stario.telemetry.sqlite import SqliteTracerfrom stario.telemetry.tty import TTYTracerfrom stario.telemetry.noop import NoOpTracerEnter each tracer before creating spans. Exiting drains pending spans (Json/SQLite) or stops the TTY display.
TTYTracer
Live span tree on the terminal for local development. Not a log format for ingestion pipelines.
JsonTracer
Writes one NDJSON line per finished span. Tune flush_interval, max_batch_spans, and max_pending_spans for latency versus back-pressure. Overflow increments stats().dropped_spans.
CLI: optional STARIO_TRACERS_JSON_FLUSH_INTERVAL, STARIO_TRACERS_JSON_MAX_PENDING_SPANS, STARIO_TRACERS_JSON_MAX_BATCH_SPANS.
SqliteTracer
Persists finished spans to a SQLite file for local SQL queries. Tables: spans, span_events, span_links. Delete old pre-v4 trace files if you still have them — there is no in-place migration.
CLI: STARIO_TRACER=sqlite and optional STARIO_TRACERS_SQLITE*. See Getting insights from SQLite tracer.
NoOpTracer
Implements the protocol and discards all span data. Use for benchmarks or deployments that keep the runtime path explicit.
Set STARIO_TRACER to auto, tty, json, noop, sqlite, or module:callable for the CLI.
Custom tracers
Implement the Tracer protocol: context manager (__enter__ / __exit__), create(..., parent=) -> Span, on_end(span), and stats() -> TelemetryStats.
create() returns a span handle compatible with the bundled RecordingSpan record shape so on_end() can read id, trace_id, parent_id, name, timestamps, attributes, events, and links after end().
Handlers call methods on the span (attr, event, step, …). The tracer does not expose span-id mutation APIs on itself.
Typical integration work:
Map flat attributes, events, and links to the vendor span model.
Batch and flush on a timer or queue threshold; mirror
JsonTracer/SqliteTracerback-pressure if needed.Policy for sampling, cardinality, and sensitive fields.
Handlers stay unchanged: they only hold Span handles.
API
class Span(*args, **kwargs)
Handle for one logical unit of work.
Lifecycle
Tracer.create() allocates a span; the clock does not start until start() or with span. end() exports the finished record to span.tracer.on_end() and freezes further mutations.
with tracer.create("request") as span: span.attrs({"request.method": "GET"})Manual control is fine when you cannot use a context manager:
span = tracer.create("work")span.start()span.end() # RuntimeError if start() was skippedattr / attrs may be set before start(). Timestamped operations (event, exception, link, and fail) record only while the span is in progress; before start() and after end() they are ignored.
Uncaught exceptions in with span record an exception event, call fail(str(exc)), then end().
Parenting
Prefer span.step(name) for children — the parent must be in progress. tracer.create(..., parent=span) is the low-level escape hatch used by step() and new_trace().
Attributes and events
Structured data belongs in attr / attrs or event attributes. Values should be JSON-serializable; sinks stringify unknown types at export. Event body is for readable diagnostics only (str, BaseException, or None).
Span.attr(name, value)
Span.attrs(attributes)
Span.end()
Span.event(name, attributes=None, /, *, body=None)
Span.exception(exc, attributes=None, /, *, body=None)
Span.fail(message)
Span.link(name, span_id, attributes=None, /)
Span.new_trace(name, attributes=None, /)
Span.start()
Span.step(name, attributes=None, /)
class Tracer(*args, **kwargs)
Creates spans and exports finished ones to a sink.
Lifecycle
Enter the tracer before create() so async writers and database handles are running. Exit drains pending spans (Json/SQLite) or flushes the TTY view.
Custom factories registered via STARIO_TRACER=module:callable must return an object implementing this protocol. create() should return spans compatible with the bundled RecordingSpan record shape so on_end() can read finished fields.
Tracer.create(name, attributes=None, /, *, parent=None)
Tracer.on_end(span)
Tracer.stats()
class TelemetryStats(dropped_spans=0, serialization_error_count=0, writer_error_count=0, last_writer_error=None, last_writer_error_at_ns=None)
Snapshot of tracer self-observation counters.
Fields
- dropped_spans(int):default:
0 - serialization_error_count(int):default:
0 - writer_error_count(int):default:
0 - last_writer_error(Union):default:
None - last_writer_error_at_ns(Union):default:
None
Json and SQLite sinks increment these when spans are dropped, serialization fails, or the writer raises. Dev (TTYTracer) and noop tracers always return zeros.