Merge pull request 'x:Metric: say which node wrote each sample' (#133) from fix/metric-node-id into main
ci / fork-checks (push) Skipped
ci / build (push) Skipped
github/ci (branch) GitHub Actions
ci / github (push) Successful in 45m8s

Reviewed-on: #133
This commit was merged in pull request #133.
This commit is contained in:
2026-09-30 18:56:20 +00:00
4 changed files with 28 additions and 1 deletions
@@ -104,6 +104,12 @@ impl StoredMetric {
pub fn timestamp(&self) -> u64 { pub fn timestamp(&self) -> u64 {
SnowflakeIdGenerator::to_timestamp(self.id) SnowflakeIdGenerator::to_timestamp(self.id)
} }
/// The node that wrote the sample. Histogram totals are per node, so a
/// reader diffs them per node.
pub fn node_id(&self) -> u64 {
SnowflakeIdGenerator::to_node_id(self.id)
}
} }
/// What the node wrote last, so counters and histograms are written as /// What the node wrote last, so counters and histograms are written as
+5 -1
View File
@@ -6,7 +6,7 @@
//! `x:Metric/get` and `/query` over the stored history (monitoring spec, //! `x:Metric/get` and `/query` over the stored history (monitoring spec,
//! "Interfaces"). Samples are server-level (MON-31) and read-only (MON-32). //! "Interfaces"). Samples are server-level (MON-31) and read-only (MON-32).
//! A sample's `timestamp` comes from its id. //! A sample's `timestamp` and `nodeId` come from its id.
use crate::{ use crate::{
api::query::QueryResponseBuilder, api::query::QueryResponseBuilder,
@@ -54,12 +54,16 @@ fn metric_type(metric: &Metric) -> MetricType {
fn to_value(sample: StoredMetric) -> JmapValue<'static> { fn to_value(sample: StoredMetric) -> JmapValue<'static> {
let timestamp = sample.timestamp(); let timestamp = sample.timestamp();
let node_id = sample.node_id();
let mut value = sample.metric.into_value(); let mut value = sample.metric.into_value();
if let JmapValue::Object(obj) = &mut value { if let JmapValue::Object(obj) = &mut value {
obj.insert_unchecked( obj.insert_unchecked(
Property::Timestamp, Property::Timestamp,
JmapValue::Str(UTCDateTime::from_timestamp(timestamp as i64).to_string().into()), JmapValue::Str(UTCDateTime::from_timestamp(timestamp as i64).to_string().into()),
); );
// Histograms are running totals per node; without this a reader
// diffs one node's total against another's
obj.insert_unchecked(Property::NodeId, JmapValue::Number(node_id.into()));
} }
value value
} }
+6
View File
@@ -108,6 +108,12 @@ impl SnowflakeIdGenerator {
(id >> (SEQUENCE_LEN + NODE_ID_LEN)) / 1000 + DEFAULT_EPOCH (id >> (SEQUENCE_LEN + NODE_ID_LEN)) / 1000 + DEFAULT_EPOCH
} }
// inbuxa: the node that made the id, so per-node history (metric
// totals) can be told apart
pub fn to_node_id(id: u64) -> u64 {
id & NODE_ID_MASK
}
#[inline(always)] #[inline(always)]
pub fn past_id(&self, period: Duration) -> Option<u64> { pub fn past_id(&self, period: Duration) -> Option<u64> {
self.epoch.elapsed().ok().map(|elapsed| { self.epoch.elapsed().ok().map(|elapsed| {
+11
View File
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]> * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* *
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/ */
use crate::utils::server::TestServer; use crate::utils::server::TestServer;
@@ -53,6 +55,15 @@ pub async fn test(test: &TestServer) {
); );
assert_eq!(metrics.len(), metric_ids.len()); assert_eq!(metrics.len(), metric_ids.len());
// Every sample says which node wrote it, so histogram totals can be
// diffed per node
for metric in metrics {
assert!(
metric.get("nodeId").is_some_and(|v| v.is_u64()),
"Missing nodeId in {metric}"
);
}
// Fetch the last 48 hours of metrics // Fetch the last 48 hours of metrics
let metric_ids = admin let metric_ids = admin
.registry_query( .registry_query(