From a3c0737ba820d8dca6c0b360f85548867191523e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 9 Aug 2022 20:29:33 +0200 Subject: [PATCH] [BS5] add host statistics --- data/Dockerfiles/dockerapi/Dockerfile | 1 + data/Dockerfiles/dockerapi/dockerapi.py | 45 ++++ data/web/debug.php | 7 + data/web/inc/functions.docker.inc.php | 19 ++ data/web/js/site/debug.js | 239 ++++++++++++++++++ data/web/json_api.php | 50 ++-- data/web/lang/lang.de.json | 5 + data/web/lang/lang.en.json | 5 + .../templates/admin/tab-config-admins.twig | 33 --- data/web/templates/debug.twig | 128 +++++++++- docker-compose.yml | 2 +- 11 files changed, 469 insertions(+), 65 deletions(-) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 41d4a78f..3768dc4f 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -8,6 +8,7 @@ RUN apk add --update --no-cache python3 \ py3-pip \ openssl \ tzdata \ + py3-psutil \ && pip3 install --upgrade pip \ docker \ flask \ diff --git a/data/Dockerfiles/dockerapi/dockerapi.py b/data/Dockerfiles/dockerapi/dockerapi.py index 20e9d0e0..314c1d65 100644 --- a/data/Dockerfiles/dockerapi/dockerapi.py +++ b/data/Dockerfiles/dockerapi/dockerapi.py @@ -6,6 +6,7 @@ from flask import jsonify from flask import Response from flask import request from threading import Thread +from datetime import datetime import docker import uuid import signal @@ -17,6 +18,7 @@ import ssl import socket import subprocess import traceback +import psutil docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') app = Flask(__name__) @@ -326,6 +328,48 @@ class container_post(Resource): else: return jsonify(type='danger', msg='command did not complete') +class host_stats_get(Resource): + def get(self): + try: + system_time = datetime.now() + + disk_io_before = psutil.disk_io_counters(perdisk=False) + net_io_before = psutil.net_io_counters(pernic=False) + time.sleep(1) + disk_io_after = psutil.disk_io_counters(perdisk=False) + net_io_after = psutil.net_io_counters(pernic=False) + + disks_read_per_sec = disk_io_after.read_bytes - disk_io_before.read_bytes + disks_write_per_sec = disk_io_after.write_bytes - disk_io_before.write_bytes + net_recv_per_sec = net_io_after.bytes_recv - net_io_before.bytes_recv + net_sent_per_sec = net_io_after.bytes_sent - net_io_before.bytes_sent + + + host_stats = { + "cpu": { + "cores": psutil.cpu_count(), + "usage": psutil.cpu_percent() + }, + "memory": { + "total": psutil.virtual_memory().total, + "usage": psutil.virtual_memory().percent, + "swap": psutil.swap_memory() + }, + "disk": { + "read_bytes": disks_read_per_sec, + "write_bytes": disks_write_per_sec + }, + "network": { + "bytes_recv": net_recv_per_sec, + "bytes_sent": net_sent_per_sec + }, + "uptime": time.time() - psutil.boot_time(), + "system_time": system_time.strftime("%d.%m.%Y %H:%M:%S") + } + return host_stats + except Exception as e: + return jsonify(type='danger', msg=str(e)) + def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): def recv_socket_data(c_socket, timeout): @@ -406,6 +450,7 @@ def startFlaskAPI(): api.add_resource(containers_get, '/containers/json') api.add_resource(container_get, '/containers//json') api.add_resource(container_post, '/containers//') +api.add_resource(host_stats_get, '/host/stats') if __name__ == '__main__': api_thread = Thread(target=startFlaskAPI) diff --git a/data/web/debug.php b/data/web/debug.php index 9cee06b9..e263e181 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -44,15 +44,22 @@ foreach ($containers as $container => $container_info) { $containers[$container]['State']['StartedAtHR'] = $started; } +// get mailconf data +$hostname = getenv('MAILCOW_HOSTNAME'); +$timezone = getenv('TZ'); + $template = 'debug.twig'; $template_data = [ 'log_lines' => getenv('LOG_LINES'), 'vmail_df' => $vmail_df, + 'hostname' => $hostname, + 'timezone' => $timezone, 'solr_status' => $solr_status, 'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60), 'clamd_status' => $clamd_status, 'containers' => $containers, 'lang_admin' => json_encode($lang['admin']), + 'lang_debug' => json_encode($lang['debug']), 'lang_datatables' => json_encode($lang['datatables']), ]; diff --git a/data/web/inc/functions.docker.inc.php b/data/web/inc/functions.docker.inc.php index e47f5e2f..260f0a41 100644 --- a/data/web/inc/functions.docker.inc.php +++ b/data/web/inc/functions.docker.inc.php @@ -32,6 +32,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex } } return false; + break; case 'containers': curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json'); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); @@ -146,5 +147,23 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex } } break; + case 'host_stats': + curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/host/stats'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POST, 0); + curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT); + $response = curl_exec($curl); + if ($response === false) { + $err = curl_error($curl); + curl_close($curl); + return $err; + } + else { + curl_close($curl); + $stats = json_decode($response, true); + if (!empty($stats)) return $stats; + } + return false; + break; } } diff --git a/data/web/js/site/debug.js b/data/web/js/site/debug.js index 7c4da192..15653252 100644 --- a/data/web/js/site/debug.js +++ b/data/web/js/site/debug.js @@ -36,6 +36,15 @@ $(document).ready(function() { $(this).text(started_local_date); } }); + + // set default ChartJs Font Color + Chart.defaults.color = '#999'; + // create net and disk charts + createNetAndDiskChart(); + // check for new version + check_update(mailcow_info.version_tag, mailcow_info.project_url); + // update system stats + update_stats(); }); jQuery(function($){ if (localStorage.getItem("current_page") === null) { @@ -998,3 +1007,233 @@ jQuery(function($){ onVisible("[id^=rspamd_history]", () => draw_rspamd_history()); onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph()); }); + + +// update system stats - every 5 seconds if system & container tab is active +function update_stats(){ + if (!$('#tab-containers').hasClass('active')) { + // tab not active - dont fetch stats - run again in n seconds + setTimeout(update_stats, 5000); + return; + } + + window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) { + return response.json(); + }).then(function(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() + "%"); + + var net_io_chart = Chart.getChart("net_io_chart"); + var disk_io_chart = Chart.getChart("disk_io_chart"); + + net_io_chart.data.labels.push(data.system_time.split(" ")[1]); + if (net_io_chart.data.labels.length > 20) { + net_io_chart.data.labels.shift(); + } + net_io_chart.data.datasets[0].data.push((data.network.bytes_recv / 1024).toFixed(4)); + net_io_chart.data.datasets[1].data.push((data.network.bytes_sent / 1024).toFixed(4)); + 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(); + } + + disk_io_chart.data.labels.push(data.system_time.split(" ")[1]); + if (disk_io_chart.data.labels.length > 20) { + disk_io_chart.data.labels.shift(); + } + disk_io_chart.data.datasets[0].data.push((data.disk.read_bytes / 1024).toFixed(4)); + disk_io_chart.data.datasets[1].data.push((data.disk.write_bytes / 1024).toFixed(4)); + 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(); + } + + net_io_chart.update(); + disk_io_chart.update(); + + // run again in n seconds + setTimeout(update_stats, 5000); + }); +} +// format hosts uptime seconds to readable string +function formatUptime(seconds){ + seconds = Number(seconds); + var d = Math.floor(seconds / (3600*24)); + var h = Math.floor(seconds % (3600*24) / 3600); + var m = Math.floor(seconds % 3600 / 60); + var s = Math.floor(seconds % 60); + + var dFormat = d > 0 ? d + "D " : ""; + var hFormat = h > 0 ? h + "H " : ""; + var mFormat = m > 0 ? m + "M " : ""; + var sFormat = s > 0 ? s + "S" : ""; + return dFormat + hFormat + mFormat + sFormat; +} +// 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"); + + var dataNet = { + labels: [], + datasets: [{ + label: "Recieve", + backgroundColor: "rgba(41, 187, 239, 0.3)", + borderColor: "rgba(41, 187, 239, 0.6)", + color: "#ff0000", + borderWidth: 2, + fill: true, + tension: 0.2, + data: [] + }, { + label: "Sent", + backgroundColor: "rgba(239, 60, 41, 0.3)", + borderColor: "rgba(239, 60, 41, 0.6)", + borderWidth: 2, + fill: true, + tension: 0.2, + data: [] + }] + }; + var optionsNet = { + scales: { + yAxis: { + min: 0, + grid: { + display: false + }, + ticks: { + callback: function(i, index, ticks) { + // b + if (i < 1000) return i.toFixed(2).toString()+' B/s'; + // b to kb + i = i / 1024; + if (i < 1000) return i.toFixed(2).toString()+' KB/s'; + // kb to mb + i = i / 1024; + if (i < 1000) return i.toFixed(2).toString()+' MB/s'; + // final mb to gb + return (i / 1024).toFixed(2).toString()+' GB/s'; + } + } + }, + xAxis: { + grid: { + display: false + } + } + } + }; + + var dataDisk = { + labels: [], + datasets: [{ + label: "Read", + backgroundColor: "rgba(41, 187, 239, 0.3)", + borderColor: "rgba(41, 187, 239, 0.6)", + color: "#ff0000", + borderWidth: 2, + fill: true, + tension: 0.2, + data: [] + }, { + label: "Write", + backgroundColor: "rgba(239, 60, 41, 0.3)", + borderColor: "rgba(239, 60, 41, 0.6)", + borderWidth: 2, + fill: true, + tension: 0.2, + data: [] + }] + }; + var optionsDisk = { + scales: { + yAxis: { + min: 0, + grid: { + display: false + }, + ticks: { + callback: function(i, index, ticks) { + // b + if (i < 1000) return i.toFixed(2).toString()+' B/s'; + // b to kb + i = i / 1024; + if (i < 1000) return i.toFixed(2).toString()+' KB/s'; + // kb to mb + i = i / 1024; + if (i < 1000) return i.toFixed(2).toString()+' MB/s'; + // final mb to gb + return (i / 1024).toFixed(2).toString()+' GB/s'; + } + } + }, + xAxis: { + grid: { + display: false + } + } + } + }; + + + var net_io_chart = new Chart(net_io_ctx, { + type: 'line', + data: dataNet, + options: optionsNet + }); + var disk_io_chart = new Chart(disk_io_ctx, { + type: 'line', + data: dataDisk, + options: optionsDisk + }); +} +// check for mailcow updates +function check_update(current_version, github_repo_url){ + var github_account = github_repo_url.split("/")[3]; + var github_repo_name = github_repo_url.split("/")[4]; + + // get details about latest release + window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) { + return response.json(); + }).then(function(latest_data) { + // get details about current release + window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) { + return response.json(); + }).then(function(current_data) { + // compare releases + var date_current = new Date(current_data.created_at); + var date_latest = new Date(latest_data.created_at); + if (date_latest.getTime() <= date_current.getTime()){ + // no update available + $("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success"); + $("#mailcow_update").html("" + lang_debug.no_update_available + ""); + } else { + // update available + $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning"); + $("#mailcow_update").html( + `` + lang_debug.update_available + ` + `+latest_data.tag_name+`` + ); + } + }).catch(err => { + // err + console.log(err); + $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger"); + $("#mailcow_update").html("Could not check for an Update"); + }); + }).catch(err => { + // err + console.log(err); + $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger"); + $("#mailcow_update").html("Could not check for an Update"); + }); +} diff --git a/data/web/json_api.php b/data/web/json_api.php index 8db4ef89..688cd63d 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1474,29 +1474,33 @@ if (isset($_GET['query'])) { 'used_percent' => $vmail_df[4] ); echo json_encode($temp, JSON_UNESCAPED_SLASHES); - break; - case "solr": - $solr_status = solr_status(); - $solr_size = ($solr_status['status']['dovecot-fts']['index']['size']); - $solr_documents = ($solr_status['status']['dovecot-fts']['index']['numDocs']); - if (strtolower(getenv('SKIP_SOLR')) != 'n') { - $solr_enabled = false; - } - else { - $solr_enabled = true; - } - echo json_encode(array( - 'type' => 'info', - 'solr_enabled' => $solr_enabled, - 'solr_size' => $solr_size, - 'solr_documents' => $solr_documents - )); - break; - case "version": - echo json_encode(array( - 'version' => $GLOBALS['MAILCOW_GIT_VERSION'] - )); - break; + break; + case "solr": + $solr_status = solr_status(); + $solr_size = ($solr_status['status']['dovecot-fts']['index']['size']); + $solr_documents = ($solr_status['status']['dovecot-fts']['index']['numDocs']); + if (strtolower(getenv('SKIP_SOLR')) != 'n') { + $solr_enabled = false; + } + else { + $solr_enabled = true; + } + echo json_encode(array( + 'type' => 'info', + 'solr_enabled' => $solr_enabled, + 'solr_size' => $solr_size, + 'solr_documents' => $solr_documents + )); + break; + case "host": + $stats = docker("host_stats"); + echo json_encode($stats); + break; + case "version": + echo json_encode(array( + 'version' => $GLOBALS['MAILCOW_GIT_VERSION'] + )); + break; } } break; diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json index 36d257e9..afe3053a 100644 --- a/data/web/lang/lang.de.json +++ b/data/web/lang/lang.de.json @@ -494,6 +494,7 @@ "containers_info": "Container-Information", "container_running": "Läuft", "container_stopped": "Angehalten", + "current_time": "Systemzeit", "disk_usage": "Festplattennutzung", "docs": "Dokumente", "external_logs": "Externe Logs", @@ -504,6 +505,7 @@ "log_info": "

