Getting insights from SQLite tracer

Run Stario with SqliteTracer so finished spans land in a SQLite file you can query with ad hoc SQL—handy for local debugging and quick operational questions. The file is not a distributed trace store; treat it as a local sink (Bundled tracers, SqliteTracer).

bash
STARIO_TRACER=sqlite STARIO_TRACERS_SQLITE=traces.sqlite3 stario serve main:bootstrap

With STARIO_TRACER=sqlite, the CLI builds a SqliteTracer from STARIO_TRACERS_SQLITE*. Unset environment variables leave constructor defaults in place:

VariableWhen unsetMeaning
STARIO_TRACERS_SQLITEstario-traces.sqlite3 in cwdSQLite file path
STARIO_TRACERS_SQLITE_FLUSH_INTERVAL0.25Writer flush interval in seconds
STARIO_TRACERS_SQLITE_MAX_PENDING_SPANS65536Pending queue capacity before finished spans are dropped
STARIO_TRACERS_SQLITE_MAX_BATCH_SPANS1024Spans written per writer batch

Only variables you set override those defaults. The same pattern applies to STARIO_TRACERS_JSON_* when you set STARIO_TRACER=json.

Example for a mounted data volume:

bash
STARIO_TRACER=sqlite STARIO_TRACERS_SQLITE=/data/traces.sqlite3 stario serve main:bootstrap

For path and tuning outside these variables, set STARIO_TRACER to a custom tracer callable (module:callable) that returns a configured SqliteTracer.

What gets stored

The v4 schema (see stario.telemetry.sqlite) has three tables:

  • spans — one row per finished span: span_id, trace_id, parent_id, name, start_ns, end_ns, duration_ns, status (ok or error), error text when failed, attrs_json for attributes.

  • span_events — named events on a span (name, time_ns, attrs_json, body). Exception records use name = 'exception'.

  • span_links — cross-span references from Span.link(name, span_id, …).

HTTP request spans record attributes such as request.method, request.path, and response.status_code inside attrs_json—not as top-level columns. Keys are flat dotted strings in JSON (not nested objects), so SQL must quote them: attrs_json ->> 'request.path' or json_extract(attrs_json, '$."request.path"'). Root spans are not only HTTP requests (server.startup, server.shutdown, and other roots may lack request.path).

Query HTTP traffic with attrs_json ->> 'request.path' IS NOT NULL. Use parent_id IS NULL for trace roots when you want top-level work units.

Example queries

Open the file with sqlite3, the SQLite extension in your editor, or any SQL client:

bash
sqlite3 traces.sqlite3

Queries below read HTTP fields from attrs_json (request.method, request.path, response.status_code).

Average request duration (HTTP root spans)

Restrict to HTTP traffic so startup and other non-request roots do not skew the average. HTTP spans are usually trace roots (parent_id IS NULL) with a request.path attribute.

sql
SELECT
  ROUND(AVG(duration_ns) / 1e6, 3) AS avg_ms,
  COUNT(*) AS n
FROM spans
WHERE parent_id IS NULL
  AND attrs_json ->> 'request.path' IS NOT NULL;

Approximate p95 latency (HTTP root spans)

Percentiles are not built into older SQLite builds; this uses an ordered offset—good enough for rough SLO checks when you have enough samples:

sql
SELECT duration_ns / 1e6 AS approx_p95_ms
FROM spans
WHERE parent_id IS NULL
  AND attrs_json ->> 'request.path' IS NOT NULL
ORDER BY duration_ns ASC
LIMIT 1 OFFSET (
  SELECT CASE
    WHEN COUNT(*) = 0 THEN 0
    ELSE MAX(0, CAST((COUNT(*) - 1) * 0.95 AS INTEGER))
  END
  FROM spans AS s2
  WHERE s2.parent_id IS NULL
    AND s2.attrs_json ->> 'request.path' IS NOT NULL
);

Most recent HTTP requests

sql
SELECT
  attrs_json ->> 'request.method' AS method,
  attrs_json ->> 'request.path' AS path,
  attrs_json ->> 'response.status_code' AS status,
  duration_ns / 1e6 AS dur_ms,
  datetime(end_ns / 1e9, 'unixepoch') AS ended_utc
FROM spans
WHERE attrs_json ->> 'request.path' IS NOT NULL
ORDER BY end_ns DESC
LIMIT 20;

Slowest HTTP requests (by duration)

sql
SELECT
  attrs_json ->> 'request.method' AS method,
  attrs_json ->> 'request.path' AS path,
  attrs_json ->> 'response.status_code' AS status,
  duration_ns / 1e6 AS dur_ms,
  datetime(end_ns / 1e9, 'unixepoch') AS ended_utc
FROM spans
WHERE attrs_json ->> 'request.path' IS NOT NULL
ORDER BY duration_ns DESC
LIMIT 20;

HTTP 5xx responses

sql
SELECT
  attrs_json ->> 'request.method' AS method,
  attrs_json ->> 'request.path' AS path,
  attrs_json ->> 'response.status_code' AS status,
  duration_ns / 1e6 AS dur_ms,
  datetime(end_ns / 1e9, 'unixepoch') AS ended_utc
FROM spans
WHERE CAST(attrs_json ->> 'response.status_code' AS INTEGER) >= 500
ORDER BY end_ns DESC
LIMIT 50;

Spans marked failed (status = 'error')

sql
SELECT
  name,
  error,
  attrs_json ->> 'request.path' AS path,
  datetime(end_ns / 1e9, 'unixepoch') AS ended_utc
FROM spans
WHERE status = 'error'
ORDER BY end_ns DESC
LIMIT 50;

Exception events (with request path when present)

sql
SELECT
  e.name AS event_name,
  s.attrs_json ->> 'request.path' AS path,
  e.body AS detail,
  datetime(e.time_ns / 1e9, 'unixepoch') AS event_utc
FROM span_events AS e
JOIN spans AS s ON s.span_id = e.span_id
WHERE e.name = 'exception'
ORDER BY e.time_ns DESC
LIMIT 50;
sql
SELECT
  s.name AS from_span,
  l.name AS link_name,
  l.target_span_id,
  datetime(s.end_ns / 1e9, 'unixepoch') AS ended_utc
FROM span_links AS l
JOIN spans AS s ON s.span_id = l.span_id
ORDER BY s.end_ns DESC
LIMIT 50;

Tracer health (drops and writer errors)

v4 does not persist tracer health in SQLite. Read counters from a SqliteTracer you construct in tests or from your own wiring in production (tracer.stats()TelemetryStats: dropped_spans, writer_error_count, serialization_error_count, last_writer_error, …). TestClient uses TestTracer, whose stats() does not reflect SQLite sink health.

Custom and remote tracers

If you need Datadog, OpenTelemetry, or another sink, implement the Tracer protocol and wire it from the CLI or Server — see Telemetry — Custom tracers.