operonx.telemetry¶
V3 tracing surface — automatic op-level recording, Consumer subclasses
convert the recorded WorkflowTrace into whatever target format they
want. See Operon(pipeline, trace=…) for how to wire consumers.
Consumer base¶
consumer
¶
Consumer base class for V3 workflow-trace consumers.
A Consumer reads a WorkflowTrace (attached to ExecutionHandle.trace
after a run) and converts it into whatever target-specific form it wants
— a directory tree on disk, a batched HTTP POST to Langfuse, a Loki
stream, a PDF report, a dict for a custom UI, and so on.
Base contract is deliberately small:
- subclasses override :meth:
consume - :meth:
sanitize, :meth:offload_media, :meth:truncateare shared utilities every consumer will want but nothing forces you to use them
No I/O, no state, no coupling to any specific backend. Consumers that DO have state (buffered clients, TTL sweepers, whatever) manage it themselves in their subclass — the base is pure.
See docs/TRACING_V3_DESIGN.md §3-5 for the full design.
Classes¶
Consumer
¶
Bases: ABC
Base class for any V3 workflow-trace consumer.
Subclasses override :meth:consume. The base offers three shared
utilities that every real consumer tends to need:
- :meth:
sanitize— strip non-JSON-serialisable values so the payload can round-trip throughjson.dumps/ Parquet / Langfuse's ingest API. - :meth:
offload_media— replace large binary payloads (audio, numpy arrays, model outputs) with content-addressed refs and write the raw bytes to a media directory. Dedup is automatic. - :meth:
truncate— cheap string truncation with a hint of the dropped length.
Rules for the base:
- No I/O, no side effects — every helper is a pure function.
- No shared state on the instance beyond
self.config(a plain dict handed in at__init__). - Every helper is opt-in — a minimal consumer can override
consumealone and ignore the rest.
Example — a trivial in-memory consumer::
class DictConsumer(Consumer):
def consume(self, trace):
return {n.op_id: n.op_name for n in trace.nodes}
Source code in operonx/telemetry/consumer.py
Methods:¶
consume
abstractmethod
¶
Convert trace to a target-specific artefact.
Return whatever makes sense for the target — a Path (files
written), URL (posted to a service), dict (in-memory view),
bytes (a rendered blob). The caller decides what to do with it.
Should raise on unrecoverable errors so the caller can log + continue with the next consumer.
Source code in operonx/telemetry/consumer.py
sanitize
¶
Recursively strip non-JSON-serialisable values.
Anything that isn't str/int/float/bool/None/dict/list/tuple
or a bytes-like object (which offload_media handles) gets
replaced with {"$unserializable": "<type_name>"}. Nested
dicts/lists are walked in place.
Numpy arrays go through unchanged — offload_media will
recognise + offload them. Callers that don't want numpy inline
should run offload_media FIRST.
Source code in operonx/telemetry/consumer.py
offload_media
¶
Recursively offload large binary values to media_dir.
Any value that's bytes / bytearray / memoryview /
numpy.ndarray / operonx.core.media.Media above threshold
bytes gets:
- serialised to bytes,
- hashed with SHA-256,
- written to
media_dir/<sha256>.<ext>(natural dedup — same bytes → same file, atomicopen("xb")withEEXISTtolerated), - replaced in the payload with
{"$media_ref": "media/<sha256>.<ext>", "size": <bytes>}.
Payloads smaller than threshold stay inline so the trace
remains greppable for small state dicts. Returns a new payload
(input is not mutated).
Source code in operonx/telemetry/consumer.py
truncate
¶
Cap s at limit chars, appending a length hint if dropped.
Concrete consumers¶
local
¶
LocalConsumer — generic disk-based V3 workflow-trace consumer.
Writes each run to <root>/<trace_id>/ in a layout designed to be
read by humans (view.txt) and machines (nodes.jsonl) with zero
tooling:
<root>/
<trace_id>/
meta.json — workflow name, timings, tags
nodes.jsonl — source of truth (one OpExecution per line,
media offloaded to refs)
view.txt — human-readable chronological rendering
(regeneratable from nodes.jsonl at any time)
media/ — content-addressed offload store
<sha256>.<ext>
latest -> <trace_id> — symlink to the most-recent call
Nothing here is callbot-specific — a subclass (CallbotLocalConsumer)
overrides _render_view to add turn-grouped headers derived from
operonx ctx. See docs/TRACING_V3_DESIGN.md §4-5.
Concurrency / safety:
- Each trace writes to a per-trace subdirectory → no shared file, no locking.
- Content-hashed media dedups across traces (create-exclusive on the
hash filename, ignore
FileExistsError). - Writes go to
<trace_id>.tmp/first; atomic rename on success → callers either see a complete directory or nothing. - TTL cleanup is external (systemd timer or cron) — the consumer just writes, it doesn't own retention policy.
Classes¶
LocalConsumer
¶
Bases: Consumer
Writes one directory per run under root.
Config keys (all optional, sensible defaults):
root(str | Path) — base directory; defaults to/tmp/operonx_traces.media_threshold(int) — bytes; payloads at or above this get offloaded tomedia/. Defaults to1024.write_view_txt(bool) — set False to skip the human-readable render (rawnodes.jsonlonly). Defaults toTrue.arrow_formatters(dict[str, Formatter]) — per-op summary formatters merged over the built-inFORMATTERS. Any op-name not in the map falls back to :func:default_arrow.
Returns the path to the final directory on success.
Source code in operonx/telemetry/consumer.py
LocalConsumerConfig
¶
Bases: YamlModel
YAML-configurable :class:LocalConsumer.
Functions:¶
default_arrow
¶
Fallback per-op arrow when nothing custom is registered.
Just shows the input / output key names — enough to see the shape of the op without spamming values.
Source code in operonx/telemetry/consumers/local.py
langfuse
¶
LangfuseConsumer — batch-ship a WorkflowTrace to Langfuse at end of call.
Converts each :class:OpExecution in the trace into one Langfuse span
observation, walks upstreams to pick a parent (first upstream wins,
matching the tree-flattening trade-off explicit in the config), and
POSTs the whole batch via :meth:LangfuseClient.ingest. Runs post-call
so it never sits in the WS hot path — the trade-off (documented in
:doc:TRACING_V3_DESIGN §5) is no live streaming during the call.
Media offload uses the base :meth:Consumer.offload_media walk to
replace big payloads with local $media_ref tokens; those aren't
Langfuse's media type — the media stays local. For real Langfuse media
uploads set upload_media=True (backlog — the current v1 keeps
media local only).
Example resources.yaml::
consumer_langfuse:
edupia:
client_resource: langfuse:edupia # reference existing client
workflow_name: callbot # sets Langfuse trace name
parent_strategy: first_upstream # or "root_only" / "sequential"
Then hand the resource key to :class:Operon::
engine = Operon(pipeline, trace="consumer_langfuse:edupia")
Classes¶
LangfuseConsumer
¶
Bases: Consumer
Ship a whole :class:WorkflowTrace to Langfuse as one batch.
Config keys (all optional, sensible defaults):
client(LangfuseClientinstance) — REQUIRED at construct time; typically injected by the ResourceHub factory.workflow_name(str) — Langfuse trace name; defaults to the run'sworkflow_name.parent_strategy(str) — how to pick ONE parent for the tree-only Langfuse span model. Choices:"first_upstream"(default) — firstUpstreamRefwins."root_only"— no parents; every node hangs off the trace root."sequential"— parent = previous node by start_time.
media_threshold(int) — bytes above which payloads get replaced with a$media_reftoken; defaults to1024.media_dir(str | Path) — where to write offloaded media blobs; defaults to a per-run temp dir. Media stays local for now.
Returns the Langfuse trace URL on success (via client.trace_url).
Source code in operonx/telemetry/consumer.py
LangfuseConsumerConfig
¶
Bases: YamlModel
YAML-configurable :class:LangfuseConsumer.
Functions:¶
Langfuse backend¶
Low-level Langfuse HTTP client + prompt manager — reused by
LangfuseConsumer for shipping traces, and available standalone for
prompt fetching.
backends
¶
Observability backends.
Each backend provides
- Config class: registered to ResourceHub via the YAML config layer
- Client class: HTTP/grpc transport, used by exporters
Available backends
- langfuse: Langfuse observability platform
Classes¶
LangfuseClient
¶
Langfuse client using the public REST API for tracing.
Pure HTTP — no SDK dependency. Used by LangfuseTracer for trace ingestion.
Example
Source code in operonx/telemetry/backends/langfuse/client.py
Methods:¶
ingest
¶
Send a batch of events to Langfuse ingestion API.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
List[Dict[str, Any]]
|
List of ingestion events (trace-create, span-create, etc.) |
required |
timeout
|
int
|
Request timeout in seconds |
30
|
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Response dict with 'successes' and 'errors' lists |
Source code in operonx/telemetry/backends/langfuse/client.py
upload_media
¶
upload_media(
*,
trace_id: str,
field: str,
content_type: str,
content: bytes,
observation_id: Optional[str] = None,
timeout: int = 30,
) -> Optional[str]
Upload a media blob and return its Langfuse media reference token.
Two-step flow per Langfuse docs
- POST /api/public/media with metadata → get mediaId + presigned URL.
- PUT the raw bytes to the presigned URL with the required headers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trace_id
|
str
|
Trace this media belongs to. |
required |
field
|
str
|
Where the media lives — |
required |
content_type
|
str
|
MIME type, e.g. |
required |
content
|
bytes
|
Raw bytes to upload. |
required |
observation_id
|
Optional[str]
|
Optional observation (span / generation) ID. |
None
|
timeout
|
int
|
HTTP timeout per request. |
30
|
Returns:
| Type | Description |
|---|---|
Optional[str]
|
The reference token string |
Optional[str]
|
|
Optional[str]
|
or |
Source code in operonx/telemetry/backends/langfuse/client.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
trace_url
¶
fetch_trace
¶
GET /api/public/traces/{traceId} — fetch a posted trace for verification.
Returns the parsed JSON body (trace + nested observations) on success, or None if the trace is not found / unauthorized / unreachable. Used by integration tests to verify trace structure landed correctly.
Source code in operonx/telemetry/backends/langfuse/client.py
auth_check
¶
Check authentication by hitting the health endpoint.
Returns:
| Type | Description |
|---|---|
bool
|
True if authentication is successful |
Source code in operonx/telemetry/backends/langfuse/client.py
LangfuseConfig
¶
Bases: YamlModel
Configuration for Langfuse observability backend.
This config is registered to ResourceHub and used to create LangfuseClient.
Attributes:
| Name | Type | Description |
|---|---|---|
public_key |
str
|
Public API key for Langfuse authentication |
secret_key |
str
|
Secret API key for Langfuse authentication |
host |
str
|
Langfuse server URL (default: cloud.langfuse.com) |
no_proxy |
Optional[str]
|
Proxy bypass setting for internal networks |
enabled |
bool
|
Whether tracing is enabled |
sample_rate |
float
|
Sampling rate for traces (0.0 to 1.0) |
Example
Methods:¶
from_env
classmethod
¶
Create config from environment variables.
Environment variables
- LANGFUSE_PUBLIC_KEY
- LANGFUSE_SECRET_KEY
- LANGFUSE_HOST (optional)
- NO_PROXY (optional)
Source code in operonx/telemetry/backends/langfuse/config.py
LangfusePromptManager
¶
Manages Langfuse prompts via the official SDK.
Requires the langfuse package (pip install operonx-telemetry[langfuse]).