|
|
|
@@ -74,13 +74,14 @@ function pick(entry) {
|
|
|
|
|
from: Math.min(live.window_start, snaps.opened ? snaps.opened.window_start : Infinity),
|
|
|
|
|
to: live.window_end,
|
|
|
|
|
current: live.current || [],
|
|
|
|
|
step: live.step_seconds,
|
|
|
|
|
isLive: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const snap = snaps.closed || snaps.opened;
|
|
|
|
|
if (snap && !snap.error) {
|
|
|
|
|
return {
|
|
|
|
|
series: merge(parseSeries(snap.series)), from: snap.window_start, to: snap.window_end, current: [], isLive: false,
|
|
|
|
|
series: merge(parseSeries(snap.series)), from: snap.window_start, to: snap.window_end, current: [], step: snap.step_seconds, isLive: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
@@ -105,7 +106,7 @@ export function alertMetrics(entry, startsAtISO, resolvedISO) {
|
|
|
|
|
const endSec = resolvedISO ? Date.parse(resolvedISO) / 1000 : null;
|
|
|
|
|
return h('div', { class: 'metrics' },
|
|
|
|
|
currentLine(entry, data),
|
|
|
|
|
data.series.length ? chart(data, startSec, endSec) : h('div', { class: 'metrics-note', text: 'No samples in this window.' }),
|
|
|
|
|
data.series.length ? (isBool(data.series) ? timeline(data, startSec, endSec) : chart(data, startSec, endSec)) : h('div', { class: 'metrics-note', text: 'No samples in this window.' }),
|
|
|
|
|
legend(data.series),
|
|
|
|
|
entry.expr && h('code', { class: 'metrics-expr', text: entry.expr }),
|
|
|
|
|
);
|
|
|
|
@@ -130,6 +131,98 @@ function legend(series) {
|
|
|
|
|
h('li', {}, h('span', { class: `mc-swatch s${i}` }), h('span', { text: seriesName(s.labels) }))));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addMarkers draws the dashed "Alert started" / "Incident resolved" lines.
|
|
|
|
|
function addMarkers(root, { t0, t1, x, top, bottom, startSec, endSec }) {
|
|
|
|
|
const marker = (t, cls, text) => {
|
|
|
|
|
if (t == null || t < t0 || t > t1) return;
|
|
|
|
|
const m = svg('line', { class: `mc-marker ${cls}`, x1: x(t), x2: x(t), y1: top, y2: bottom });
|
|
|
|
|
m.appendChild(svg('title', {}, `${text} ${when(iso(t))}`));
|
|
|
|
|
root.appendChild(m);
|
|
|
|
|
};
|
|
|
|
|
marker(startSec, 'start', 'Alert started');
|
|
|
|
|
marker(endSec, 'end', 'Incident resolved');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isBool is true when every drawn series only ever reads 0 or 1; such a metric
|
|
|
|
|
// says when something was on, so it is drawn as a timeline, not a line.
|
|
|
|
|
function isBool(allSeries) {
|
|
|
|
|
const series = allSeries.slice(0, MAX_SERIES);
|
|
|
|
|
let any = false;
|
|
|
|
|
for (const s of series) {
|
|
|
|
|
for (const [, v] of s.points) {
|
|
|
|
|
if (v !== 0 && v !== 1) return false;
|
|
|
|
|
any = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return any;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runsOf folds a series into [from, to, value] spans. A sample lasts until the
|
|
|
|
|
// next one, or one step when the next is missing or more than 1.5 steps away.
|
|
|
|
|
function runsOf(points, step, tEnd) {
|
|
|
|
|
const runs = [];
|
|
|
|
|
points.forEach(([t, v], n) => {
|
|
|
|
|
const next = points[n + 1];
|
|
|
|
|
const contiguous = next && (!step || next[0] - t <= step * 1.5);
|
|
|
|
|
const end = Math.min(contiguous ? next[0] : t + (step || 0), tEnd);
|
|
|
|
|
const last = runs[runs.length - 1];
|
|
|
|
|
if (last && last[2] === v && last[1] === t) last[1] = end;
|
|
|
|
|
else runs.push([t, end, v]);
|
|
|
|
|
});
|
|
|
|
|
return runs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function timeline(data, startSec, endSec) {
|
|
|
|
|
const W = 480;
|
|
|
|
|
const padL = 40;
|
|
|
|
|
const padR = 8;
|
|
|
|
|
const padT = 8;
|
|
|
|
|
const padB = 18;
|
|
|
|
|
const rowH = 16;
|
|
|
|
|
const rowGap = 4;
|
|
|
|
|
const series = data.series.slice(0, MAX_SERIES);
|
|
|
|
|
const H = padT + padB + series.length * rowH + (series.length - 1) * rowGap;
|
|
|
|
|
|
|
|
|
|
let t0 = data.from; let t1 = data.to;
|
|
|
|
|
for (const s of series) {
|
|
|
|
|
for (const [t] of s.points) {
|
|
|
|
|
if (t < t0) t0 = t;
|
|
|
|
|
if (t > t1) t1 = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (t1 === t0) t1 = t0 + 1;
|
|
|
|
|
const x = (t) => padL + ((t - t0) / (t1 - t0)) * (W - padL - padR);
|
|
|
|
|
|
|
|
|
|
const root = svg('svg', {
|
|
|
|
|
class: 'metric-chart', viewBox: `0 0 ${W} ${H}`, role: 'img',
|
|
|
|
|
'aria-label': `On/off timeline from ${when(iso(t0))} to ${when(iso(t1))}`,
|
|
|
|
|
});
|
|
|
|
|
root.appendChild(svg('text', { class: 'mc-label', x: padL, y: H - 4, 'text-anchor': 'start' }, when(iso(t0))));
|
|
|
|
|
root.appendChild(svg('text', { class: 'mc-label', x: W - padR, y: H - 4, 'text-anchor': 'end' }, when(iso(t1))));
|
|
|
|
|
|
|
|
|
|
series.forEach((s, i) => {
|
|
|
|
|
const top = padT + i * (rowH + rowGap);
|
|
|
|
|
const name = seriesName(s.labels);
|
|
|
|
|
// Without a sample the alert condition did not hold: the bare track.
|
|
|
|
|
root.appendChild(svg('rect', { class: 'mc-track', x: padL, y: top, width: W - padL - padR, height: rowH }));
|
|
|
|
|
if (series.length > 1) {
|
|
|
|
|
root.appendChild(svg('text', { class: 'mc-label', x: padL - 4, y: top + rowH / 2 + 3, 'text-anchor': 'end' }, `${i + 1}`));
|
|
|
|
|
}
|
|
|
|
|
for (const [from, to, v] of runsOf(s.points, data.step, t1)) {
|
|
|
|
|
const w = Math.max(x(to) - x(from), 1.5);
|
|
|
|
|
const on = v === 1;
|
|
|
|
|
const rect = svg('rect', {
|
|
|
|
|
class: on ? `mc-on s${i}` : 'mc-off',
|
|
|
|
|
x: x(from), y: on ? top : top + rowH / 2 - 1.5, width: w, height: on ? rowH : 3,
|
|
|
|
|
});
|
|
|
|
|
rect.appendChild(svg('title', {}, `${name}: ${v} from ${when(iso(from))} to ${when(iso(to))}`));
|
|
|
|
|
root.appendChild(rect);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
addMarkers(root, { t0, t1, x, top: padT, bottom: H - padB, startSec, endSec });
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function chart(data, startSec, endSec) {
|
|
|
|
|
const W = 480;
|
|
|
|
|
const H = 130;
|
|
|
|
@@ -172,14 +265,7 @@ function chart(data, startSec, endSec) {
|
|
|
|
|
root.appendChild(svg('text', { class: 'mc-label', x: padL, y: H - 4, 'text-anchor': 'start' }, when(iso(t0))));
|
|
|
|
|
root.appendChild(svg('text', { class: 'mc-label', x: W - padR, y: H - 4, 'text-anchor': 'end' }, when(iso(t1))));
|
|
|
|
|
|
|
|
|
|
const marker = (t, cls, text) => {
|
|
|
|
|
if (t == null || t < t0 || t > t1) return;
|
|
|
|
|
const m = svg('line', { class: `mc-marker ${cls}`, x1: x(t), x2: x(t), y1: padT, y2: H - padB });
|
|
|
|
|
m.appendChild(svg('title', {}, `${text} ${when(iso(t))}`));
|
|
|
|
|
root.appendChild(m);
|
|
|
|
|
};
|
|
|
|
|
marker(startSec, 'start', 'Alert started');
|
|
|
|
|
marker(endSec, 'end', 'Incident resolved');
|
|
|
|
|
addMarkers(root, { t0, t1, x, top: padT, bottom: H - padB, startSec, endSec });
|
|
|
|
|
|
|
|
|
|
series.forEach((s, i) => {
|
|
|
|
|
if (!s.points.length) return;
|
|
|
|
|