Dashboard: diff histogram totals per node #73
No files matched your search
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
Reference in new issue
Block a user