Dashboard: diff histogram totals per node #73

Merged
jcoffey-dev merged 1 commits from fix/histogram-deltas-per-node into main 2026-09-30 18:56:39 +00:00
3 changed files with 79 additions and 6 deletions

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, expect, it } from 'vitest';
import type { Metric } from './types/metrics';
import { deltaHistograms } from './helpers';
const total = (timestamp: string, count: number, sum: number, nodeId?: number): Metric => ({
'@type': 'Histogram',
metric: 'delivery.attempt-time',
count,
sum,
timestamp,
nodeId,
});
const sums = (samples: Metric[]) =>
samples.reduce(
(acc, m) => (m['@type'] === 'Histogram' ? { count: acc.count + m.count, sum: acc.sum + m.sum } : acc),
{ count: 0, sum: 0 },
);
describe('deltaHistograms', () => {
it('diffs one node’s running totals', () => {
const out = deltaHistograms([
total('2026-09-30T10:00:00Z', 10, 1000),
total('2026-09-30T11:00:00Z', 12, 1400),
total('2026-09-30T12:00:00Z', 15, 2000),
]);
expect(sums(out)).toEqual({ count: 5, sum: 1000 });
});
it('diffs each node against itself, never against another node', () => {
// A busy fast node and a quiet node with one slow attempt, interleaved
const out = deltaHistograms([
total('2026-09-30T10:00:00Z', 100, 20_000, 0),
total('2026-09-30T10:00:01Z', 4, 300_000, 1),
total('2026-09-30T11:00:00Z', 110, 22_000, 0),
total('2026-09-30T11:00:01Z', 4, 300_000, 1),
total('2026-09-30T12:00:00Z', 120, 24_000, 0),
total('2026-09-30T12:00:01Z', 5, 301_000, 1),
]);
expect(sums(out)).toEqual({ count: 21, sum: 5_000 });
});
it('skips a restart, where a node’s totals start again from zero', () => {
const out = deltaHistograms([
total('2026-09-30T10:00:00Z', 50, 5000, 2),
total('2026-09-30T11:00:00Z', 3, 300, 2),
total('2026-09-30T12:00:00Z', 5, 700, 2),
]);
expect(sums(out)).toEqual({ count: 2, sum: 400 });
});
it('passes counters and gauges through untouched', () => {
const counter: Metric = { '@type': 'Counter', metric: 'auth.success', count: 3, nodeId: 1 };
expect(deltaHistograms([counter])).toEqual([counter]);
});
});
+10 -3
View File
@@ -1,7 +1,10 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
import type { Metric, MetricId, Period } from './types/metrics';
@@ -15,19 +18,23 @@ export function metricToScalar(m: Metric): number {
return m.count;
}
// INBUXA: histograms are running totals per node, so they're diffed per
// node; in a cluster, diffing across nodes subtracts one node's total from
// another's.
export function deltaHistograms(samples: Metric[]): Metric[] {
const nonHistograms: Metric[] = [];
const byMetric = new Map<MetricId, Metric[]>();
const byMetric = new Map<string, Metric[]>();
for (const m of samples) {
if (m['@type'] !== 'Histogram') {
nonHistograms.push(m);
continue;
}
let list = byMetric.get(m.metric);
const key = `${m.metric}|${m.nodeId ?? ''}`;
let list = byMetric.get(key);
if (!list) {
list = [];
byMetric.set(m.metric, list);
byMetric.set(key, list);
}
list.push(m);
}
+7 -3
View File
@@ -1,15 +1,19 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
export type MetricId = string;
// INBUXA: nodeId is the node that wrote the sample (servers from 2026.9.30 on)
export type Metric =
| { '@type': 'Counter'; metric: MetricId; count: number; timestamp?: string }
| { '@type': 'Gauge'; metric: MetricId; count: number; timestamp?: string }
| { '@type': 'Histogram'; metric: MetricId; count: number; sum: number; timestamp?: string };
| { '@type': 'Counter'; metric: MetricId; count: number; timestamp?: string; nodeId?: number }
| { '@type': 'Gauge'; metric: MetricId; count: number; timestamp?: string; nodeId?: number }
| { '@type': 'Histogram'; metric: MetricId; count: number; sum: number; timestamp?: string; nodeId?: number };
export type Period = { kind: 'preset'; preset: PresetKey } | { kind: 'custom'; from: Date; to: Date };