Prometheus Metrics
Status: design (2026-07-09), implemented.
Problem
Query-path performance work (BMP pruning and Seismic nomination quality, rerank IO, memory-bound degradation) also uses debug log lines and opt-in query work counters — unusable for dashboards, alerting, or regression tracking in production.
Design
- summa-core emits through the
metricsfacade behind a new non-defaultmetricsfeature (native-only; wasm never enables it). Without an installed recorder the macros are ~1ns no-ops, and every emission happens at aggregation points (query end, phase end, IO call) — never per block/superblock inside scoring loops, so the hot-path allocation/atomics budget is untouched. - summa-server enables the feature, installs
metrics-exporter-prometheus, and servesGET /metricson--metrics-addr(default0.0.0.0:9184). - Emission sites live behind tiny
observe::*helper fns so call sites carry no#[cfg]noise; the helpers compile to nothing when the feature is off.
Metric set
Histograms are seconds unless noted. field labels are field names and every metric carries an index label (the registry index name, embedded in the schema at creation; “unknown” for pre-existing indexes until recreated or patched — see below). Directory-layer metrics (summa_directory_read_*, summa_cold_write_bytes_total) have no schema in scope, so the label is attached late: Index::open/create call Directory::set_index_label on the index’s directory instance once the schema is loaded.
| Metric | Type | Labels | Meaning |
|---|---|---|---|
summa_bmp_query_duration_seconds | histogram | field | full BMP executor wall time |
summa_bmp_query_prepare_duration_seconds | histogram | field | shared query resolution/quantization; normally paid once at query-global planning and near-zero in each planned segment |
summa_bmp_d_grid_duration_seconds | histogram | field | D-grid decoding and per-block upper-bound calculation |
summa_bmp_prefetch_duration_seconds | histogram | field | userspace time preparing/coalescing D and block-payload MADV_WILLNEED ranges (not asynchronous page-fault completion) |
summa_bmp_block_score_duration_seconds | histogram | field | visited block payload scoring |
summa_bmp_docmap_duration_seconds | histogram | field | virtual-record to document/ordinal resolution after threshold rejection |
summa_bmp_prefetched_bytes_total / _ranges_total | counter | field | bounded D/block-payload byte ranges submitted to the kernel |
summa_bmp_superblocks_visited_total / _skipped_total | counter | field | superblock iteration vs pruned — skip ratio via PromQL |
summa_bmp_blocks_scored_total / _skipped_total | counter | field | block-level skip ratio |
summa_bmp_blocks_scored_per_query | histogram | field | per-query distribution (tail queries) |
summa_bmp_docmap_lookups_total / _per_query | counter / histogram | field | scattered doc-map resolutions for candidates that survive the score floor (strictly lower candidates are rejected before this lookup) |
summa_bmp_lsp_duration_seconds | histogram | field | query-global H/E hierarchy planning wall time |
summa_bmp_lsp_prepare_duration_seconds | histogram | field | one-time sparse query preparation before segment planning |
summa_bmp_lsp_h_scan_duration_seconds | histogram | field | scan of the pinned coarse H hierarchy across segments |
summa_bmp_lsp_select_duration_seconds | histogram | field | best-first H expansion, selected E-group decoding, and plan construction |
summa_bmp_lsp_superblocks / _gamma | histogram | field | total physical E cells and configured global selection cap |
summa_bmp_lsp_coarse_groups / _expanded | histogram | field | total H cells versus cells expanded into E; their ratio shows hierarchy effectiveness |
summa_bmp_lsp_superblocks_evaluated | histogram | field | E cells actually decoded while finding exact global top-gamma |
summa_bmp_integrity_fault_queries_total | counter | field | BMP queries that hit at least one corrupt block, corrupt term range, out-of-range posting slot, or invalid doc-map entry (also warn!-logged with back-off) |
summa_bmp_corrupt_blocks_total / _corrupt_terms_total / _dropped_postings_total / _invalid_docmap_entries_total | counter | field | per-kind BMP integrity fault counts; a corrupt block is not counted as scored |
summa_reorder_granularity_total | counter | field, granularity (records/blocks) | reorder passes by chosen granularity (docs/block-level-reorder.md) |
summa_reorder_coherence | histogram | field | raw block coherence d (avg records per block×dim pair) measured at each reorder decision |
summa_reorder_coherence_norm | histogram | field | normalized coherence, 0 = random order, 1 = as coherent as the dim-frequency distribution allows — this drives the Auto decision (threshold 0.5) |
summa_seismic_query_duration_seconds | histogram | field | sparse candidate traversal and result ordinal hydration time |
summa_seismic_clusters_total | counter | field | clusters visited across nomination runs |
summa_seismic_documents_total | counter | field | eligible candidate documents scored with complete ordinal aggregation |
summa_seismic_budget_truncations_total | counter | field | query executions stopped at bounded nomination expansion limits |
summa_sparse_maxscore_query_duration_seconds | histogram | field | sparse MaxScore executor wall time |
summa_dense_l1_duration_seconds | histogram | field, kind (see below) | ANN / brute-force candidate generation |
summa_dense_rerank_duration_seconds | histogram | field | rerank total (resolve+read+score) |
summa_dense_rerank_resolve_duration_seconds | histogram | field | doc→flat-slot indirection (flat store is not reordered) |
summa_dense_rerank_read_duration_seconds | histogram | field | raw-vector reads within rerank (page-fault sensitive) |
summa_dense_rerank_vectors | histogram | field | candidates reranked per query |
summa_directory_read_duration_seconds | histogram | index, op (lazy_range) | Directory-layer read latency — only Lazy handles (HTTP/custom read_fn) do real IO here; mmap slices are zero-copy and their fault latency lands inside the phase histograms |
summa_directory_read_bytes | histogram | index, op | read sizes |
summa_slice_cache_hits_total / _misses_total | counter | index | SliceCachingDirectory range reads served from cache vs. forwarded to the inner directory (HTTP/WASM path); hits are flushed in batches of 1024 from an in-process counter |
summa_slice_cache_hit_bytes / _miss_bytes | histogram | index | sizes of cached / forwarded range reads (hit sizes sampled at each flush) |
summa_slice_cache_evicted_slices_total / _bytes_total | counter | index | LRU eviction volume; a high rate against a flat hit rate means the cache budget is too small for the working set |
summa_rerank_candidates_skipped_total | counter | index, kind (dense/binary) | L2 rerank candidates dropped because their segment is gone or has no stored vectors for the field (also warn!-logged per request) |
summa_dense_ann_scans_total | counter | field, kind, regime (serial/parallel) | dense ANN segment scans by execution regime; the serial-vs-Rayon choice used to be invisible |
summa_dense_ann_blocks_scored / _blocks_pruned | histogram | field, kind | per-scan IVF-TQ/TQ blocks scored vs skipped by the scale upper bound (pruning ratio via PromQL) |
summa_dense_ann_postings_probed | histogram | field, kind | postings in the probed leaves; what a combined-document (non-Max) scan buffers before grouping |
summa_dense_ann_non_finite_dropped_total | counter | field, kind | candidates dropped because their estimated score was NaN/±inf — a degenerate stored vector or codebook (also warn!-logged with back-off) |
summa_ann_non_finite_scores_dropped_total | counter | — | non-finite leaf scores dropped while combining multi-valued ANN candidates (no field in scope at that layer) |
summa_scann_fast_scan_degenerate_queries_total | counter | — | float ScaNN queries whose FastScan lookup table collapsed to a constant (all-zero AH scores); every leaf row scores as its centroid dot |
summa_store_get_duration_seconds | histogram | index | document store fetch (decompression + faults), single- and multi-field paths |
summa_cold_write_bytes_total | counter | index | merge/reorder bytes written via the page-cache-dropping cold path (docs/cold-io.md) |
summa_reorder_bp_passes_total | counter | index, field, entity_kind, stop_reason, converged | completed record/block BP graph passes and why refinement stopped (complete, objective, time_budget, or memory_budget) |
summa_reorder_bp_duration_seconds | histogram | index, field, entity_kind | BP graph wall time (forward-index construction and blob rewrite are separate surrounding phases) |
summa_reorder_bp_entities / _postings | histogram | index, field, entity_kind | input graph size for each BP pass |
summa_reorder_bp_partitions / _iterations_per_pass | histogram | index, field, entity_kind | completed recursive partitions and refinement iterations |
summa_reorder_bp_entity_passes_per_pass / _swaps | histogram | index, field, entity_kind | entities visited across gain iterations and entities moved |
summa_reorder_bp_active_passes | gauge | index, field, entity_kind | currently running BP graph passes; cancellation/panic-safe |
summa_search_duration_seconds | histogram | index, status | server: full search RPC |
summa_search_requests_total | counter | index, status | server: RPC outcomes |
Skip-ratio note: ratios are derived in PromQL (rate(scored) / (rate(scored) + rate(skipped))) rather than emitted, so they aggregate correctly across instances and windows.
Dense L1 kind labels follow the active reader path: tq_flat, ivf_tq, scann_ah, binary_ivf, scann_binary, global_binary_ivf, binary_scann, binary_flat, or flat. The binary names differ between the shared and binary-specific entry points. IVF-PQ has been removed. The source of truth is dense_ann_kind_label and its call sites.
Broker metrics (summa-broker)
Emitted by the broker process (see broker.md), same conventions. Every fallback path the broker can take is counted — nothing degrades silently.
| Metric | Type | Labels | Meaning |
|---|---|---|---|
summa_broker_search_duration_seconds | histogram | index, status | full broker-side Search RPC |
summa_broker_search_requests_total | counter | index, status | Search outcomes |
summa_broker_admission_rejected_total | counter | index, scope | try-acquire rejections before any backend RPC (global | backend) |
summa_broker_backend_request_duration_seconds | histogram | backend, rpc | outbound RPC latency per backend |
summa_broker_backend_requests_total | counter | backend, rpc, code | outbound outcomes by gRPC code |
summa_broker_backend_healthy | gauge | backend, shard | 1 healthy / 0.5 suspect / 0 evicted |
summa_broker_backends | gauge | state | discovered backend counts (healthy/suspect/evicted/unready) |
summa_broker_discovery_events_total | counter | type | pod watcher churn (added/removed) |
summa_broker_index_map_age_seconds | gauge | backend | staleness of the backend’s learned index list |
summa_broker_ambiguous_index_total | counter | index | reads routed to a deterministically-picked shard because the index exists on several shards with no rule |
summa_broker_stale_topology_serves_total | counter | backend | reads served by a Suspect backend off its last-known index map |
summa_broker_write_rejected_total | counter | index, reason | write RPCs the broker itself refused (reason = gRPC code label) |
summa_broker_stream_flushes_total | counter | index | streaming IndexDocuments runs forwarded as BatchIndexDocuments |
Pre-existing indexes and index="unknown"
Indexes created before the label shipped have no index_name in their stored schema. Recreate them, or patch the stored metadata in place. Do it with the server stopped (or the index not open for writing) — the server rewrites metadata.json on commit/merge and would overwrite a live patch:
cd <data_dir>/<index_name>
jq --arg name "<index_name>" '.schema.index_name = $name' metadata.json \
> metadata.json.patched && mv metadata.json.patched metadata.json
Note: metadata.json.tmp is reserved for crash recovery — never use it as a scratch name.
Dashboard
grafana/dashboard.json — importable Grafana dashboard designed for k8s-deployed Summa (template variables: datasource / namespace / pod; cross-pod histogram_quantile aggregation; cAdvisor pod-resource row for page-fault correlation). The server configures explicit histogram buckets so quantiles aggregate across pods.
The Search time breakdown by operation panel (top of the overview row) stacks the mean time each disjoint phase — sparse candidate search (Seismic), MaxScore (DAAT), ANN L1 candidate gen, rerank, store fetch, directory read — contributes per search request: rate(<phase>_duration_seconds_sum) / rate(summa_search_requests_total). Because segments/sub-queries run concurrently, the stack is CPU-time-equivalent per request, not wall-clock latency — read it for where time goes, and the p50/p95/p99 latency panels for wall-clock.
Non-goals / future
- Per-page-fault disk latency: mmap faults are invisible to userspace timers; they show up in the phase histograms that contain them (rerank read, Seismic query). True per-read IO latency requires the Phase 2 DirectIO path.
- Exemplars, per-query tracing: out of scope; use the existing debug logs.
Seismic opt-in query diagnostics also report forward rows and encoded forward bytes scored, including exact backfill and ordinal hydration. The fixed-size counters merge across segment workers; they allocate no per-dimension labels.