Expose database/table/volume/node storage sizes as gauges - #49
Expose database/table/volume/node storage sizes as gauges#49sleekmountaincat wants to merge 2 commits into
Conversation
Harper records database-size, table-size and storage-volume analytics (plus node-storage on v5), but none of them were mapped to metrics, so the exporter had no size visibility at all. Add plain gauges for each: per-database size/used/free/audit bytes, per-table size, per-database volume statfs, and node storage. These cannot ride customMetrics: that path renders quantile summaries and these records are point-in-time snapshots, so it emits zeros. node_storage is unlabeled and its source records are interval-gated, so it is removed from the registry when no record is present rather than rendering prom-client's default 0 for a registered-but-unset gauge. Verified live against harpersystems/harper 4.7.38 (LMDB branch, seeded table; node-storage absent-case) and against production v5 analytics record shapes (RocksDB branch, node-storage present-case). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request adds several Prometheus gauges to track storage-size snapshots (such as database, table, node storage, and volume sizes) populated from analytics records. The feedback suggests a more efficient and idiomatic approach to handling the unlabeled node_storage_gauge by using its built-in remove() method to clear the series instead of dynamically unregistering and re-registering the entire metric from the Prometheus registry.
| // node_storage_gauge is unlabeled, and an unlabeled prom-client gauge that | ||
| // has been registered renders `0` even when never set. node-storage | ||
| // analytics are interval-gated (analytics.storageInterval) and absent on | ||
| // instances that disable them, so a registered-but-unset gauge would report | ||
| // a fake "0 bytes". Remove it from the registry instead; the analytics | ||
| // case below re-registers it whenever a record is actually present. | ||
| Prometheus.register.removeSingleMetric("harperdb_node_storage_bytes"); |
There was a problem hiding this comment.
Unregistering and re-registering a metric on every scrape is an anti-pattern in Prometheus client libraries and can lead to performance overhead or registry pollution. Instead of removing the entire metric from the registry, you can use the built-in remove() method on the gauge itself. For an unlabeled gauge, calling node_storage_gauge.remove() will remove the unlabeled series from the gauge's internal map, preventing it from being exported when no data is present, while keeping the metric registered.
// node_storage_gauge is unlabeled, and an unlabeled prom-client gauge that
// has been registered renders `0` even when never set. node-storage
// analytics are interval-gated (analytics.storageInterval) and absent on
// instances that disable them, so a registered-but-unset gauge would report
// a fake "0 bytes". Remove the unlabeled series from the gauge instead of
// unregistering/re-registering it, which is more efficient and idiomatic.
node_storage_gauge.remove();| case "node-storage": | ||
| if (!Prometheus.register.getSingleMetric("harperdb_node_storage_bytes")) { | ||
| Prometheus.register.registerMetric(node_storage_gauge); | ||
| } | ||
| gaugeSet(node_storage_gauge, {}, metric.size); | ||
| break; |
There was a problem hiding this comment.
node-storage analytics are interval-gated (analytics.storageInterval, default every 10th aggregation cycle), so their write cadence is longer than the windowed search (1.5 aggregation periods) and the windowed lookup left the gauge absent between writes - present on only a fraction of scrapes in a fleet scrape. Fetch the newest record directly (id-descending, first match) so the gauge carries the latest known measurement on every scrape, and stays absent only when no record exists at all. Verified on harper 5.2.7 (npm, RocksDB default): gauge present with the correct value on every consecutive scrape, including immediately after a restart with the newest record ~20 minutes old. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v5 verification (harper 5.2.7 via npm, RocksDB default, TLS endpoints)Ran the same live test against v5.2.7 ( Working on v5 / RocksDB branch:
Design change in the follow-up commit (node-storage): the windowed search can't carry this metric — Upstream findings (Harper core, not this component):
Combined coverage: v4.7.38/LMDB e2e (all gauges incl. per-table) + v5.2.7/RocksDB e2e (all applicable gauges incl. node-storage present-case) + prod record-shape cross-checks on harper-pro 5.2.0. |
What
Surface Harper's storage-size analytics as Prometheus gauges. Harper already records
database-size,table-size, andstorage-volumeintohdb_analyticson every aggregation cycle (v4 and v5; plusnode-storageon v5), but the exporter never mapped those records, so no size metric exists anywhere in its output today. New gauges:harperdb_database_size_bytesdatabasedatabase-size.size(RocksDB: sum of.sst; LMDB: data file size)harperdb_database_audit_size_bytesdatabasedatabase-size.transactionLog(RocksDB) /.audit(LMDB)harperdb_database_used_bytesdatabasedatabase-size.used(LMDB only; set only when present)harperdb_database_free_bytesdatabasedatabase-size.free(LMDB only; set only when present)harperdb_table_size_bytesdatabase,tabletable-size.sizeharperdb_node_storage_bytesnode-storage.size(whole HDB dir)harperdb_database_volume_size_bytesdatabasestorage-volume.size(statfs of the DB's volume)harperdb_database_volume_free_bytesdatabasestorage-volume.freeharperdb_database_volume_available_bytesdatabasestorage-volume.availableMechanically: gauge definitions beside the existing
harperdb_database_*block, resets inget()with the others (the stuck-value guard), and four new cases ingenerateMetricsFromAnalytics's switch ahead ofdefault:.Why not customMetrics
These records cannot be surfaced through the existing
customMetricssetting: that path renders the quantile-summary shape (p1/p10/median/mean/count), and the size records are plain{database, size}snapshots — configured that way it emits all zeros, silently. Plain gauges are the correct representation. (A follow-up worth its own issue: teachcustomMetricsa gauge mode so record shapes like these are configurable without code changes. Also noticed in passing: thefilesystem_*_bytesgauges are defined and reset but never set anywhere — dead code from an earlier iteration; left untouched here.)Notes
transactionLog ?? audit, andused/freeset only when present, so RocksDB databases don't emit fake zero series.harperdb_node_storage_bytesneeds special treatment:node-storageanalytics are interval-gated (analytics.storageInterval, off by default), and a registered-but-never-set unlabeled prom-client gauge renders0— a fake "node storage: 0 bytes" on every instance with the interval disabled. The gauge is therefore removed from the registry each scrape and re-registered only when a record is present, so it is absent rather than zero when the data doesn't exist. (Labeled gauges get this behavior for free; verified both behaviors against prom-client directly.)Verification (live,
harpersystems/harpercontainer + this component mounted)node --check resources.jsclean.data.widgetstable with 5 records, waited an aggregation cycle, scraped with the admin credential andAccept: text/plain(note for anyone testing by hand: without a textAccept, the REST layer JSON-encodes the whole exposition as one string):harperdb_database_size_bytes{database="data"} 151552/{database="system"} 532480harperdb_database_used_bytes/_free_bytespopulated (LMDB branch),harperdb_database_audit_size_bytesmatches the analytics records exactlyharperdb_table_size_bytes{database="data",table="widgets"} 24576(and one series per system table)harperdb_database_volume_{size,free,available}_bytesmatch the container volume's statfsharpersystems/harper:latestis 4.7.38, so the LMDB-branch fields (used/free/audit) above are v4's own writer output.harperdb_node_storage_bytes: the 4.7.38 image has no node-storage writer (v5 feature), which exercised the absent case end to end — the gauge is absent, not a fake zero. The present case is covered two ways: the remove/re-register idiom verified directly against prom-client (register -> set -> renders the value), and the record shape it consumes ({metric: "node-storage", size}) verified against live production analytics on a v5 fabric instance (fresh records, size 1.46GB).usgm-er-teflon) confirms the RocksDB-branchdatabase-sizeshape ({database, size, transactionLog}) maps onto the same cases.Motivation
Walmart asked for per-database size visibility. With this change the series flow automatically through the existing fleet scrape of
/prometheus_exporter/metrics(no pipeline changes) on every cluster where the exporter is converged — including both Walmart clusters — and become alertable/trendable in Mimir. (Their other route, the Grafana Connector'sget_analyticspanels, already chartstable-sizelive per cluster; this PR is the fleet-metrics half.)🤖 Generated with Claude Code