From 7f70b0f703a995a547a61bbf486af684a577711d Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 22 Aug 2022 16:08:01 +0200 Subject: [PATCH] [BS5] add container disk and network stats --- data/web/css/build/013-mailcow.css | 15 ++ data/web/css/themes/mailcow-darkmode.css | 8 + data/web/inc/functions.docker.inc.php | 27 ++ data/web/js/site/debug.js | 309 +++++++++++++++++------ data/web/json_api.php | 4 + data/web/templates/debug.twig | 253 ++++++++++++------- 6 files changed, 452 insertions(+), 164 deletions(-) diff --git a/data/web/css/build/013-mailcow.css b/data/web/css/build/013-mailcow.css index bc4e07aa..49a47693 100644 --- a/data/web/css/build/013-mailcow.css +++ b/data/web/css/build/013-mailcow.css @@ -378,3 +378,18 @@ table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] { .btn-check-label { color: #555; } + +.caret { + transform: rotate(0deg); +} +a[aria-expanded='true'] > .caret, +button[aria-expanded='true'] > .caret { + transform: rotate(-180deg); +} + +.list-group-details { + background: #fff; +} +.list-group-header { + background: #f7f7f7; +} \ No newline at end of file diff --git a/data/web/css/themes/mailcow-darkmode.css b/data/web/css/themes/mailcow-darkmode.css index 4010ac0d..a0388aa0 100644 --- a/data/web/css/themes/mailcow-darkmode.css +++ b/data/web/css/themes/mailcow-darkmode.css @@ -339,4 +339,12 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty { .inputMissingAttr { border-color: #FF4136 !important; +} + + +.list-group-details { + background: #444444; +} +.list-group-header { + background: #333; } \ No newline at end of file diff --git a/data/web/inc/functions.docker.inc.php b/data/web/inc/functions.docker.inc.php index 260f0a41..78efac03 100644 --- a/data/web/inc/functions.docker.inc.php +++ b/data/web/inc/functions.docker.inc.php @@ -1,6 +1,7 @@ draw_rspamd_history()); onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph()); - // start polling stats if tab is active + + + // start polling host stats if tab is active onVisible("[id^=tab-containers]", () => update_stats()); + // start polling container stats if collapse is active + var containerElements = document.querySelectorAll(".container-details-collapse"); + for (let i = 0; i < containerElements.length; i++){ + new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if(entry.intersectionRatio > 0) { + + if (!containerElements[i].classList.contains("show")){ + var container = containerElements[i].id.replace("Collapse", ""); + var container_id = containerElements[i].getAttribute("data-id"); + + // check if chart exists or needs to be created + if (!Chart.getChart(container + "_DiskIOChart")) + createReadWriteChart(container + "_DiskIOChart", "Read", "Write"); + if (!Chart.getChart(container + "_NetIOChart")) + createReadWriteChart(container + "_NetIOChart", "Recv", "Sent"); + + // add container to polling list + containersToUpdate[container] = { + id: container_id, + state: "idle" + } + + // stop polling if collapse is closed + containerElements[i].addEventListener('hidden.bs.collapse', function () { + var diskIOCtx = Chart.getChart(container + "_DiskIOChart"); + var netIOCtx = Chart.getChart(container + "_NetIOChart"); + + diskIOCtx.data.datasets[0].data = []; + diskIOCtx.data.datasets[1].data = []; + diskIOCtx.data.labels = []; + netIOCtx.data.datasets[0].data = []; + netIOCtx.data.datasets[1].data = []; + netIOCtx.data.labels = []; + + diskIOCtx.update(); + netIOCtx.update(); + + delete containersToUpdate[container]; + }); + } + + } + }); + }).observe(containerElements[i]); + } }); // update system stats - every 5 seconds if system & container tab is active -function update_stats(prev_stats = null){ +function update_stats(timeout=5){ if (!$('#tab-containers').hasClass('active')) { // tab not active - dont fetch stats - run again in n seconds return; @@ -1020,62 +1071,115 @@ function update_stats(prev_stats = null){ window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) { return response.json(); }).then(function(data) { - // display table data - $("#host_date").text(data.system_time); - $("#host_uptime").text(formatUptime(data.uptime)); - $("#host_cpu_cores").text(data.cpu.cores); - $("#host_cpu_usage").text(parseInt(data.cpu.usage).toString() + "%"); - $("#host_memory_total").text((data.memory.total / (1024 ** 3)).toFixed(2).toString() + "GB"); - $("#host_memory_usage").text(parseInt(data.memory.usage).toString() + "%"); + console.log(data); + if (data){ + // display table data + $("#host_date").text(data.system_time); + $("#host_uptime").text(formatUptime(data.uptime)); + $("#host_cpu_cores").text(data.cpu.cores); + $("#host_cpu_usage").text(parseInt(data.cpu.usage).toString() + "%"); + $("#host_memory_total").text((data.memory.total / (1024 ** 3)).toFixed(2).toString() + "GB"); + $("#host_memory_usage").text(parseInt(data.memory.usage).toString() + "%"); - // display network and disk i/o - if (prev_stats != null){ - // get chart instances by elemId - var net_io_chart = Chart.getChart("net_io_chart"); - var disk_io_chart = Chart.getChart("disk_io_chart"); + // update cpu and mem chart + var cpu_chart = Chart.getChart("host_cpu_chart"); + var mem_chart = Chart.getChart("host_mem_chart"); + cpu_chart.data.labels.push(data.system_time.split(" ")[1]); + if (cpu_chart.data.labels.length > 30) cpu_chart.data.labels.shift(); + mem_chart.data.labels.push(data.system_time.split(" ")[1]); + if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift(); - // calc time diff - var time_diff = (new Date(data.system_time) - new Date(prev_stats.system_time)) / 1000; - // push time label for x-axis - net_io_chart.data.labels.push(data.system_time.split(" ")[1]); - // shift data if more than 20 entires exists - if (net_io_chart.data.labels.length > 20) net_io_chart.data.labels.shift(); + cpu_chart.data.datasets[0].data.push(data.cpu.usage); + if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift(); + mem_chart.data.datasets[0].data.push(data.memory.usage); + if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift(); - var diff_bytes_recv = (data.network.bytes_recv_total - prev_stats.network.bytes_recv_total) / time_diff; - var diff_bytes_sent = (data.network.bytes_sent_total - prev_stats.network.bytes_sent_total) / time_diff; - net_io_chart.data.datasets[0].data.push(diff_bytes_recv); - net_io_chart.data.datasets[1].data.push(diff_bytes_sent); - // shift data if more than 20 entires exists - if (net_io_chart.data.datasets[0].data.length > 20) net_io_chart.data.datasets[0].data.shift(); - if (net_io_chart.data.datasets[1].data.length > 20) net_io_chart.data.datasets[1].data.shift(); - - - // push time label for x-axis - disk_io_chart.data.labels.push(data.system_time.split(" ")[1]); - // shift data if more than 20 entires exists - if (disk_io_chart.data.labels.length > 20) disk_io_chart.data.labels.shift(); - - var diff_bytes_read = (data.disk.bytes_read_total - prev_stats.disk.bytes_read_total) / time_diff; - var diff_bytes_write = (data.disk.bytes_write_total - prev_stats.disk.bytes_write_total) / time_diff; - disk_io_chart.data.datasets[0].data.push(diff_bytes_read); - disk_io_chart.data.datasets[1].data.push(diff_bytes_write); - // shift data if more than 20 entires exists - if (disk_io_chart.data.datasets[0].data.length > 20) disk_io_chart.data.datasets[0].data.shift(); - if (disk_io_chart.data.datasets[1].data.length > 20) disk_io_chart.data.datasets[1].data.shift(); - - - // update charts - net_io_chart.update(); - disk_io_chart.update(); + cpu_chart.update(); + mem_chart.update(); } // run again in n seconds - prev_stats = data; - setTimeout(update_stats(prev_stats), 2500); + setTimeout(update_stats, timeout * 1000); }); } +// update specific container stats - every n (default 5s) seconds +function update_container_stats(timeout=5){ + for (let container in containersToUpdate){ + container_id = containersToUpdate[container].id; + if (containersToUpdate[container].state == "running") + continue; + containersToUpdate[container].state = "running"; + + + window.fetch("/api/v1/get/status/container/" + container_id, {method:'GET',cache:'no-cache'}).then(function(response) { + return response.json(); + }).then(function(data) { + var diskIOCtx = Chart.getChart(container + "_DiskIOChart"); + var netIOCtx = Chart.getChart(container + "_NetIOChart"); + + console.log(container); + console.log(data); + prev_stats = null; + if (data.length >= 2) + prev_stats = data[data.length -2] + data = data[data.length -1]; + + if (prev_stats != null){ + // calc time diff + var time_diff = (new Date(data.read) - new Date(prev_stats.read)) / 1000; + + // calc disk io b/s + var prev_read_bytes = 0; + var prev_write_bytes = 0; + for (var i = 0; i < prev_stats.blkio_stats.io_service_bytes_recursive.length; i++){ + if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "read") + prev_read_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value; + else if (prev_stats.blkio_stats.io_service_bytes_recursive[i].op == "write") + prev_write_bytes = prev_stats.blkio_stats.io_service_bytes_recursive[i].value; + } + var read_bytes = 0; + var write_bytes = 0; + for (var i = 0; i < data.blkio_stats.io_service_bytes_recursive.length; i++){ + if (data.blkio_stats.io_service_bytes_recursive[i].op == "read") + read_bytes = data.blkio_stats.io_service_bytes_recursive[i].value; + else if (data.blkio_stats.io_service_bytes_recursive[i].op == "write") + write_bytes = data.blkio_stats.io_service_bytes_recursive[i].value; + } + var diff_bytes_read = (read_bytes - prev_read_bytes) / time_diff; + var diff_bytes_write = (write_bytes - prev_write_bytes) / time_diff; + + // calc net io b/s + var prev_recv_bytes = 0; + var prev_sent_bytes = 0; + for (var key in prev_stats.networks){ + prev_recv_bytes += prev_stats.networks[key].rx_bytes; + prev_sent_bytes += prev_stats.networks[key].tx_bytes; + } + var recv_bytes = 0; + var sent_bytes = 0; + for (var key in data.networks){ + recv_bytes += data.networks[key].rx_bytes; + sent_bytes += data.networks[key].tx_bytes; + } + var diff_bytes_recv = (recv_bytes - prev_recv_bytes) / time_diff; + var diff_bytes_sent = (sent_bytes - prev_sent_bytes) / time_diff; + + addReadWriteChart(diskIOCtx, diff_bytes_read, diff_bytes_write, ""); + addReadWriteChart(netIOCtx, diff_bytes_recv, diff_bytes_sent, ""); + } + + // run again in n seconds + containersToUpdate[container].state = "idle"; + }).catch(err => { + console.log(err); + }); + } + + // run again in n seconds + setTimeout(update_container_stats, timeout * 1000); +} // format hosts uptime seconds to readable string function formatUptime(seconds){ seconds = Number(seconds); @@ -1103,15 +1207,14 @@ function formatBytes(bytes){ // final mb to gb return (bytes / 1024).toFixed(2).toString()+' GB/s'; } -// create network and disk chart -function createNetAndDiskChart(){ - var net_io_ctx = document.getElementById("net_io_chart"); - var disk_io_ctx = document.getElementById("disk_io_chart"); +// create read write line chart +function createReadWriteChart(chart_id, read_lable, write_lable){ + var ctx = document.getElementById(chart_id); var dataNet = { labels: [], datasets: [{ - label: "Recieve", + label: read_lable, backgroundColor: "rgba(41, 187, 239, 0.3)", borderColor: "rgba(41, 187, 239, 0.6)", pointRadius: 1, @@ -1121,7 +1224,7 @@ function createNetAndDiskChart(){ tension: 0.2, data: [] }, { - label: "Sent", + label: write_lable, backgroundColor: "rgba(239, 60, 41, 0.3)", borderColor: "rgba(239, 60, 41, 0.6)", pointRadius: 1, @@ -1155,11 +1258,37 @@ function createNetAndDiskChart(){ } } }; + + return new Chart(ctx, { + type: 'line', + data: dataNet, + options: optionsNet + }); +} +// add to read write line chart +function addReadWriteChart(chart_context, read_point, write_point, time, limit = 30){ + // push time label for x-axis + chart_context.data.labels.push(time); + if (chart_context.data.labels.length > limit) chart_context.data.labels.shift(); - var dataDisk = { + // push datapoints + chart_context.data.datasets[0].data.push(read_point); + chart_context.data.datasets[1].data.push(write_point); + // shift data if more than 20 entires exists + if (chart_context.data.datasets[0].data.length > limit) chart_context.data.datasets[0].data.shift(); + if (chart_context.data.datasets[1].data.length > limit) chart_context.data.datasets[1].data.shift(); + + chart_context.update(); +} +// create host cpu and mem chart +function createHostCpuAndMemChart(){ + var cpu_ctx = document.getElementById("host_cpu_chart"); + var mem_ctx = document.getElementById("host_mem_chart"); + + var dataCpu = { labels: [], datasets: [{ - label: "Read", + label: "CPU %", backgroundColor: "rgba(41, 187, 239, 0.3)", borderColor: "rgba(41, 187, 239, 0.6)", pointRadius: 1, @@ -1168,19 +1297,9 @@ function createNetAndDiskChart(){ fill: true, tension: 0.2, data: [] - }, { - label: "Write", - backgroundColor: "rgba(239, 60, 41, 0.3)", - borderColor: "rgba(239, 60, 41, 0.6)", - pointRadius: 1, - pointHitRadius: 6, - borderWidth: 2, - fill: true, - tension: 0.2, - data: [] }] }; - var optionsDisk = { + var optionsCpu = { interaction: { mode: 'index' }, @@ -1192,7 +1311,45 @@ function createNetAndDiskChart(){ }, ticks: { callback: function(i, index, ticks) { - return formatBytes(i); + return i.toFixed(0).toString() + "%"; + } + } + }, + xAxis: { + grid: { + display: false + } + } + } + }; + + var dataMem = { + labels: [], + datasets: [{ + label: "MEM %", + backgroundColor: "rgba(41, 187, 239, 0.3)", + borderColor: "rgba(41, 187, 239, 0.6)", + pointRadius: 1, + pointHitRadius: 6, + borderWidth: 2, + fill: true, + tension: 0.2, + data: [] + }] + }; + var optionsMem = { + interaction: { + mode: 'index' + }, + scales: { + yAxis: { + min: 0, + grid: { + display: false + }, + ticks: { + callback: function(i, index, ticks) { + return i.toFixed(0).toString() + "%"; } } }, @@ -1205,15 +1362,15 @@ function createNetAndDiskChart(){ }; - var net_io_chart = new Chart(net_io_ctx, { + var net_io_chart = new Chart(cpu_ctx, { type: 'line', - data: dataNet, - options: optionsNet + data: dataCpu, + options: optionsCpu }); - var disk_io_chart = new Chart(disk_io_ctx, { + var disk_io_chart = new Chart(mem_ctx, { type: 'line', - data: dataDisk, - options: optionsDisk + data: dataMem, + options: optionsMem }); } // check for mailcow updates diff --git a/data/web/json_api.php b/data/web/json_api.php index 688cd63d..f8c88d33 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1463,6 +1463,10 @@ if (isset($_GET['query'])) { } echo json_encode($temp, JSON_UNESCAPED_SLASHES); break; + case "container": + $container_stats = docker('container_stats', $extra); + echo json_encode($container_stats); + break; case "vmail": $exec_fields_vmail = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail'); $vmail_df = explode(',', json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields_vmail), true)); diff --git a/data/web/templates/debug.twig b/data/web/templates/debug.twig index e722302a..90a3b7b6 100644 --- a/data/web/templates/debug.twig +++ b/data/web/templates/debug.twig @@ -37,16 +37,7 @@
- mailcow-logo -
- {{ vmail_df[0] }} - {{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }}) -
-
-
-
-
-
+ mailcow-logo
@@ -84,17 +75,17 @@ - - CPU + Disk Info - Cores -
- Usage - - - - {{ lang.debug.memory }} - - Total -
- Usage +
+ {{ vmail_df[0] }} + {{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }}) +
+
+
+
+
+
@@ -103,12 +94,14 @@
-

