Documentation
¶
Overview ¶
Package observability — Auth metric bag (Plan 04-06 GREEN per MET-15 / GAP-6).
Owns one family per ADR §2.3 + CONTEXT GAP-6:
nornicdb_auth_attempts_total{result, protocol}
Closed enums (CONTEXT D-05e):
result ∈ AllowedAuthResults = {success, failure, denied}
protocol ∈ AllowedAuthProtocols = {bolt, http, grpc}
Cardinality ceiling = |result| × |protocol| = 3 × 3 = 9 (RESEARCH §Q11). No tenant flag — auth is per-process global, not per-database (CONTEXT MET-21 omits `database` for auth; surfacing tenant identity on an unauthenticated counter would itself be a leak).
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `user`, `user_id`, `email`, `ip` are all in ForbiddenLabels and the registration helper panics on attempt — defense in depth against PII surfacing through label values. The closed `result` enum prevents callers from passing a raw error message as the result label.
MET-25 hot path: callers cache `Attempts.WithLabelValues(result, protocol)` in a local var or struct field; the per-attempt observation pays only an atomic add. The cardinality ceiling is so low (9) that we do not even expose a Bind helper — direct WithLabelValues at the call site is fine.
D-02d leaf-package boundary: pkg/observability never imports pkg/auth. The bag is constructed at cmd/nornicdb startup and injected into:
- pkg/bolt/server.SetAuthMetrics (Plan 04-02 added the call site)
- pkg/auth.Authenticator.SetMetrics (HTTP path; this plan wires it)
- gRPC interceptor (TBD by integration site)
Package observability — Bolt metric bag (Plan 04-02 GREEN).
Owns six families per MET-07 + ADR §2.3:
nornicdb_bolt_connections_active
nornicdb_bolt_connections_total{result}
nornicdb_bolt_session_duration_seconds
nornicdb_bolt_messages_total{op, result}
nornicdb_bolt_message_duration_seconds{op}
nornicdb_bolt_packstream_decode_errors_total{reason}
Closed enums (CONTEXT D-11 / D-11a / D-11c):
result ∈ AllowedBoltResults = {success, error, timeout}
op ∈ AllowedBoltOps = {hello, run, pull, begin, commit,
discard, reset, goodbye, route,
ack_failure}
reason ∈ AllowedPackstreamReasons =
{truncated, invalid_marker,
wrong_type, oversize}
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `op`, `result`, and `reason` are NOT in the forbidden list — closed-enum string literals at the call site enforce cardinality. The reason enum is the highest-risk axis (free-form `err.Error()` would be a cardinality bomb); CONTEXT D-11c locks it to four values, classified at the packstream decode boundary via `reasonFromError(err)`.
PULL chunks NOT separately observed (CONTEXT D-11b). Chunk timing rolls up into the parent PULL `message_duration_seconds`. Aligns with Phase 8 TRC-13 ("PULL chunks do not span") — same rationale: per-chunk observation taxes the streaming hot path with no SRE alerting benefit.
No `database` label on Bolt subsystem: sessions/messages cross databases via USE clauses; per-DB instrumentation lives at the Cypher subsystem. CONTEXT D-08 bool not threaded through this constructor.
Package observability — Cache + Runtime metric bag (Plan 04-01 GREEN).
Owns six families per MET-16 + ADR §2.3:
nornicdb_cache_hits_total{cache}
nornicdb_cache_misses_total{cache}
nornicdb_cache_size_bytes{cache}
nornicdb_cache_evictions_total{cache, reason}
nornicdb_process_uptime_seconds (GaugeFunc; time.Since(start).Seconds())
nornicdb_build_info (GaugeFunc; constant 1 + const labels)
Closed enums (CONTEXT D-12, D-12b):
cache ∈ AllowedCacheNames = {query_result, schema, label, node_lookup}
reason ∈ AllowedEvictionReasons = {lru, ttl, capacity, manual}
RISK-4 carry-forward: if a downstream plan determines a cache name has no actual increment site, prune the slice here AND amend ADR §2.3. Today we keep all four to match the planning contract; the catalog_cache_test.go AssertCardinalityCeiling(4) passes whether 3 or 4 emit (helper has no lower bound — RESEARCH RISK-7 / Plan 04-07 ships the lower-bound test).
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `cache` and `reason` are NOT in the forbidden list — closed-enum string literals at the call site enforce cardinality. Subsystem callers MUST use only AllowedCacheNames / AllowedEvictionReasons values.
Pitfall 1 / RESEARCH RISK-8 mitigation: every GaugeFunc body wraps a defer-recover that returns 0 on panic, preventing a buggy callback from poisoning the entire /metrics scrape (Pitfall 1: shutdown ordering vs. live scrape window — listener drains LAST per OBS-08).
Leaf-package boundary (Phase 1 D-01a / boundary_test.go) preserved: imports limited to stdlib + prometheus/client_golang + pkg/buildinfo. pkg/buildinfo is a leaf utility (it embeds VERSION + carries Commit ldflag vars), not a forbidden subsystem package.
Package observability — Cypher metric bag (Plan 04-03 GREEN).
Owns eleven families per MET-08 + ADR §2.3:
nornicdb_cypher_queries_total{op_type[, database]}
nornicdb_cypher_query_duration_seconds{op_type[, database]}
nornicdb_cypher_planner_duration_seconds{op_type}
nornicdb_cypher_planner_cache_hits_total
nornicdb_cypher_planner_cache_misses_total
nornicdb_cypher_planner_cache_size
nornicdb_cypher_rows_returned{op_type}
nornicdb_cypher_active_transactions
nornicdb_cypher_transaction_conflicts_total{[database]}
nornicdb_cypher_slow_queries_total{[database]}
nornicdb_cypher_slow_query_threshold_seconds (GaugeFunc; D-15b)
Closed enums (CONTEXT D-04 corrected per RISK-1; D-04b parse_error sixth value):
op_type ∈ AllowedCypherOpTypes = {read, write, schema, admin, fabric, parse_error}
RISK-1 corrected classifier surface — the actual normal-path classifier reads `*QueryInfo` from QueryAnalyzer (see pkg/cypher/op_type.go), NOT a non-existent `plan.Root.Op` field. The closed enum above is the contract; `pkg/cypher/op_type.go::classifyOpType` is the producer; this bag is the consumer. Three observation sites (admin-dispatch, parse-error site, normal-path-after-Analyze) cover all six values.
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `query`, `user`, `user_id`, `ip`, `uuid`, `email` would all be cardinality bombs for Cypher and panic at registration. `op_type` is bounded by the closed enum above; `database` is bounded by the per-tenant-flag axis (D-08); callers MUST NEVER pass raw query text or query_hash as a Prometheus label.
D-12a planner cache locality: planner_cache_hits_total / planner_cache_misses_total / planner_cache_size live HERE under the cypher subsystem, not under the cross-cutting cache subsystem. Planner cache is Cypher-specific. The cross-cutting cache bag (catalog_cache.go) carries a separate `cache_hits_total{cache="query_result"}` mirror that the Cypher query-result cache emits in parallel — both layers see the metric.
MET-25 hot path: subsystems pre-bind via `BindQueryDuration("read", db)` and cache the resulting BoundLatencyObserver in struct fields at constructor time (see pkg/cypher/executor.go). The chokepoint Observe() call funnels through observeWithExemplar — zero exemplar overhead under Phase 1's NeverSample default; lights up automatically at Phase 6's sampler flip with no second-pass migration.
MET-26 / D-15b slow_query_threshold_seconds: registered directly on reg via prometheus.NewGaugeFunc(slowQueryThresholdFn) — NO struct field. The callback wraps a defer-recover that returns 0 on panic per RESEARCH RISK-8 / Pitfall 1. The value reflects cfg.Logging.SlowQueryThreshold (or whatever the injected callback reads) on every scrape — config reload is automatic; no event wiring required.
D-08 forward-compat: NewCypherMetrics(reg, tenantLabelsEnabled bool, slowQueryThresholdFn func() float64) decides label-set shape ONCE at construction. When false (Phase 4 default outside K8s), the `database` label is OMITTED from the labelnames slice — not set to empty string. Phase 5's K8s autodetect flips the bool's default value with no re-registration.
Package observability — Embeddings metric bag (Plan 04-05 GREEN).
Owns six families per MET-12 + ADR §2.3, plus the D-09 FFI panic counter:
nornicdb_embed_queue_depth (GaugeFunc; D-15b)
nornicdb_embed_processed_total{provider, model, result, mode}
nornicdb_embed_duration_seconds{provider, model, mode} (long-tail; MET-05)
nornicdb_embed_cache_hits_total
nornicdb_embed_cache_misses_total
nornicdb_embed_worker_running (Set(0)/Set(1) at lifecycle)
nornicdb_embed_ffi_panics_total{mode} (D-09 — pkg/embed FFI recovery)
Closed enums (CONTEXT D-06a):
mode ∈ AllowedEmbedBackends = {gpu, cpu, cuda, metal, vulkan}
result ∈ AllowedEmbedResults = {success, failure, cached}
provider ∈ AllowedEmbedProviders = {ollama, openai, local, other}
model is open-ish (bounded ceiling 250 per RESEARCH §Q11)
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `embedding_text` is the keystone — embedding *content* MUST NEVER be a label value. The forbidden-label panic at registration prevents anyone from labeling by raw input text. The other ForbiddenLabels (path, query, user, user_id, ip, uuid, trace_id, span_id, email) are not reachable from this subsystem.
MET-25 hot path: per-call observation uses pre-bound observers cached at the call site (see Plan 04-05 wiring + BenchmarkObserve_HotEmbed). The bag's Duration.Bind(provider, model, mode) returns a BoundEmbeddingLatencyObserver that is value-typed and concurrent-safe; callers store it in struct fields per MET-25.
D-15b GaugeFunc panic safety: the queue_depth callback wraps defer-recover that returns 0 on probe panic — concurrent shutdown of the embed queue cannot poison /metrics. Same pattern as Plan 04-04 MVCC GaugeFuncs.
D-15b distinction (worker_running): binary lifecycle state uses `Set(0)` / `Set(1)` at the lifecycle start/stop hook sites, NOT a GaugeFunc — the lifecycle event boundary is the right observation chokepoint.
D-02d leaf-package boundary: pkg/observability never imports pkg/embed or pkg/nornicdb. EmbedProbe is declared HERE; the embedder (or the embed-queue accessor adapter) satisfies it via a thin shim at the cmd/nornicdb wiring site.
D-08 forward-compat: Embed metrics are NOT tenant-tagged. provider, model, and mode are global (not per-database). Per CONTEXT MET-21 omission of embed/runtime/cache from the tenant axis. NewEmbedMetrics therefore omits the tenantLabelsEnabled bool parameter.
D-09 FFI panic counter: ffi_panics_total{mode} is the only counter where pkg/embed observably crashed during a CGo call — incremented from the deferred-recover wrapper in pkg/embed/ffi_recover.go (Plan 04-05-03). Closed mode enum prevents arbitrary panic-class labeling.
Package observability — HTTP metric bag (Plan 04-02 GREEN).
Owns five families per MET-06 + ADR §2.3:
nornicdb_http_requests_total{method, path_template, status_class[, database]}
nornicdb_http_request_duration_seconds{method, path_template, status_class[, database]}
nornicdb_http_in_flight_requests
nornicdb_http_request_body_bytes{method, path_template}
nornicdb_http_response_body_bytes{method, path_template}
Closed enums:
status_class ∈ AllowedStatusClasses = {1xx, 2xx, 3xx, 4xx, 5xx}
method — bounded by HTTP method enum (GET, POST, PUT, DELETE,
PATCH, HEAD, OPTIONS, CONNECT) ≈ 8 entries
path_template — open shape but bounded by route table (~15 templates;
see RESEARCH §Q11 ceiling=1000 for headroom)
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `path` is in ForbiddenLabels; passing the raw URL would panic at registration. The label name `path_template` is NOT forbidden — but values MUST come from `r.Pattern` (Go 1.22+ stdlib ServeMux), NEVER from `r.URL.Path`. The instrumentedMux wrapper in pkg/server/server.go is the SOLE call site (D-03 single chokepoint).
Tenant-flag forward-compat (CONTEXT D-08): `NewHTTPMetrics(reg, tenantLabelsEnabled bool)` decides label-set shape ONCE at construction. When false (Phase 4 default outside K8s), the `database` label is OMITTED from the labelnames slice — not set to empty string. When true (Phase 5 K8s autodetect default), `database` is included. Subsystems use the bool-agnostic `BindRequestDuration` / `BindRequests` helpers; they thread the database arg through but the helper drops it when the bag was constructed with tenantLabelsEnabled=false.
Package observability — Knowledge-policy metric bag.
Owns ten families (see docs/plans/knowledge-policy-observability-plan.md):
nornicdb_knowledge_policy_scored_total{entity_kind,result[, database]}
nornicdb_knowledge_policy_decay_score{entity_kind[, database]} (sampled 1/32)
nornicdb_knowledge_policy_suppressions_total{entity_kind,reason[, database]}
nornicdb_knowledge_policy_access_flush_batch_rows (no tenant label)
nornicdb_knowledge_policy_access_flush_duration_seconds (no tenant label)
nornicdb_knowledge_policy_access_flush_buffer_fullness (GaugeFunc; D-15b)
nornicdb_knowledge_policy_on_access_mutations_total{result[, database]}
nornicdb_knowledge_policy_deindex_enqueued_total{entity_kind[, database]}
nornicdb_knowledge_policy_read_filter_dropped_total{entity_kind[, database]}
nornicdb_knowledge_policy_reconcile_total{trigger[, database]}
Closed enums:
entity_kind ∈ AllowedKnowledgePolicyEntityKinds = {node, edge, property}
result ∈ AllowedKnowledgePolicyScoreResults = {visible, suppressed, no_decay}
reason ∈ AllowedKnowledgePolicySuppressReasons = {below_threshold, score_floor,
on_access, explicit_flag, rule_cap}
on_access_result ∈ AllowedKnowledgePolicyOnAccessResults = {applied, skipped_no_policy, error}
trigger ∈ AllowedKnowledgePolicyReconcileTriggers = {schema_change, startup, manual}
Forbidden-label discipline: the graph-schema node `label` axis and user property keys are DELIBERATELY excluded — user DDL can create unbounded values. Use exemplar trace attributes or structured logs for per-label detail, NEVER Prometheus labels.
Hot-path discipline (MET-25): `Scorer.score()` runs per node returned from any Cypher match when decay is enabled. The bag pre-binds the per- entity_kind counters and histogram at construction; the caller caches the Bound* observers in a struct field and calls Observe/Inc without a WithLabelValues lookup on the hot path.
Sampling: DecayScore is sampled 1/32 via ObserveDecayScoreSampled so a 1M-row scan pays only ~31k histogram Observe calls instead of 1M. Counter fires (ScoredTotal, SuppressionsTotal) are cheap enough to fire every time.
D-08 forward-compat: NewKnowledgePolicyMetrics(reg, tenantLabelsEnabled, bufferFullnessFn) decides label-set shape ONCE at construction. When tenantLabelsEnabled is false, the `database` label is OMITTED from all labelnames. Flush-level metrics (batch_rows, duration, buffer_fullness) are intentionally NOT tenant-scoped — the AccessFlusher is cross-namespace.
Package observability — MVCC metric bag (Plan 04-04 GREEN).
Owns four families per MET-11 + ADR §2.3:
nornicdb_mvcc_pressure_band{[database], band}
nornicdb_mvcc_pinned_bytes (GaugeFunc; D-15b)
nornicdb_mvcc_oldest_reader_age_seconds (GaugeFunc; D-15b)
nornicdb_mvcc_active_readers (GaugeFunc; D-15b)
Closed enums (CONTEXT D-14):
band ∈ AllowedMVCCBands = {normal, warn, high, critical}
D-14 thresholds (constants below):
ratio < 0.50 → normal 0.50 ≤ ratio < 0.75 → warn 0.75 ≤ ratio < 0.90 → high ratio ≥ 0.90 → critical
PressureBand is set indicator-style: the active band gauge holds value 1 while the other three bands hold value 0 for the same database. The indicator pattern lets dashboards do `max by (database) (... == 1)` to pick the current band; alert rules can fire on `nornicdb_mvcc_pressure_band{band="critical"} == 1`.
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `query`, `user`, `user_id`, `ip`, `uuid`, `email` are never used here; `band` is bounded by AllowedMVCCBands; `database` is bounded by the per-tenant-flag axis (D-08).
RISK-2 fixed (Plan 04-04-01): the three GaugeFunc callbacks read the MVCCProbe accessors (PinnedBytes / OldestReaderAgeSeconds / ActiveReaders) which now exist on *BadgerEngine. Each callback is wrapped in a defer-recover that returns 0 on panic per RESEARCH RISK-8 / Pitfall 1 — concurrent shutdown cannot poison the /metrics scrape.
D-02d leaf-package boundary: pkg/observability never imports pkg/storage. MVCCProbe is declared HERE; pkg/storage's *BadgerEngine satisfies it via the accessors in pkg/storage/badger_mvcc.go.
D-08 forward-compat: NewMVCCMetrics(reg, tenantLabelsEnabled, probe) decides whether the `database` label is included in pressure_band ONCE at construction. The three live-read gauges have no labels (single process-wide value) so they are tenant-flag-independent.
Package observability — minimal type stubs for catalog bags whose GREEN implementations land in Plans 04-03..04-06 (Cypher, Storage, MVCC, Embed, Search, Replication, Auth).
These stubs exist to keep pkg/observability compiling while the per-bag RED tests sit in their `t.Skip("RED: pending Plan 04-NN")` state (Plan 04-01 Wave-0 RED-first cadence). Each stub:
- Declares the struct fields the RED tests reference (e.g. `Bytes`, `IndexRebuild`, `LagBytes`, `AuthAttempts`, `FFIPanicTotal`).
- Provides a constructor that PANICS at runtime — the RED test's leading `t.Skip` ensures the constructor is never called from a running test, so the panic is purely a load-bearing guard against accidental production usage before the GREEN bag lands.
When Plan 04-NN ships its real `NewXxxMetrics` constructor + bag, that plan REPLACES the stub here with the production bag (split into a per-subsystem catalog_<sub>.go file per CONTEXT D-02a).
Plan ownership map:
NewCypherMetrics — Plan 04-03 (SHIPPED — see catalog_cypher.go)
NewStorageMetrics — Plan 04-04 (SHIPPED — see catalog_storage.go)
NewMVCCMetrics — Plan 04-04 (SHIPPED — see catalog_mvcc.go;
RISK-2 PinnedBytes accessor lives on
*BadgerEngine in pkg/storage/badger_mvcc.go)
NewEmbedMetrics — Plan 04-05 (SHIPPED — see catalog_embed.go)
NewSearchMetrics — Plan 04-05 (SHIPPED — see catalog_search.go)
NewReplicationMetrics — Plan 04-06 (with RISK-3 PeerConfig.ID + GAP-1
last_contact_seconds + per-mode cardinality)
NewAuthMetrics — Plan 04-06 (GAP-6 / MET-15)
Plan 04-02 (this plan) DELIVERS NewHTTPMetrics + NewBoltMetrics — those live in catalog_http.go and catalog_bolt.go respectively (NOT in this stubs file).
As of Plan 04-06 all bags have shipped — the stub bodies are empty and this file remains as a historical pointer to the per-subsystem catalog files (one entry per <plan ownership map> row).
Package observability — Replication metric bag (Plan 04-06 GREEN per MET-14 + GAP-1 + RISK-3 corrected).
Owns ten families per ADR §2.3 + CONTEXT §domain:
nornicdb_replication_role (Gauge int enum)
nornicdb_replication_term (Gauge int)
nornicdb_replication_commit_index (Gauge int)
nornicdb_replication_apply_index (Gauge int)
nornicdb_replication_lag_bytes{peer} (GaugeVec)
nornicdb_replication_lag_entries{peer} (GaugeVec)
nornicdb_replication_apply_duration_seconds (LatencyHistogram)
nornicdb_replication_rtt_seconds{peer} (HistogramVec)
nornicdb_replication_leader_changes_total (Counter)
nornicdb_replication_last_contact_seconds{peer} (GaugeVec — GAP-1)
**RISK-3 FIX (RESEARCH §RISK-3 verified):** pkg/replication/config.PeerConfig has fields {ID string; Addr string} — NOT `Name`. CONTEXT D-05 wording is obsolete. PeerLabel(p) returns `p.ID` if non-empty, falling back to `p.Addr`. NEVER reads from `conn.RemoteAddr()` — runtime sockets would explode cardinality.
**D-05a mode-aware ceiling:**
ha_standby: 8 (single peer + churn slack) raft: 16 (5-node typical quorum + churn slack) multi_region: 64 (3-region × ~5-peer + churn slack)
Stored in modeCeiling and accessible via Mode() / Ceiling() so the AssertCardinalityCeiling test parameterizes by mode.
**D-08a tenant-flag accepted but IGNORED at registration:** Replication is per-cluster, not per-database. `tenantLabelsEnabled` is part of the constructor signature so callers pass cfg uniformly across all subsystem bags, but the database label is never added to any replication family. Documented inline; verified by TestReplicationMetrics_*.
**D-15a per-event observation (zero polling):** pkg/replication.RaftReplicator + HAStandbyReplicator + MultiRegionReplicator call metrics.Role.Set(RoleEnum("leader")) at the SAME sites that emit the existing role-transition log line. No background goroutine polls state. Pre-bound observers cached in struct fields per MET-25.
**D-05b stale-peer GC (lifecycle.Component) lives in pkg/replication/peer_metrics_gc.go.** It calls DeleteLabelValues on the per-peer GaugeVecs after a configurable staleness threshold. Replicator rebinds on reconnect (Pitfall 3 mitigation per RESEARCH §Q7).
**D-02d leaf-package boundary:** pkg/observability never imports pkg/replication. The replication code imports pkg/observability and satisfies its peer-label inputs with PeerConfig (whose two-field shape {ID, Addr} is documented here for the test suite).
Package observability — Search metric bag (Plan 04-05 GREEN).
Owns four families per MET-13 + ADR §2.3:
nornicdb_search_requests_total{[database,] mode, result}
nornicdb_search_duration_seconds{[database,] mode, stage}
nornicdb_search_candidates (RowCountHistogram)
nornicdb_search_index_size_bytes{kind} (GaugeFunc; D-15b)
Closed enums (CONTEXT MET-13 / D-15b):
mode ∈ AllowedSearchModes = {vector, bm25, hybrid}
result ∈ AllowedSearchResults = {success, no_results, error}
stage ∈ AllowedSearchStages = {embed, index, fuse}
kind ∈ AllowedSearchIndexKinds = {hnsw, bm25}
stage is the keystone: per-stage observation lets SREs see whether embed (LLM call), index (vector / BM25 lookup), or fuse (RRF + rerank) dominates the latency budget. Closed enum at internal call sites prevents arbitrary stage-name labeling.
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `query` is the keystone — raw query text MUST NEVER be a label value. The registration.go panic catches anyone passing `query` as a label.
MET-25 hot path: per-stage observation uses pre-bound observers cached at the call site. The bag's BindDuration helper returns a BoundLatencyObserver that callers store in struct fields per MET-25.
D-15b GaugeFunc panic safety: the index_size_bytes callback per kind wraps defer-recover that returns 0 on probe panic — same pattern as Plan 04-04 storage.bytes / mvcc.pinned_bytes.
D-02d leaf-package boundary: pkg/observability never imports pkg/search. SearchProbe is declared HERE; pkg/search.Service satisfies it via thin adapter or a setter that AttachesMetrics.
D-08 forward-compat: NewSearchMetrics(reg, tenantLabelsEnabled, probe) decides whether the `database` label is included in requests_total + duration_seconds ONCE at construction. candidates and index_size_bytes are global — bytes per index kind, candidates per request distribution.
Package observability — Storage metric bag (Plan 04-04 GREEN).
Owns eight families per MET-10 + ADR §2.3:
nornicdb_storage_nodes_total (GaugeFunc; D-15b)
nornicdb_storage_edges_total (GaugeFunc; D-15b)
nornicdb_storage_bytes{kind}
nornicdb_storage_op_duration_seconds{op}
nornicdb_storage_compactions_total{level, result}
nornicdb_storage_compaction_duration_seconds{level}
nornicdb_storage_wal_lag_bytes
nornicdb_storage_index_rebuild_total{[database,] index, result}
Closed enums (CONTEXT D-07 / D-13c):
kind ∈ AllowedStorageBytesKinds = {nodes, edges, index, wal, search}
op ∈ AllowedStorageOps = {get, put, delete, scan}
index ∈ AllowedStorageIndexes = {label, edge_between, temporal,
embedding, user_created}
result ∈ AllowedStorageResults = {success, failure, aborted}
D-13c the `index` enum is the keystone of T-04-02 mitigation: arbitrary user-created index names NEVER become label values. The pure mapping function `pkg/storage.classifyIndexName(internal string) string` buckets any unknown name to `user_created`. RESEARCH §Q11 cardinality ceiling.
Forbidden-label discipline (Phase 3 D-03 / registration.go ForbiddenLabels): `path`, `query`, `user`, `user_id`, `ip`, `uuid`, `embedding_text`, `trace_id`, `span_id`, `email` would all be cardinality bombs and panic at registration. Storage hot paths never label by raw key bytes or full paths.
D-07 bytes{kind} is populated by a 30s lifecycle.Component sweep that calls `badger.DB.EstimateSize(prefix)` for each prefix. Sweep lives in pkg/storage/bytes_metrics.go (Plan 04-04-04). RESEARCH §Q3.
MET-25 hot path: per-op observation uses pre-bound BoundLatencyObserver cached in BadgerEngine struct fields at constructor time. The bag's OpDuration.Bind("get") is hoisted out of the request loop. CompactionDuration is bound at compaction-event hook attachment.
RISK-6 wal_lag_bytes semantics: best-effort heuristic estimate of WAL backlog (vlog size minus LSM size from `badger.DB.Size()`). Alerting SHOULD use 5-minute trends, not single scrapes. The metric is documented here and in the bytes_metrics.go sweeper that populates it.
D-08 forward-compat: NewStorageMetrics(reg, tenantLabelsEnabled, probe) decides whether the `database` label is included in IndexRebuildTotal ONCE at construction. The plan deliberately does NOT add database to op_duration (D-08a) — per-database op latency lives at the Cypher subsystem (Plan 04-03).
Package observability is NornicDB's telemetry seam: metrics (Prometheus + OTel), traces (OTLP), and the building blocks for /metrics, /livez, /readyz, /version, and opt-in pprof endpoints.
Architecture ¶
observability is a leaf package. It depends on stdlib, OpenTelemetry SDK, prometheus/client_golang, and pkg/lifecycle (one-way; pkg/lifecycle has zero observability deps). Business packages (cypher, storage, bolt, server) MUST NOT import this package directly — they receive instrumentation via accessors passed through dependency injection from cmd/nornicdb/main.go.
Wiring ¶
Construct a *Provider via:
prov, err := observability.New(ctx, cfg.Observability, observability.ServiceInfo{
Name: "nornicdb",
Version: buildinfo.Version(),
NodeID: cfg.NodeID,
})
New always succeeds at the Provider-construction level: a misconfigured or unreachable OTLP collector is reported via WARN log and a noop tracer provider is installed (OBS-11). Process startup never fails because of observability init failure.
Init order (OBS-03) ¶
New constructs in this order, mandated by the ADR-0001 §2.5 contract:
- logger (deferred to Phase 2 slog migration; Phase 1 uses stdlib log).
- resource attributes (semconv.ServiceName/Version/InstanceID).
- meter + tracer providers (real SDK or noop fallback).
- registries (the Prometheus registry and the OTel→Prom bridge).
- middleware + listeners (Plan 03 — listener.go, health.go, pprof.go).
Endpoint precedence (OBS-12) ¶
OTLP endpoint resolution honors:
OTEL_EXPORTER_OTLP_ENDPOINT > YAML tracing.endpoint > built-in default
See TracingConfig.OTLPEndpoint().
service.instance.id resolution (OBS-10) ¶
resolveInstanceID resolves through this chain:
cfg.NodeID → POD_NAME env → os.Hostname() → "standalone"
The resolved value and its source are logged once at startup.
Compliance boundary ¶
pkg/audit (Phase-2 untouched) is NOT migrated to OTel logs. The compliance audit trail keeps its existing retention/signing/serialization. This package only owns observability — operator-facing telemetry — not compliance.
Test isolation ¶
Each test owns a fresh *prometheus.Registry, an in-memory span exporter, and a discard logger. Never touch prometheus.DefaultRegisterer or slog.SetDefault in tests. The NewTestEnv helper that codifies this ships in Plan 03.
Package observability — typed exemplar-wrapper layer for histogram observation (Phase 3, MET-24 + MET-25).
Design (CONTEXT D-02):
- Wrappers (LatencyHistogram etc.) own the MET-24 exemplar-emission chokepoint. Subsystems use the wrapper for normal Observe paths; the raw *prometheus.HistogramVec from metrics.go remains accessible via Vec() for cardinality-ceiling assertions and edge cases.
- Bind(lvs ...) returns a Bound*Observer that subsystems cache as a struct field at construction time — eliminates per-Observe WithLabelValues lookup (MET-25 hot-path discipline).
- The IsValid()→ExemplarObserver chokepoint is a SINGLE function (observeWithExemplar) per AGENTS.md §7 DRY. Every wrapper Observe and every Bound Observe funnel through it.
Forward-compat (CONTEXT D-02a + Phase 2 D-05 precedent):
Phase 1 ships sdktrace.NeverSample() ⇒ every emitted SpanContext has IsSampled()=false ⇒ chokepoint short-circuits ⇒ ZERO exemplar-path allocations today (also true on the truly-empty noop SpanContext where IsValid()=false). Phase 6 flips the sampler in ONE place (provider.go); ALL Phase-3-wrapped histograms emit exemplars automatically — no second-pass migration of the ~60 Phase 4 metric families. Same posture as Phase 2's mandatory_fields trace-id resolution.
Allocation budget (CONTEXT D-02b, falsified by BenchmarkObserve_Hot):
hot_no_span sub-bench: ≤ 2 allocs/op (target: 0 — chokepoint short-circuits) hot_with_span sub-bench: ≤ 4 allocs/op (1 for Labels map, 2 for *.String() calls) If hot_with_span exceeds 4, D-02b1 escalation triggers: sync.Pool[*prometheus.Labels] — captured here so future plans don't re-derive.
Counter/gauge exemplars deferred (CONTEXT D-02e). Phase 3 ships histograms only.
Package observability — Phase 5 K8s autodetect.
k8sProbe inspects env + filesystem signals (KUBERNETES_SERVICE_HOST and /var/run/secrets/kubernetes.io/serviceaccount/token presence) to decide whether tenant labels should default ON. Conservative AND-signal logic per CONTEXT.md D-02. Pure-ish function: only stdlib os reads.
Phase 5 / Plan 05-03 fills the Detect + ResolveTenantLabels bodies.
Package observability — knowledge-policy metrics global ref.
The Badger read-path filter (pkg/storage/badger_decay_filter.go) fires on EVERY node returned from a Cypher MATCH when decay is enabled. Threading a *KnowledgePolicyMetrics handle through every storage iterator signature would widen CreateNode / GetNode / iterateNodesVisibleAtInTxn and their many callers. Instead we use the same atomic.Pointer bridge the BSP self- metrics pattern (bsp_self_metrics.go) already established for injecting observability into a deep-nested subsystem without inverting ownership.
Semantics:
- Set once at Provider init (from cmd/nornicdb/main.go, after NewKnowledgePolicyMetrics returns).
- Overwritten on each New() so per-test TestEnv isolation still works.
- Nil safe: GetKnowledgePolicyMetrics returns nil before Set is called or when metrics are disabled; all IncScored / IncSuppression / etc. helpers short-circuit on nil receiver.
Package observability — Phase 5 legacy translation layer.
RenderLegacy walks the unified pkg/observability registry and emits the 12 metric families that customer scrapers expect on :7474/metrics. Pure function: input *prometheus.Registry + time.Time, output []byte in Prometheus exposition format v0.0.4.
Phase 5 / Plan 05-02 fills the legacyMappings table function fields and the RenderLegacy body; Wave-0 (05-01) declared the public-API surface.
Package observability — typed metric constructors that enforce naming, bucket, and label discipline at registration time (Phase 3, MET-01..MET-05).
Design (CONTEXT D-01):
- Bucket choice is encoded in the constructor function name; subsystem authors cannot pass arbitrary Buckets. Single source of truth (D-01).
- Namespace="nornicdb" is injected by the helper; caller never sets it (D-01b/D-01c — final name is nornicdb_<subsystem>_<name>).
- Subsystem is enforced against the closed allowedSubsystems list (registration.go).
- Forbidden labels (cardinality bombs + PII) panic at registration (registration.go::validateLabels — D-03a).
Dual-access pattern (CONTEXT D-02 + <specifics>):
- These constructors return native *prometheus.HistogramVec / *prometheus.CounterVec / *prometheus.GaugeVec so subsystems can pre-bind via WithLabelValues for MET-25 hot-path discipline.
- The typed wrapper structs in exemplar.go (Plan 03-03) wrap the same *HistogramVec to centralize the MET-24 exemplar-emission chokepoint.
- Wrappers do NOT hide the raw types — they're parallel returns. Phase 4 subsystems receive both: the raw *Vec for cardinality assertions and edge cases, the wrapper for normal Observe() paths.
Pitfall 8 / MustRegister precedent (analog: registry.go:28-29): validation failure at construction is a programming bug; panic IS the desired startup behavior — surfaces the bug before any traffic reaches the binary.
Package observability — registration-time validation primitives for the metrics-helper layer (Phase 3, MET-01..MET-05). Each helper in metrics.go calls these primitives BEFORE invoking reg.MustRegister; a violation is a programming bug per Phase 1 Pitfall 8 / MustRegister precedent — panic IS the desired startup behavior.
Doc analog: pkg/observability/registry.go:28-29 (MustRegister panic-as-feature).
Index ¶
- Constants
- Variables
- func DefaultK8sProbe() k8sProbe
- func NewCounterVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.CounterVec
- func NewEmbeddingLatencyHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
- func NewGaugeVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.GaugeVec
- func NewLatencyHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
- func NewLogger(cfg LoggerConfig, info ServiceInfo) (*slog.Logger, io.Writer, error)
- func NewPprofListener(cfg PprofConfig) (*pprofListener, error)
- func NewRedactingSpanProcessor(inner sdktrace.SpanProcessor) sdktrace.SpanProcessor
- func NewRowCountHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
- func NewScoreHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
- func NewSizeHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
- func NewTelemetryListener(prov *Provider, health *Health) (*telemetryListener, error)
- func PeerLabel(p PeerConfigLike) string
- func RenderLegacy(reg *prometheus.Registry, now time.Time) []byte
- func ResolveAndLogTenantLabels(explicit *bool, logger *slog.Logger) bool
- func ResolveTenantLabels(explicit *bool, probe k8sProbe) (resolved bool, source string)
- func RoleEnum(role string) float64
- func SetKnowledgePolicyMetrics(m *KnowledgePolicyMetrics)
- type AuthMetrics
- type BoltMetrics
- type BoundEmbeddingLatencyObserver
- type BoundLatencyObserver
- type BoundRowCountObserver
- type BoundSizeObserver
- type CacheMetrics
- type CardinalityT
- type CheckFunc
- type CheckOpts
- type CheckStatus
- type CypherMetrics
- func (c *CypherMetrics) BindQueries(opType, database string) prometheus.Counter
- func (c *CypherMetrics) BindQueryDuration(opType, database string) BoundLatencyObserver
- func (c *CypherMetrics) BindSlowQueries(database string) prometheus.Counter
- func (c *CypherMetrics) BindTransactionConflicts(database string) prometheus.Counter
- func (c *CypherMetrics) ObserveQueryDuration(ctx context.Context, opType, database string, sec float64)
- func (c *CypherMetrics) TenantLabelsEnabled() bool
- type EmbedMetrics
- type EmbedProbe
- type EmbeddingLatencyHistogram
- type FilteredBaggagePropagator
- type HTTPMetrics
- func (h *HTTPMetrics) BindRequestDuration(method, template, statusClass, database string) BoundLatencyObserver
- func (h *HTTPMetrics) BindRequests(method, template, statusClass, database string) prometheus.Counter
- func (h *HTTPMetrics) ObserveRequestDuration(ctx context.Context, method, template, statusClass, database string, ...)
- func (h *HTTPMetrics) TenantLabelsEnabled() bool
- type Health
- type KnowledgePolicyMetrics
- func (k *KnowledgePolicyMetrics) IncDeindexEnqueued(entityKind, database string)
- func (k *KnowledgePolicyMetrics) IncOnAccess(result, database string)
- func (k *KnowledgePolicyMetrics) IncReadFilterDropped(entityKind, database string)
- func (k *KnowledgePolicyMetrics) IncReconcile(trigger, database string)
- func (k *KnowledgePolicyMetrics) IncScored(entityKind, result, database string)
- func (k *KnowledgePolicyMetrics) IncSuppression(entityKind, reason, database string)
- func (k *KnowledgePolicyMetrics) ObserveAccessFlushBatchRows(ctx context.Context, rows float64)
- func (k *KnowledgePolicyMetrics) ObserveAccessFlushDuration(ctx context.Context, sec float64)
- func (k *KnowledgePolicyMetrics) ObserveDecayScoreSampled(ctx context.Context, entityKind, database string, score float64)
- func (k *KnowledgePolicyMetrics) TenantLabelsEnabled() bool
- type LatencyHistogram
- type LoggerConfig
- type MVCCMetrics
- type MVCCProbe
- type MetricOpts
- type MetricsConfig
- type ObservabilityConfig
- type ParentMode
- type PeerConfigLike
- type PprofConfig
- type Provider
- func (p *Provider) Config() ObservabilityConfig
- func (p *Provider) InstanceID() string
- func (p *Provider) InstanceIDSource() string
- func (p *Provider) Logger() *slog.Logger
- func (p *Provider) MeterProvider() *sdkmetric.MeterProvider
- func (p *Provider) MetricsEnabled() bool
- func (p *Provider) Registry() *prometheus.Registry
- func (p *Provider) Shutdown(ctx context.Context) error
- func (p *Provider) TracerProvider() trace.TracerProvider
- type ReadyResult
- type ReplicationMetrics
- type RowCountHistogram
- type SearchMetrics
- type SearchProbe
- type ServiceInfo
- type SizeHistogram
- type StorageMetrics
- type StorageProbe
- type TestEnv
- type TracingConfig
Constants ¶
const ( MVCCBandWarn = 0.50 MVCCBandHigh = 0.75 MVCCBandCritical = 0.90 )
D-14 pressure-band thresholds. Ratio = PinnedBytes / MVCCBudgetBytes.
const ( ReasonExplicitYAML = "explicit_yaml" ReasonK8sDetected = "k8s_detected" ReasonServiceHostAbsent = "not_k8s_service_host_absent" ReasonTokenFileAbsent = "not_k8s_token_file_absent" ReasonTokenFileEmpty = "not_k8s_token_file_empty" ReasonTokenStatError = "not_k8s_token_stat_error" )
Reason* are the closed-set source strings logged at startup (D-02b).
const ( LegacySunset = "Fri, 31 Dec 2027 23:59:59 GMT" LegacyDeprecation = "true" LegacyContentType = "text/plain; version=0.0.4; charset=utf-8" )
Public-API contract bytes — frozen in Wave-0. Any change requires ADR amendment per CLAUDE.md "Public API contract — Metric names and span names are versioned. Deprecations require Sunset header overlap of one minor release minimum."
const DecayScoreSampleDenominator = 32
DecayScoreSampleDenominator is the 1/N sampling rate for the decay_score histogram. One sample in DecayScoreSampleDenominator is recorded; the rest are dropped. Keep power-of-two for cheap bitmask tests.
Variables ¶
var ( AllowedKnowledgePolicyEntityKinds = []string{ "node", "edge", "property", } AllowedKnowledgePolicyScoreResults = []string{ "visible", "suppressed", "no_decay", } AllowedKnowledgePolicySuppressReasons = []string{ "below_threshold", "score_floor", "on_access", "explicit_flag", "rule_cap", } AllowedKnowledgePolicyOnAccessResults = []string{ "applied", "skipped_no_policy", "error", } AllowedKnowledgePolicyReconcileTriggers = []string{ "schema_change", "startup", "manual", } )
Closed-enum values. Keep in sync with docs/plans/knowledge-policy- observability-plan.md and catalog_knowledge_policy_test.go.
var AllowedAuthProtocols = []string{"bolt", "http", "grpc"}
AllowedAuthProtocols is the closed enum for the `protocol` label. The label is set at the protocol-specific adapter chokepoint (HELLO handler for Bolt; HTTP middleware for HTTP; UnaryInterceptor for gRPC) — the shared core Authenticator is protocol-agnostic and does NOT increment.
var AllowedAuthResults = []string{"success", "failure", "denied"}
AllowedAuthResults is the closed enum for the `result` label. Mirrors the three semantic outcomes per CONTEXT D-05e:
- success: credentials validated; user identity established
- failure: credentials presented but rejected (bad password, expired token)
- denied: request rejected before credential evaluation (auth required but absent, unsupported scheme, account locked, role denied)
Adding a new result requires an ADR amendment AND callers must update classifyAuthResult in pkg/auth.
var AllowedBoltOps = []string{
"hello",
"run",
"pull",
"begin",
"commit",
"discard",
"reset",
"goodbye",
"route",
"ack_failure",
}
AllowedBoltOps is the closed enum for the `op` label per CONTEXT D-11a. Sourced from pkg/bolt/server.go MsgHello/MsgRun/... message-type constants. Adding a new Bolt message op = enum update + ADR §2.3 amendment.
var AllowedBoltResults = []string{"success", "error", "timeout"}
AllowedBoltResults is the closed enum for the `result` label per CONTEXT D-11. Mirrors the connection-close + message-dispatch outcome.
var AllowedCacheNames = []string{
"query_result",
"schema",
"label",
"node_lookup",
}
AllowedCacheNames is the closed enum for the `cache` label per CONTEXT D-12. Subsystem callers (cypher cache.go, schema/label/node_lookup callers) MUST pass only one of these values. Adding a new cache name requires a constants update here AND an ADR §2.3 amendment.
Mirrors Phase 3 D-01d allowedSubsystems cadence — closure-by-construction rather than runtime panic (the registration-time forbidden-label panic catches label NAMES; closed-value enforcement is a per-call discipline).
var AllowedCypherOpTypes = []string{
"read",
"write",
"schema",
"admin",
"fabric",
"parse_error",
}
AllowedCypherOpTypes is the closed enum for the `op_type` Cypher label per CONTEXT D-04 (corrected per RISK-1). Mirrors the QueryAnalyzer-derived classification + admin-dispatch + fabric-routing + parse-error sites.
Adding a new op_type = enum update + ADR §2.3 amendment + new test case in pkg/cypher/op_type_test.go::TestOpType_AllClauseShapes (D-04c table-driven).
var AllowedEmbedBackends = []string{"gpu", "cpu", "cuda", "metal", "vulkan"}
AllowedEmbedBackends is the closed enum for the `mode` label per CONTEXT D-06a. Mirrors the Embedder.Backend() return values declared in pkg/embed (build-tag matrix). Adding a new backend = enum update HERE + pkg/embed Backend() implementer + ADR §2.3 amendment.
var AllowedEmbedProviders = []string{"ollama", "openai", "local", "other"}
AllowedEmbedProviders is the closed enum for the `provider` label on processed_total + duration_seconds. Adding a provider = enum update + ADR amendment. The "other" bucket catches custom embedder factories.
var AllowedEmbedResults = []string{"success", "failure", "cached"}
AllowedEmbedResults is the closed enum for the `result` label on processed_total. Closed at the call site in pkg/embed wrappers (no user input flows here).
var AllowedEvictionReasons = []string{
"lru",
"ttl",
"capacity",
"manual",
}
AllowedEvictionReasons is the closed enum for the `reason` label on cache_evictions_total per CONTEXT D-12b.
var AllowedMVCCBands = []string{"normal", "warn", "high", "critical"}
AllowedMVCCBands is the closed enum for the `band` label per CONTEXT D-14. Adding a new band = enum update + ADR §2.3 amendment + threshold review.
var AllowedPackstreamReasons = []string{
"truncated",
"invalid_marker",
"wrong_type",
"oversize",
}
AllowedPackstreamReasons is the closed enum for the `reason` label per CONTEXT D-11c. Free-form `err.Error()` strings would be a cardinality bomb; the four-value enum is enforced by `reasonFromError(err)` at the decode boundary in pkg/bolt/packstream.go.
var AllowedReplicationModes = []string{"standalone", "ha_standby", "raft", "multi_region"}
AllowedReplicationModes is the closed enum of replication modes. Mirrors pkg/replication.ReplicationMode constants. `standalone` is permitted at the bag level but the standalone replicator never observes (see pkg/replication.StandaloneReplicator — zero-overhead noop).
var AllowedReplicationRoles = []string{"follower", "candidate", "leader", "standby"}
AllowedReplicationRoles is the closed enum for the role gauge value lookup. The role is published as a NUMERIC enum (Gauge.Set), not a label, because per-role series would either explode cardinality or produce stale "follower=0 / leader=1" series across transitions.
var AllowedSearchIndexKinds = []string{"hnsw", "bm25"}
AllowedSearchIndexKinds is the closed enum for the `kind` label on index_size_bytes per CONTEXT MET-13 / D-15b. Two index kinds:
- hnsw: vector index (HNSW or IVF-HNSW or IVFPQ — all vector indexes bucket here from a capacity-planning perspective)
- bm25: full-text index
var AllowedSearchModes = []string{"vector", "bm25", "hybrid"}
AllowedSearchModes is the closed enum for the `mode` label per CONTEXT MET-13. Mirrors the three search code paths in pkg/search/search.go: vectorSearchOnly (vector), fullTextSearchOnly (bm25), rrfHybridSearch (hybrid). Adding a mode = enum update + ADR §2.3 amendment.
var AllowedSearchResults = []string{"success", "no_results", "error"}
AllowedSearchResults is the closed enum for the `result` label on requests_total. no_results is distinct from error — an empty result set is a successful search; error reflects a pipeline failure.
var AllowedSearchStages = []string{"embed", "index", "fuse"}
AllowedSearchStages is the closed enum for the `stage` label on duration_seconds per CONTEXT MET-13. Three pipeline stages:
- embed: text → vector (the embedder call)
- index: vector / BM25 lookup
- fuse: RRF + rerank + MMR
Closed at the call site (string literal at each observation point).
var AllowedStatusClasses = []string{"1xx", "2xx", "3xx", "4xx", "5xx"}
AllowedStatusClasses is the closed enum for the `status_class` HTTP label. Mirrors the standard 1xx-5xx HTTP response class buckets per ADR §2.3.
var AllowedStorageBytesKinds = []string{"nodes", "edges", "index", "wal", "search"}
AllowedStorageBytesKinds is the closed enum for the `kind` label per CONTEXT D-07. Adding a new bucket = enum update + ADR §2.3 amendment + new sweep entry in pkg/storage/bytes_metrics.go.
var AllowedStorageIndexes = []string{"label", "edge_between", "temporal", "embedding", "user_created"}
AllowedStorageIndexes is the closed enum for the `index` label per CONTEXT D-13c. The `user_created` bucket catches arbitrary user index names — the pure classifier function `pkg/storage.classifyIndexName` maps unknown names here. Cardinality bounded at 5.
var AllowedStorageOps = []string{"get", "put", "delete", "scan"}
AllowedStorageOps is the closed enum for the `op` label per CONTEXT D-07. Mirrors the four storage-engine chokepoints: GetNode/GetEdge (get), CreateNode/CreateEdge (put), DeleteNode/DeleteEdge (delete), iterator scans (scan). Hot-path observation uses pre-bound observers (MET-25).
var AllowedStorageResults = []string{"success", "failure", "aborted"}
AllowedStorageResults is the closed enum for the `result` label on compactions_total and index_rebuild_total. Note: NOT used on op_duration_seconds (per D-16, op_duration omits result; conflict classification lives at Cypher subsystem).
var BaggageAllowList = map[string]bool{}
BaggageAllowList is the set of baggage keys forwarded through the system (SEC-02). Default is empty — all unknown baggage keys are dropped at the HTTP edge. Operators configure allowed keys via NORNICDB_BAGGAGE_ALLOW_LIST.
var DefaultRedactKeys = []string{
"password", "token", "authorization", "secret", "api_key", "credentials",
}
DefaultRedactKeys is the canonical sensitive-key allow-list for slog records. Keys are matched case-insensitively, regardless of slog.Group nesting depth. Extend at runtime via NORNICDB_LOG_REDACT_EXTRA (comma-separated).
Note: pkg/audit/audit.go owns its own signed audit stream and is intentionally NOT a consumer of this list (LOG-10 / SEC-05). A future leaf package pkg/redaction/ can canonicalize the constants if audit ever grows its own redaction needs — that's a post-M1 consolidation, out of Phase 2 scope.
`credentials` is included specifically to catch Bolt HELLO auth tokens (D-03a) — the protocol's own field name.
var EmbeddingLatencyBucketsSeconds = []float64{
.001, .01, .1, .5, 1, 5, 10, 30, 60, 120, 300, 600,
}
EmbeddingLatencyBucketsSeconds covers ~1ms to 600s for LLM/local embedding calls. Long-tail (last bucket ≥ 60s) per MET-05 — verified by TestEmbeddingLatency_UsesLongTailBuckets. Used by NewEmbeddingLatencyHistogramVec (MET-05).
var ForbiddenLabels = []string{
"path",
"query",
"user",
"user_id",
"ip",
"uuid",
"embedding_text",
"trace_id",
"span_id",
"email",
}
ForbiddenLabels enumerates label names rejected at registration time. Case-insensitive match. Mirrors REQ MET-04 + ADR §2.2 verbatim (D-03a).
Mutating this list requires an ADR amendment — these label names represent either cardinality bombs (full HTTP path, raw Cypher text, UUIDs, IPs) or PII (user, user_id, email) or values that belong elsewhere in the exposition format (trace_id and span_id belong in exemplars, never as labels — see exemplar.go).
Falsifiability: TestForbiddenLabels_PanicsAtRegistration (60 cases: 10 labels × 6 helper constructors) + TestForbiddenLabels_CaseInsensitive + TestForbiddenLabels_AllTenEntriesPresent.
var LatencyBucketsSeconds = []float64{
.0001, .0005, .001, .005, .01, .05, .1, .5, 1, 5, 10,
}
LatencyBucketsSeconds covers ~100us to 10s for request-latency histograms. Used by NewLatencyHistogramVec (MET-03).
var RowCountBuckets = []float64{
1, 10, 100, 1000, 10000, 100000, 1000000,
}
RowCountBuckets covers 1 to 1M for query result-set sizes. Used by NewRowCountHistogramVec (MET-03).
var ScoreBuckets = []float64{
0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 1.0,
}
ScoreBuckets covers 0.0..1.0 for knowledge-policy decay / promotion score histograms. Ten equally-spaced buckets catch the bimodal fresh/stale distribution operators look for when validating half-life choices. Used by NewScoreHistogramVec (knowledge_policy subsystem).
var SensitiveKeys = map[string]bool{ "auth.token": true, "auth.password": true, "auth.credentials": true, "db.password": true, "bolt.auth_token": true, "http.authorization": true, "user.password": true, "user.token": true, "credentials": true, "password": true, "secret": true, "api_key": true, "access_token": true, "refresh_token": true, "session_token": true, "private_key": true, }
SensitiveKeys is the set of span attribute keys that must be redacted before export (SEC-01). Shared with pkg/audit via this exported variable so the redaction list has a single source of truth.
var SizeBucketsBytes = []float64{
64, 256, 1024, 4096, 16384, 65536,
262144, 1048576, 4194304, 16777216, 67108864,
}
SizeBucketsBytes covers 64B to 64MB for payload-size histograms. Used by NewSizeHistogramVec (MET-03).
Functions ¶
func DefaultK8sProbe ¶
func DefaultK8sProbe() k8sProbe
DefaultK8sProbe returns a probe wired to the live OS reads.
func NewCounterVec ¶
func NewCounterVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.CounterVec
NewCounterVec constructs a counter on reg. Same validation + Pitfall-8 panic semantics. Suffix: _total (MET-02).
func NewEmbeddingLatencyHistogramVec ¶
func NewEmbeddingLatencyHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
NewEmbeddingLatencyHistogramVec constructs a long-tail latency histogram for LLM / local embedding calls (MET-05). Same validation + Pitfall-8 panic semantics. Buckets: EmbeddingLatencyBucketsSeconds (tail ≥ 60s). Suffix: _seconds.
func NewGaugeVec ¶
func NewGaugeVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.GaugeVec
NewGaugeVec constructs a gauge on reg. No suffix validation — ADR §2.2 does not lock a single gauge suffix (gauges may be unitless like _ratio, sized like _bytes, count like _count — context-specific). Subsystem and label discipline still enforced.
func NewLatencyHistogramVec ¶
func NewLatencyHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
NewLatencyHistogramVec constructs a request-latency histogram on reg. Validates Subsystem/Name/labels/Help per D-01a (panics on violation — programming bug per Pitfall 8). Returns the raw *prometheus.HistogramVec for hot-path WithLabelValues pre-binding (MET-25); see exemplar.go for the typed LatencyHistogram wrapper that centralizes exemplar emission.
Final metric name: nornicdb_<opts.Subsystem>_<opts.Name>. Buckets locked to LatencyBucketsSeconds (MET-03 single source of truth). Suffix locked to _seconds (MET-02).
func NewLogger ¶
func NewLogger(cfg LoggerConfig, info ServiceInfo) (*slog.Logger, io.Writer, error)
NewLogger constructs the production *slog.Logger with the 4-layer handler stack per D-02a (outermost → innermost):
recoveringHandler (D-09: catches panics in any inner layer)
└─ mandatoryFieldsHandler (D-05: service/version/node_id + trace ctx)
└─ redactingHandler (D-03/D-03b: PII allow-list + CRLF strip)
└─ nornicdbJSONHandler (D-02: ≤2 allocs/record)
Returns:
- *slog.Logger: the assembled logger; never nil even on error.
- io.Writer: the underlying writer (file/stderr/stdout) so the caller can stash it in Provider.writerRef for D-09a opportunistic Sync().
- error: non-nil if cfg.Output points at an unopenable path. The logger remains usable (writes to stderr) so the process keeps running — OBS-11 fail-closed analog.
Per D-08 bootstrap order: cmd/nornicdb MUST call NewLogger BEFORE observability.New so the *slog.Logger can be threaded through the Provider construction.
Per Pitfall 10: the LevelVar is allocated as a pointer (`&slog.LevelVar{}`) so the handler holds a stable address that survives the function return.
func NewPprofListener ¶
func NewPprofListener(cfg PprofConfig) (*pprofListener, error)
NewPprofListener returns the optional :9091 listener.
Returns (nil, nil) — NOT an error — when cfg.Enabled is false. The caller (Plan 04 main.go) skips registration as a Component in that case, so pprof imposes zero runtime cost when disabled.
Default Listen is "127.0.0.1:9091" per ADR §A9; ":9091" or "0.0.0.0:9091" only if the operator explicitly overrides via NORNICDB_PPROF_LISTEN. The /debug/pprof/* handlers expose goroutine stacks and heap data; a non-loopback default would be a security bug.
Handlers are registered EXPLICITLY on a custom mux. We do NOT use the blank/underscore side-effect import form because that registers on http.DefaultServeMux, which we deliberately avoid for isolation. A dedicated mux also prevents accidental cross-pollution if some unrelated import elsewhere ever registers something on DefaultServeMux.
func NewRedactingSpanProcessor ¶
func NewRedactingSpanProcessor(inner sdktrace.SpanProcessor) sdktrace.SpanProcessor
NewRedactingSpanProcessor wraps inner with attribute redaction (SEC-01).
func NewRowCountHistogramVec ¶
func NewRowCountHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
NewRowCountHistogramVec constructs a result-set-size histogram on reg. Same validation + Pitfall-8 panic semantics. Buckets: RowCountBuckets. Suffix: _rows (MET-02).
func NewScoreHistogramVec ¶
func NewScoreHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
NewScoreHistogramVec constructs a 0.0..1.0 score histogram on reg for knowledge-policy decay / promotion score distributions. Same validation + Pitfall-8 panic semantics. Buckets: ScoreBuckets. Suffix: MUST end in _score (unitless score families follow ADR §2.2's "_score" convention).
func NewSizeHistogramVec ¶
func NewSizeHistogramVec(reg *prometheus.Registry, opts MetricOpts, labels []string) *prometheus.HistogramVec
NewSizeHistogramVec constructs a payload-size histogram on reg. Same validation + Pitfall-8 panic semantics as NewLatencyHistogramVec. Buckets: SizeBucketsBytes. Suffix: _bytes (MET-02).
func NewTelemetryListener ¶
NewTelemetryListener builds the :9090 listener.
The net.Listener is opened in this constructor so that bind failures (EADDRINUSE) surface during observability.New rather than asynchronously inside the supervisor goroutine — matching Pattern 6 / Plan-02 idioms.
/metrics is registered ONLY when prov.MetricsEnabled() && prov.Registry() is non-nil (OBS-04). When metrics are disabled, the route is not registered and the mux falls through to a 404 — operators can still hit /livez, /readyz, /version.
func PeerLabel ¶
func PeerLabel(p PeerConfigLike) string
PeerLabel returns the stable peer label value per RISK-3 (corrected). The two-field PeerConfigLike interface is the leaf-package boundary indirection (D-02d) — pkg/replication/config.PeerConfig satisfies it via its actual {ID, Addr} fields.
Resolution order:
- ID (non-empty) — UUID-stable across restarts; preferred.
- Addr (fallback) — stable across reconnects to the same configured peer.
- "" (empty) — allowed but a sign of misconfiguration; tests flag.
**NEVER** read from runtime sockets (`conn.RemoteAddr()`). That would produce a fresh label every reconnect and explode cardinality — exactly the RISK-3 failure mode the corrected wiring prevents.
func RenderLegacy ¶
func RenderLegacy(reg *prometheus.Registry, now time.Time) []byte
RenderLegacy walks reg.Gather() once, indexes families by name, and emits the 12 legacy metric families in lexicographic LegacyName order (D-01d) using Prometheus exposition format v0.0.4. Returns nil-safe empty buffer when reg is nil; tolerates partial-state Gather() errors (RESEARCH Pitfall 2).
The now parameter is reserved for future relative-timestamp emission (CONTEXT D-01 future-proofs the API); currently unused.
func ResolveAndLogTenantLabels ¶
ResolveAndLogTenantLabels is the cmd-level convenience wrapper used at startup to resolve the tenant-labels-enabled bool AND emit the single MET-22 forensic log line in one call. It:
- Constructs the production K8s probe (DefaultK8sProbe).
- Resolves the bool via ResolveTenantLabels(explicit, probe).
- Re-derives the two boolean signal flags from the same probe inputs so the log line documents what was actually checked (no duplicate AND-logic).
- Emits exactly one slog INFO record via the supplied logger with the four canonical fields: enabled, reason, service_host_present, token_file_present.
LOG-09 compliance: the caller must inject the *slog.Logger — this helper never touches slog.Default() / slog.SetDefault(). cmd/nornicdb/main.go passes the same Phase 2 D-08 logger that flows to pkg/server / pkg/bolt.
Returns the resolved bool which the caller writes into cfg.Observability.Metrics.TenantLabelsEnabled before any Phase 4 bag constructor reads it.
func ResolveTenantLabels ¶
ResolveTenantLabels enforces D-02a precedence:
explicit YAML (TenantLabelsExplicit *bool, R-02) > K8s autodetect > default false.
When explicit is non-nil the autodetect probe is short-circuited and the operator's intent wins (returning ReasonExplicitYAML). When explicit is nil the probe's Detect outcome and reason are returned verbatim.
func RoleEnum ¶
RoleEnum maps a role string to a numeric gauge value. Closed mapping — any caller passing an unknown role gets -1 so the gauge value is distinguishable from a legitimate role state during debugging.
Mapping per CONTEXT §domain:
-1 = unknown (defensive — never set in production) 0 = follower 1 = candidate 2 = leader 3 = standby (HA mode)
The numeric values are the WIRE contract for alert rules — flipping them is a breaking change requiring an ADR amendment. Operators write rules like `nornicdb_replication_role == 2` for "is leader".
func SetKnowledgePolicyMetrics ¶
func SetKnowledgePolicyMetrics(m *KnowledgePolicyMetrics)
SetKnowledgePolicyMetrics publishes the active metrics handle for the read-path filter to consume. Safe to call multiple times; the last call wins. Pass nil to tear down (used in tests that reset global state).
Types ¶
type AuthMetrics ¶
type AuthMetrics struct {
// AuthAttempts counts authentication attempts by result and protocol.
// Cardinality ceiling = 9 (RESEARCH §Q11). Closed enums enforced by the
// call sites; the registration helper rejects any attempt to add `user`,
// `user_id`, `email`, or `ip` as a label (Phase 3 D-03a).
AuthAttempts *prometheus.CounterVec
}
AuthMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the Auth subsystem. One bag per Provider, constructed at cmd/nornicdb startup and injected into pkg/bolt and pkg/auth. Bolt's HELLO call site nil-checks before observing (Plan 04-02 contract); this plan supplies the non-nil bag.
Dual-access pattern (D-02): the raw *prometheus.CounterVec is exposed so subsystem tests can drive AssertCardinalityCeiling directly. The hot path (Inc) does not need a wrapper — counters do not emit exemplars in M1 (CONTEXT D-02e defers counter exemplars).
func NewAuthMetrics ¶
func NewAuthMetrics(reg *prometheus.Registry) *AuthMetrics
NewAuthMetrics constructs the auth bag against reg.
No tenant flag — auth events are global per CONTEXT MET-21. Surfacing a `database` label on an unauthenticated counter would leak tenant identity at the K8s scrape boundary; the auth subsystem deliberately omits it.
Validation chain inherited from pkg/observability.NewCounterVec:
- subsystem "auth" must be in allowedSubsystems (metrics.go line 59)
- name must end in _total (registration.go validateNameSuffix)
- labels must NOT include any ForbiddenLabel (registration.go validateLabels)
Pitfall 8 / MustRegister precedent: validation failure panics — programming bug surfaces at startup before any traffic.
type BoltMetrics ¶
type BoltMetrics struct {
// ConnectionsActive is the live connection count gauge; Inc on accept,
// Dec on close (deferred). Single value, no labels (cardinality=1).
ConnectionsActive prometheus.Gauge
// ConnectionsTotal counts connection terminations by result.
// Cardinality ceiling = 3 (RESEARCH §Q11; len(AllowedBoltResults)).
ConnectionsTotal *prometheus.CounterVec
// SessionDuration histograms the wall-clock lifetime of each Bolt session
// (accept → close). Phase-3-locked LatencyBucketsSeconds. No labels.
SessionDuration *LatencyHistogram
// MessagesTotal counts dispatched Bolt messages by op + result.
// Cardinality ceiling = 30 (10 ops × 3 results; AllowedBoltOps ×
// AllowedBoltResults — RESEARCH §Q11).
MessagesTotal *prometheus.CounterVec
// MessageDuration histograms the per-message dispatch duration.
// Cardinality ceiling = 10 (len(AllowedBoltOps)).
MessageDuration *LatencyHistogram
// PackstreamDecodeErrors counts decode failures classified by closed
// reason enum. Cardinality ceiling = 4 (len(AllowedPackstreamReasons);
// RESEARCH §Q11). Free-form `err.Error()` MUST NEVER reach this Vec —
// classification happens via `reasonFromError(err)` at the decode
// boundary.
PackstreamDecodeErrors *prometheus.CounterVec
}
BoltMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the Bolt subsystem. One bag per Provider; constructed at cmd/nornicdb startup and injected into pkg/bolt.Server via SetBoltMetrics(...) so the connection- accept goroutine and per-message dispatch loop observe through pre-bound observers (MET-25).
Hot-path discipline (MET-25): the Bolt server pre-builds a per-op `[]BoundLatencyObserver` indexed by AllowedBoltOps so the dispatch loop pays zero WithLabelValues overhead per message. The `BindMessageDuration(op)` helper amortizes the lookup at session construction (the per-message observe call uses the cached observer).
func NewBoltMetrics ¶
func NewBoltMetrics(reg *prometheus.Registry) *BoltMetrics
NewBoltMetrics constructs the Bolt bag against reg.
Validation + Pitfall-8 panic semantics inherited from Phase 3 typed constructors: missing _total/_seconds suffixes or forbidden labels panic at registration.
Construction is idempotent against this bag's six families ONLY for a fresh registry — re-constructing on the same registry triggers AlreadyRegisteredError per Pitfall 8.
func (*BoltMetrics) BindMessageDuration ¶
func (b *BoltMetrics) BindMessageDuration(op string) BoundLatencyObserver
BindMessageDuration returns a pre-bound BoundLatencyObserver for the given op. Used by pkg/bolt.Server to pre-build a per-op slice at session construction (MET-25 hot-path discipline) so the dispatch loop pays zero WithLabelValues overhead per message.
type BoundEmbeddingLatencyObserver ¶
type BoundEmbeddingLatencyObserver struct {
// contains filtered or unexported fields
}
BoundEmbeddingLatencyObserver is the pre-bound hot-path observer for EmbeddingLatencyHistogram.
type BoundLatencyObserver ¶
type BoundLatencyObserver struct {
// contains filtered or unexported fields
}
BoundLatencyObserver is the struct-field-cacheable hot-path observer. Stateless beyond the embedded prometheus.Observer — concurrent Observe calls are race-clean per client_golang HistogramVec promise (RESEARCH §1/§4).
func (BoundLatencyObserver) Observe ¶
func (b BoundLatencyObserver) Observe(ctx context.Context, sec float64)
Observe routes through the sampled-span ExemplarObserver chokepoint (D-02a). On Phase 1 NeverSample default ⇒ IsSampled()=false ⇒ zero exemplar overhead. On Phase 6 sampler flip ⇒ exemplars emit automatically.
type BoundRowCountObserver ¶
type BoundRowCountObserver struct {
// contains filtered or unexported fields
}
BoundRowCountObserver is the pre-bound hot-path observer for RowCountHistogram.
type BoundSizeObserver ¶
type BoundSizeObserver struct {
// contains filtered or unexported fields
}
BoundSizeObserver is the pre-bound hot-path observer for SizeHistogram.
type CacheMetrics ¶
type CacheMetrics struct {
// Hits / Misses / SizeBytes use the {cache} label set; closed enum per
// AllowedCacheNames.
Hits *prometheus.CounterVec
Misses *prometheus.CounterVec
SizeBytes *prometheus.GaugeVec
// Evictions uses the {cache, reason} label set; closed enums per
// AllowedCacheNames × AllowedEvictionReasons (cardinality ≤ 16).
Evictions *prometheus.CounterVec
// contains filtered or unexported fields
}
CacheMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the Cache + Runtime subsystem. One bag per Provider; constructed once at cmd/nornicdb startup between Phase 1's "registries" and "listeners" init steps (D-02c). Subsystems receive the bag via DI and call `bag.Hits.WithLabelValues("query_result").Inc()` etc.
Hot-path discipline (MET-25): subsystem callers SHOULD pre-bind via `bag.Hits.WithLabelValues("query_result")` once at construction and cache the resulting prometheus.Counter in a struct field — Phase 3's BenchmarkObserve_Hot template applies (see exemplar_bench_test.go).
func NewCacheMetrics ¶
func NewCacheMetrics(reg *prometheus.Registry) *CacheMetrics
NewCacheMetrics constructs the Cache + Runtime bag against reg.
Validation + Pitfall-8 panic semantics inherited from Phase 3 typed constructors: invalid subsystem names, missing _total/_bytes suffixes, or forbidden labels panic at registration. process_uptime_seconds and build_info register directly via prometheus.NewGaugeFunc (Phase 3's MetricOpts intentionally omits ConstLabels — CONTEXT D-13a uses GaugeFunc to side-step the omission).
Construction is idempotent against this bag's six families ONLY for a fresh registry — re-constructing on the same registry triggers AlreadyRegisteredError per Pitfall 8.
type CardinalityT ¶
type CardinalityT interface {
Helper()
Errorf(format string, args ...interface{})
FailNow()
}
CardinalityT is the minimal *testing.T-shaped surface that AssertCardinalityCeiling consumes. *testing.T satisfies it transparently; negative-falsifiability sub-tests can plug in an in-package fake that captures Errorf/FailNow without propagating failure into the parent *testing.T (Go's c.Fail() unconditionally walks c.parent.Fail() — there is no way to scope a *testing.T failure to a sub-test alone).
Mirrors github.com/stretchr/testify/require.TestingT plus Helper(); kept local so we don't take a public dependency on require's internal type.
type CheckFunc ¶
CheckFunc is the readiness probe contract.
Callers (storage, search, replication, ...) inject implementations from the composition root (cmd/nornicdb/main.go). This keeps pkg/observability a leaf in the import graph (OBS-01) — the registry holds func values, never types from business packages.
Implementations MUST be safe for concurrent use and MUST honor ctx cancellation; the registry runs checks in parallel and bounds each one with a per-check timeout.
type CheckOpts ¶
type CheckOpts struct {
// Required: when true, a failing check flips ReadyResult.OK to false (and
// the /readyz handler returns 503). When false (the default), the failure
// still appears in the JSON response but the overall status stays OK —
// useful for informational probes (downstream service health, warmup
// progress, etc.) that should not block kubelet rollouts.
Required bool
// Timeout is the per-check budget. Default 1s. A check that exceeds its
// budget reports the deadline error and Ready returns promptly.
Timeout time.Duration
}
CheckOpts configures a single registered check.
type CheckStatus ¶
type CheckStatus struct {
OK bool `json:"ok"`
Latency int64 `json:"latency_ms"`
Error string `json:"error,omitempty"`
}
CheckStatus is one entry in ReadyResult.Checks.
The JSON contract is intentionally narrow: ok, latency_ms, and an optional error string. Phase 1 deliberately omits the `progress` field that K8S-06 will add — operators reading Phase-1 /readyz must not see it; tests (TestHealth_RenderJSON_HasNoProgressFieldInPhase1) enforce the omission.
type CypherMetrics ¶
type CypherMetrics struct {
// Queries is the per-op_type query-count counter; same label-set as
// QueryDuration so the `_total` and `_seconds_count` series have
// identical cardinality.
Queries *prometheus.CounterVec
// QueryDuration is a latency histogram with Phase-3-locked buckets
// (LatencyBucketsSeconds: ~100us to 10s). Labels: {op_type[, database]}.
QueryDuration *LatencyHistogram
// PlannerDuration is the latency histogram for the planning step
// (parse + plan + cache lookup); same buckets as QueryDuration.
// Labels: {op_type} only — planner cost is the same regardless of
// tenant. Tenant-flag-independent surface.
PlannerDuration *LatencyHistogram
// PlannerCacheHits / PlannerCacheMisses are the planner-specific
// cache counters per D-12a (Cypher subsystem owns these — NOT the
// cross-cutting cache subsystem).
PlannerCacheHits *prometheus.CounterVec
PlannerCacheMisses *prometheus.CounterVec
// PlannerCacheSize is the gauge tracking current planner cache
// occupancy. Set by pkg/cypher/cache.go on Get/Put/Evict.
PlannerCacheSize prometheus.Gauge
// RowsReturned is the result-set-size histogram with Phase-3-locked
// buckets (RowCountBuckets: 1 to 1M). Labels: {op_type}.
RowsReturned *RowCountHistogram
// ActiveTransactions is the gauge of currently-open Cypher
// transactions. Inc'd at transaction Begin; Dec'd at Commit/Rollback.
ActiveTransactions prometheus.Gauge
// TransactionConflicts counts ErrConflict events surfaced from the
// storage layer (D-16: storage detects, Cypher counts — preserves
// AGENTS.md §8 separation). Labels: {[database]}.
TransactionConflicts *prometheus.CounterVec
// SlowQueries counts queries that exceeded the slow-query threshold
// (matches the Phase 2 D-04c slow-query log emission gate).
// Labels: {[database]}.
SlowQueries *prometheus.CounterVec
// contains filtered or unexported fields
}
CypherMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the Cypher subsystem. One bag per Provider; constructed at cmd/nornicdb startup between Phase 1's "registries" and "listeners" init steps (D-02c).
Hot-path discipline (MET-25): the Cypher executor (pkg/cypher/executor.go) is the SOLE call site; it threads (op_type[, database]) through the `BindQueryDuration` / `BindQueries` helpers which return tenant-flag- agnostic Bound observers. StorageExecutor caches BoundLatencyObserver in struct fields for the high-frequency op_types (read, write).
Dual-access pattern (D-02): the bag exposes the raw *prometheus.CounterVec and Phase-3 typed *LatencyHistogram / *RowCountHistogram so subsystem tests can drive AssertCardinalityCeiling and edge cases.
MET-26 slow_query_threshold_seconds is NOT a struct field — it's a GaugeFunc registered directly on reg. The callback (slowQueryThresholdFn) reads cfg.Logging.SlowQueryThreshold().Seconds() on every scrape so config reload flows through automatically (D-15b). RISK-8 / Pitfall 1: callback wraps a defer-recover that returns 0 on panic.
func NewCypherMetrics ¶
func NewCypherMetrics(reg *prometheus.Registry, tenantLabelsEnabled bool, slowQueryThresholdFn func() float64) *CypherMetrics
NewCypherMetrics constructs the Cypher bag against reg.
tenantLabelsEnabled is the D-08 forward-compat hook. When true, the `database` label is included in Queries, QueryDuration, TransactionConflicts, SlowQueries; when false, it is omitted. Phase 5's K8s autodetect (MET-22) decides the value.
slowQueryThresholdFn is the D-15b live-read callback for the slow_query_threshold_seconds GaugeFunc — typically reads cfg.Logging.SlowQueryThreshold().Seconds(). The callback is wrapped in a defer-recover that returns 0 on panic (RESEARCH RISK-8 / Pitfall 1) so a buggy callback cannot poison the entire /metrics scrape.
Validation + Pitfall-8 panic semantics inherited from Phase 3 typed constructors: missing _total/_seconds/_rows suffixes or forbidden labels (e.g. accidentally passing "query" as a label) panic at registration.
func (*CypherMetrics) BindQueries ¶
func (c *CypherMetrics) BindQueries(opType, database string) prometheus.Counter
BindQueries returns a pre-bound prometheus.Counter for the (opType, database) tuple. Tenant-flag-aware per BindQueryDuration above.
func (*CypherMetrics) BindQueryDuration ¶
func (c *CypherMetrics) BindQueryDuration(opType, database string) BoundLatencyObserver
BindQueryDuration returns a pre-bound BoundLatencyObserver for the (opType, database) tuple. When the bag was constructed with tenantLabelsEnabled=false, the database arg is dropped. Subsystems are bool-agnostic and pass database unconditionally.
Hot-path discipline (MET-25): subsystem callers SHOULD hoist the Bind call out of the request loop (cache the BoundLatencyObserver in a struct field at construction). Per-call BindQueryDuration still pays a WithLabelValues lookup.
func (*CypherMetrics) BindSlowQueries ¶
func (c *CypherMetrics) BindSlowQueries(database string) prometheus.Counter
BindSlowQueries returns a pre-bound prometheus.Counter for the (database) tuple — emitted at the Phase 2 D-04c slow-query gate alongside the existing slow-query log record. Tenant-flag-aware.
func (*CypherMetrics) BindTransactionConflicts ¶
func (c *CypherMetrics) BindTransactionConflicts(database string) prometheus.Counter
BindTransactionConflicts returns a pre-bound prometheus.Counter for the (database) tuple — D-16 wiring site at the Cypher transaction wrapper where storage's ErrConflict surfaces. Tenant-flag-aware: drops the database arg when the bag was constructed with tenantLabelsEnabled=false.
func (*CypherMetrics) ObserveQueryDuration ¶
func (c *CypherMetrics) ObserveQueryDuration(ctx context.Context, opType, database string, sec float64)
ObserveQueryDuration is a thin convenience wrapper around BindQueryDuration().Observe — used by tests and cold paths. Hot-path callers should hoist Bind calls out of the request loop.
func (*CypherMetrics) TenantLabelsEnabled ¶
func (c *CypherMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled. Read-only after construction.
type EmbedMetrics ¶
type EmbedMetrics struct {
// Processed counts embedding outcomes per (provider, model, result, mode).
// Cardinality ceiling 250 per RESEARCH §Q11 (4 providers × ~30 models ×
// 3 results × ~5 modes; over-provisioned to absorb new providers).
Processed *prometheus.CounterVec
// Duration is the per-call latency histogram with long-tail buckets
// (EmbeddingLatencyBucketsSeconds, tail to 600s for slow local llama).
// Hot-path: pre-bound via Duration.Bind(provider, model, mode) cached
// in caller struct fields per MET-25.
Duration *EmbeddingLatencyHistogram
// CacheHits / CacheMisses count embed-cache outcomes — provider-agnostic
// (the cache is a generic LRU keyed by hash). Bridges into the
// cross-cutting Cache bag (Plan 04-01) at the cached_embedder.go call
// sites. No labels — single flat counter pair per CONTEXT MET-12.
CacheHits prometheus.Counter
CacheMisses prometheus.Counter
// WorkerRunning is the binary lifecycle gauge (Set(0) on stop, Set(1)
// on start). Per D-15b distinction: NOT a GaugeFunc — the lifecycle
// event boundary is the right observation chokepoint.
WorkerRunning prometheus.Gauge
// FFIPanicTotal counts CGo / purego panics recovered in the FFI call
// site wrapper (D-09; pkg/embed/ffi_recover.go). Mode = the build-tag
// backend at the time of panic; closed enum.
FFIPanicTotal *prometheus.CounterVec
}
EmbedMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the embeddings subsystem. One bag per Provider; constructed at cmd/nornicdb startup. The mode label is bound at observation time using the embedder's Backend() value (D-06).
Hot-path discipline (MET-25): subsystem callers cache BoundEmbeddingLatencyObserver in struct fields at constructor time so per-call observation pays zero WithLabelValues lookup overhead. The processed_total CounterVec is incremented via `WithLabelValues(...)` at the embed completion site.
Dual-access pattern (D-02): the bag exposes raw *prometheus.CounterVec / Gauge / *EmbeddingLatencyHistogram so subsystem tests can drive AssertCardinalityCeiling and edge cases.
func NewEmbedMetrics ¶
func NewEmbedMetrics(reg *prometheus.Registry, probe EmbedProbe) *EmbedMetrics
NewEmbedMetrics constructs the embeddings bag against reg.
probe is the EmbedProbe accessor surface — typically the EmbedWorker or a thin adapter wrapping it. nil is tolerated (queue_depth GaugeFunc returns 0). The probe is consulted on every /metrics scrape; the callback wraps defer-recover so a probe panic does not poison the scrape (RISK-8 / Pitfall 1).
No tenantLabelsEnabled parameter: embed families are global per CONTEXT MET-21 omission (provider/model/mode are not per-DB attributes; per-DB embedding latency lives at the Cypher subsystem if needed).
type EmbedProbe ¶
type EmbedProbe interface {
QueueLen() int
}
EmbedProbe is the seam between pkg/nornicdb embed-queue accessors and the observability queue_depth GaugeFunc callback (D-02d leaf-package boundary — pkg/observability never imports pkg/nornicdb or pkg/embed). The embed worker (or a thin adapter at cmd/nornicdb) satisfies this interface.
QueueLen returns the current number of nodes pending embedding. The pull-based EmbedWorker model means this is typically 0 in steady state; during back-pressure / cold start it surfaces the real depth. Defensive callers may return 0 if no metric source is available.
type EmbeddingLatencyHistogram ¶
type EmbeddingLatencyHistogram struct {
// contains filtered or unexported fields
}
EmbeddingLatencyHistogram wraps *prometheus.HistogramVec for LLM/embedding latency families (MET-05 long-tail buckets). See LatencyHistogram for design notes.
func NewEmbeddingLatencyHistogram ¶
func NewEmbeddingLatencyHistogram(reg *prometheus.Registry, opts MetricOpts, labels []string) *EmbeddingLatencyHistogram
func (*EmbeddingLatencyHistogram) Bind ¶
func (h *EmbeddingLatencyHistogram) Bind(lvs ...string) BoundEmbeddingLatencyObserver
func (*EmbeddingLatencyHistogram) Observe ¶
func (h *EmbeddingLatencyHistogram) Observe(ctx context.Context, lvs []string, sec float64)
func (*EmbeddingLatencyHistogram) Vec ¶
func (h *EmbeddingLatencyHistogram) Vec() *prometheus.HistogramVec
type FilteredBaggagePropagator ¶
type FilteredBaggagePropagator struct {
propagation.Baggage
}
FilteredBaggagePropagator wraps propagation.Baggage and strips keys not in BaggageAllowList on Extract. Inject is unmodified (we only control inbound).
func (FilteredBaggagePropagator) Extract ¶
func (f FilteredBaggagePropagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context
type HTTPMetrics ¶
type HTTPMetrics struct {
// RequestDuration is a latency histogram with Phase-3-locked buckets
// (LatencyBucketsSeconds: ~100us to 10s).
RequestDuration *LatencyHistogram
// Requests is the request count counter; same label-set as RequestDuration
// so the `_total` and `_seconds_count` series have identical cardinality.
Requests *prometheus.CounterVec
// InFlight is the active-request gauge; instrumentedMux Inc() on entry,
// Dec() on exit (deferred). Single value, no labels (cardinality=1).
InFlight prometheus.Gauge
// RequestBodyBytes / ResponseBodyBytes are payload-size histograms with
// Phase-3-locked buckets (SizeBucketsBytes: 64B to 64MB).
RequestBodyBytes *SizeHistogram
ResponseBodyBytes *SizeHistogram
// contains filtered or unexported fields
}
HTTPMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the HTTP subsystem. One bag per Provider; constructed at cmd/nornicdb startup between Phase 1's "registries" and "listeners" init steps.
Hot-path discipline (MET-25): the instrumentedMux chokepoint (pkg/server/server.go) is the SOLE call site; it threads (method, template, status_class[, database]) through the `BindRequestDuration` / `BindRequests` helpers which return tenant-flag- agnostic Bound observers. Per-template Bind cache lives at the chokepoint (not in the bag) because path_template is not known until route resolution.
Dual-access pattern (D-02): the bag exposes the raw *prometheus.CounterVec and Phase-3 typed *LatencyHistogram / *SizeHistogram so subsystem tests can drive AssertCardinalityCeiling and edge cases.
func NewHTTPMetrics ¶
func NewHTTPMetrics(reg *prometheus.Registry, tenantLabelsEnabled bool) *HTTPMetrics
NewHTTPMetrics constructs the HTTP bag against reg.
tenantLabelsEnabled is the D-08 forward-compat hook. When true, the `database` label is included in RequestDuration and Requests; when false, it is omitted. Phase 5's K8s autodetect (MET-22) decides the value.
Validation + Pitfall-8 panic semantics inherited from Phase 3 typed constructors: missing _total/_seconds/_bytes suffixes or forbidden labels (e.g. accidentally passing "path" instead of "path_template") panic at registration.
func (*HTTPMetrics) BindRequestDuration ¶
func (h *HTTPMetrics) BindRequestDuration(method, template, statusClass, database string) BoundLatencyObserver
BindRequestDuration returns a pre-bound BoundLatencyObserver for the (method, template, statusClass, database) tuple. When the bag was constructed with tenantLabelsEnabled=false, the database arg is dropped. Subsystems are bool-agnostic and pass database unconditionally.
Hot-path discipline (MET-25): the per-template Bind cache lives at the instrumentedMux chokepoint, NOT inside this helper — calling BindRequestDuration per request still pays a WithLabelValues lookup. The chokepoint caches the BoundLatencyObserver in a sync.Map keyed by the label tuple so per-request cost is amortized to near-zero.
func (*HTTPMetrics) BindRequests ¶
func (h *HTTPMetrics) BindRequests(method, template, statusClass, database string) prometheus.Counter
BindRequests returns a pre-bound prometheus.Counter for the (method, template, statusClass, database) tuple. Tenant-flag-aware per BindRequestDuration above.
func (*HTTPMetrics) ObserveRequestDuration ¶
func (h *HTTPMetrics) ObserveRequestDuration(ctx context.Context, method, template, statusClass, database string, sec float64)
ObserveRequestDuration is a thin convenience wrapper around BindRequestDuration().Observe — used by tests and cold paths. Hot-path callers should hoist Bind calls out of the request loop.
func (*HTTPMetrics) TenantLabelsEnabled ¶
func (h *HTTPMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled. Read-only after construction.
type Health ¶
type Health struct {
// contains filtered or unexported fields
}
Health is the readiness check registry served by /readyz.
Lookup is read-heavy (every kubelet probe — every few seconds in production) and registration is one-shot at startup (or once per t.Cleanup in tests), so sync.RWMutex matches the access pattern. Tests stress concurrent register/deregister/Ready under -race.
func (*Health) Deregister ¶
Deregister removes a check by name. Deregistering an unknown name is a no-op (idempotent — supports test teardown via t.Cleanup without coupling to registration order).
func (*Health) Ready ¶
func (h *Health) Ready(ctx context.Context) ReadyResult
Ready runs every registered check in parallel and aggregates results.
Per-check timeout (CheckOpts.Timeout, default 1s) is enforced via a child context derived from ctx. The aggregate `OK` is true iff every REQUIRED check passed; informational (Required=false) failures appear in the Checks map but don't flip OK.
Implementation note: a registry snapshot under RLock, then run checks without holding the lock so a concurrent Register/Deregister can't be blocked by a slow check. This matches the access-pattern documentation on Health and is safe under -race.
func (*Health) Register ¶
Register adds (or replaces) a check by name.
Re-registering the same name OVERWRITES the previous entry — this is the idiomatic behavior for t.Cleanup re-registration in tests and for hot-reload scenarios in long-running daemons. Concurrent-safe.
If opts is omitted (variadic empty), the zero value is used: Required=false, Timeout=1s default.
type KnowledgePolicyMetrics ¶
type KnowledgePolicyMetrics struct {
// Scored counts scoring evaluations by entity_kind × result.
Scored *prometheus.CounterVec
// DecayScore is a 0.0..1.0 score distribution sampled 1/32. See
// ObserveDecayScoreSampled for the chokepoint.
DecayScore *prometheus.HistogramVec
// Suppressions counts suppression events by entity_kind × reason.
Suppressions *prometheus.CounterVec
// AccessFlushBatchRows observes the row count of each AccessFlusher
// flush — reused RowCountBuckets (1..1M).
AccessFlushBatchRows *RowCountHistogram
// AccessFlushDuration observes end-to-end flush wall-clock time.
AccessFlushDuration *LatencyHistogram
// OnAccessMutations counts on-access policy evaluations by result.
OnAccessMutations *prometheus.CounterVec
// DeindexEnqueued counts visibility-flip deindex enqueues.
DeindexEnqueued *prometheus.CounterVec
// ReadFilterDropped counts read-path filter suppressions (called on
// every node/edge returned by Badger when decay is enabled).
ReadFilterDropped *prometheus.CounterVec
// Reconcile counts policy-change reconcile passes by trigger.
Reconcile *prometheus.CounterVec
// contains filtered or unexported fields
}
KnowledgePolicyMetrics is the typed handle-bag for the knowledge_policy subsystem. One bag per Provider; constructed at cmd/nornicdb startup and attached to the Scorer/AccessFlusher through constructor injection, plus published to the pkg/storage read-path filter via SetKnowledgePolicyMetrics.
func GetKnowledgePolicyMetrics ¶
func GetKnowledgePolicyMetrics() *KnowledgePolicyMetrics
GetKnowledgePolicyMetrics returns the currently-published metrics handle, or nil if none has been set. Callers MUST nil-check the return value (all Inc* / Observe* methods on *KnowledgePolicyMetrics are nil-safe, so the idiomatic pattern is `observability.GetKnowledgePolicyMetrics(). IncReadFilterDropped("node", "")` without a separate nil branch).
func NewKnowledgePolicyMetrics ¶
func NewKnowledgePolicyMetrics( reg *prometheus.Registry, tenantLabelsEnabled bool, bufferFullnessFn func() float64, ) *KnowledgePolicyMetrics
NewKnowledgePolicyMetrics constructs the knowledge-policy bag against reg.
bufferFullnessFn is the passive-scrape callback for access_flush_buffer_fullness — typically reads len(accumulator.buffer)/maxBufferSize from the current AccessFlusher. The callback is wrapped in a defer-recover that returns 0 on panic (RISK-8 / Pitfall 1) so a buggy callback cannot poison the scrape.
Validation + Pitfall-8 panic semantics inherited from Phase 3 typed constructors: missing suffix, forbidden labels, or invalid subsystem panic at registration.
func (*KnowledgePolicyMetrics) IncDeindexEnqueued ¶
func (k *KnowledgePolicyMetrics) IncDeindexEnqueued(entityKind, database string)
IncDeindexEnqueued increments deindex_enqueued_total for the entity_kind.
func (*KnowledgePolicyMetrics) IncOnAccess ¶
func (k *KnowledgePolicyMetrics) IncOnAccess(result, database string)
IncOnAccess increments on_access_mutations_total for the given result.
func (*KnowledgePolicyMetrics) IncReadFilterDropped ¶
func (k *KnowledgePolicyMetrics) IncReadFilterDropped(entityKind, database string)
IncReadFilterDropped increments read_filter_dropped_total. Called from the storage layer via the atomic-pointer bridge (GetKnowledgePolicyMetrics).
func (*KnowledgePolicyMetrics) IncReconcile ¶
func (k *KnowledgePolicyMetrics) IncReconcile(trigger, database string)
IncReconcile increments reconcile_total for the trigger.
func (*KnowledgePolicyMetrics) IncScored ¶
func (k *KnowledgePolicyMetrics) IncScored(entityKind, result, database string)
IncScored increments scored_total for the given (entity_kind, result) tuple. Tenant-flag-aware: database arg dropped when tenantLabelsEnabled is false. Callers pass the database unconditionally.
func (*KnowledgePolicyMetrics) IncSuppression ¶
func (k *KnowledgePolicyMetrics) IncSuppression(entityKind, reason, database string)
IncSuppression increments suppressions_total for the given tuple.
func (*KnowledgePolicyMetrics) ObserveAccessFlushBatchRows ¶
func (k *KnowledgePolicyMetrics) ObserveAccessFlushBatchRows(ctx context.Context, rows float64)
ObserveAccessFlushBatchRows records one batch-size sample.
func (*KnowledgePolicyMetrics) ObserveAccessFlushDuration ¶
func (k *KnowledgePolicyMetrics) ObserveAccessFlushDuration(ctx context.Context, sec float64)
ObserveAccessFlushDuration records one flush-duration sample.
func (*KnowledgePolicyMetrics) ObserveDecayScoreSampled ¶
func (k *KnowledgePolicyMetrics) ObserveDecayScoreSampled( ctx context.Context, entityKind, database string, score float64, )
ObserveDecayScoreSampled samples the decay_score histogram at 1/ DecayScoreSampleDenominator. Callers invoke this on every score evaluation; the helper drops ~31/32 without a lock or per-goroutine RNG state so the hot path pays only a single atomic increment and a bitmask test.
Using a uniform deterministic sampler (rather than rand.IntN) produces a slightly biased sample but the distribution shape is preserved at the histogram-bucket level — which is all operators care about.
func (*KnowledgePolicyMetrics) TenantLabelsEnabled ¶
func (k *KnowledgePolicyMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled. Read-only after construction.
type LatencyHistogram ¶
type LatencyHistogram struct {
// contains filtered or unexported fields
}
LatencyHistogram wraps *prometheus.HistogramVec to centralize the MET-24 exemplar-emission chokepoint for latency histograms. Subsystems use this wrapper for normal Observe paths; the raw *HistogramVec remains accessible via Vec() for cardinality-ceiling assertions (D-02 dual-access pattern).
func NewLatencyHistogram ¶
func NewLatencyHistogram(reg *prometheus.Registry, opts MetricOpts, labels []string) *LatencyHistogram
NewLatencyHistogram constructs the wrapper around a fresh NewLatencyHistogramVec. Validation + Pitfall-8 panic semantics inherited from metrics.go.
func (*LatencyHistogram) Bind ¶
func (h *LatencyHistogram) Bind(lvs ...string) BoundLatencyObserver
Bind returns a pre-bound observer cached as a struct field at construction time (MET-25). WithLabelValues lookup amortized away from request-loop hot path. The returned BoundLatencyObserver is value-typed (no pointer alloc).
func (*LatencyHistogram) Observe ¶
func (h *LatencyHistogram) Observe(ctx context.Context, lvs []string, sec float64)
Observe is the cold-path entry: pays a WithLabelValues lookup per call. Funnels through observeWithExemplar (D-02a chokepoint).
func (*LatencyHistogram) Vec ¶
func (h *LatencyHistogram) Vec() *prometheus.HistogramVec
Vec returns the underlying *prometheus.HistogramVec — used by Phase 4 subsystem tests for AssertCardinalityCeiling and edge-case observation (D-02 dual-access pattern; <specifics> §6).
type LoggerConfig ¶
LoggerConfig is the local mirror of the relevant subset of pkg/config.LoggingConfig. We can't import pkg/config here because pkg/config already imports pkg/observability for ObservabilityConfig (would create an import cycle). The runServe site translates between the two structs.
Fields:
- Level: "debug" / "info" / "warn" / "error" (case-insensitive). Default: info.
- Format: "json" (default) or "text". Phase 2 only ships JSON; the field is held for forward compat with future TextHandler dev mode.
- Output: "stdout" (default), "stderr", or a filesystem path.
type MVCCMetrics ¶
type MVCCMetrics struct {
// PressureBand holds one indicator gauge per (database, band) tuple.
// Use UpdateBand(database, ratio) to set the active band to 1 and
// reset the other three to 0 for the same database.
PressureBand *prometheus.GaugeVec
// contains filtered or unexported fields
}
MVCCMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the MVCC subsystem. One bag per Provider; constructed at cmd/nornicdb startup.
PressureBand is exposed as a struct field so subsystem callers (pkg/storage at MVCC reader open/close sites) can call `bag.UpdateBand(database, ratio)` to flip the active band gauge. The three live-read gauges (pinned_bytes / oldest_reader_age_seconds / active_readers) are NOT struct fields — they are GaugeFunc registrations that read the probe on every scrape. RESEARCH Pattern 1.
func NewMVCCMetrics ¶
func NewMVCCMetrics(reg *prometheus.Registry, tenantLabelsEnabled bool, probe MVCCProbe) *MVCCMetrics
NewMVCCMetrics constructs the MVCC bag against reg.
tenantLabelsEnabled (D-08) decides whether `database` is included in the pressure_band label-set. The three GaugeFunc gauges have no labels.
probe is the MVCCProbe accessor surface — typically *BadgerEngine. nil is tolerated as a defensive fallback (returns 0 from each gauge); the RISK-2 accessors are always safe to call so this is paranoia, not a real fallback path in production.
func (*MVCCMetrics) TenantLabelsEnabled ¶
func (m *MVCCMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled.
func (*MVCCMetrics) UpdateBand ¶
func (m *MVCCMetrics) UpdateBand(database string, ratio float64)
UpdateBand sets the indicator gauge for the active band to 1 and resets the other three bands to 0 for the same database. Threshold mapping per D-14 — callers pass the raw ratio (PinnedBytes / MVCCBudgetBytes) and this helper picks the band.
When the bag was constructed with tenantLabelsEnabled=false, the database arg is dropped at the WithLabelValues call site.
type MVCCProbe ¶
type MVCCProbe interface {
PinnedBytes() int64
OldestReaderAgeSeconds() float64
ActiveReaders() int64
}
MVCCProbe is the seam between pkg/storage MVCC accessors and the observability GaugeFunc callbacks (D-02d leaf-package boundary — pkg/observability never imports pkg/storage). *BadgerEngine satisfies this interface via Plan 04-04-01 (RISK-2 fix).
Plan 04-01 Wave-0 published this interface stub so the RED tests compile; Plan 04-04 ships the production GREEN bag here. The signatures are stable across both — int64 and float64 to keep the wire-shape trivial and panic-free even when the engine is in shutdown.
type MetricOpts ¶
type MetricOpts struct {
Subsystem string // MUST be one of allowedSubsystems (D-01d — declared below in this file)
Name string // MUST end in _total | _seconds | _bytes | _rows per type (MET-02)
Help string // MUST be non-empty (client_golang convention)
}
MetricOpts carries the helper-injected Subsystem/Name/Help triple (CONTEXT D-01). Namespace is always "nornicdb" — caller cannot override (D-01b). ConstLabels intentionally omitted in M1; see CONTEXT.md "Deferred Ideas" for rationale.
type MetricsConfig ¶
type MetricsConfig struct {
// Enabled defaults to true. When false, pkg/observability.New returns a
// Provider with a nil registry and the Plan-03 listener does NOT register
// the /metrics handler (OBS-04).
Enabled bool
// Listen is the bind address for the telemetry mux. Default ":9090".
Listen string
// TenantLabelsEnabled is the resolved per-process tenant-labels switch.
// Phase 5 startup hook (cmd/nornicdb/main.go) writes this from the
// explicit YAML value (TenantLabelsExplicit) or K8s autodetect
// (DefaultK8sProbe + ResolveTenantLabels) BEFORE any Phase 4 bag
// constructor reads it. Bag constructors continue to read this bool
// directly per Phase 4 D-08 plumbing — do NOT bypass them by reading
// TenantLabelsExplicit. Default false (D-02c) until startup hook runs.
TenantLabelsEnabled bool
// TenantLabelsExplicit is the operator's YAML intent before defaulting.
// nil = field omitted in YAML; non-nil = operator explicitly set true
// or false. Phase 5 ResolveTenantLabels reads this to enforce
// precedence (explicit YAML > K8s autodetect > default false).
// R-02: this sentinel is the smallest blast-radius change to allow
// YAML "explicit false" to win over autodetect "true on K8s".
TenantLabelsExplicit *bool
}
MetricsConfig governs the :9090/metrics surface.
type ObservabilityConfig ¶
type ObservabilityConfig struct {
// Metrics controls the :9090 Prometheus surface.
Metrics MetricsConfig
// Tracing controls the OTLP trace exporter.
Tracing TracingConfig
// Pprof controls the optional :9091 pprof listener.
Pprof PprofConfig
}
ObservabilityConfig is the root telemetry config block bound from nornicdb.yaml's `observability:` section and overlaid with NORNICDB_* env vars.
func DefaultConfig ¶
func DefaultConfig() ObservabilityConfig
DefaultConfig returns the Phase-1 defaults. Operators may override any field via YAML or NORNICDB_* env vars.
func (*ObservabilityConfig) ApplyEnv ¶
func (c *ObservabilityConfig) ApplyEnv()
ApplyEnv overlays NORNICDB_* env vars onto c. Env vars take precedence over YAML-set fields (env > YAML > default), per OBS-02.
OTEL_EXPORTER_OTLP_* env vars are intentionally NOT consumed here — they are read on-demand by OTLPEndpoint() at exporter-init time, otherwise the resolved value would be frozen at config-load and break OBS-12 precedence.
type ParentMode ¶
type ParentMode string
ParentMode controls how the sampler treats upstream parent-span decisions.
ParentModeNone (the v1 default per TRC-05, KD-05): sampling is a standalone TraceIDRatioBased(ratio). The storage layer controls its own trace volume; upstream samplers cannot drive NornicDB to 100% volume.
ParentModeCapped (TRC-06): honors upstream sampled=true up to a QPS cap, then falls back to the ratio-based sampler for additional sampled-parent spans. Operators that want *some* upstream honor without unbounded volume.
ParentModeStrict (TRC-07): full upstream honor. Equivalent to OTel ParentBased + TraceIDRatioBased(ratio). Emits a WARN at startup about unbounded volume risk.
const ( ParentModeNone ParentMode = "" ParentModeCapped ParentMode = "capped" ParentModeStrict ParentMode = "strict" )
type PeerConfigLike ¶
PeerConfigLike is the minimal accessor surface this package needs from pkg/replication.PeerConfig (D-02d boundary indirection). The replication package satisfies it with a tiny adapter over its concrete PeerConfig struct — keeps pkg/observability free of pkg/replication dependencies.
We use accessor methods rather than struct fields because Go's structural typing matches by method set (struct field equality across packages requires identical declarations, which would force a shared type and reintroduce the import cycle).
type PprofConfig ¶
type PprofConfig struct {
// Enabled gates whether the pprof listener is started.
Enabled bool
// Listen is the bind address. Default "127.0.0.1:9091" per ADR §A9 to
// keep pprof off non-loopback interfaces by default.
Listen string
}
PprofConfig governs the optional :9091 debug surface.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider is the entry point for all observability surfaces. Plan 03 listeners and Plan 04 main.go consume Provider via its accessors.
Provider is goroutine-safe for read; mutation after construction is forbidden.
func New ¶
func New(ctx context.Context, cfg ObservabilityConfig, info ServiceInfo, logger *slog.Logger, writerRef io.Writer) (*Provider, error)
New constructs a *Provider following the OBS-03 init order:
- resource attributes (service.name/version/instance.id resolved via OBS-10 chain).
- Prometheus registry + OTel→Prom bridge (skipped when cfg.Metrics.Enabled=false — OBS-04).
- TracerProvider (SDK + BSP + OTLP exporter, OR noop on failure — OBS-11).
New NEVER returns a non-nil error from OTLP failure — telemetry init failure is logged at WARN and a noop tracer provider is installed. Process startup is unconditionally robust against observability misconfiguration.
The provided ctx bounds OTLP exporter dial. A context-with-timeout derived from cfg.Tracing.Timeout (default 5s) further bounds the dial so a misconfigured collector cannot hang startup (Pitfall 2).
Per D-08 two-phase bootstrap: the caller MUST call observability.NewLogger BEFORE this function so logger / writerRef can be threaded through. logger MAY be nil for legacy callers (provider falls back to a discard logger); writerRef MAY be nil (no Sync attempt during Shutdown).
func (*Provider) Config ¶
func (p *Provider) Config() ObservabilityConfig
Config returns a copy of the construction-time config.
func (*Provider) InstanceID ¶
InstanceID returns the resolved service.instance.id (OBS-10).
func (*Provider) InstanceIDSource ¶
InstanceIDSource returns the resolution leg that fired ("config", "POD_NAME", "hostname", or "fallback"). Useful for Plan 03 /version handler.
func (*Provider) Logger ¶
Logger returns the production *slog.Logger that downstream business packages (pkg/server, pkg/cypher, pkg/storage, pkg/bolt) consume per the D-01 constructor-injection pattern. Returns nil if the Provider was constructed via a legacy code path that did not pass a logger; callers SHOULD nil-guard with `slog.New(slog.NewTextHandler(io.Discard, nil))` as a fallback.
func (*Provider) MeterProvider ¶
func (p *Provider) MeterProvider() *sdkmetric.MeterProvider
MeterProvider returns the OTel meter provider. nil when metrics disabled.
func (*Provider) MetricsEnabled ¶
MetricsEnabled mirrors cfg.Metrics.Enabled (OBS-04).
func (*Provider) Registry ¶
func (p *Provider) Registry() *prometheus.Registry
Registry returns the Prometheus registry. nil when metrics disabled (OBS-04). Plan 03 listener uses this nil-ness to skip /metrics handler registration.
func (*Provider) Shutdown ¶
Shutdown flushes the BSP and shuts down the meter provider. Idempotent in the sense that it can be called multiple times safely; the underlying SDK providers are themselves idempotent on Shutdown.
Called by the telemetry listener's Shutdown in Plan 03 (per Open Question 4 resolution — the lifecycle.Component owns the Provider's flush budget).
func (*Provider) TracerProvider ¶
func (p *Provider) TracerProvider() trace.TracerProvider
TracerProvider returns the tracer provider. Always non-nil; may be a noop (when cfg.Tracing.Enabled=false OR OTLP exporter init failed — OBS-11).
type ReadyResult ¶
type ReadyResult struct {
OK bool `json:"ok"`
Checks map[string]CheckStatus `json:"checks"`
}
ReadyResult is the JSON response body for /readyz.
The shape is locked by D-03 (CONTEXT.md): top-level {ok, checks} where `checks` is a map keyed by check name. Phase 9 (K8S-06) will add an optional `progress` field to CheckStatus as an additive non-breaking change — Phase 1 must NOT emit it.
type ReplicationMetrics ¶
type ReplicationMetrics struct {
// Role is the current role gauge. Published as a numeric enum via
// Set(RoleEnum(roleString)) at lifecycle transition log sites (D-15a).
// Per-cluster, no labels — there is exactly one role per process.
Role prometheus.Gauge
// Term is the Raft term gauge. Set at term-change log sites (D-15a).
Term prometheus.Gauge
// CommitIndex is the Raft last-committed-log-index gauge. Set at
// commit-advance log sites (D-15a).
CommitIndex prometheus.Gauge
// ApplyIndex is the Raft last-applied-log-index gauge. Set at
// apply-advance log sites (D-15a).
ApplyIndex prometheus.Gauge
// LagBytes is the per-peer replication-lag-bytes gauge. Hot-path —
// observed at every replicate/heartbeat call. peer label is stable
// per PeerLabel (RISK-3 fix). Mode-aware ceiling stored in modeCeiling.
LagBytes *prometheus.GaugeVec
// LagEntries is the per-peer replication-lag-entries gauge. Same
// observation cadence as LagBytes.
LagEntries *prometheus.GaugeVec
// ApplyDuration is the histogram for command-apply latency. No labels —
// per-cluster distribution. Pre-bound observer cached in
// pkg/replication.Replicator struct field per MET-25.
ApplyDuration *LatencyHistogram
// RTTSeconds is the per-peer round-trip-time histogram. Observed at
// AppendEntries response chokepoint. Hot-path — pre-bound per peer.
RTTSeconds *prometheus.HistogramVec
// LeaderChangesTotal counts every role transition that crosses the
// leader boundary (non-leader→leader OR leader→non-leader). The
// keystone alert metric for cluster instability — increments at the
// SAME sites that emit "became leader" / "stepped down" log lines.
LeaderChangesTotal prometheus.Counter
// LastContactSeconds is the GAP-1 gauge — wall-clock seconds since the
// last successful AppendEntries from a peer (or sent to a peer for
// leader perspective). Operators alert on
// `time() - nornicdb_replication_last_contact_seconds{peer="..."} > N`
// per Phase 9 Helm/Grafana plan. Per-peer; populated at heartbeat
// observation sites; cleared by peer_metrics_gc.
LastContactSeconds *prometheus.GaugeVec
// contains filtered or unexported fields
}
ReplicationMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the Replication subsystem. One bag per Provider, constructed at cmd/nornicdb startup and injected into pkg/replication.Replicator implementations.
**Hot-path discipline (MET-25):** the per-peer GaugeVec families are pre-bound at peer-tracker mark time (replicator rebinds on connect; the peer_metrics_gc lifecycle.Component drops bindings on staleness — Pitfall 3 mitigation per RESEARCH §Q7). The per-cluster scalar gauges (Role, Term, CommitIndex, ApplyIndex, LeaderChanges) need no Bind — they're already singletons.
**Dual-access (D-02):** the raw *prometheus.GaugeVec / *Counter / Gauge is exposed so subsystem tests drive AssertCardinalityCeiling and edge cases. ApplyDuration uses the LatencyHistogram wrapper for MET-24 exemplar centralization (callers Bind() once at apply chokepoint).
func NewReplicationMetrics ¶
func NewReplicationMetrics(reg *prometheus.Registry, mode string, tenantLabelsEnabled bool) *ReplicationMetrics
NewReplicationMetrics constructs the replication bag against reg.
Constructor signature per D-05a + D-08a:
mode (string): one of AllowedReplicationModes; stored for ceiling assertions. An unrecognized mode is permitted at construction (the bag still functions) but mode-aware ceiling lookups return 0 — tests must use a recognized mode. tenantLabelsEnabled (bool): accepted for callsite uniformity across subsystem bags, but IGNORED — replication is per-cluster, not per-database. No `database` label is ever added to any family. Documented in CONTEXT D-08a.
Validation chain inherited from pkg/observability typed constructors:
- subsystem "replication" must be in allowedSubsystems (metrics.go:59)
- histogram name must end in _seconds (registration.go validateNameSuffix)
- counter name must end in _total
- labels rejected against ForbiddenLabels (path/query/user/user_id/ip/uuid/...)
Pitfall 8 / MustRegister precedent: validation failure panics — programming bug surfaces at startup before any traffic.
func (*ReplicationMetrics) Ceiling ¶
func (r *ReplicationMetrics) Ceiling() int
Ceiling returns the mode-aware peer cardinality ceiling per D-05a. Returns 0 for unknown modes — the caller should treat that as a test bug.
func (*ReplicationMetrics) Mode ¶
func (r *ReplicationMetrics) Mode() string
Mode returns the replication mode this bag was constructed for. Used by mode-aware cardinality ceiling assertions.
func (*ReplicationMetrics) TenantLabelsEnabled ¶
func (r *ReplicationMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports the D-08a flag value. Diagnostic only — no replication family is tenant-labeled.
type RowCountHistogram ¶
type RowCountHistogram struct {
// contains filtered or unexported fields
}
RowCountHistogram wraps *prometheus.HistogramVec for result-set-size families (MET-03 RowCountBuckets). See LatencyHistogram for design notes.
func NewRowCountHistogram ¶
func NewRowCountHistogram(reg *prometheus.Registry, opts MetricOpts, labels []string) *RowCountHistogram
func (*RowCountHistogram) Bind ¶
func (h *RowCountHistogram) Bind(lvs ...string) BoundRowCountObserver
func (*RowCountHistogram) Observe ¶
func (h *RowCountHistogram) Observe(ctx context.Context, lvs []string, rows float64)
func (*RowCountHistogram) Vec ¶
func (h *RowCountHistogram) Vec() *prometheus.HistogramVec
type SearchMetrics ¶
type SearchMetrics struct {
// Requests counts search requests by [database], mode, result.
// Cardinality ceiling per RESEARCH §Q11 — 9 tenant-OFF (3 modes ×
// 3 results); ceiling × max-databases when tenant-ON.
Requests *prometheus.CounterVec
// Duration is the per-stage latency histogram. Hot-path: pre-bound
// via BindDuration([database], mode, stage) cached in
// pkg/search.Service struct fields per MET-25. Phase-3-locked
// LatencyBucketsSeconds (≤10s tail; embed stage's long-tail belongs
// to nornicdb_embed_duration_seconds).
Duration *LatencyHistogram
// Candidates is the per-request candidates-count distribution
// (count of results considered before the fuse stage trims to
// SearchOptions.Limit). RowCountBuckets up to 1M.
Candidates *RowCountHistogram
// IndexSizeBytes is the per-kind size gauge. Populated by the
// SearchProbe.IndexSizeBytes(kind) callback on every scrape. Closed
// kind enum {hnsw, bm25}.
IndexSizeBytes *prometheus.GaugeVec
// contains filtered or unexported fields
}
SearchMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the search subsystem. One bag per Provider; constructed at cmd/nornicdb startup and injected into pkg/search.Service via AttachMetrics.
Hot-path discipline (MET-25): subsystem callers cache BoundLatencyObserver in struct fields per (database, mode, stage) tuple at constructor time. The Duration.Bind helper is the entry point.
Dual-access pattern (D-02): the bag exposes raw *prometheus.CounterVec / *RowCountHistogram / *prometheus.GaugeVec so subsystem tests can drive AssertCardinalityCeiling and edge cases.
func NewSearchMetrics ¶
func NewSearchMetrics(reg *prometheus.Registry, tenantLabelsEnabled bool, probe SearchProbe) *SearchMetrics
NewSearchMetrics constructs the search bag against reg.
tenantLabelsEnabled (D-08) decides whether `database` is included in requests_total + duration_seconds. candidates is unlabeled (per-request distribution) and index_size_bytes uses only the kind label (process-wide bytes per kind).
probe is the SearchProbe accessor surface — typically a thin adapter over pkg/search.Service. nil is tolerated as a defensive fallback (returns 0 from each kind gauge); the GaugeFunc callbacks wrap defer-recover returning 0 on panic per RISK-8.
func (*SearchMetrics) BindDuration ¶
func (s *SearchMetrics) BindDuration(database, mode, stage string) BoundLatencyObserver
BindDuration returns a pre-bound BoundLatencyObserver for the (database, mode, stage) tuple per MET-25. Tenant-flag-aware — drops database when the bag was constructed with tenantLabelsEnabled=false.
Subsystem callers cache this in struct fields at construction time; the per-request observation pays zero WithLabelValues lookup overhead.
func (*SearchMetrics) IncRequest ¶
func (s *SearchMetrics) IncRequest(database, mode, result string)
IncRequest is the tenant-flag-aware helper to bump requests_total. Drops database when the bag was constructed with tenantLabelsEnabled=false.
func (*SearchMetrics) TenantLabelsEnabled ¶
func (s *SearchMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled.
type SearchProbe ¶
SearchProbe is the seam between pkg/search index-size accessors and the observability index_size_bytes GaugeFunc callback (D-02d leaf-package boundary — pkg/observability never imports pkg/search). pkg/search (or a thin adapter) satisfies this interface.
IndexSizeBytes returns the on-disk or in-memory size of the named index kind. Implementations should sum across all per-database indexes when a process hosts multiple databases. Defensive callers may return 0 if the size is not yet computable (e.g., index still building).
type ServiceInfo ¶
type ServiceInfo struct {
// Name is the OTel service.name. REQUIRED.
Name string
// Version is the OTel service.version. REQUIRED.
Version string
// Component is an optional sub-component label.
Component string
// NodeID feeds the service.instance.id resolution chain (OBS-10).
NodeID string
// ClusterMode is the deployment topology (e.g. "standalone", "cluster").
// TRC-10: emitted as nornicdb.cluster.mode resource attribute.
ClusterMode string
// ReplicationRole is the node's role (e.g. "primary", "replica", "standalone").
// TRC-10: emitted as nornicdb.replication.role resource attribute.
ReplicationRole string
// ExtraResourceAttrs are merged after semconv defaults; duplicate keys win.
ExtraResourceAttrs []attribute.KeyValue
}
ServiceInfo identifies the running binary for telemetry resource attrs.
Multi-binary forward-compat (D-02b): future binaries (cmd/metrics-doc-gen, per-service binaries) override Name and Component without forking the resource construction code. Phase-1 callers pass Name="nornicdb".
ExtraResourceAttrs are merged AFTER semconv keys via resource.Merge, which is last-wins. A caller setting ExtraResourceAttrs={service.name: "x"} will override the semconv default.
type SizeHistogram ¶
type SizeHistogram struct {
// contains filtered or unexported fields
}
SizeHistogram wraps *prometheus.HistogramVec for payload-size families (MET-03 SizeBucketsBytes). See LatencyHistogram for design notes.
func NewSizeHistogram ¶
func NewSizeHistogram(reg *prometheus.Registry, opts MetricOpts, labels []string) *SizeHistogram
func (*SizeHistogram) Bind ¶
func (h *SizeHistogram) Bind(lvs ...string) BoundSizeObserver
func (*SizeHistogram) Observe ¶
func (h *SizeHistogram) Observe(ctx context.Context, lvs []string, bytes float64)
func (*SizeHistogram) Vec ¶
func (h *SizeHistogram) Vec() *prometheus.HistogramVec
type StorageMetrics ¶
type StorageMetrics struct {
// Bytes is the storage size gauge per kind. Populated every 30s by
// the bytes_metrics_sweeper lifecycle.Component (D-07).
Bytes *prometheus.GaugeVec
// OpDuration is the per-op latency histogram. Hot-path observation
// uses pre-bound observers cached in BadgerEngine struct fields
// (MET-25). Phase-3-locked LatencyBucketsSeconds.
OpDuration *LatencyHistogram
// CompactionsTotal counts BadgerDB compactions by level and result.
// Cardinality ceiling = ~14 (7 levels × 2 results). Bounded by
// BadgerDB's own compaction-level surface — no user input flows here.
CompactionsTotal *prometheus.CounterVec
// CompactionDuration histograms compaction wall-clock time per level.
CompactionDuration *LatencyHistogram
// WALLagBytes is a heuristic estimate of WAL backlog (vlog size
// minus LSM size from badger.DB.Size()). Best-effort; alerting
// should use 5-minute trends, not single scrapes. RESEARCH §Q3 /
// RISK-6.
WALLagBytes prometheus.Gauge
// IndexRebuildTotal counts index-rebuild events by [database], index,
// result. Closed `index` enum per D-13c. database label gated by
// tenant flag (D-08).
IndexRebuildTotal *prometheus.CounterVec
// contains filtered or unexported fields
}
StorageMetrics is the typed handle-bag (CONTEXT D-02 / D-02a) for the storage subsystem. One bag per Provider; constructed at cmd/nornicdb startup and injected into *BadgerEngine via AttachMetrics(...).
Hot-path discipline (MET-25): the storage struct caches BoundLatencyObserver in struct fields at constructor time so per-op observation pays zero WithLabelValues lookup overhead.
Dual-access pattern (D-02): the bag exposes the raw *prometheus.GaugeVec / *prometheus.CounterVec / *LatencyHistogram so subsystem tests can drive AssertCardinalityCeiling and edge cases.
func NewStorageMetrics ¶
func NewStorageMetrics(reg *prometheus.Registry, tenantLabelsEnabled bool, probe StorageProbe) *StorageMetrics
NewStorageMetrics constructs the storage bag against reg.
tenantLabelsEnabled (D-08) decides whether `database` is included in IndexRebuildTotal. The other families are tenant-flag-independent:
- bytes/op_duration aggregate across databases at the storage layer (per-DB lives at Cypher subsystem, Plan 04-03).
- nodes_total/edges_total are process-wide gauges.
- compactions/wal are BadgerDB-level — single physical database.
probe is the StorageProbe accessor surface — typically *BadgerEngine. nil is tolerated as a defensive fallback (returns 0 from each gauge).
func (*StorageMetrics) BindIndexRebuild ¶
func (s *StorageMetrics) BindIndexRebuild(database, index, result string) prometheus.Counter
BindIndexRebuild returns a pre-bound prometheus.Counter for the (database, index, result) tuple. Tenant-flag-aware — drops database when the bag was constructed with tenantLabelsEnabled=false.
func (*StorageMetrics) TenantLabelsEnabled ¶
func (s *StorageMetrics) TenantLabelsEnabled() bool
TenantLabelsEnabled reports whether this bag was constructed with the D-08 tenant-flag enabled.
type StorageProbe ¶
type StorageProbe interface {
NodeCount() int64
EdgeCount() int64
IDDictCounterNodes() uint64
IDDictCounterEdges() uint64
IDDictFreelistNodes() int64
IDDictFreelistEdges() int64
}
StorageProbe is the seam between pkg/storage stat accessors and the observability nodes_total / edges_total GaugeFunc callbacks (D-02d leaf-package boundary — pkg/observability never imports pkg/storage). *BadgerEngine's existing NodeCount() / EdgeCount() return (int64, error); callers wrap the error-discarding form via a thin adapter at the cmd/nornicdb wiring site (see Plan 04-04-07).
IDDictCounterNodes / IDDictCounterEdges expose the monotonic counters of allocated numIDs. IDDictFreelistNodes / IDDictFreelistEdges report the number of entries currently parked on the debounced freelist, awaiting TTL expiry before they can be reclaimed. Together these give operators visibility into (counter-freelist) = roughly the live-entity count and freelist pending work.
type TestEnv ¶
type TestEnv struct {
Registry *prometheus.Registry
Exporter *tracetest.InMemoryExporter
Logger *slog.Logger
Provider *Provider
Health *Health
// Buffer is the lazily-allocated record-capture sink. Populated by the
// first call to CaptureRecords(); nil otherwise. Per D-12 the discard
// handler stays the default; tests opt-in to capture via CaptureRecords.
Buffer *bytes.Buffer
// contains filtered or unexported fields
}
TestEnv carries per-test isolated observability primitives. It is the canonical TEST-01 fixture (ADR §2.8.1 / A10b) — every Phase 3+ test package SHOULD construct one of these via NewTestEnv(t).
Each TestEnv has:
- its own *prometheus.Registry (never DefaultRegisterer);
- its own *tracetest.InMemoryExporter wired through SimpleSpanProcessor so emitted spans are visible synchronously (no BSP batching);
- its own *slog.Logger using a discard handler (suppresses unless a test explicitly writes against a captured handler);
- a *Provider built against those primitives (sampler: sdktrace.AlwaysSample so tests CAN observe spans they emit, unlike the production NeverSample default);
- a fresh *Health registry.
Provider.Shutdown is registered on t.Cleanup automatically — callers don't need to call it explicitly.
func NewTestEnv ¶
NewTestEnv constructs an isolated observability environment for one test. Race-detector stable across `go test -race -count=10`.
The constructed *Provider uses SimpleSpanProcessor(exp) + AlwaysSample rather than the production BSP + NeverSample combination, so tests can observe spans synchronously via env.Exporter.GetSpans(). This is a test-only path; production code goes through observability.New.
func (*TestEnv) AssertCardinalityCeiling ¶
func (te *TestEnv) AssertCardinalityCeiling(t CardinalityT, name string, ceiling int, drive func(tenant string))
AssertCardinalityCeiling exercises the named *Vec across 1000 deterministic synthetic tenant UUIDs concurrently across 8 goroutines, then asserts the test's isolated registry observes <= ceiling distinct series for `name`. Implements TEST-02 (CONTEXT D-04 / D-04a / D-04c).
Called once per *Vec from Phase 4 subsystem tests; the "1000 UUIDs + 8-goroutine drive" knowledge lives ONCE here per AGENTS.md §7 (DRY).
API choice: caller-supplied `drive` callback (Pattern A in RESEARCH §4). Keeps testenv.go agnostic of subsystem label shapes — a *Vec with labels []string{"database","op_type","result"} is driven via
te.AssertCardinalityCeiling(t, name, ceiling, func(tenant string) {
cv.WithLabelValues(tenant, "read", "success").Inc()
})
— and a *Vec with labels []string{"database"} is driven via
te.AssertCardinalityCeiling(t, name, ceiling, func(tenant string) {
cv.WithLabelValues(tenant).Inc()
})
Race-safety:
- errgroup.SetLimit(8) caps fan-out so -race -count=N -parallel=M doesn't explode goroutine counts (D-04c).
- client_golang.HistogramVec.WithLabelValues / CounterVec.WithLabelValues are documented race-safe (RESEARCH §1/§4 — internal sharded sync.Mutex per label-set hash).
- The helper holds no state across calls; each invocation builds a fresh errgroup.
API note (RESEARCH §4 CRITICAL CORRECTION):
testutil.GatherAndCount takes a Gatherer (*prometheus.Registry implements). DO NOT use testutil.CollectAndCount — that takes a Collector (it builds its own pedantic registry internally) and will not compile against *prometheus.Registry. CONTEXT.md draft shorthand `CollectAndCount(reg, …)` was misleading; this helper uses the correct API.
Signature note (Rule 1 deviation from literal D-04 form):
The parameter is typed as the small CardinalityT interface rather than the concrete *testing.T. Production callers pass *testing.T transparently (it satisfies the interface). Negative-falsifiability sub-tests pass an in-package fake to capture the helper's t.FailNow() call without Go's t.Run-propagates-failure-to-parent semantics tripping the parent test. (Go testing.go:962 c.Fail() unconditionally propagates via c.parent.Fail(); there is no way to assert "this helper called Fatalf" from within a *testing.T-typed sub-test without parent contamination.) Plan 03-04 acceptance criterion grep for the literal `*testing.T` form is relaxed to the equivalent `CardinalityT` interface.
func (*TestEnv) CaptureRecords ¶
func (te *TestEnv) CaptureRecords()
CaptureRecords rewires te.Logger to write JSON records into te.Buffer (D-12). Idempotent: subsequent calls are no-ops, preserving any records already written. The default discard handler is replaced only on the first call so tests can opt-in to capture without resetting state.
Concurrency: the underlying slog.JSONHandler serializes its writes via its own internal mutex; te.Buffer is therefore safe for concurrent loggers spawned after CaptureRecords returns. Use a sync.Mutex-guarded buffer wrapper if you need ordering guarantees across multiple goroutines.
func (*TestEnv) LoggedRecords ¶
LoggedRecords parses the captured buffer line-by-line into a slice of JSON-decoded maps. Tolerates an empty buffer (returns nil) and skips blank trailing lines. Each call re-parses the buffer so tests CAN call it multiple times if they wish to observe streaming.
type TracingConfig ¶
type TracingConfig struct {
// Enabled gates whether the SDK TracerProvider is built. When false a noop
// provider is installed and no exporter is initialized.
Enabled bool
// Endpoint is the YAML-configured OTLP collector address. The actual
// endpoint used at runtime is resolved by OTLPEndpoint() — env vars
// override this value (OBS-12).
Endpoint string
// Protocol is "grpc" (default) or "http". Phase 1 wires gRPC; HTTP is the
// fallback for Phase 6 hardening.
Protocol string
// Insecure allows a plaintext OTLP connection. When an env var sets an
// http:// endpoint and Insecure is false, Phase 6 TRC-09 rejects the
// configuration and installs a noop provider.
Insecure bool
// Timeout bounds exporter init. Default 5s. A misconfigured collector
// MUST NOT hang process startup (OBS-11).
Timeout time.Duration
// SampleRatio is the TraceIDRatioBased sampler ratio (TRC-05). Defaults to
// 0.01 (1%) when Enabled=true. Clamped to [0, 1] at sampler construction.
SampleRatio float64
// ParentMode selects the parent-honoring sampler policy (TRC-05/06/07).
// Empty/"none" (default) = standalone TraceIDRatioBased (KD-05 default).
// "capped" = parentCappedSampler (TRC-06).
// "strict" = ParentBased (TRC-07; WARN at startup).
ParentMode string
// ParentMaxQPS caps the number of sampled-parent spans honored per second
// when ParentMode="capped" (TRC-06). Default 100. Clamped to >=1.
ParentMaxQPS int
}
TracingConfig governs OTLP/gRPC trace egress.
func (TracingConfig) OTLPEndpoint ¶
func (c TracingConfig) OTLPEndpoint() (endpoint string, fromEnv bool)
OTLPEndpoint resolves the OBS-12 precedence chain:
OTEL_EXPORTER_OTLP_ENDPOINT > OTEL_EXPORTER_OTLP_TRACES_ENDPOINT > c.Endpoint (YAML) > "" (default — caller installs noop or relies on SDK default).
The fromEnv return tells the caller whether the value came from an env var. When fromEnv is true, the caller MUST NOT pass otlptracegrpc.WithEndpoint (the SDK reads the env var itself; passing the option overrides the env — see Pitfall 9).
Source Files
¶
- baggage_filter.go
- bsp_self_metrics.go
- build_default.go
- catalog_auth.go
- catalog_bolt.go
- catalog_cache.go
- catalog_cypher.go
- catalog_embed.go
- catalog_http.go
- catalog_knowledge_policy.go
- catalog_mvcc.go
- catalog_redstubs.go
- catalog_replication.go
- catalog_search.go
- catalog_storage.go
- config.go
- doc.go
- exemplar.go
- health.go
- k8s_detect.go
- knowledgepolicy_metrics_ref.go
- legacy_translation.go
- listener.go
- logger.go
- logging.go
- mandatory_fields.go
- metrics.go
- pprof.go
- provider.go
- recovering.go
- redaction.go
- registration.go
- registry.go
- resource.go
- sampler.go
- span_redactor.go
- testenv.go