diff --git a/dashboard/src/components/SystemMetrics.vue b/dashboard/src/components/SystemMetrics.vue index 52561b58b..7753b9dac 100644 --- a/dashboard/src/components/SystemMetrics.vue +++ b/dashboard/src/components/SystemMetrics.vue @@ -37,6 +37,12 @@ let metricStream = null; const LIVE_REFRESH_INTERVAL_MSECS = 500; const LIVE_REFRESH_HISTORY_MSECS = 5*60*1000; // last 5 mins +function pruneGraphData(dataset, options) { + while (dataset.data.length && (dataset.data[0].x < options.scales.x.min)) { // remove elements beyond our tme window + dataset.data.shift(); + } +} + async function liveRefresh() { metricStream = await systemModel.getMetricStream(LIVE_REFRESH_INTERVAL_MSECS); @@ -44,22 +50,27 @@ async function liveRefresh() { metricStream.onmessage = (message) => { const data = JSON.parse(message.data); - if (data.cpu[0]) { // value can be null if no previous value + if (data.cpu[0]) { // since cpu% is relative, value can be null if no previous value cpuGraph.data.datasets[0].data.push({ x: data.cpu[1] * 1000, // cpuGraph.options.scales.x.max can be used for window edge, if we don't trust server timestamps . but using server timestamps handles network lags better y: data.cpu[0] }); - while (cpuGraph.data.datasets[0].data.length && (cpuGraph.data.datasets[0].data[0].x < cpuGraph.options.scales.x.min)) { // remove elements beyond our tme window - cpuGraph.data.datasets[0].data.shift(); - } - + pruneGraphData(cpuGraph.data.datasets[0], cpuGraph.options); cpuGraph.update('none'); } - memoryGraph.data.labels.push(moment(data.memory[1]*1000).format('hh:mm')); - memoryGraph.data.datasets[0].data.push((data.memory[0] / 1024 / 1024 / 1024).toFixed(2)); - memoryGraph.data.datasets[1].data.push((data.swap[0] / 1024 / 1024 / 1024).toFixed(2) + 2); + memoryGraph.data.datasets[0].data.push({ + x: data.memory[1] * 1000, + y: (data.memory[0] / 1024 / 1024 / 1024).toFixed(2) + }); + pruneGraphData(memoryGraph.data.datasets[0], memoryGraph.options); + + memoryGraph.data.datasets[1].data.push({ + x: data.swap[1] * 1000, + y: (data.swap[0] / 1024 / 1024 / 1024).toFixed(2) + }); + pruneGraphData(memoryGraph.data.datasets[1], memoryGraph.options); memoryGraph.update('none'); }; @@ -68,6 +79,10 @@ async function liveRefresh() { cpuGraph.options.scales.x.min += LIVE_REFRESH_INTERVAL_MSECS; cpuGraph.options.scales.x.max += LIVE_REFRESH_INTERVAL_MSECS; cpuGraph.update('none'); + + memoryGraph.options.scales.x.min += LIVE_REFRESH_INTERVAL_MSECS; + memoryGraph.options.scales.x.max += LIVE_REFRESH_INTERVAL_MSECS; + memoryGraph.update('none'); }, LIVE_REFRESH_INTERVAL_MSECS); } @@ -86,7 +101,7 @@ async function getMetrics(hours) { y: (v[0] / 1024 / 1024 / 1024).toFixed(2) }; }); - // assume that there is 1:1 timeline for swap and memory data + const swapData = result.swap.map(v => { return { x: v[1]*1000, @@ -210,19 +225,30 @@ async function refresh() { const memoryGraphOptions = { maintainAspectRatio: false, plugins: { - legend: false, + legend: { + display: false + }, + tooltip: { + callbacks: { + title: (tooltipItem) => moment(tooltipItem[0].raw.x).format(periods.find((p) => p.id === period.value).tooltipFormat) + } + } }, scales: { x: { + // we used to use 'time' type but it relies on the data to generate ticks. we may not have data for our time periods type: 'linear', - bounds: 'ticks', // otherwise data bound. https://www.chartjs.org/docs/latest/axes/cartesian/time.html#changing-the-scale-type-from-time-scale-to-logarithmic-linear-scale - min: now - period.value*60*60*1000, + min: now - (period.value === 0 ? LIVE_REFRESH_HISTORY_MSECS : period.value*60*60*1000), max: now, ticks: { autoSkip: true, // skip tick labels as needed autoSkipPadding: 20, // padding between ticks maxRotation: 0, // don't rotate the labels - maxTicksLimit: 15, // max tick labels to show + count: 7, // tick labels to show. anything more than 7 will not work for "7 days" + callback: function (value) { + if (period.value === 0) return `${5-(value-this.min)/60000}min`; + return moment(value).format(periods.find((p) => p.id === period.value).format); + } }, grid: { drawOnChartArea: false, @@ -250,8 +276,18 @@ async function refresh() { } }; - if (memoryGraph) memoryGraph.destroy(); - memoryGraph = new Chart(memoryGraphNode.value, { type: 'line', data: memoryGraphData, options: memoryGraphOptions }); + if (period.value === 0) { + // for realtime graph, generate steps of 1min and appropriate tick text + memoryGraphOptions.scales.x.ticks.stepSize = 60*1000; // 1min + } + + if (!memoryGraph) { + memoryGraph = new Chart(memoryGraphNode.value, { type: 'line', data: memoryGraphData, options: memoryGraphOptions }); + } else { + memoryGraph.data = memoryGraphData; + memoryGraph.options = memoryGraphOptions; + memoryGraph.update('none'); + } if (metricStream) { clearInterval(metricStream.intervalId); diff --git a/src/metrics.js b/src/metrics.js index f4984ae08..d913af911 100644 --- a/src/metrics.js +++ b/src/metrics.js @@ -16,6 +16,7 @@ const apps = require('./apps.js'), debug = require('debug')('box:metrics'), docker = require('./docker.js'), execSync = require('child_process').execSync, + fs = require('fs'), net = require('net'), os = require('os'), { Readable } = require('stream'), @@ -94,20 +95,27 @@ async function getDockerMetrics() { } async function getMemoryMetrics() { - // we can also read /proc/meminfo but complicated to match the 'used' output of free - const output = execSync('free --bytes --wide', { encoding: 'utf8' }).trim(); // --line is not in older ubuntu - const memoryRe = /Mem:\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)/; - const swapRe = /Swap:\s+(?\d+)\s+(?\d+)\s+(?\d+)/; + const output = await fs.promises.readFile('/proc/meminfo', { encoding: 'utf8' }); - const memory = output.match(memoryRe); - if (!memory) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find memory used'); + const totalMemoryMatch = output.match(/^MemTotal:\s+(\d+)/m); + const freeMemoryMatch = output.match(/^MemFree:\s+(\d+)/m); + const buffersMatch = output.match(/^Buffers:\s+(\d+)/m); + const cachedMatch = output.match(/^Cached:\s+(\d+)/m); - const swap = output.match(swapRe); - if (!swap) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find swap used'); + if (!totalMemoryMatch || !freeMemoryMatch || !buffersMatch || !cachedMatch) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find memory used'); + + const memoryUsed = parseInt(totalMemoryMatch[1]) * 1024 - parseInt(freeMemoryMatch[1]) * 1024 - parseInt(buffersMatch[1]) * 1024 - parseInt(cachedMatch[1]) * 1024; + + const swapTotalMatch = output.match(/^SwapTotal:\s+(\d+)/m); + const swapFreeMatch = output.match(/^SwapFree:\s+(\d+)/m); + + if (!swapTotalMatch || !swapFreeMatch) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not find swap used'); + + const swapUsed = parseInt(swapTotalMatch[1]) * 1024 - parseInt(swapFreeMatch[1]) * 1024; return { - memoryUsed: parseInt(memory.groups.used, 10), - swapUsed: parseInt(swap.groups.used, 10) + memoryUsed, + swapUsed }; }