symfonic.memory.telemetry¶
telemetry ¶
Telemetry sinks for memory-layer write observability.
The episodic telemetry sink (see :mod:episodic_sink) captures a
structured record for every successful episodic write so downstream
corpus-evaluation and v6.2 extractor work has ground truth to train /
evaluate against.
The sink API is intentionally minimal: construct a sink via
:func:resolve_episodic_sink, call await sink.emit(record), that is
the whole contract. Sinks are fire-and-forget from the caller's
perspective; the default None path is a no-op and adds zero cost.
EpisodicTelemetryRecord
dataclass
¶
EpisodicTelemetryRecord(
timestamp: str,
tenant_id: str,
scope: str,
episode_id: str,
node_count: int,
source_phase: str,
action_type: str | None = None,
extras: dict[str, object] = dict(),
tokens_in: int | None = None,
tokens_out: int | None = None,
input_tokens: int | None = None,
output_tokens: int | None = None,
cost_usd: float | None = None,
model: str | None = None,
embedding_latency_ms: float | None = None,
)
Structured telemetry record for a single successful episodic write.
The field set is the minimum viable shape. v6.2 (T05) added six
optional fields relevant to the LLM-extractor corpus: tokens_in,
tokens_out, input_tokens, output_tokens, cost_usd,
model, and embedding_latency_ms. All are None by default
so the existing JSONL shape is unchanged for records that omit them;
the to_dict / to_jsonl serialisers lift populated values
into the output and drop fields that remain None to keep the
corpus lean.
Do NOT remove or rename fields without a major-version bump — downstream consumers (the eventual LLM extractor corpus) depend on the shape.
to_dict ¶
Return a JSON-serialisable dict representation.
Fields that remain None (i.e. the engine did not source the
signal for this write) are dropped so the JSONL stream keeps
the v6.1 shape for records that carry none of the v6.2 fields.
Source code in src/symfonic/memory/telemetry/episodic_sink.py
to_jsonl ¶
EpisodicTelemetrySink ¶
Bases: Protocol
Protocol implemented by all episodic telemetry sinks.
JsonlEpisodicSink ¶
Append records to a JSON-lines file at the configured path.
- Path is created lazily on first
emit. - Writes are append-only; no in-process buffering (the OS buffer is sufficient for the expected traffic shape — a single write per successful episodic entry).
- File-handle management is per-call: we reopen on each emit. This is intentional: keeps the sink reentrant across event loops and avoids stale handles after log rotation.
Source code in src/symfonic/memory/telemetry/episodic_sink.py
LoggingEpisodicSink ¶
Emit records to the stdlib logging framework at INFO level.
The log record carries the structured dict in extra so a
downstream JSON log handler can lift the fields without parsing the
formatted message. Intended for deployments that already pipe
python logging into a structured log collector (Loki, Datadog).
Level requirement (important)¶
The record is emitted at INFO, but the named logger
"symfonic.memory.telemetry.episodic" inherits its effective
level from the root logger, which defaults to WARNING in
production. Without caller configuration, INFO records are filtered
BEFORE reaching any handler and the sink appears silent -- a
customer setting FrameworkConfig.episodic_telemetry_sink="logger"
and seeing no output is almost always a level misconfiguration,
not a broken sink.
Add this once in your application's logging config::
import logging
logging.getLogger("symfonic.memory.telemetry.episodic").setLevel(
logging.INFO,
)
This sink intentionally does NOT auto-install a handler or raise
the logger level -- that would be too magic for a Protocol sink
shared by every EpisodicLayer.store_event call.
Source code in src/symfonic/memory/telemetry/episodic_sink.py
NullEpisodicSink ¶
No-op sink. Used when telemetry is disabled.
EpisodicLayer branches on sink is None BEFORE constructing the
record, so this class is only used as an explicit marker for tests
and type contracts — the hot path does not instantiate it.
resolve_episodic_sink ¶
Turn a FrameworkConfig.episodic_telemetry_sink value into a sink.
Mapping:
Noneor empty string ->None(sink disabled, zero cost)."logger"-> :class:LoggingEpisodicSink.- Anything else -> :class:
JsonlEpisodicSinkwith the value treated as a filesystem path.