Skip to content

Observability

Document type: Normative.

Scope: Safe trace, metric, outcome, and correlation semantics; process-host provider ownership; and the boundary between advisory telemetry and durable application authority.

1. Safe signal contract

Telemetry is advisory. Durable records, typed results, exceptions, receipts, and state transitions remain authoritative. No Archetype signal operation or Archetype-owned adapter failure may change an application result, retry decision, commit, authorization decision, or exception identity. A process host remains responsible for failure behavior in handlers it installs itself.

Family and core code emit telemetry through the private archetype._obs boundary and diagnostics through stdlib logging. They do not import an OTel SDK, exporter, collector, or vendor integration. Process hosts own provider, exporter, and handler installation through the private archetype._logging adapter. Both modules remain internal and do not expand the supported Python or REST surface.

The signal boundary cannot import archetype.redaction: core imports _obs, so that edge would invert the family dependency direction. Instead, _obs is safe by construction through a closed schema. An outer adapter handling a content-bearing event or export record still consumes the canonical RedactionService before its own durable or external write. These protections are complementary.

2. Vocabulary and validation

SIGNAL_SCHEMA_VERSION, SPAN_NAMES, LEGACY_SPAN_NAMES, TRACE_ATTRIBUTE_KEYS, METRIC_NAMES, METRIC_LABEL_KEYS, EVENT_NAMES, ERROR_TYPES, FAILURE_DISPOSITIONS, OUTCOMES, SPAN_NAME_ALIASES, TRACE_ATTRIBUTE_ALIASES, RECORDER_METRIC_NAMES, and RECORDER_METRIC_LABEL_KEYS in archetype._obs are the single machine-readable vocabulary. The repository audit consumes these literals; it must not maintain a second allowlist.

Signal names are fixed. Unknown or dynamic names are no-ops. Custom attributes use the archetype.* namespace; error.type is the approved standard semantic key. Unknown keys, secret/content keys, unsupported mappings, and invalid values are omitted without calling arbitrary str(), repr(), iteration, or object properties.

Attribute class Accepted representation
World, run, actor, or command ID Canonical lowercase, hyphenated UUID string
Bundle, attempt, idempotency, or correlation digest Precomputed lowercase 64-character hexadecimal digest
Entity ID, tick, or count Exact non-boolean integer from zero through signed 64-bit maximum
Component signature Canonical a_<count>c_s<16 lowercase hex> storage signature
Failure disposition, outcome, operation, or error type Exact member of its immutable literal vocabulary
Redaction rule IDs Non-empty exact tuple of at most 16 bounded rule identifiers

Raw attempt IDs, idempotency keys, and arbitrary correlation strings are not aliases and are not hashed at the signal boundary, even when their text happens to look like a digest. A producer may supply an explicitly named semantic digest only after its owning layer has validated the source value. This avoids turning a credential or PII value into a stable, offline-guessable telemetry identifier. Canonical attributes take precedence when a temporary legacy alias is also present.

Approved metric labels are only bounded operation, outcome, failure disposition, and error categories. World, run, actor, command, artifact, attempt, idempotency, mission, task, evaluation, and entity identifiers are never metric labels.

The retained canonical span vocabulary is:

  • gateway.create_entity, gateway.create_world, and gateway.get_world_info;
  • artifact.publish, artifact.upload, and artifact.index; and
  • the existing bounded world.query and world.update scopes, without an execution-attribution claim; and
  • the legacy world.materialize and world.execute phases listed in section 7 pending measured attribution.

The gateway.* names remain a versioned compatibility vocabulary after the gateway adapter's removal; no current API or dispatcher source emits them. The former gate.create_world and gate.get_world_info spellings are neither accepted legacy names nor aliases. SPAN_NAME_ALIASES remains part of the versioned vocabulary and is currently empty.

3. Failure and outcome semantics

A span helper yields None; callers never receive a raw OTel span that could bypass validation. The OTel current context contains a non-recording view with the same span context, retaining child-span parentage without exposing mutable attributes, events, or status. OpenTelemetry automatic exception recording and automatic status derivation are disabled.

When an application Exception propagates through a span, _obs:

  1. preserves and re-raises the identical exception object;
  2. sets ERROR status without a description;
  3. records only a bounded error.type; and
  4. records no exception message, stack, object representation, or exception event.

Application control flow represented by BaseException outside Exception, including cancellation, KeyboardInterrupt, and SystemExit, propagates unchanged and leaves span status unset.

record_failure(..., disposition="handled" | "retrying") emits a fixed safe event and bounded counter. It does not mark a successful enclosing operation as failed. record_outcome() emits only an approved advisory outcome. Neither helper replaces the family-owned result, receipt, retry row, or durable settlement that proves what happened.

