Errors and tracing conventions
Status: in force. Code: brisvox_core::{error, telemetry},
brisvox_server::telemetry.
Errors
- Every library crate has
src/error.rswith one#[non_exhaustive] pub enum Errorbuilt withthiserror, pluspub type Result<T, E = Error>. Both are re-exported at the crate root. brisvox_core::Errorholds the cross-cutting cases (Cancelled,Timeout { what, after },ChannelClosed). Every other crate wraps it asError::Core(#[from] brisvox_core::Error)(#[error(transparent)]), so?converts it without extra code.- No
anyhow/eyrein library crates. Binaries report errors witheprintln!+ExitCode. - Error messages never contain credentials. Provider error
detailstrings are written by our adapters and must not echo request headers or URLs that carry keys. Cancelledis not a fault: log it at DEBUG at most, never at ERROR.
Spans
Create spans only through the constructors in brisvox_core::telemetry, so names and fields
stay stable for log queries:
| Span | Constructor | Level | Fields | Lifetime |
|---|---|---|---|---|
session | session_span(id) | INFO | id | whole session (root of all session work) |
turn | turn_span(epoch) | INFO | epoch | one turn; the epoch is the interrupt epoch |
stage | stage_span(name) | DEBUG | name ("vad", "stt", "llm", "tts", …) | one stage's work inside a turn |
Nesting is session{id}:turn{epoch}:stage{name}. Spawned tasks must carry their parent span
(.instrument(span)), or their events lose the session context.
Levels
- ERROR: a session failed or a server-level fault happened. Someone should look.
- WARN: degraded but recovered (retry, fallback provider, dropped frames by overflow policy).
- INFO: lifecycle (session start/end, turn boundaries, provider connect/close). Low volume only.
- DEBUG: per-stage detail, decisions (turn-end, interruption reasons).
- TRACE: per-frame / per-token detail. Never emit above TRACE on the per-frame hot path, and never create spans per audio frame. Disabled callsites are near-free (cached interest check).
Secrets
- Every credential (API keys, tokens, auth headers, signed URLs) is stored as
brisvox_core::Secret<T>. ItsDebugandDisplayprint[REDACTED], so logging a config struct with?cfgis safe. - Read the value only with
Secret::expose()where it is sent to its destination. Grep forexpose()when auditing. Secrethas noPartialEq/Deref/AsRef/Hash:==on credentials is not constant-time, and implicit access would bypassexpose(). When serde arrives: implementDeserializeonly; anySerializemust write[REDACTED].- Test
telemetry::tests::secret_is_redacted_in_tracing_outputproves secrets do not leak through%,?, nestedDebugand format arguments.
Subscriber
Binaries call brisvox_server::telemetry::init(format) once at startup:
- Filter from
RUST_LOG(defaultinfo; unset or blank both mean the default). An invalid or non-unicodeRUST_LOGis a startup error, not a silent fallback. - The TURN client crates' events and spans (
turn_client_proto,stun_proto,turn_types) belowwarnare dropped by a per-layer filter in front of the output, whateverRUST_LOGsays (they print whole STUN messages attrace);BRISVOX_LOG_TURN_CLIENT=1lifts it. Embedders with their own subscriber applybrisvox_transport::webrtc::turn_client_log_allowed(orbrisvox_core::telemetry::turn_client_log_allowed) themselves. - The SIP stack's events and spans (
rsipstack) belowwarnare held back the same way, whateverRUST_LOGsays: the stack logs every SIP message atinfo, whole, which on a secure call includes the SDES SRTP master keys.BRISVOX_LOG_SIP_STACK=1lifts it (debugging only); embedders applybrisvox_core::telemetry::sip_stack_log_allowed. - Format from
BRISVOX_LOG_FORMAT:text(default) orjson(one object per line). - A second
init/installreturnsError::Telemetryand does not panic.install(format, filter)is the environment-free variant (used by tests and embedders).
Libraries never install a subscriber. Tests that need log output install a scoped one with
tracing::subscriber::with_default.
Metrics, traces and log redaction
brisvox_server::observability (operator view: docs/OPERATIONS.md, "Observability"):
- Turn traces are built from the timeline, not from
tracingspans. The per-stage marks (brisvox_core::timeline) already hold every stage's start and end on the call'sClock. When a turn finishes, theTimelineSinkof the call's recorder turns itsTurnRecordinto one OpenTelemetry trace (aturnroot and a child span per stage) and the stage latencies into Prometheus histograms. Thesession/turn/stagespans above stay what they are: log context. Nothing is added per audio frame, token or chunk. - Cost. One finished turn costs a handful of atomics, about a dozen small allocations for the
trace and one non-blocking
export;observability::tests::a_turn_costs_microsecondsmeasures the whole observed turn, 11 marks included. - Exporters implement
TraceExporterand must not block.MemoryExporter(bounded) is for tests and embedders;OtlpExporter(featureotlp) queues to a bounded channel (drops the newest when full, counted) and posts OTLP/HTTP JSON in batches under a timeout. - Redaction.
telemetry::initinstalls the subscriber with aRedactingMakeWriter: each event is buffered whole and scrubbed (Redactor) before it is written, so a credential in an error text, a URL or a header dump never reaches stdout.Secretremains the first line;observability::tests::secrets_never_appear_in_{text,json}_logsrun the real subscriber. A new place that writes logs by another route (a custom writer) must use the same wrapper. WithBRISVOX_LOG_REDACT_PII=truethe same pass also replaces personal identifiers (emails, IBANs, IDs, card and phone numbers;Redactor::with_pii,docs/PRIVACY.md"Logs").