operonx.telemetry.runs¶
Where finished runs live, and how they are asked for. A store keeps each
run in full and as a summary with per-op rollups; it is a trace consumer
and a resource (run_store:). The guide: Runs: stores, retention and
alerts.
runs
¶
Run stores — where finished runs live, and how they are asked for.
A :class:RunStore keeps each run twice over: in full (every execution
with its values) and as a summary with per-op rollups, so one run opens
fully and a month of runs summarises without opening any. It is a trace
consumer, so the engine writes into it like into any other; it is a
resource (run_store: in resources.yaml), so a project picks its
backend in configuration:
files— run directories plus a SQLite index (the default)sqlite— one filelangfuse— read-only, over runs a LangfuseConsumer shipped
The contract is five methods on purpose; see :mod:.base.
:func:project_stores names the stores a project's own [tracing]
sinks can be read from, without importing the project.
The contract¶
RunStore
¶
Bases: Consumer
See the module docstring.
Source code in operonx/telemetry/runs/base.py
Methods:¶
consume
¶
put_trace
abstractmethod
¶
list_runs
abstractmethod
¶
list_runs(
where: Optional[RunFilter] = None,
order: str = "started_desc",
limit: int = 50,
cursor: Optional[str] = None,
) -> Page
Runs matching where, one page at a time.
get_run
abstractmethod
¶
rollups
abstractmethod
¶
op_stats
¶
Each op across the runs where matches — slowest total first.
Backends with a query engine may override; the default combines
:meth:rollups in Python.
Source code in operonx/telemetry/runs/base.py
groups
¶
groups(
where: Optional[RunFilter] = None, by: Sequence[str] = ("origin", "name")
) -> List[Dict[str, Any]]
Runs where matches, counted per group of by (columns from
:data:GROUP_FIELDS): runs, errors, first_started,
last_started, cost_usd (None when nothing was priced) and
duration_ms (summed). Newest group first. SQL backends
override; this default pages through :meth:list_runs.
Source code in operonx/telemetry/runs/base.py
delete_runs
abstractmethod
¶
count
¶
How many runs where matches.
Source code in operonx/telemetry/runs/base.py
refresh
¶
Pick up runs written by something other than this store (the files backend indexes directories a plain LocalConsumer wrote). Returns how many were added; most backends have nothing to do.
The data¶
RunFilter
dataclass
¶
RunFilter(
origin: Optional[str] = None,
name: Optional[str] = None,
status: Optional[str] = None,
since: Optional[float] = None,
until: Optional[float] = None,
version: Optional[str] = None,
job_run: Optional[str] = None,
runbook_run: Optional[str] = None,
trace_ids: Optional[Sequence[str]] = None,
metadata: Dict[str, Any] = dict(),
search: Optional[str] = None,
)
Which runs — a small data object, never a query language, so every backend implements it natively.
metadata matches metadata keys exactly ({"session_id": "0912"});
search is a case-insensitive substring over the trace id, the key
and the metadata. Times are epoch seconds, since inclusive.
Methods:¶
matches
¶
The filter in Python — for backends without a query engine.
Source code in operonx/telemetry/runs/model.py
RunSummary
dataclass
¶
RunSummary(
trace_id: str,
workflow: str = "",
origin: str = "adhoc",
name: str = "",
status: str = "ok",
started_at: float = 0.0,
duration_ms: float = 0.0,
executions: int = 0,
ops: int = 0,
errors: int = 0,
first_error: Optional[str] = None,
cost_usd: Optional[float] = None,
unpriced: int = 0,
llm_calls: int = 0,
tokens_in: int = 0,
tokens_out: int = 0,
tokens_cached: int = 0,
version: Optional[str] = None,
version_dirty: Optional[bool] = None,
service: Optional[str] = None,
transport: Optional[str] = None,
variant: Optional[str] = None,
job: Optional[str] = None,
job_run: Optional[str] = None,
key: Optional[str] = None,
runbook: Optional[str] = None,
runbook_run: Optional[str] = None,
project: Optional[str] = None,
session_id: Optional[str] = None,
user_id: Optional[str] = None,
request_id: Optional[str] = None,
metadata: Dict[str, Any] = dict(),
location: Optional[str] = None,
)
One run, in a line: who made it, how it went, what it cost.
RunRecord
dataclass
¶
RunRecord(
summary: RunSummary,
nodes: List[Dict[str, Any]] = list(),
meta: Dict[str, Any] = dict(),
media_root: Optional[str] = None,
)
One run in full: the summary, the trace-level metadata, and every
execution as the row a consumer wrote (inputs and outputs included,
media as references relative to media_root).
A row stored with inputs_from (a later record of a generator
invocation, see :func:row_of) reads with its invocation's inputs:
every store's get_run builds one of these, so every reader gets
them, by reference, never copied.
OpRollup
dataclass
¶
OpRollup(
trace_id: str,
op: str,
op_type: str = "",
count: int = 0,
total_ms: float = 0.0,
max_ms: float = 0.0,
errors: int = 0,
cost_usd: Optional[float] = None,
unpriced: int = 0,
tokens_in: int = 0,
tokens_out: int = 0,
samples: List[float] = list(),
exact: bool = True,
)
One op in one run: how often, how long, how costly.
OpStats
dataclass
¶
OpStats(
op: str,
op_type: str = "",
runs: int = 0,
count: int = 0,
total_ms: float = 0.0,
avg_ms: float = 0.0,
p50_ms: float = 0.0,
p95_ms: float = 0.0,
p99_ms: float = 0.0,
max_ms: float = 0.0,
errors: int = 0,
cost_usd: Optional[float] = None,
unpriced: int = 0,
tokens_in: int = 0,
tokens_out: int = 0,
exact: bool = True,
)
One op across every run a filter matched — a Monitor table row.
Page
dataclass
¶
One page of runs, and the cursor for the next (None: the end).
summarize
¶
summarize(
trace_id: str,
rows: Iterable[Dict[str, Any]],
meta: Optional[Dict[str, Any]] = None,
location: Optional[str] = None,
) -> Tuple[RunSummary, List[OpRollup]]
A run's summary and per-op rollups from its rows (the shape
nodes.jsonl holds) and its meta.json.
Cost follows operonx's own rule: an op that reports cost_usd is
priced when the value is a number (zero included — a declared zero)
and unpriced when it is None; the run's cost is None only
when nothing reported a price at all.
Source code in operonx/telemetry/runs/model.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
percentile
¶
The q-th percentile (0–100), linear between closest ranks.
Source code in operonx/telemetry/runs/model.py
Configuration¶
RunStoreConfig
¶
Bases: YamlModel
YAML-configurable :class:RunStore.
open_run_store
¶
A store from a plain mapping (the YAML block's fields). The studio opens a project's store this way without importing the project.
Source code in operonx/telemetry/runs/config.py
A project's own stores¶
project_stores
¶
One :class:StoreSource per trace sink the project's operonx.toml
names in [tracing] — the project-wide list, every
[tracing.services.<n>] and [tracing.jobs.<n>], and the
trace = of any [[serve]] / [[job]] block none of those
overrides — each sink once, in that order.
env is the environment to resolve ${VAR} in (default: this
process's); the project's .env fills in what it lacks. An empty
list when there is no manifest or no [tracing] table. A malformed
[tracing] raises :class:~operonx.app.manifest.ManifestError.
Source code in operonx/telemetry/runs/project.py
StoreSource
dataclass
¶
StoreSource(
sink: str,
spec: Optional[Dict[str, Any]],
levels: Tuple[str, ...],
reason: str = "",
)
One trace sink of a project, as a store that can be read.
spec is what :func:~operonx.telemetry.runs.open_run_store takes,
or None when the sink cannot be read — reason says why.
levels are the places in operonx.toml that name the sink
([tracing], [tracing.services.call], ...); source says
the same in one line: "[tracing] → trace_clickhouse:default".
ClickHouse¶
ClickHouseRunStore
¶
ClickHouseRunStore(
host: str = "localhost",
port: int = 0,
user: str = "default",
password: str = "",
database: str = "operonx",
secure: bool = False,
ttl_days: Optional[float] = None,
media_dir: Any = "",
media_threshold: int = 1024,
batch_size: int = 10000,
flush_interval: float = 1.0,
queue_size: int = 1000,
timeout: float = 10.0,
read_timeout: float = 5.0,
media: Any = "local",
client: Any = None,
media_batch_bytes: int = 32 << 20,
live: bool = True,
)
Bases: ClickHouseConnection, RunStore
See the module docstring.
client= takes a ready client (anything with query,
command and insert the way clickhouse_connect's has):
tests hand in a fake. read_timeout is how long a read waits for
this process's queued runs to land first.
media is where blobs go: "local" (the default) a directory,
media_dir; "clickhouse" the media table, written in the
same batches as the runs (media_dir is then unused, and
media_batch_bytes caps the blob bytes one insert carries); or a
:class:~operonx.core.media_store.MediaStore of your own.
live (the default) writes a run while it goes, on live_writer
(bounded at :attr:live_queue_size items; past it, live rows are
dropped and counted, and the final write still has them all).
Source code in operonx/telemetry/runs/clickhouse.py
Methods:¶
consume
¶
The engine's hook: queue the run and return. False when the
queue was full and the run was dropped (counted in
writer.stats).
on_start
¶
on_execution
¶
Live: queue one execution's row at its index in trace.nodes.
build
¶
A finished trace as its rows: (summary, run row, node rows,
rollup rows). Blobs go to :attr:media on the way.
Source code in operonx/telemetry/runs/clickhouse.py
put_trace
¶
Store one run now, synchronously (scripts, backfills). The
engine's path is :meth:consume.
Source code in operonx/telemetry/runs/clickhouse.py
flush
¶
Wait for queued runs (and live rows) to be written (or dropped).
Source code in operonx/telemetry/runs/clickhouse.py
op_stats
¶
Native: sums in ClickHouse, percentiles with
quantilesExactInclusive over the kept samples — the same
linear-between-ranks rule :func:percentile uses.
Source code in operonx/telemetry/runs/clickhouse.py
delete_runs
¶
Lightweight DELETE of the matching runs from every table.
Blobs stay (other runs may share them): see :meth:prune_media.
Source code in operonx/telemetry/runs/clickhouse.py
prune_media
¶
Delete blobs no stored execution references any more, written
more than older_than_s ago (younger ones may belong to runs still
on their way in). Returns how many were deleted. With
media="clickhouse" blobs also expire on their own, with the
last run that wrote them.
Source code in operonx/telemetry/runs/clickhouse.py
ClickHouseConsumerConfig
¶
Bases: YamlModel
YAML-configurable ClickHouse trace consumer.
Media¶
media_store
¶
Media blobs: what one is, and where it is kept.
A blob (audio, an image, a PDF, an array) is addressed by the SHA-256 of its bytes, so the same bytes are kept once however many records mention them.
- :func:
detect_medianames a blob's type from its first bytes (WAV, MP3, OGG/Opus, FLAC, WebM, PNG, JPEG, GIF, WebP, PDF,.npy) and reads a duration where the header carries one. Raw PCM has no header: it takes the rate a :class:~operonx.core.media.Mediadeclares in its mime parameters (audio/L16;rate=16000;channels=1). - :class:
MediaStoreis the five-method contract (put,get,exists,delete,keys); :class:LocalMediaStorekeeps blobs in a directory. Another backend (S3, MinIO, a database table) implements the same five methods.
Traces use it (:mod:operonx.telemetry.media swaps a run's blobs for
references to a store), and so can anything else that keeps blobs by
content, such as a document store's page images. It lived in
operonx.telemetry.media until 1.14; that module still exports every
name.
Nothing here imports a database driver, or anything outside the stdlib.
detect_media
¶
A blob's type from its magic bytes, else from declared.
The bytes win whenever they identify a format: a producer that labels
WAV as audio/mp3 gets audio/wav. When they do not, a declared
type is kept, and a declared raw-PCM type (audio/L16, audio/L8,
audio/L24, audio/pcm, audio/raw) with a rate= parameter
gets a duration from its size, channels= (default 1) and the bit
depth (from the L number, else bits=, default 16). Never raises.
Source code in operonx/core/media_store.py
MediaInfo
dataclass
¶
MediaInfo(
mime: str = OCTET,
ext: str = "bin",
size: int = 0,
duration_s: Optional[float] = None,
sample_rate: Optional[int] = None,
channels: Optional[int] = None,
)
What a blob is. duration_s, sample_rate and channels are
set only when the bytes (or a declared PCM type) say so.
Methods:¶
ref
¶
The reference a record (a trace row, say) keeps in place of the bytes.
Source code in operonx/core/media_store.py
MediaStore
¶
LocalMediaStore
¶
Bases: MediaStore
Blobs in a directory: <root>/<sha[:2]>/<sha>.<ext>.
The extension is the detected type's, so a file opens in the right
tool. Writes go through a temporary file and :func:os.replace, so a
reader never sees half a blob and two writers of the same bytes both
succeed.
Source code in operonx/core/media_store.py
media
¶
Trace media: swapping a run's blobs for references to a store.
A trace's large values (audio, images, arrays) are kept apart from the row that mentions them. The row holds a small reference::
{"$media": "<sha256>", "mime": "audio/wav", "size": 48044,
"duration_s": 1.5, "sample_rate": 16000, "channels": 1, "store": "local"}
and the bytes live in a :class:~operonx.core.media_store.MediaStore,
addressed by their SHA-256, so the same audio is kept once however many
runs carry it.
- :func:
offload_to_storewalks a sanitised value and swaps blobs for references; any run store can use it. - :func:
json_defaultdoes the same whileorjsonserialises.
The detector and the store (:func:detect_media, :class:MediaInfo,
:class:MediaStore, :class:LocalMediaStore) live in
:mod:operonx.core.media_store, since nothing about them is specific to
traces. They are exported here too, so imports from this module keep
working.
Nothing here imports a database driver.
offload_to_store
¶
payload with its blobs moved into store and replaced by refs.
A :class:~operonx.core.media.Media is always stored, whatever its
size: its producer said it is media. bytes-like values and numpy
arrays are stored at threshold bytes or more and stay inline below
it. A Media holding a URL or path keeps it as
{"$media_url": ..., "mime": ...}. The input is not mutated.
Source code in operonx/telemetry/media.py
json_default
¶
A default= hook for orjson.dumps / json.dumps that does
what :meth:~operonx.telemetry.consumer.Consumer.sanitize and
:func:offload_to_store do, while the value is being serialised.
One pass, and the walk itself runs in orjson's C code: the hook is only
called for what JSON cannot hold. A Media (passed through with
OPT_PASSTHROUGH_DATACLASS), large bytes and large arrays become
refs; small bytes their repr text, small arrays lists, numpy
scalars numbers; anything else {"$unserializable": "<type>"}.
Source code in operonx/telemetry/media.py
The background writer¶
BackgroundWriter
¶
BackgroundWriter(
sink: Callable[[List[Any]], Any],
*,
name: str = "writer",
max_queue: int = 1000,
batch_size: int = 1000,
flush_interval: float = 1.0,
weight: Optional[Callable[[Any], int]] = None,
max_retries: int = 3,
retry_backoff: Tuple[float, float] = (0.5, 30.0),
exit_timeout: float = 5.0,
)
See the module docstring.
Source code in operonx/telemetry/writer.py
Attributes¶
Methods:¶
submit
¶
Queue item; False when it was dropped (full, or closed).
Source code in operonx/telemetry/writer.py
flush
¶
Wait until everything submitted so far is written or dropped.
False if timeout (seconds) ran out first.
Source code in operonx/telemetry/writer.py
close
¶
Write what is queued (up to timeout), then stop the thread. Nothing is accepted afterwards. Idempotent.
Source code in operonx/telemetry/writer.py
Retention¶
retention
¶
How long runs are kept, per origin.
A policy maps an origin to days, or to None for "forever"; an origin
the policy does not name is kept. The defaults are the platform plan's:
services 30 days, jobs and evals forever, the playground 7 days, ad hoc
30 days. A project overrides them (the studio's Settings writes
[studio.retention] in operonx.toml); :func:apply_retention
enforces whichever applies.
Classes¶
Functions:¶
plan_retention
¶
plan_retention(
store: RunStore,
policy: Optional[Mapping[str, Optional[float]]] = None,
now: Optional[float] = None,
) -> Dict[str, int]
How many runs per origin policy would delete — without deleting.
Source code in operonx/telemetry/runs/retention.py
apply_retention
¶
apply_retention(
store: RunStore,
policy: Optional[Mapping[str, Optional[float]]] = None,
now: Optional[float] = None,
) -> Dict[str, int]
Delete runs older than policy allows; return how many per origin.
Source code in operonx/telemetry/runs/retention.py
Alerts¶
alerts
¶
Alerts on runs: a threshold per service or job, checked on the summaries.
An alert watches one origin and name (service:call, job:score_calls)
over a trailing window and fires when a number crosses its threshold:
error_rate— the share of runs that failed;p95_ms— the 95th-percentile run duration, or one op's whenopis set (a key op: time to first audio, the LLM call);cost_per_hour— priced cost in the window, per hour (unpriced runs are counted beside it, never as $0);runs— fires when there are fewer than the threshold (a service that went quiet);score_mean:<score>— the mean of a score an online eval wrote for those runs; fires when it drops below the threshold (quality going down);score_fail_rate:<score>— the share of those scores that failed.
The score metrics read a score store (evaluate(..., scores=)) over the
scores written in the window; min_runs counts scores there. It is evaluated from what the run store already keeps — summaries and op
rollups, no extra recording. min_runs keeps a window of two runs from
paging anyone. :func:step turns an evaluation into what to send: a
firing once when it crosses, a reminder every repeat_min while it
stays over, a resolved when it comes back. :func:deliver posts the
message to a webhook as {"text": …} plus the fields — the shape Slack
and Teams incoming webhooks take, and any relay can read.
Classes¶
AlertState
dataclass
¶
AlertState(
firing: bool = False,
value: Optional[float] = None,
runs: int = 0,
unpriced: int = 0,
since: float = 0.0,
until: float = 0.0,
evaluated_at: float = 0.0,
fired_at: Optional[float] = None,
sent_at: Optional[float] = None,
note: str = "",
extra: Dict[str, Any] = dict(),
)
What an evaluation found, and what has been sent about it.
Functions:¶
score_metric
¶
("score_mean", "polite") for "score_mean:polite"; None
for a run metric.
Source code in operonx/telemetry/runs/alerts.py
evaluate
¶
evaluate(
store: RunStore,
alert: Alert,
now: Optional[float] = None,
*,
scores: Any = None,
) -> AlertState
The alert's number over its window, and whether it crosses. A score metric reads scores (a ScoreStore).
Source code in operonx/telemetry/runs/alerts.py
step
¶
What to send now: "firing", "reminder", "resolved" or
nothing. Carries fired_at / sent_at over from prev into cur.
Source code in operonx/telemetry/runs/alerts.py
message
¶
message(
alert: Alert,
st: AlertState,
kind: str,
*,
project: str = "",
link: str = "",
) -> Dict[str, Any]
The webhook body: a text line people read, and the fields.
Source code in operonx/telemetry/runs/alerts.py
deliver
¶
POST body as JSON to webhook; the HTTP status.