Failures while starting, entering, mutating, ending, exporting, or incrementing a signal are isolated at that exact signal operation. This rule does not permit broad suppression of application work under an “observability” label.

4. Context and metric semantics

bind_context() stores only validated canonical correlation coordinates in a ContextVar; nested bindings restore their predecessor on normal return or failure. capture_context() returns a detached copy that the private host logging adapter revalidates before enriching a record. Context never contains payloads, prompts, paths, URLs, headers, exception text, or arbitrary object strings.

Proxy tracers and counters may be created before a provider. Once a host registers a provider, future calls use it. Signals emitted before registration are dropped rather than buffered or replayed. Durable evidence is therefore the only source for recovery or retrospective truth.

5. Process-host ownership

Importing _obs installs no provider, exporter, logging handler, or vendor integration. configure_tracing() is an explicit process-host adapter used by runtime/server startup:

  • an existing host provider is respected and never replaced or shut down;
  • a no-backend decision remains unlatched so a later host can configure;
  • an Archetype-created provider candidate is installed once under a lock;
  • a candidate that loses every global signal-provider registration is shut down without touching the host; a Logfire meter or logger that won its independent global slot is never shut down merely because an external tracer won;
  • adapter failures produce only fixed diagnostics and remain retryable; and
  • Archetype-owned processors snapshot and revalidate approved Archetype scope, names, attributes, events, status, context, and resource before enqueue or export. Foreign fields, links, trace state, and status descriptions are discarded.

The SDK bundled for the explicit debug-console host path is not permission for family imports. Optional OTLP and Logfire integrations remain host concerns. RunConfig.debug controls execution diagnostics and is not a telemetry-export switch.

Importing Archetype, its runtime, or its API installs no handler or provider; create_app() is also inert. The explicit configuration points are runtime construction for trusted scripts, CLI serve startup for the server process, and each FastAPI lifespan for the serving worker. Repeated worker setup is safe. create_app() does not automatically invoke Logfire FastAPI instrumentation; explicit backend selection continues through the vendor-neutral host adapter.

When ARCHETYPE_LOG=debug|info|warning|error or the runtime's log= argument enables logging, the host adapter owns at most one archetype package stderr handler. Otherwise the explicit host boundary installs a package-owned null handler so Python's lastResort handler cannot emit warnings or errors; any handler the host explicitly installed remains authoritative through normal propagation. Later quiet/default setup does not downgrade an already enabled owned stderr handler. The adapter does not alter root handlers, root filters, root level, the global LogRecordFactory, or foreign handlers and filters. Its fail-open filter first removes forged reserved fields and then restores only valid lowercase trace_id and span_id coordinates plus revalidated TRACE_ATTRIBUTE_KEYS. LOG_RECORD_FIELDS is exactly that derived union, not a second attribute vocabulary. With no active context the fields are absent. Correlation never reads or renders payloads, prompts, arbitrary objects, exception objects, traceback text, or exception messages. The default formatter suppresses traceback rendering and writes human diagnostics to stderr, then restores the producer's message, arguments, and exception/stack fields for later host-owned handlers. It cannot sanitize sensitive text already embedded in a primitive string; producer-side policy and foreign-handler export safety remain host responsibilities. Stdout remains owned by application and machine-result output. When enabled, the stderr handler runs first, so a host handler attached to the archetype package logger receives the same enriched correlation fields without moving the filter onto that foreign handler or the root logger.

The former contrib.logfire_observer hook surface has been removed. The private archetype._obs boundary owns the retained vendor-neutral vocabulary; process hosts select Logfire or OTLP exporters. There is no alternate hook factory that can create a second tick root, retain an open-span table, or bypass the safe signal boundary.

6. Family dispositions

Family or layer Current signal disposition Authoritative outcome
Runtime host Explicit construction-time provider and owned-handler setup; no family workflow span Runtime lifecycle and returned/raised result
CLI and API serve and worker lifespan configure the host; imports and create_app() remain inert; no API operation span is currently approved HTTP result and dispatcher/domain result
Runtime handles No direct signal yet; lower owning family remains visible Typed family result/exception
Commands No direct signal yet Commands-owned policy decision, typed result or exception, durable command ledger and outbox, and manifest-coupled settlement
World registry, lifecycle, mutation, simulation, and durable reads Existing query/update scopes without execution-attribution claims; materialize/execute names are legacy pending #518/#519 Tick manifest, world record, retained committed receipt, and typed result/exception
Storage No direct signal yet Store/catalog state and returned frame
Redaction No direct signal; safe rule IDs may be carried by approved callers Redaction receipt or quarantine exception
Artifacts Child spans for publish, upload, and index Publication row, object/index state, and publish receipt
Evaluation No direct signal yet Snapshot-pinned evaluation receipts
Research No direct signal yet Persisted experiment, run, result, and branch-head rows plus the typed workflow result
Audit Logging only; no direct signal yet Durable control-catalog outbox event; access evidence is advisory and Iceberg rows and their watermark are analytical projection state
Missions and sandboxes No direct signal yet Typed transition rows, attempt state, checkpoints, and artifacts
Physical-AI workflow, providers, and pure search No direct signal yet Persisted evaluation rows and report for the workflow; provider state and returned values at provider boundaries; returned proposals for pure search

