OBSERVABILITY.md
This document is the living spec for observability in this repo.
It defines:
x-request-id meansIf 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/api is one OTEL serviceapps/desktop is one Sentry/desktop servicehyprnote.subsystemCurrent canonical subsystem values include:
edgellmsttsubscriptionWe use three separate concepts:
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.environmentCurrent canonical service names:
apidesktopUse one service.name per deployable/runtime process.
Do not create separate service.name values for:
For example, edge, llm, stt, and subscription inside apps/api are not separate services. They are subsystems within the api service.
Use:
hyprnote.subsystemExamples:
hyprnote.subsystem = "edge"hyprnote.subsystem = "llm"hyprnote.subsystem = "stt"Do not use a bare service span field for this.
For distributed tracing, use W3C Trace Context:
traceparentbaggage only when intentionally neededSentry headers may also exist:
sentry-tracebaggageBut OTEL trace stitching must work through W3C propagation.
Inbound requests:
Outbound requests:
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.tsxDo not put user identity or device identifiers into baggage by default.
In particular, do not propagate:
enduser.idenduser.pseudo.idas 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:
traceparenthyprnote.request.idNever do this:
x-request-id = trace_idx-request-idAPI 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_tokenshyprnote.*If OTEL does not define a field, use:
hyprnote.*Do not use:
app.*service, provider, status, session_id, user_idWe avoid app.* because OpenTelemetry owns that namespace.
If a concept already has an approved key, reuse it everywhere:
Do not rename the same concept differently per backend.
enduser.idenduser.pseudo.idUse:
enduser.id for authenticated user IDenduser.pseudo.id for device fingerprint or other stable pseudonymous device identityhyprnote.request.idhyprnote.duration_mshyprnote.retry.delay_mshyprnote.timeout_shyprnote.timeout.elapsedhttp.request.methodhttp.routehttp.response.status_codeurl.pathurl.full when neededotel.kindotel.nameIngress 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_tokensUse 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.nameUse:
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.deviceKeep 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_previewDo not put large raw payloads on high-volume spans by default.
Honeycomb service views come from OTEL resource attributes, especially:
service.nameBecause of that:
apps/api must stay one Honeycomb service: apihyprnote.subsystemHoneycomb 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.idDo not avoid useful IDs just because they are high cardinality.
Server entry spans should:
otel.kind = "server"otel.nameWhen 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:
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_codesUse contexts for structured objects that are too rich for tags.
Canonical context names include:
gen_ai.requestgen_ai.responsehyprnote.stt.requesthyprnote.enduser.claimshyprnote.sessionSet scope.set_user(...) when identity is available.
API:
Desktop:
Do not invent Sentry-only field names for concepts that already exist in OTEL unless Sentry forces it.
Good:
enduser.idservice.nameerror.typeBad:
user_idserviceupstream.statusllm.model when gen_ai.request.model already existsUse:
error.type for machine-readable classificationerror.message for the display/debug messageerror.code when an external or protocol code existsExamples:
Avoid ad hoc variants such as:
messageerrorerror_typeerror_codeCanonical headers used in this repo:
traceparentbaggagesentry-tracex-request-idx-device-fingerprintMeaning:
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 identifierhyprnote.* field.x-request-id separate from trace propagation.Do not do any of the following:
service = "llm" style fieldsx-request-id = trace_idapp.* custom fieldsspan.record without declaring them firstThe 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.rsTreat this document as normative.
If code and this file disagree:
Do not let drift accumulate.