mailcow in-memory Logs werden in Redis Listen gespeichert, die maximale Anzahl der Einträge pro Anwendung richtet sich nach LOG_LINES (%d).\r\n
In-memory Logs sind vergänglich und nicht zur ständigen Aufbewahrung bestimmt. Alle Anwendungen, die in-memory protokollieren, schreiben ebenso in den Docker Daemon.\r\n
Das in-memory Protokoll versteht sich als schnelle Übersicht zum Debugging eines Containers, für komplexere Protokolle sollte der Docker Daemon konsultiert werden.

\r\n

Externe Logs werden via API externer Applikationen bezogen.

\r\n

Statische Logs sind weitestgehend Aktivitätsprotokolle, die nicht in den Docker Daemon geschrieben werden, jedoch permanent verfügbar sein müssen (ausgeschlossen API Logs).

", "login_time": "Zeit", "logs": "Protokolle", + "memory": "Arbeitsspeicher", "online_users": "Benutzer online", "restart_container": "Neustart", "service": "Dienst", @@ -515,7 +517,10 @@ "static_logs": "Statische Logs", "success": "Erfolg", "system_containers": "System & Container", + "timezone": "Zeitzone", "uptime": "Uptime", + "update_available": "Es ist ein Update verfügbar", + "no_update_available": "Das System ist auf aktuellem Stand", "username": "Benutzername" }, "diagnostics": { diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json index 47b664b0..23c03d6b 100644 --- a/data/web/lang/lang.en.json +++ b/data/web/lang/lang.en.json @@ -494,6 +494,7 @@ "containers_info": "Container information", "container_running": "Running", "container_stopped": "Stopped", + "current_time": "System Time", "disk_usage": "Disk usage", "docs": "Docs", "external_logs": "External logs", @@ -504,6 +505,7 @@ "log_info": "