For actor-aware operations, the commands-owned policy decision and the typed operation result or exception remain authoritative. Any later AccessSummary/AuditRow is bounded, advisory evidence, including after it is appended to Iceberg. For durable command transitions, the transactional control-catalog outbox event remains the source of truth. The audit Iceberg rows and projection watermark describe analytical delivery, which may lag or replay; neither can authorize, settle, or change the typed operation outcome.

The machine authority is one independently owned manifest per family under quality/observability/<family>.toml. The required universe begins with every callable member — method, async method, or property — of every Protocol declared under a registered packages/archetype-ecs/src/archetype/<family>/ package, not only protocols co-located in interfaces.py.

A family manifest may also register an exact concrete_operation_surface for a reviewed state owner or a module_operation_surface for family-owned public functions. Registration discovers the complete public callable surface from source; missing, phantom, overlapping, cross-owner, unknown, or empty surfaces fail closed.

During migration, two definitions may not collapse to the same family-relative operation name; compatibility modules re-export the owning definition instead. Every discovered operation has exactly one disposition row in its owning family manifest. Rows use exact qualified names; wildcards, method ranges, and inherited blanket dispositions are forbidden. A family may add an exact workflow row for an instrumented internal operation that is not a discovered operation; there is no reverse requirement that every safe internal emitter have a workflow row.

Each row declares plural signals and outcomes, its authoritative durable or typed evidence when one exists, and only the fixed names, fields, and bounded metric labels it uses. Every workflow claim must exactly match literal emissions in its declared callable. Context-manager factories count only when entered directly and decorator factories only when applied directly; merely constructing either object is not an emission. Called helpers and nested callables are not attributed transitively. The fixed metric contract of record_failure() and record_outcome() comes from _obs's machine-readable recorder vocabulary. A positive protocol disposition lists same-owner emission_workflows and its fixed signal claims must equal the union of those source-backed workflows. This binds positive intent to source without inventing protocol-to-implementation mappings or requiring a workflow for every internal emitter.

root and child are mutually exclusive. none is exclusive, requires a rationale, and records approval intent: no new signal is approved for that contract. It does not claim the operation lacks an outcome, nor does it pretend to prove source absence without a protocol-to-implementation registry. A temporary legacy exception names one exact rule, path, qualified scope, and target together with its owner, issue, reason, and objective expiry condition. A missing, duplicate, phantom, wildcard, stale, cross-owner, or source-divergent row fails the audit.

An owner cannot absorb another package's workflow or legacy debt. The sole current cross-package ownership is the world family's two explicit AsyncWorld compute/commit workflows. Host capabilities live only in hosts.toml: provider setup and console export remain in _obs, logging configuration remains in _logging, and runtime/API/CLI hosts may only invoke those private adapters.

root describes Archetype's logical ingress ownership; it does not discard an upstream distributed parent. Runtime and API ingress workflows may own roots when explicitly approved, while registered family handlers own their declared children. The repository audit enforces the vocabulary, declared ownership, and root/child/none exclusivity. It also binds fixed workflow fields to exact lexical source emissions, but it does not prove runtime topology. Issue #515 owns any coherent ingress-root model and corresponding instrumentation.

scripts/check_observability.py provides deterministic syntax and disposition enforcement from source and these manifests. It does not parse exported telemetry or depend on a collector. The existing footgun reviewer separately checks semantic boundary/authority and safety/cardinality mistakes that syntax cannot prove, including values smuggled under an approved key and telemetry used as application authority. Focused behavior contracts still prove typed failure identity, retry behavior, and durable evidence.

7. Lazy execution honesty

world.materialize and world.execute are retained legacy names, not claims that Daft work was materialized or executed inside those spans. Processor methods commonly build lazy expressions whose work runs at a later terminal boundary. Telemetry must not add .collect(), .to_pylist(), or any other materialization to manufacture a duration.

Issue #518 must characterize Daft execution attribution and worker context with the locked version and supported runner. #519 then replaces or redefines these world phases from that evidence. Until then, no processor planning duration may be described as processor execution duration.