feat(syslog): lines beside the readings: around an anomaly, in get_anomaly, and marked on host and SNMP charts (#6002)
get_anomaly's circumstances gain the host's or SNMP device's syslog warnings and worse (mcp_events / mcp_resource_events). An anomaly row opens "Syslog around it", read only when opened. A shared Chart.js plugin (chart-marks.js) draws a dashed line at each syslog error on the host metrics charts and an SNMP device's charts, with a line saying how many. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Marks on a time chart: a thin line at each moment something was logged
|
||||
// (#6002, the syslog errors of a host or a device), so a reading's jump can be
|
||||
// read against what the device said at the time.
|
||||
//
|
||||
// A chart opts in by holding marks in options.plugins.stewardMarks.marks, as
|
||||
// [{at: epoch ms, level, text}]. A linear x axis takes the time itself; a
|
||||
// category axis of ISO times takes the nearest label. Hovering near a mark
|
||||
// shows its text in the chart's title attribute.
|
||||
(function () {
|
||||
if (!window.Chart) return;
|
||||
function xOf(chart, at) {
|
||||
var x = chart.scales.x;
|
||||
if (!x) return null;
|
||||
if (x.type !== 'category') return x.getPixelForValue(at);
|
||||
var labels = chart.data.labels || [];
|
||||
if (!labels.length) return null;
|
||||
var first = Date.parse(labels[0]), last = Date.parse(labels[labels.length - 1]);
|
||||
if (at < first || at > last) return null;
|
||||
var best = 0, gap = Infinity;
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
var d = Math.abs(Date.parse(labels[i]) - at);
|
||||
if (d < gap) { gap = d; best = i; }
|
||||
}
|
||||
return x.getPixelForValue(best);
|
||||
}
|
||||
Chart.register({
|
||||
id: 'stewardMarks',
|
||||
afterDatasetsDraw: function (chart, args, opts) {
|
||||
var marks = (opts && opts.marks) || [];
|
||||
if (!marks.length) return;
|
||||
var area = chart.chartArea, ctx = chart.ctx;
|
||||
var crit = window.stewardColor ? stewardColor('--red') : 'red';
|
||||
chart.$stewardMarks = [];
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([3, 3]);
|
||||
ctx.strokeStyle = crit;
|
||||
ctx.globalAlpha = 0.55;
|
||||
marks.forEach(function (m) {
|
||||
var px = xOf(chart, m.at);
|
||||
if (px == null || px < area.left || px > area.right) return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px, area.top);
|
||||
ctx.lineTo(px, area.bottom);
|
||||
ctx.stroke();
|
||||
chart.$stewardMarks.push({ x: px, text: m.text, at: m.at });
|
||||
});
|
||||
ctx.restore();
|
||||
},
|
||||
afterEvent: function (chart, args) {
|
||||
var near = (chart.$stewardMarks || []).filter(function (m) {
|
||||
return Math.abs(m.x - args.event.x) <= 3;
|
||||
});
|
||||
chart.canvas.title = near.map(function (m) {
|
||||
return new Date(m.at).toLocaleString([], { hour12: false }) + ' ' + m.text;
|
||||
}).join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch marks from `url` and put them on each chart, redrawing it; `note`
|
||||
// (an element, optional) says in words how many there are.
|
||||
window.stewardMarkCharts = function (url, charts, note) {
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) { return r.ok ? r.json() : { marks: [] }; })
|
||||
.then(function (body) {
|
||||
var marks = body.marks || [];
|
||||
charts.forEach(function (ch) {
|
||||
if (!ch) return;
|
||||
ch.options.plugins.stewardMarks = { marks: marks };
|
||||
ch.update('none');
|
||||
});
|
||||
if (note) {
|
||||
note.hidden = !marks.length;
|
||||
note.textContent = marks.length
|
||||
? (body.more ? 'The newest ' : '') + marks.length + ' syslog error' + (marks.length === 1 ? '' : 's') +
|
||||
' in this span marked with dashed red lines; hover one to read it.'
|
||||
: '';
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user