mailcow in-memory logs are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\r\n
In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\r\n
The in-memory log type should be used for debugging minor issues with containers.

\r\n

External logs are collected via API of the given application.

\r\n

Static logs are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).

", "login_time": "Time", "logs": "Logs", + "memory": "Memory", "online_users": "Users online", "restart_container": "Restart", "service": "Service", @@ -515,7 +517,10 @@ "static_logs": "Static logs", "success": "Success", "system_containers": "System & Containers", + "timezone": "Timezone", "uptime": "Uptime", + "update_available": "There is an update available", + "no_update_available": "The System is on the latest version", "username": "Username" }, "diagnostics": { diff --git a/data/web/templates/admin/tab-config-admins.twig b/data/web/templates/admin/tab-config-admins.twig index e9018683..1d903b56 100644 --- a/data/web/templates/admin/tab-config-admins.twig +++ b/data/web/templates/admin/tab-config-admins.twig @@ -89,39 +89,6 @@
- - {{ lang.admin.guid_and_license }} - -
-
-
-
- -
-
- - - - -
-

- {{ lang.admin.customer_id }}: {{ gal.c|default('?')|raw }} - - {{ lang.admin.service_id }}: {{ gal.s|default('?')|raw }} - - {{ lang.admin.sal_level }}: {{ gal.m|default('?')|raw }} -

