metrics: overlay app metrics over system metrics
This commit is contained in:
@@ -4,14 +4,16 @@ import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, onUnmounted, useTemplateRef, nextTick } from 'vue';
|
||||
import { SingleSelect } from 'pankow';
|
||||
import { ref, onMounted, onUnmounted, useTemplateRef, nextTick, watch } from 'vue';
|
||||
import { SingleSelect, MultiSelect } from 'pankow';
|
||||
import Section from './Section.vue';
|
||||
import SystemModel from '../models/SystemModel.js';
|
||||
import { prettyDecimalSize } from 'pankow/utils';
|
||||
import GraphItem from './GraphItem.vue';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
|
||||
const systemModel = SystemModel.create();
|
||||
const appsModel = AppsModel.create();
|
||||
|
||||
const periods = [
|
||||
{ hours: 0, label: t('app.graphs.period.live'), tickFormat: 'hh:mm A', tooltipFormat: 'hh:mm:ss A' },
|
||||
@@ -36,73 +38,148 @@ const networkWriteTotal = ref(0);
|
||||
const blockReadTotal = ref(0);
|
||||
const blockWriteTotal = ref(0);
|
||||
|
||||
const containers = ref([]);
|
||||
const allContainers = ref([]);
|
||||
|
||||
let systemMemory = {};
|
||||
let systemCpus = {};
|
||||
let metricStream = null;
|
||||
|
||||
async function liveRefresh() {
|
||||
metricStream = await systemModel.getMetricStream();
|
||||
const options = {
|
||||
system: true,
|
||||
appIds: containers.value.map(c => c.id),
|
||||
serviceIds: []
|
||||
};
|
||||
metricStream = await systemModel.getMetricStream(options);
|
||||
metricStream.onerror = (error) => console.log('event stream error:', error);
|
||||
metricStream.onmessage = (message) => {
|
||||
const data = JSON.parse(message.data);
|
||||
|
||||
cpuGraphItem.value.pushData(data.cpu);
|
||||
memoryGraphItem.value.pushData(data.memory, data.swap);
|
||||
diskGraphItem.value.pushData(data.blockReadRate, data.blockWriteRate);
|
||||
networkGraphItem.value.pushData(data.networkReadRate, data.networkWriteRate);
|
||||
for (const [id, metric] of Object.entries(data)) {
|
||||
const idx = id !== 'system' ? containers.value.findIndex(c => c.id === id) : containers.value.length;
|
||||
|
||||
blockReadTotal.value = prettyDecimalSize(data.blockReadTotal);
|
||||
blockWriteTotal.value = prettyDecimalSize(data.blockWriteTotal);
|
||||
networkReadTotal.value = prettyDecimalSize(data.networkReadTotal);
|
||||
networkWriteTotal.value = prettyDecimalSize(data.networkWriteTotal);
|
||||
cpuGraphItem.value.pushData(idx, metric.cpu);
|
||||
memoryGraphItem.value.pushData(idx*2, metric.memory, metric.swap || []); // apps have no swap
|
||||
diskGraphItem.value.pushData(idx*2, metric.blockReadRate, metric.blockWriteRate);
|
||||
networkGraphItem.value.pushData(idx*2, metric.networkReadRate, metric.networkWriteRate);
|
||||
|
||||
if (id === 'system') {
|
||||
blockReadTotal.value = prettyDecimalSize(metric.blockReadTotal);
|
||||
blockWriteTotal.value = prettyDecimalSize(metric.blockWriteTotal);
|
||||
networkReadTotal.value = prettyDecimalSize(metric.networkReadTotal);
|
||||
networkWriteTotal.value = prettyDecimalSize(metric.networkWriteTotal);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function onPeriodChange() {
|
||||
function generateConsistentColors(n, saturation = 90, lightness = 90) {
|
||||
const baseHue = 204; // from #9ad0f5 → hsl(204,82%,78%)
|
||||
const colors = [];
|
||||
const step = 360 / n;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const hue = Math.round((baseHue + step * i) % 360); // rotate hue, wrap at 360
|
||||
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
function createDatasets() {
|
||||
const colors = generateConsistentColors((containers.value.length+1)*2); // 1 for the 'system'
|
||||
|
||||
const datasets = {
|
||||
cpu: [],
|
||||
memory: [],
|
||||
disk: [],
|
||||
network: [],
|
||||
};
|
||||
const appIds = containers.value.map(c => c.id);
|
||||
for (const [idx, id] of appIds.concat(['system']).entries()) { // live stream code depends on this concat order!
|
||||
const prefix = id === 'system' ? 'System' : containers.value[idx].label;
|
||||
|
||||
datasets.cpu.push({ label: `${prefix} CPU`, color: colors[idx*2], stack: `${prefix}-cpu`, data: [] });
|
||||
datasets.memory.push({ label: `${prefix} Memory`, color: colors[idx*2], stack: `${prefix}-memswap`, data: [] });
|
||||
datasets.memory.push({ label: `${prefix} Swap`, color: colors[idx*2 + 1], stack: `${prefix}-memswap`, data: [] });
|
||||
|
||||
datasets.disk.push({ label: `${prefix} Read`, color: colors[idx*2], stack: `${prefix}-read`, data: [] });
|
||||
datasets.disk.push({ label: `${prefix} Write`, color: colors[idx*2 + 1], stack: `${prefix}-write`, prefix, data: [] });
|
||||
|
||||
datasets.network.push({ label: `${prefix} RX`, color: colors[idx*2], stack: `${prefix}-rx`, data: [] });
|
||||
datasets.network.push({ label: `${prefix} TX`, color: colors[idx*2 + 1], stack: `${prefix}-tx`, data: [] });
|
||||
}
|
||||
|
||||
return datasets;
|
||||
}
|
||||
|
||||
async function rebuild() {
|
||||
if (metricStream) {
|
||||
metricStream.close();
|
||||
metricStream = null;
|
||||
}
|
||||
|
||||
const datasets = createDatasets();
|
||||
|
||||
if (period.value.hours !== 0) {
|
||||
const options = {
|
||||
fromSecs: period.value.hours * 60 * 60,
|
||||
intervalSecs: period.value.intervalSecs,
|
||||
system: true,
|
||||
appIds: containers.value.map(c => c.id),
|
||||
serviceIds: []
|
||||
};
|
||||
const [error, metrics] = await systemModel.getMetrics(options);
|
||||
if (error) return console.error(error);
|
||||
|
||||
const appIds = containers.value.map(c => c.id);
|
||||
for (const [idx, id] of appIds.concat(['system']).entries()) {
|
||||
if (!metrics[id]) continue;
|
||||
datasets.cpu[idx].data = metrics[id].cpu;
|
||||
datasets.memory[idx*2].data = metrics[id].memory;
|
||||
datasets.memory[idx*2 + 1].data = metrics[id].swap || []; // apps have no swap
|
||||
datasets.disk[idx*2].data = metrics[id].blockReadRate;
|
||||
datasets.disk[idx*2 + 1].data = metrics[id].blockWriteRate;
|
||||
datasets.network[idx*2].data = metrics[id].networkReadRate;
|
||||
datasets.network[idx*2 + 1].data = metrics[id].networkWriteRate;
|
||||
}
|
||||
|
||||
networkReadTotal.value = prettyDecimalSize(metrics.system.networkReadTotal);
|
||||
networkWriteTotal.value = prettyDecimalSize(metrics.system.networkWriteTotal);
|
||||
blockReadTotal.value = prettyDecimalSize(metrics.system.blockReadTotal);
|
||||
blockWriteTotal.value = prettyDecimalSize(metrics.system.blockWriteTotal);
|
||||
}
|
||||
|
||||
cpuGraphItem.value.setDatasets(datasets.cpu);
|
||||
memoryGraphItem.value.setDatasets(datasets.memory);
|
||||
diskGraphItem.value.setDatasets(datasets.disk);
|
||||
networkGraphItem.value.setDatasets(datasets.network);
|
||||
|
||||
if (period.value.hours === 0) return await liveRefresh();
|
||||
|
||||
const options = {
|
||||
fromSecs: period.value.hours * 60 * 60,
|
||||
intervalSecs: period.value.intervalSecs,
|
||||
system: true,
|
||||
appIds: [],
|
||||
serviceIds: []
|
||||
};
|
||||
const [error, metrics] = await systemModel.getMetrics(options);
|
||||
if (error) return console.error(error);
|
||||
|
||||
cpuGraphItem.value.setData(metrics.system.cpu);
|
||||
memoryGraphItem.value.setData(metrics.system.memory, metrics.system.swap);
|
||||
diskGraphItem.value.setData(metrics.system.blockReadRate, metrics.system.blockWriteRate);
|
||||
networkGraphItem.value.setData(metrics.system.networkReadRate, metrics.system.networkWriteRate);
|
||||
|
||||
networkReadTotal.value = prettyDecimalSize(metrics.system.networkReadTotal);
|
||||
networkWriteTotal.value = prettyDecimalSize(metrics.system.networkWriteTotal);
|
||||
blockReadTotal.value = prettyDecimalSize(metrics.system.blockReadTotal);
|
||||
blockWriteTotal.value = prettyDecimalSize(metrics.system.blockWriteTotal);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let error, result;
|
||||
|
||||
[error, result] = await systemModel.memory();
|
||||
if (error) return console.error(error);
|
||||
|
||||
systemMemory = result;
|
||||
|
||||
[error, result] = await systemModel.cpus();
|
||||
if (error) return console.error(error);
|
||||
|
||||
systemCpus = result;
|
||||
|
||||
[error, result] = await appsModel.list();
|
||||
if (error) return console.error(error);
|
||||
result.forEach(a => a.label = (a.label || a.fqdn));
|
||||
allContainers.value = result;
|
||||
containers.value = [];
|
||||
|
||||
busy.value = false;
|
||||
await nextTick();
|
||||
|
||||
await onPeriodChange();
|
||||
await rebuild();
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
@@ -114,7 +191,8 @@ onUnmounted(async () => {
|
||||
<template>
|
||||
<Section :title="$t('system.graphs.title')">
|
||||
<template #header-buttons>
|
||||
<SingleSelect @select="onPeriodChange()" v-model="period" :options="periods" option-label="label"/>
|
||||
<MultiSelect @select="rebuild()" v-model="containers" :options="allContainers" option-label="label" :search-threshold="20"/>
|
||||
<SingleSelect @select="rebuild()" v-model="period" :options="periods" option-label="label"/>
|
||||
</template>
|
||||
|
||||
<div class="graphs" v-if="!busy">
|
||||
@@ -123,8 +201,6 @@ onUnmounted(async () => {
|
||||
:subtext='systemCpus.length ? `${systemCpus.length} Core "${systemCpus[0].model}"` : ""'
|
||||
:period="period"
|
||||
yscale="cpu"
|
||||
:dataset-labels="['CPU']"
|
||||
:dataset-colors="['#9ad0f5']"
|
||||
:cpu-count="systemCpus.length"
|
||||
>
|
||||
</GraphItem>
|
||||
@@ -134,8 +210,6 @@ onUnmounted(async () => {
|
||||
:subtext="`RAM: ${prettyDecimalSize(systemMemory.memory)} Swap: ${prettyDecimalSize(systemMemory.swap)}`"
|
||||
:period="period"
|
||||
yscale="memory"
|
||||
:dataset-labels="['Memory', 'Swap']"
|
||||
:dataset-colors="['#9ad0f5', '#ffb1c1']"
|
||||
:memory="systemMemory.memory + systemMemory.swap"
|
||||
>
|
||||
</GraphItem>
|
||||
@@ -145,8 +219,6 @@ onUnmounted(async () => {
|
||||
:subtext="$t('app.graphs.diskIOTotal', { read: blockReadTotal, write: blockWriteTotal })"
|
||||
:period="period"
|
||||
yscale="disk"
|
||||
:dataset-labels="['Read', 'Write']"
|
||||
:dataset-colors="['#9ad0f5', '#ffb1c1']"
|
||||
>
|
||||
</GraphItem>
|
||||
|
||||
@@ -155,8 +227,6 @@ onUnmounted(async () => {
|
||||
:subtext="$t('app.graphs.networkIOTotal', { inbound: networkReadTotal, outbound: networkWriteTotal })"
|
||||
:period="period"
|
||||
yscale="network"
|
||||
:dataset-labels="['RX', 'TX']"
|
||||
:dataset-colors="['#9ad0f5', '#ffb1c1']"
|
||||
>
|
||||
</GraphItem>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user