Hub emits OpenTelemetry metrics from both hub-api and hub-worker. They are exported only when a metrics exporter is configured (see OTEL_METRICS_EXPORTER); with none set, metric recording is a no-op. Standard HTTP server metrics (request duration, counts) are also emitted for the API via OpenTelemetry’s net/http instrumentation — the tables below cover Hub’s own instruments.
Label cardinality is deliberately bounded: label values come from fixed sets, and an unexpected value collapses to other rather than opening a new series.
Embeddings, translation, sentiment, and emotions each emit the same five instruments under a per-type prefix — {type} is one of embedding, translation, sentiment, emotions:
| Metric |
Type |
Labels |
Description |
hub_{type}_jobs_enqueued_total |
counter |
— |
Jobs enqueued by the enqueue provider. |
hub_{type}_provider_errors_total |
counter |
reason |
Enqueue-path failures (for example enqueue_failed, settings_read_failed). |
hub_{type}_outcomes_total |
counter |
status |
Worker outcomes: success, retry, failed_final, skipped. |
hub_{type}_worker_errors_total |
counter |
reason |
Worker-side errors (for example {type}_api_failed, get_record_failed, rate_limited, superseded, tenant_write_conflict). input_over_cap counts translations refused before any provider call because value_text is over TRANSLATION_MAX_INPUT_BYTES; they are not {type}_api_failed. |
hub_{type}_duration_seconds |
histogram |
status |
Time to process one job. |
Enrichment-wide instruments:
| Metric |
Type |
Labels |
Description |
hub_provider_panics_total |
counter |
event_type |
Recovered panics in an event fan-out provider. Expected to stay flat; a rising value means a provider is repeatedly crashing. |
hub_enrichment_outputs_cleared_total |
counter |
output |
Enrichment outputs (sentiment, emotions, translation) nulled by an edit’s eager-clear. A high rate with no matching re-enrichment points at backfill-recovery cases. |
hub_enrichment_text_trimmed_total |
counter |
enrichment |
Provider calls whose input was cut to its type’s configured cap (*_MAX_INPUT_BYTES), counted once per attempt. The call still runs — trimming is not a failure — so a rising rate means the cap is too low for this deployment’s records or its model. Translation never appears here: it refuses over-cap input instead (hub_translation_worker_errors_total{reason="input_over_cap"}). |
| Metric |
Type |
Labels |
Description |
hub_hnsw_iterative_scan_degraded |
gauge (0/1) |
— |
1 when HNSW iterative scan is latched off (pgvector < 0.8 fallback; recall capped at ef_search until restart), else 0. |
| Metric |
Type |
Labels |
Description |
hub_webhook_jobs_enqueued_total |
counter |
— |
Webhook delivery jobs enqueued. |
hub_webhook_provider_errors_total |
counter |
reason |
Enqueue-path failures. |
hub_webhook_deliveries_total |
counter |
status |
Delivery outcomes: success, retry, failed_final. |
hub_webhook_disabled_total |
counter |
reason |
Endpoints auto-disabled (410_gone, max_attempts). |
hub_webhook_dispatch_errors_total |
counter |
reason |
Dispatch-time errors. |
hub_webhook_delivery_duration_seconds |
histogram |
status |
Outbound webhook request duration. |
| Metric |
Type |
Labels |
Description |
hub_events_discarded_total |
counter |
— |
Events dropped because the in-memory fan-out channel was full. Should be ~0; a rise means the publisher can’t keep up. |
hub_message_publisher_fan_out_duration_seconds |
histogram |
— |
Time to fan one event out to all providers. |
hub_event_channel_depth |
gauge |
— |
Current depth of the event fan-out channel. |
hub_river_queue_depth |
gauge |
queue |
Waiting River jobs per queue (available / retryable / scheduled). |
hub_river_queue_oldest_age_seconds |
gauge |
queue |
Age in seconds of the oldest waiting job per queue (0 when empty) — drives lag SLOs where depth alone cannot. |
| Metric |
Type |
Labels |
Description |
hub_cache_hits_total |
counter |
cache |
Cache hits. cache is search_query_embedding or tenant_settings. |
hub_cache_misses_total |
counter |
cache |
Cache misses. Hit ratio = rate(hits) / (rate(hits) + rate(misses)) per cache. |
All *_duration_seconds histograms share explicit second buckets up to 60s (…, 5, 7.5, 10, 30, 60), covering the 30s enrichment/LLM job timeouts so a final-attempt latency lands in a real bucket rather than +Inf.