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-idmeans - 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.
This repo has multiple binaries and runtime surfaces, but the same conventions apply everywhere:
apps/apiis one OTEL serviceapps/desktopis one Sentry/desktop service- internal route groups or modules are not separate OTEL services
- internal logical breakdowns use
hyprnote.subsystem
Current canonical subsystem values include:
edgellmsttsubscription
We use three separate concepts:
- OpenTelemetry
- canonical tracing model
- canonical attribute naming model
- canonical propagation model
- Honeycomb
- primary trace analysis backend
- expects OTEL resources, spans, and high-cardinality fields
- 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.
Every process should set:
service.namespace = "hyprnote"service.name = <logical process name>service.versiondeployment.environment
Current canonical service names:
- API:
api - Desktop:
desktop
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.
Use:
hyprnote.subsystem
Examples:
- API ingress span:
hyprnote.subsystem = "edge" - LLM handler span:
hyprnote.subsystem = "llm" - STT websocket/session spans:
hyprnote.subsystem = "stt"
Do not use a bare service span field for this.
For distributed tracing, use W3C Trace Context:
traceparentbaggageonly when intentionally needed
Sentry headers may also exist:
sentry-tracebaggage
But OTEL trace stitching must work through W3C propagation.
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.
Rust shared helpers live in:
API ingress extraction and root HTTP span setup live in:
Desktop request header creation lives in:
apps/desktop/src/shared/utils.tsapps/desktop/src/ai/traced-fetch.tsapps/desktop/src/auth/context.tsx
Do not put user identity or device identifiers into baggage by default.
In particular, do not propagate:
enduser.idenduser.pseudo.id- device fingerprints
as baggage unless there is an explicit need and a privacy review.
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
- generate it once at ingress if missing
- forward it unchanged when useful
- record it as
hyprnote.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
API ingress uses request-id middleware and records the value on the root span:
Desktop client requests add x-request-id separately from traceparent:
If OTEL defines a field for the concept, use the OTEL field.
Examples:
service.namespaceservice.namehttp.request.methodhttp.routehttp.response.status_codeurl.pathenduser.idenduser.pseudo.iderror.typeerror.messageerror.codeservice.peer.namegen_ai.operation.namegen_ai.provider.namegen_ai.request.modelgen_ai.response.modelgen_ai.response.idgen_ai.usage.input_tokensgen_ai.usage.output_tokens
If OTEL does not define a field, use:
hyprnote.*
Do not use:
app.*- bare ad hoc names like
service,provider,status,session_id,user_id
We avoid app.* because OpenTelemetry owns that namespace.
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.
enduser.idenduser.pseudo.id
Use:
enduser.idfor authenticated user IDenduser.pseudo.idfor device fingerprint or other stable pseudonymous device identity
hyprnote.request.idhyprnote.duration_mshyprnote.retry.delay_mshyprnote.timeout_shyprnote.timeout.elapsed
http.request.methodhttp.routehttp.response.status_codeurl.pathurl.fullwhen neededotel.kindotel.name
Ingress HTTP spans should be otel.kind = "server".
Use OTEL GenAI fields where available:
gen_ai.operation.namegen_ai.provider.namegen_ai.request.modelgen_ai.response.modelgen_ai.response.idgen_ai.usage.input_tokensgen_ai.usage.output_tokens
Use hyprnote.* for Hyprnote-specific request metadata:
hyprnote.gen_ai.request.streaminghyprnote.gen_ai.request.message_counthyprnote.gen_ai.request.model_candidate_counthyprnote.gen_ai.request.tool_callinghyprnote.task.name
Use:
hyprnote.stt.provider.namehyprnote.stt.routing_strategyhyprnote.stt.modelhyprnote.stt.language_codeshyprnote.stt.language_codehyprnote.stt.session.idhyprnote.stt.job.idhyprnote.stt.provider_session.idhyprnote.stt.provider_session.duration_shyprnote.stt.provider_session.expires_athyprnote.stt.provider.error_codehyprnote.audio.sample_rate_hzhyprnote.audio.channel_counthyprnote.audio.channel_indexhyprnote.audio.size_byteshyprnote.audio.duration_shyprnote.audio.device
Keep vendor-specific fields namespaced:
hyprnote.supabase.*hyprnote.stripe.*hyprnote.connection.*hyprnote.integration.*hyprnote.bot.*
Always prefer service.peer.name for the downstream system name.
If raw payload capture is necessary for debug logs, use:
hyprnote.payload.rawhyprnote.http.response.bodyhyprnote.http.body_preview
Do not put large raw payloads on high-volume spans by default.
Honeycomb service views come from OTEL resource attributes, especially:
service.name
Because of that:
apps/apimust stay one Honeycomb service:api- internal analysis should use
hyprnote.subsystem
Honeycomb handles high-cardinality fields well. IDs are allowed when they help debugging.
Good high-cardinality examples:
hyprnote.request.idenduser.idenduser.pseudo.idgen_ai.response.idhyprnote.stt.job.id- provider session IDs
Do not avoid useful IDs just because they are high cardinality.
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
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 is for:
- errors
- crash reports
- request-local debugging context
It is not the canonical trace schema. OTEL is.
Reuse OTEL names when possible.
Canonical Sentry tags include:
service.namespaceservice.nameenduser.idenduser.pseudo.idhttp.response.status_codeerror.typegen_ai.provider.namegen_ai.request.modelhyprnote.gen_ai.request.streaminghyprnote.stt.provider.namehyprnote.stt.routing_strategyhyprnote.stt.modelhyprnote.stt.language_codes
Use contexts for structured objects that are too rich for tags.
Canonical context names include:
gen_ai.requestgen_ai.responsehyprnote.stt.requesthyprnote.enduser.claimshyprnote.session
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
Do not invent Sentry-only field names for concepts that already exist in OTEL unless Sentry forces it.
Good:
enduser.idservice.nameerror.type
Bad:
user_idserviceupstream.statusllm.modelwhengen_ai.request.modelalready exists
Use:
error.typefor machine-readable classificationerror.messagefor the display/debug messageerror.codewhen an external or protocol code exists
Examples:
- provider returned a structured error code
- timeout class
- invalid payload class
Avoid ad hoc variants such as:
messageerrorerror_typeerror_code
Canonical headers used in this repo:
traceparentbaggagesentry-tracex-request-idx-device-fingerprint
Meaning:
traceparent: canonical trace propagationbaggage: optional propagation metadata, usually originating from Sentry on desktop HTTP requestssentry-trace: Sentry tracing integrationx-request-id: request correlation onlyx-device-fingerprint: local pseudonymous device identifier
- Decide whether the concept already has an OTEL semantic convention.
- If yes, use the OTEL field name.
- If no, add a
hyprnote.*field. - If the field will be recorded later on a span, declare it at span creation.
- If the code crosses a network boundary, extract or inject W3C trace context.
- If request correlation is needed, keep
x-request-idseparate from trace propagation. - Mirror the most important fields into Sentry tags or contexts using the same names.
- Update this file if you introduce a new field family or a new rule.
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.recordwithout declaring them first - using route groups as separate Honeycomb services
The current implementation that this spec describes is centered in:
apps/api/src/observability.rsapps/api/src/main.rsapps/api/src/auth.rscrates/observability/src/lib.rscrates/llm-proxy/src/handler/mod.rscrates/llm-proxy/src/handler/non_streaming.rscrates/llm-proxy/src/handler/streaming.rscrates/transcribe-proxy/src/routes/streaming/mod.rscrates/transcribe-proxy/src/routes/streaming/session.rsapps/desktop/src/shared/utils.tsapps/desktop/src/ai/traced-fetch.tsapps/desktop/src/auth/context.tsxapps/desktop/src-tauri/src/lib.rs
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.