-
-
-
-
-

{{ lang.admin.license_info|raw }}

-
- -
-
-
-
-
- API diff --git a/data/web/templates/debug.twig b/data/web/templates/debug.twig index 53db5194..732d5cbc 100644 --- a/data/web/templates/debug.twig +++ b/data/web/templates/debug.twig @@ -28,26 +28,125 @@
-
{{ lang.debug.log_info|format(log_lines+1)|raw }}
+
-

{{ lang.debug.disk_usage }}

+

mailcow

-
-

{{ vmail_df[0] }}

-

{{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }})

+
+ mailcow-logo +
+ {{ vmail_df[0] }} + {{ vmail_df[2] }} / {{ vmail_df[1] }} ({{ vmail_df[4] }}) +
+
+
+
+
+
-
-
-
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Hostname
+

{{ hostname }}

+
Version
+

{{ mailcow_info.version_tag }}

+

+
Changelog + {{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }} +
{{ lang.debug.current_time }}-
{{ lang.debug.timezone }}{{ timezone }}
{{ lang.debug.uptime }}-
CPU + Cores -
+ Usage +
{{ lang.debug.memory }} + Total -
+ Usage +
+
+
+ +
+ +
+
+ +
+ +
+ + {{ lang.admin.guid_and_license }} + +
+
+
+
+ +
+
+ + + + +
+

+ {{ lang.admin.customer_id }}: {{ gal.c|default('?')|raw }} - + {{ lang.admin.service_id }}: {{ gal.s|default('?')|raw }} - + {{ lang.admin.sal_level }}: {{ gal.m|default('?')|raw }} +

+
+
+
+
+

{{ lang.admin.license_info|raw }}

+
+ +
+
+
+
+

{{ lang.debug.solr_status }}

@@ -124,6 +223,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Postfix
@@ -139,6 +239,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Mailcow UI
@@ -154,6 +255,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
SASL
@@ -169,6 +271,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Dovecot
@@ -184,6 +287,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
SOGo
@@ -199,6 +303,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Netfilter
@@ -214,6 +319,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Rspamd history
@@ -234,6 +340,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Autodiscover
@@ -249,6 +356,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Watchdog
@@ -264,6 +372,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
ACME
@@ -279,6 +388,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
API
@@ -294,6 +404,7 @@
+
{{ lang.debug.log_info|format(log_lines+1)|raw }}
Ratelimits
@@ -315,6 +426,7 @@