Telemetry

At a glance

  • Default CLI tracer — unset STARIO_TRACER is auto (TTY when stdout is a TTY, otherwise JSON).

  • Request span — App sets request.method, request.path, and response.status_code; add your own dimensions with c.span.attr / attrs.

  • Per request — c.span is 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 bare asyncio.create_task), child.end() in the task body.

  • Enter the tracer with with tracer: before create() so background writers and sinks are active.

  • Tracers — pick at process start (STARIO_TRACER, Server, or a custom factory). Handlers only see Span handles.

  • Bundled sinks — import from submodules (stario.telemetry.json.JsonTracer, stario.telemetry.sqlite.SqliteTracer, stario.telemetry.tty.TTYTracer, stario.telemetry.noop.NoOpTracer).

  • Health — tracer.stats() returns TelemetryStats (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.

python
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

python
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:

python
from stario.telemetry.json import JsonTracer
from stario.telemetry.sqlite import SqliteTracer
from stario.telemetry.tty import TTYTracer
from stario.telemetry.noop import NoOpTracer

Enter 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 / SqliteTracer back-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.

python
with tracer.create("request") as span:
    span.attrs({"request.method": "GET"})

Manual control is fine when you cannot use a context manager:

python
span = tracer.create("work")
span.start()
span.end()  # RuntimeError if start() was skipped

attr / 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.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.