Telemetry

One span per request, one tracer per process.

Stario creates a span for every request and hands it to your handler as c.span.

The default tracer is auto when STARIO_TRACER is unset: TTY when stdout is a TTY, otherwise JSON. Pick the tracer at process start via STARIO_TRACER, the tracer= argument to serve(), or a custom factory. Handlers only see Span handles. Bundled sinks live in submodules: stario.telemetry.json.JsonTracer, stario.telemetry.sqlite.SqliteTracer, stario.telemetry.tty.TTYTracer, stario.telemetry.noop.NoOpTracer.

The request span carries request.method, request.path, http.route, and response.status_code. The span is renamed to c.match.pattern when a route matches; 404 and 405 keep the method name. Call methods on the span handle: attr, attrs, event, step(), new_trace(), link(). For work that outlives the handler, start a step and finish it inside a task scheduled with c.app.create_task(...), not bare asyncio.create_task.

tracer.stats() returns TelemetryStats with dropped spans, serialization errors, and writer errors.

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 with 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().

Request span example#

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

Environment: 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. There is no in-place migration for old trace files — delete them.

Environment: STARIO_TRACER=sqlite plus STARIO_TRACERS_SQLITE* tuning. See Traces in SQLite.

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, a module that exports make_tracer(), or module:callable for a custom factory.

Custom tracers#

Implement the Tracer protocol: context manager (__enter__ / __exit__), create(..., parent=) -> Span, on_end(span), and stats() -> TelemetryStats.

A package can set STARIO_TRACER to its import name when that module defines make_tracer(). Use module:callable when the factory has another name.

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.

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 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.rename(name)#

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 (make_tracer) or 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(str | None):default: None
  • last_writer_error_at_ns(int | None):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.