Cluster roster: each node's mail and connections over the period #80

Merged
jcoffey-dev merged 1 commits from feat/node-sparklines into main 2026-10-05 22:13:22 +00:00
5 changed files with 107 additions and 20 deletions

No files matched your search

@@ -10,6 +10,7 @@ import {
cleanShare,
latest,
load,
nodeSeries,
mean,
nearQuota,
queueHealth,
@@ -138,3 +139,25 @@ describe('cleanShare', () => {
expect(cleanShare([])).toBe(1);
});
});
describe('nodeSeries', () => {
const from = new Date('2026-10-05T00:00:00Z');
const to = new Date('2026-10-05T04:00:00Z');
const count = (hour: number, n: number, nodeId?: number): Metric => ({
'@type': 'Counter',
metric: 'queue.message-queued',
count: n,
timestamp: `2026-10-05T0${hour}:30:00Z`,
nodeId,
});
const samples = [count(0, 3, 1), count(0, 5, 2), count(2, 4, 1), count(3, 7, 2)];
it('counts only what the node wrote, slice by slice', () => {
expect(nodeSeries(samples, 1, ['queue.message-queued'], from, to, 4)).toEqual([3, 0, 4, 0]);
expect(nodeSeries(samples, 2, ['queue.message-queued'], from, to, 4)).toEqual([5, 0, 0, 7]);
});
it('finds nothing in samples that don’t say which node wrote them', () => {
expect(nodeSeries([count(0, 3), count(1, 2)], 1, ['queue.message-queued'], from, to, 4)).toEqual([0, 0, 0, 0]);
});
});
+31 -2
View File
@@ -187,7 +187,7 @@ export function summarize(samples: Metric[], ids: string[], timing: boolean): nu
return total(samples, ids);
}
const CONNECTION_STARTS = [
export const CONNECTION_STARTS = [
'smtp.connection-start',
'imap.connection-start',
'pop3.connection-start',
@@ -224,7 +224,36 @@ export const STORE_ERRORS = [
'store.http-store-error',
];
export const THREAD_ERRORS = ['server.thread-error'];
export const CLUSTER_IDS = new Set([...COORDINATION, ...STORE_ERRORS, ...THREAD_ERRORS]);
/** inbuxa: what each node handled, for the strips on its card in the cluster roster. */
export const NODE_MAIL = [...RECEIVED, ...SENT];
export const CLUSTER_IDS = new Set([
...COORDINATION,
...STORE_ERRORS,
...THREAD_ERRORS,
...NODE_MAIL,
...CONNECTION_STARTS,
]);
/**
* inbuxa: one node's share of `series`: only the samples it wrote. Servers
* before 2026.9.30 don't say which node wrote a sample, so nothing matches.
*/
export function nodeSeries(
samples: Metric[],
nodeId: number,
ids: string[],
from: Date,
to: Date,
buckets: number,
): number[] {
return series(
samples.filter((m) => m.nodeId === nodeId),
ids,
from,
to,
buckets,
);
}
/** A node is reported stale when it hasn't renewed its lease in this long. */
export const STALE_AFTER_MS = 3 * 60_000;
@@ -15,12 +15,15 @@ import { useLiveMetricsStore } from '../stores/liveMetricsStore';
import { formatValue, getBucketCount } from '../helpers';
import {
CLUSTER_IDS,
CONNECTION_STARTS,
COORDINATION,
LIVE_IDS,
STALE_AFTER_MS,
STORE_ERRORS,
THREAD_ERRORS,
NODE_MAIL,
cleanShare,
nodeSeries,
series,
total,
} from '../commandCenter';
@@ -30,7 +33,7 @@ import { usePageHistory } from '../usePageHistory';
import type { Tone } from '../tones';
import { Gauge } from './Gauge';
import { Panel } from './Panel';
import { Readout } from './Readout';
import { BarStrip, Readout } from './Readout';
import { TrendChart } from './TrendChart';
const NODES_VIEW = 'x:ClusterNode';
@@ -145,6 +148,16 @@ export function ClusterView() {
];
const nodesHref = nodesSection && canViewObject(NODES_VIEW) ? `/${nodesSection}/${NODES_VIEW}` : null;
const n = (v: number) => formatValue(v, 'number');
// inbuxa: each node's own traffic, from servers that say which node wrote a sample.
const perNode = samples.some((m) => m.nodeId !== undefined);
const nodeStrips = (nodeId: number) =>
[
{ label: t('cc.nodeMail', 'Mail'), ids: NODE_MAIL },
{ label: t('cc.nodeConnections', 'Connections'), ids: CONNECTION_STARTS },
].map((row) => {
const bars = nodeSeries(samples, nodeId, row.ids, window.from, window.to, STRIP);
return { ...row, bars, total: bars.reduce((a, b) => a + b, 0) };
});
return (
<div className="space-y-5">
@@ -278,6 +291,21 @@ export function ClusterView() {
style={{ width: `${Math.max(2, share * 100)}%` }}
/>
</div>
{perNode && node.nodeId !== undefined && (
<div className="mt-3 space-y-1.5 border-t pt-2.5">
{nodeStrips(Number(node.nodeId)).map((row) => (
<div key={row.label} className="grid grid-cols-[6.5rem_1fr] items-end gap-2">
<div className="flex items-baseline justify-between gap-1 text-[10px] uppercase tracking-wider text-muted-foreground">
<span className="truncate">{row.label}</span>
<span className="font-mono text-xs font-semibold tabular-nums text-foreground">
{n(row.total)}
</span>
</div>
<BarStrip bars={row.bars} tone={up ? 'primary' : 'crit'} className="h-4" />
</div>
))}
</div>
)}
</div>
);
})}
+21 -16
View File
@@ -25,7 +25,6 @@ interface ReadoutProps {
* strip of bars showing how it moved over the period.
*/
export function Readout({ label, value, detail, bars, tone = 'primary', href }: ReadoutProps) {
const max = bars ? Math.max(1, ...bars) : 1;
const body = (
<div
className={cn(
@@ -38,21 +37,7 @@ export function Readout({ label, value, detail, bars, tone = 'primary', href }:
</div>
<div className="mt-1.5 font-mono text-2xl font-semibold leading-none tabular-nums tracking-tight">{value}</div>
{detail && <div className="mt-1 truncate text-[11px] text-muted-foreground">{detail}</div>}
{bars && bars.length > 0 && (
<div className="mt-auto flex h-5 items-end gap-px pt-2" aria-hidden>
{bars.map((b, i) => (
<span
key={i}
className="flex-1 rounded-[1px]"
style={{
height: `${Math.max(8, (b / max) * 100)}%`,
background: TONE_VAR[tone],
opacity: b === 0 ? 0.15 : 0.35 + 0.65 * (b / max),
}}
/>
))}
</div>
)}
{bars && bars.length > 0 && <BarStrip bars={bars} tone={tone} className="mt-auto pt-2" />}
</div>
);
return href ? (
@@ -66,3 +51,23 @@ export function Readout({ label, value, detail, bars, tone = 'primary', href }:
body
);
}
/** The period in slices, as a strip of bars: taller and brighter as the slice is busier. */
export function BarStrip({ bars, tone = 'primary', className }: { bars: number[]; tone?: Tone; className?: string }) {
const max = Math.max(1, ...bars);
return (
<div className={cn('flex h-5 items-end gap-px', className)} aria-hidden>
{bars.map((b, i) => (
<span
key={i}
className="flex-1 rounded-[1px]"
style={{
height: `${Math.max(8, (b / max) * 100)}%`,
background: TONE_VAR[tone],
opacity: b === 0 ? 0.15 : 0.35 + 0.65 * (b / max),
}}
/>
))}
</div>
);
}
+3 -1
View File
@@ -1954,7 +1954,9 @@
"average": "average",
"zoomReset": "Back to {{period}}",
"zoomed": "Zoom",
"dragToZoom": "Drag to zoom"
"dragToZoom": "Drag to zoom",
"nodeMail": "Mail",
"nodeConnections": "Connections"
},
"commands": {
"actionHint": "Server action",