Network I/O

- +

CPU

+
- Cores @
+
-

Disk I/O

- +

MEMORY

+
- @
+
@@ -149,77 +142,161 @@
-
-
-

{{ lang.debug.solr_status }}

-
-
-
-
-

Solr Logo

-
-
- {% if solr_status != false %} -
-
-
-

{{ lang.debug.jvm_memory_solr }}: {{ (solr_status.jvm.memory.total - solr_status.jvm.memory.free) }} / {{ solr_status.jvm.memory.total }} - ({{ solr_status.jvm.memory.raw['used%']|round }}%)

-
-

{{ lang.debug.uptime }}: {{ solr_uptime }}h

-

{{ lang.debug.started_at }}: {{ solr_status.status['dovecot-fts'].startTime }}

-

{{ lang.debug.last_modified }}: {{ solr_status.status['dovecot-fts'].index.lastModified }}

-

{{ lang.debug.size }}: {{ solr_status.status['dovecot-fts'].index.size }}

-

{{ lang.debug.docs }}: {{ solr_status.status['dovecot-fts'].index.numDocs }}

- {% else %} -

{{ lang.debug.solr_dead }}

- {% endif %} -
-
-
-
+ + +

{{ lang.debug.containers_info }}

-
-
+
+ + + {% for container, container_info in containers %} + {% if container != "solr-mailcow" %} +
+
+
+
+ {{ container }} + ({{ container_info.Config.Image }}) + ({{ lang.debug.started_on }} {{ container_info.State.StartedAtHR }}) + {% if container_info.State.Running == 1 %} + + {{ lang.debug.container_running }} + . + . + . + + {% elseif container_info.State %} + + {{ lang.debug.container_stopped }} + + + {% endif %} +
+
+ +
+
+
+
+
+
Disk I/O
+ +
+
+
Net I/O
+ +
+ +
+
+
+
+ {% endif %} {% endfor %} - +