OBSERVABILITY.md
Rama main · réplica de lectura del repositorio original.
# Observability
This document is the living spec for observability in this repo.
It defines:
- how we use OpenTelemetry
- how data is expected to appear in Honeycomb
- what `x-request-id` means
- how we use Sentry tags, contexts, and user identity
- which attribute names are allowed
If a change introduces new tracing fields, propagation behavior, or Sentry tagging conventions, update this file in the same change.
## Scope
This repo has multiple binaries and runtime surfaces, but the same conventions apply everywhere:
- `apps/api` is one OTEL service
- `apps/desktop` is one Sentry/desktop service
- internal route groups or modules are not separate OTEL services
- internal logical breakdowns use `anarlog.subsystem`
Current canonical subsystem values include:
- `edge`
- `llm`
- `stt`
- `subscription`
## Observability Stack
We use three separate concepts:
1. OpenTelemetry
- canonical tracing model
- canonical attribute naming model
- canonical propagation model
2. Honeycomb
- primary trace analysis backend
- expects OTEL resources, spans, and high-cardinality fields
3. Sentry
- error reporting and local debugging context
- should mirror OTEL naming where practical
`x-request-id` is not trace propagation. It is a separate request-correlation mechanism.
## Resource And Service Model
### Canonical OTEL resource attributes
Every process should set:
- `service.namespace = "anarlog"`
- `service.name = <logical process name>`
- `service.version`
- `deployment.environment`
Current canonical service names:
- API: `api`
- Desktop: `desktop`
### What counts as a service
Use one `service.name` per deployable/runtime process.
Do not create separate `service.name` values for:
- axum route groups
- internal modules
- handler categories
- provider adapters
For example, `edge`, `llm`, `stt`, and `subscription` inside `apps/api` are not separate services. They are subsystems within the `api` service.
### Subsystems
Use:
- `anarlog.subsystem`
Examples:
- API ingress span: `anarlog.subsystem = "edge"`
- LLM handler span: `anarlog.subsystem = "llm"`
- STT websocket/session spans: `anarlog.subsystem = "stt"`
Do not use a bare `service` span field for this.
## Propagation
### Canonical propagation format
For distributed tracing, use W3C Trace Context:
- `traceparent`
- `baggage` only when intentionally needed
Sentry headers may also exist:
- `sentry-trace`
- `baggage`
But OTEL trace stitching must work through W3C propagation.
### Rules
Inbound requests:
- extract remote W3C trace context
- set the server span parent from the extracted context
Outbound requests:
- inject current W3C trace context
Do not use custom trace propagation headers when W3C exists.
### Current implementation
Rust shared helpers live in:
- [`crates/observability/src/lib.rs`](crates/observability/src/lib.rs)
API ingress extraction and root HTTP span setup live in:
- [`apps/api/src/main.rs`](apps/api/src/main.rs)
Desktop request header creation lives in:
- [`apps/desktop/src/shared/utils.ts`](apps/desktop/src/shared/utils.ts)
- [`apps/desktop/src/ai/traced-fetch.ts`](apps/desktop/src/ai/traced-fetch.ts)
- [`apps/desktop/src/auth/context.tsx`](apps/desktop/src/auth/context.tsx)
### Baggage policy
Do not put user identity or device identifiers into baggage by default.
In particular, do not propagate:
- `enduser.id`
- `enduser.pseudo.id`
- device fingerprints
as baggage unless there is an explicit need and a privacy review.
## Request ID
### Meaning
`x-request-id` is a correlation ID for support, logs, and local debugging.
It is not:
- a trace ID
- a span ID
- a substitute for `traceparent`
### Rules
- generate it once at ingress if missing
- forward it unchanged when useful
- record it as `anarlog.request.id`
- keep it semantically separate from OTEL trace context
Never do this:
- `x-request-id = trace_id`
- reconstruct trace relationships from `x-request-id`
### Current implementation
API ingress uses request-id middleware and records the value on the root span:
- [`apps/api/src/main.rs`](apps/api/src/main.rs)
Desktop client requests add `x-request-id` separately from `traceparent`:
- [`apps/desktop/src/ai/traced-fetch.ts`](apps/desktop/src/ai/traced-fetch.ts)
- [`apps/desktop/src/auth/context.tsx`](apps/desktop/src/auth/context.tsx)
## Naming Rules
### Rule 1: Prefer OTEL semantic conventions
If OTEL defines a field for the concept, use the OTEL field.
Examples:
- `service.namespace`
- `service.name`
- `http.request.method`
- `http.route`
- `http.response.status_code`
- `url.path`
- `enduser.id`
- `enduser.pseudo.id`
- `error.type`
- `error.message`
- `error.code`
- `service.peer.name`
- `gen_ai.operation.name`
- `gen_ai.provider.name`
- `gen_ai.request.model`
- `gen_ai.response.model`
- `gen_ai.response.id`
- `gen_ai.usage.input_tokens`
- `gen_ai.usage.output_tokens`
### Rule 2: Custom fields must use `anarlog.*`
If OTEL does not define a field, use:
- `anarlog.*`
Do not use:
- `app.*`
- bare ad hoc names like `service`, `provider`, `status`, `session_id`, `user_id`
We avoid `app.*` because OpenTelemetry owns that namespace.
### Rule 3: One concept, one name
If a concept already has an approved key, reuse it everywhere:
- OTEL spans
- tracing logs/events
- Sentry tags
- Sentry contexts
Do not rename the same concept differently per backend.
## Canonical Field Families
### Identity
- `enduser.id`
- `enduser.pseudo.id`
Use:
- `enduser.id` for authenticated user ID
- `enduser.pseudo.id` for device fingerprint or other stable pseudonymous device identity
### Request and duration
- `anarlog.request.id`
- `anarlog.duration_ms`
- `anarlog.retry.delay_ms`
- `anarlog.timeout_s`
- `anarlog.timeout.elapsed`
### HTTP and routing
- `http.request.method`
- `http.route`
- `http.response.status_code`
- `url.path`
- `url.full` when needed
- `otel.kind`
- `otel.name`
Ingress HTTP spans should be `otel.kind = "server"`.
### LLM
Use OTEL GenAI fields where available:
- `gen_ai.operation.name`
- `gen_ai.provider.name`
- `gen_ai.request.model`
- `gen_ai.response.model`
- `gen_ai.response.id`
- `gen_ai.usage.input_tokens`
- `gen_ai.usage.output_tokens`
Use `anarlog.*` for Anarlog-specific request metadata:
- `anarlog.gen_ai.request.streaming`
- `anarlog.gen_ai.request.message_count`
- `anarlog.gen_ai.request.model_candidate_count`
- `anarlog.gen_ai.request.tool_calling`
- `anarlog.task.name`
### STT and audio
Use:
- `anarlog.stt.provider.name`
- `anarlog.stt.routing_strategy`
- `anarlog.stt.model`
- `anarlog.stt.language_codes`
- `anarlog.stt.language_code`
- `anarlog.stt.session.id`
- `anarlog.stt.job.id`
- `anarlog.stt.provider_session.id`
- `anarlog.stt.provider_session.duration_s`
- `anarlog.stt.provider_session.expires_at`
- `anarlog.stt.provider.error_code`
- `anarlog.audio.sample_rate_hz`
- `anarlog.audio.channel_count`
- `anarlog.audio.channel_index`
- `anarlog.audio.size_bytes`
- `anarlog.audio.duration_s`
- `anarlog.audio.device`
### Vendor-specific fields
Keep vendor-specific fields namespaced:
- `anarlog.supabase.*`
- `anarlog.stripe.*`
- `anarlog.connection.*`
- `anarlog.integration.*`
- `anarlog.bot.*`
Always prefer `service.peer.name` for the downstream system name.
### Payload and debug-only fields
If raw payload capture is necessary for debug logs, use:
- `anarlog.payload.raw`
- `anarlog.http.response.body`
- `anarlog.http.body_preview`
Do not put large raw payloads on high-volume spans by default.
## Honeycomb Conventions
### Service breakdown
Honeycomb service views come from OTEL resource attributes, especially:
- `service.name`
Because of that:
- `apps/api` must stay one Honeycomb service: `api`
- internal analysis should use `anarlog.subsystem`
### High cardinality
Honeycomb handles high-cardinality fields well. IDs are allowed when they help debugging.
Good high-cardinality examples:
- `anarlog.request.id`
- `enduser.id`
- `enduser.pseudo.id`
- `gen_ai.response.id`
- `anarlog.stt.job.id`
- provider session IDs
Do not avoid useful IDs just because they are high cardinality.
### Root span quality
Server entry spans should:
- have a remote parent if the request carries one
- set `otel.kind = "server"`
- set `otel.name`
- record HTTP route and status
### Span field declaration rule
When using `tracing`, declare fields up front if you plan to `record` them later.
This matters for:
- `#[tracing::instrument(fields(...))]`
- `tracing::info_span!(...)`
If a field is not declared on span creation, later `span.record(...)` calls will not create a new OTEL attribute.
## Sentry Conventions
### Purpose
Sentry is for:
- errors
- crash reports
- request-local debugging context
It is not the canonical trace schema. OTEL is.
### Dropped events
Failures caused by the end user's own account state (exhausted provider credits, expired plans, rejected API keys) are dropped before they leave the process. `crates/user-error` owns the marker list and is wired into every Rust `before_send` (desktop, API, CLI); `apps/desktop/src/error-reporting.ts` keeps a matching list for the desktop webview. Add new markers in both places.
### Tag naming
Reuse OTEL names when possible.
Canonical Sentry tags include:
- `service.namespace`
- `service.name`
- `enduser.id`
- `enduser.pseudo.id`
- `http.response.status_code`
- `error.type`
- `gen_ai.provider.name`
- `gen_ai.request.model`
- `anarlog.gen_ai.request.streaming`
- `anarlog.stt.provider.name`
- `anarlog.stt.routing_strategy`
- `anarlog.stt.model`
- `anarlog.stt.language_codes`
### Context naming
Use contexts for structured objects that are too rich for tags.
Canonical context names include:
- `gen_ai.request`
- `gen_ai.response`
- `anarlog.stt.request`
- `anarlog.enduser.claims`
- `anarlog.session`
### Sentry user
Set `scope.set_user(...)` when identity is available.
API:
- authenticated requests use the auth subject as the Sentry user ID
Desktop:
- use a pseudonymous device identity when no authenticated user exists yet
### Alignment rule
Do not invent Sentry-only field names for concepts that already exist in OTEL unless Sentry forces it.
Good:
- `enduser.id`
- `service.name`
- `error.type`
Bad:
- `user_id`
- `service`
- `upstream.status`
- `llm.model` when `gen_ai.request.model` already exists
## Error Conventions
Use:
- `error.type` for machine-readable classification
- `error.message` for the display/debug message
- `error.code` when an external or protocol code exists
Examples:
- provider returned a structured error code
- timeout class
- invalid payload class
Avoid ad hoc variants such as:
- `message`
- `error`
- `error_type`
- `error_code`
## Header Conventions
Canonical headers used in this repo:
- `traceparent`
- `baggage`
- `sentry-trace`
- `x-request-id`
- `x-device-fingerprint`
Meaning:
- `traceparent`: canonical trace propagation
- `baggage`: optional propagation metadata, usually originating from Sentry on desktop HTTP requests
- `sentry-trace`: Sentry tracing integration
- `x-request-id`: request correlation only
- `x-device-fingerprint`: local pseudonymous device identifier
## What To Do When Adding Instrumentation
1. Decide whether the concept already has an OTEL semantic convention.
2. If yes, use the OTEL field name.
3. If no, add a `anarlog.*` field.
4. If the field will be recorded later on a span, declare it at span creation.
5. If the code crosses a network boundary, extract or inject W3C trace context.
6. If request correlation is needed, keep `x-request-id` separate from trace propagation.
7. Mirror the most important fields into Sentry tags or contexts using the same names.
8. Update this file if you introduce a new field family or a new rule.
## Anti-Patterns
Do not do any of the following:
- per-span `service = "llm"` style fields
- `x-request-id = trace_id`
- custom propagation instead of W3C trace context
- `app.*` custom fields
- different names for the same concept across OTEL and Sentry
- stuffing user identity into baggage by default
- creating new span attributes with `span.record` without declaring them first
- using route groups as separate Honeycomb services
## Current Reference Points
The current implementation that this spec describes is centered in:
- [`apps/api/src/observability.rs`](apps/api/src/observability.rs)
- [`apps/api/src/main.rs`](apps/api/src/main.rs)
- [`apps/api/src/auth.rs`](apps/api/src/auth.rs)
- [`crates/observability/src/lib.rs`](crates/observability/src/lib.rs)
- [`crates/llm-proxy/src/handler/mod.rs`](crates/llm-proxy/src/handler/mod.rs)
- [`crates/llm-proxy/src/handler/non_streaming.rs`](crates/llm-proxy/src/handler/non_streaming.rs)
- [`crates/llm-proxy/src/handler/streaming.rs`](crates/llm-proxy/src/handler/streaming.rs)
- [`crates/transcribe-proxy/src/routes/streaming/mod.rs`](crates/transcribe-proxy/src/routes/streaming/mod.rs)
- [`crates/transcribe-proxy/src/routes/streaming/session.rs`](crates/transcribe-proxy/src/routes/streaming/session.rs)
- [`apps/desktop/src/shared/utils.ts`](apps/desktop/src/shared/utils.ts)
- [`apps/desktop/src/ai/traced-fetch.ts`](apps/desktop/src/ai/traced-fetch.ts)
- [`apps/desktop/src/auth/context.tsx`](apps/desktop/src/auth/context.tsx)
- [`apps/desktop/src-tauri/src/lib.rs`](apps/desktop/src-tauri/src/lib.rs)
## Change Policy
Treat this document as normative.
If code and this file disagree:
- update the code to match this spec, or
- update this spec in the same change with a deliberate rationale
Do not let drift accumulate.