symfonic.core.observability¶
observability ¶
symfonic.core.observability — Instrumentation hooks.
NODE_LABELS
module-attribute
¶
NODE_LABELS: dict[str, str] = {
NodeName.REACT.value: "React Loop",
NodeName.METACOGNITION_CRITIC.value: "Metacognition Critic",
NodeName.METACOGNITION_SKILL_GATE.value: "Skill-Gate Critic",
NodeName.CONTEXT_WINDOW_SUMMARISER.value: "Context Window Summariser",
NodeName.INSIGHT_EXTRACTOR.value: "Insight Extractor",
NodeName.ENTITY_EXTRACTOR_LLM.value: "Entity Extractor (LLM)",
NodeName.CONSOLIDATION_EXTRACTOR.value: "Consolidation Extractor",
NodeName.PROCEDURAL_ROUTER.value: "Procedural Router",
NodeName.PROCEDURAL_EXTRACTOR_LLM.value: "Procedural Extractor",
}
UI-friendly labels for the canonical node_name set.
Adopters wiring an admin dashboard can surface these directly instead of synthesising labels at the UI layer::
from symfonic.core.observability import human_label
row_label = human_label(event.node_name) # round-trips unknown names
NODE_NAMES
module-attribute
¶
Set of canonical node_name strings emitted by the framework.
Adopters can pre-populate dashboards, validate pricing-table coverage, or allocate per-node budget ceilings using this set::
from symfonic.core.observability import NODE_NAMES
for node_name in NODE_NAMES:
budget_ceilings[node_name] = float(env.get(f"BUDGET_{node_name.upper()}", 0.50))
AuditLogEntry ¶
Bases: BaseModel
Immutable audit event emitted for every destructive operation.
Fields intentionally kept small so both in-memory and Postgres
implementations can store them without a schema migration dance.
metadata is the escape hatch for per-action context (counts,
previous value, confirmation token, ...).
action
class-attribute
instance-attribute
¶
Free-form action code. Convention: snake_case verb +
optional resource suffix, e.g. delete_memory, bulk_delete,
export_data, erase_all.
resource_type
class-attribute
instance-attribute
¶
E.g. memory_node, edge, procedure, tenant.
to_dict ¶
Return a JSON-safe dict representation.
Source code in src/symfonic/core/observability/audit.py
AuditLogger ¶
Bases: Protocol
Destination for audit events.
Implementations MAY persist synchronously or asynchronously. The contract is "record this entry, or raise" — retry / batching is an implementation detail. Callers in the router layer shield failures (fire-and-forget) so a sick audit store never 500s a user request.
BudgetCheckResult
dataclass
¶
BudgetCheckResult(
allowed: bool,
reason: str | None = None,
daily_used_usd: float = 0.0,
daily_limit_usd: float | None = None,
monthly_used_usd: float = 0.0,
monthly_limit_usd: float | None = None,
)
Result of a pre-call budget check.
BudgetStore ¶
Bases: Protocol
Optional persistence layer for daily aggregates.
Implementations should be idempotent on conflict — the tracker calls
upsert_daily after every LLM call, so the underlying storage must
handle UPSERT semantics (ON CONFLICT ... DO UPDATE).
The optional read_today / read_month methods hydrate the
tracker's in-memory buckets on demand so that multiple app replicas
converge on the same view of a tenant's cumulative usage. A store
that only implements upsert_daily still works (writes only) —
multi-replica correctness requires all three.
read_month
async
¶
Return the aggregated month-to-date usage for tenant_id.
year_month is the YYYY-MM key produced by
:func:month_key. Optional — see :meth:read_today.
Source code in src/symfonic/core/observability/budget_models.py
read_today
async
¶
Return today's usage row for tenant_id (None if absent).
Optional — implementations without this method get write-through semantics only (single-process correctness).
Source code in src/symfonic/core/observability/budget_models.py
BudgetUsage
dataclass
¶
BudgetUsage(
tenant_id: str,
window: str,
window_key: str,
input_tokens: int = 0,
output_tokens: int = 0,
cached_tokens: int = 0,
cache_write_tokens: int = 0,
cost_usd: float = 0.0,
request_count: int = 0,
updated_at: datetime = (lambda: datetime.now(UTC))(),
)
Cumulative usage for a single (tenant, window) pair.
InMemoryAuditLogger ¶
Default in-process logger — keeps entries in an in-memory list.
Primarily for unit tests and local development. Production
deployments should register a durable implementation (e.g. the
scaffolded Postgres logger). The list is bounded by
max_entries to prevent unbounded growth when no one is draining
it.
Source code in src/symfonic/core/observability/audit.py
drain
async
¶
Return all buffered entries, clearing the buffer.
Tests use this to assert what was emitted during a request.
Source code in src/symfonic/core/observability/audit.py
entries ¶
Return a non-draining snapshot (sync, for inspection).
Not guaranteed to be consistent under concurrent record()
calls — prefer drain() in assertions.
Source code in src/symfonic/core/observability/audit.py
record
async
¶
Append entry to the in-memory buffer (drops oldest when full).
Source code in src/symfonic/core/observability/audit.py
LLMLifecycleHook ¶
Bases: Protocol
Async instrumentation for LLM call start/end lifecycle.
NoOpObservabilityHook ¶
Zero-overhead default hook. All methods are no-ops.
Always registered in BaseAgentDeps so nodes can safely call deps.require(ObservabilityHook) without risk of MissingCapabilityError.
LSP: signatures match ObservabilityHook protocol exactly.
NodeLifecycleHook ¶
Bases: Protocol
Async instrumentation for graph-node start/end/error lifecycle.
NodeName ¶
Bases: StrEnum
Canonical LLM emission-site discriminators (v7.15.0).
ObservabilityHook ¶
Bases: NodeLifecycleHook, LLMLifecycleHook, ToolLifecycleHook, Protocol
Composed protocol -- union of all lifecycle hooks.
Backward-compatible: any class implementing all 7 methods satisfies this protocol. New consumers should prefer the granular protocols when they only need a subset of methods.
TokenBudgetTracker ¶
TokenBudgetTracker(
*,
default_daily_budget_usd: float | None = None,
default_monthly_budget_usd: float | None = None,
store: BudgetStore | None = None,
)
Tracks cumulative token usage + cost per tenant.
Two in-memory buckets per tenant (day, month) roll over
lazily on date/month boundaries. Limits configured globally via
default_*_budget_usd or per-tenant via meth:
set_tenant_budget;
None means unlimited. Admin callers bypass the breaker.
Source code in src/symfonic/core/observability/budget.py
check_budget
async
¶
Return whether tenant_id may incur another LLM call.
When a BudgetStore with read_today / read_month is
registered, the in-memory buckets are first hydrated from the
store so every replica shares one view of the running total.
Store errors are logged at WARNING and fall through to in-memory
state — the ledger is never allowed to break auth. Admin
callers bypass the breaker.
Source code in src/symfonic/core/observability/budget.py
clear_tenant_budget ¶
Remove per-tenant overrides so the defaults apply again.
get_tenant_limits ¶
Return (daily_limit, monthly_limit) applied to tenant_id.
Source code in src/symfonic/core/observability/budget.py
record
async
¶
Record token usage for a single LLM call; returns its cost_usd.
Source code in src/symfonic/core/observability/budget.py
reset
async
¶
Reset in-memory day + month counters for a tenant.
When a persistent store is wired, the shared Postgres row is NOT cleared — use the admin billing endpoint to zero the ledger.
Source code in src/symfonic/core/observability/budget.py
set_tenant_budget ¶
set_tenant_budget(
tenant_id: str,
*,
daily_usd: float | None = None,
monthly_usd: float | None = None,
) -> None
Override budget ceilings for a tenant; None = unlimited.
Source code in src/symfonic/core/observability/budget.py
ToolLifecycleHook ¶
Bases: Protocol
Async instrumentation for tool invocation start/end lifecycle.
emit_audit_event ¶
Schedule an audit write without blocking the caller.
Never raises — failures are logged. If no logger is registered the call is a complete no-op (zero allocations beyond the entry itself).
The FastAPI routers use this directly in handlers so a slow / sick audit backend can never 500 a user request. The task is scheduled on the currently-running loop; when called outside an async context (defensive guard) we log a warning and drop the entry — no audit logger is expected to live that far up the stack.
Source code in src/symfonic/core/observability/audit.py
get_audit_logger ¶
get_budget_tracker ¶
human_label ¶
Return the UI-friendly label for node_name.
Unknown names (adopter-extended sites) round-trip unchanged so dashboards do not lose attribution -- this is the explicit non-validation contract documented at module level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_name
|
str
|
The discriminator string from |
required |
Returns:
| Type | Description |
|---|---|
str
|
The matching label from :data: |
str
|
|
Source code in src/symfonic/core/observability/node_names.py
observed_node ¶
observed_node(
node_name: str,
*,
swallow_errors: bool = False,
always_log: bool = False,
) -> Callable[..., Callable[..., Any]]
Decorator factory for observable graph nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node_name
|
str
|
Identifier recorded in node_execution_log and passed to ObservabilityHook callbacks. |
required |
swallow_errors
|
bool
|
When True, catch non-programming exceptions
(not MissingCapabilityError, ConfigurationError) and return
|
False
|
always_log
|
bool
|
When True, append to node_execution_log even when
the wrapped function returns |
False
|
Usage::
@observed_node("react")
async def react(state: dict[str, Any]) -> dict[str, Any]:
... # pure business logic
Source code in src/symfonic/core/observability/decorators.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 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 | |
set_audit_logger ¶
Register (or clear) the process-wide audit logger.
Call once at app startup. Passing None disables audit emission
— useful for tests that registered a logger and want to tear down.
Source code in src/symfonic/core/observability/audit.py
set_budget_tracker ¶
Register (or clear) the process-wide budget tracker.
Consulted by check_budget_before_llm and SymfonicAgent.run();
when unset both skip the check.