From 148a1f72955232b1702c3726264a541eac7b86cf Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 3 Aug 2026 20:53:18 +0200 Subject: [PATCH] Add script 'traffic-peak-monitor.sh'. --- conf/traffic-peak-monitor-IFACE.conf.sample | 107 +++++++++ .../traffic-peak-monitor.service | 40 ++++ traffic-peak-monitor.sh | 224 ++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 conf/traffic-peak-monitor-IFACE.conf.sample create mode 100644 systemd-service-files/traffic-peak-monitor.service create mode 100755 traffic-peak-monitor.sh diff --git a/conf/traffic-peak-monitor-IFACE.conf.sample b/conf/traffic-peak-monitor-IFACE.conf.sample new file mode 100644 index 0000000..cc96f6c --- /dev/null +++ b/conf/traffic-peak-monitor-IFACE.conf.sample @@ -0,0 +1,107 @@ +# ============================================================================ +# traffic-peak-monitor-IFACE.conf.sample +# ============================================================================ +# +# Beispiel-/Vorlagen-Konfiguration fuer traffic-peak-monitor.sh +# +# VERWENDUNG: +# 1. Diese Datei kopieren und "IFACE" im Dateinamen durch den echten +# Namen des zu ueberwachenden Netzwerk-Interfaces ersetzen. +# Der Dateiname MUSS folgendem Schema entsprechen: +# +# traffic-peak-monitor-.conf +# +# Beispiele: +# cp traffic-peak-monitor-IFACE.conf.sample traffic-peak-monitor-lan4.conf +# cp traffic-peak-monitor-IFACE.conf.sample traffic-peak-monitor-lan4.11.conf +# +# 2. Werte unten an die tatsaechlichen Gegebenheiten anpassen. +# +# 3. Die Endung ".sample" ist wichtig: Dateien mit dieser Endung werden +# vom Launcher-Modus des Scripts NICHT eingelesen (nur echte +# *.conf Dateien werden gesucht) - diese Datei hier kann also +# gefahrlos als Vorlage im conf/-Verzeichnis liegen bleiben, ohne +# selbst einen Monitoring-Prozess auszuloesen. +# +# HINWEIS: +# Diese Datei wird von "traffic-peak-monitor.sh" per "source" eingelesen, +# d.h. es ist technisch gesehen ein kleines Bash-Script. Nur einfache +# Variablenzuweisungen verwenden (VARIABLE=wert), keine Leerzeichen um +# das "=", keine Shell-Kommandos einfuegen. +# +# ============================================================================ + + +# ---------------------------------------------------------------------- +# INTERVAL - Messintervall in Sekunden +# ---------------------------------------------------------------------- +# Legt fest, wie oft der Byte-Zaehler des Interfaces ausgelesen wird, um +# daraus die aktuelle Uebertragungsrate zu berechnen. +# +# Je KLEINER der Wert, desto GENAUER werden kurze Lastspitzen erfasst - +# ein Peak, der nur 1-2 Sekunden dauert, wird bei INTERVAL=10 evtl. gar +# nicht oder nur stark abgeschwaecht sichtbar, weil er "weggemittelt" wird. +# +# Je KLEINER der Wert, desto MEHR (minimale) Last erzeugt die Messung +# selbst, da haeufiger Dateien gelesen und Berechnungen durchgefuehrt +# werden. Fuer ein einzelnes Interface ist das i.d.R. vernachlaessigbar. +# +# Empfehlung: +# 2 = guter Kompromiss aus Genauigkeit und Overhead (Standard) +# 1 = sehr genau, minimal mehr Last +# 5-10 = grober, reicht z.B. bei eher traegen/gleichmaessigen Leitungen +# +# Default im Script, falls hier nicht gesetzt: 2 +INTERVAL=2 + + +# ---------------------------------------------------------------------- +# LINE_SPEED_IN_MBIT - Download-Kapazitaet der Leitung in Mbit/s +# ---------------------------------------------------------------------- +# Die maximale EINGEHENDE (rx) Bandbreite der Leitung, an der dieses +# Interface haengt - also das, was z.B. im Vertrag mit dem Provider als +# "Download-Geschwindigkeit" angegeben ist. +# +# Wird verwendet, um die gemessenen rx-Spitzenwerte als Prozentwert der +# verfuegbaren Kapazitaet auszugeben (z.B. "45.2%" bedeutet: die Spitze +# hat 45,2 % der theoretisch moeglichen Download-Rate ausgenutzt). +# +# WICHTIG: Angabe in Mbit/s (Megabit), NICHT MByte/s (Megabyte). +# Netzwerk-Bandbreiten werden ueblicherweise in Bit/s angegeben - genau +# wie es auch im Vertrag beim Provider steht (z.B. "100 Mbit/s Download"). +# +# Beispiel-Werte gaengiger Anschluesse: +# DSL 50/10 -> LINE_SPEED_IN_MBIT=50 +# DSL 100/40 -> LINE_SPEED_IN_MBIT=100 +# Kabel 500/50 -> LINE_SPEED_IN_MBIT=500 +# Glasfaser 1000/500 -> LINE_SPEED_IN_MBIT=1000 +# +# Default im Script, falls hier nicht gesetzt: 100 +LINE_SPEED_IN_MBIT=100 + + +# ---------------------------------------------------------------------- +# LINE_SPEED_OUT_MBIT - Upload-Kapazitaet der Leitung in Mbit/s +# ---------------------------------------------------------------------- +# Die maximale AUSGEHENDE (tx) Bandbreite der Leitung - also die +# "Upload-Geschwindigkeit" laut Vertrag. +# +# Viele Anschluesse sind ASYMMETRISCH, d.h. Download- und Upload-Rate +# unterscheiden sich deutlich (z.B. DSL 100/40 = 100 Mbit/s Download, +# aber nur 40 Mbit/s Upload). Deshalb wird dieser Wert getrennt von +# LINE_SPEED_IN_MBIT angegeben - eine gemeinsame Prozentrechnung ueber +# einen einzigen Wert wuerde bei asymmetrischen Anschluessen falsche +# bzw. irrefuehrende Prozentzahlen liefern. +# +# Beispiel-Werte gaengiger Anschluesse (Fortsetzung von oben): +# DSL 50/10 -> LINE_SPEED_OUT_MBIT=10 +# DSL 100/40 -> LINE_SPEED_OUT_MBIT=40 +# Kabel 500/50 -> LINE_SPEED_OUT_MBIT=50 +# Glasfaser 1000/500 -> LINE_SPEED_OUT_MBIT=500 +# +# Bei SYMMETRISCHEN Anschluessen (z.B. manche Glasfaser-Vertraege mit +# z.B. 1000/1000) einfach den gleichen Wert wie bei LINE_SPEED_IN_MBIT +# eintragen. +# +# Default im Script, falls hier nicht gesetzt: 100 +LINE_SPEED_OUT_MBIT=100 diff --git a/systemd-service-files/traffic-peak-monitor.service b/systemd-service-files/traffic-peak-monitor.service new file mode 100644 index 0000000..647273a --- /dev/null +++ b/systemd-service-files/traffic-peak-monitor.service @@ -0,0 +1,40 @@ +# traffic-peak-monitor.service +# ------------------------------ +# Startet den traffic-peak-monitor.sh im Launcher-Modus (ohne Parameter). +# Der Launcher durchsucht selbststaendig das conf/-Verzeichnis und startet +# fuer jede gefundene Konfigurationsdatei einen eigenen Worker-Prozess. +# +# Es wird also NUR EINE einzige Service-Instanz benoetigt, unabhaengig +# davon wie viele Interfaces (lan4, lan4.11, ...) ueberwacht werden - +# neue Interfaces werden einfach per neuer .conf-Datei in conf/ +# hinzugefuegt, ohne die Service-Datei anzufassen. +# +# Installation: +# cp traffic-peak-monitor.service /etc/systemd/system/ +# systemctl daemon-reload +# systemctl enable --now traffic-peak-monitor.service +# +# Hinweis: Um eine neue .conf-Datei zu aktivieren, muss der Service +# neu gestartet werden (systemctl restart traffic-peak-monitor.service), +# damit der Launcher erneut nach Konfigurationen sucht. + +[Unit] +Description=Traffic Peak Monitor (Launcher fuer alle konfigurierten Interfaces) +After=network.target + +[Service] +# Kein "sudo" noetig - der Service laeuft laut [Service]-Block unten +# bereits direkt als root (User=root) +User=root + +# Launcher-Modus: ohne Parameter aufrufen, das Script findet seine +# Konfigurationen selbststaendig im conf/-Unterverzeichnis +ExecStart=/root/bin/monitoring/traffic-peak-monitor.sh + +# Falls der Launcher (oder einer seiner Worker-Kindprozesse, wodurch +# "wait" im Script zurueckkehrt) unerwartet endet, automatisch neu starten +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/traffic-peak-monitor.sh b/traffic-peak-monitor.sh new file mode 100755 index 0000000..1d336f2 --- /dev/null +++ b/traffic-peak-monitor.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +# +# traffic-peak-monitor.sh +# ------------------------ +# Ermittelt fuer ein Netzwerk-Interface die stuendlichen Spitzenwerte +# (Peaks) der ein- und ausgehenden Datenrate und schreibt sie stuendlich +# in eine Log-Datei. +# +# AUFRUF-MODI: +# +# 1) OHNE Parameter -> "Launcher-Modus" +# ./traffic-peak-monitor.sh +# Durchsucht das Verzeichnis "conf/" nach allen passenden +# Konfigurationsdateien (traffic-peak-monitor-*.conf), z.B.: +# conf/traffic-peak-monitor-lan4.conf +# conf/traffic-peak-monitor-lan4.11.conf +# und startet fuer JEDE gefundene Konfiguration einen eigenen +# Hintergrundprozess (das Script ruft sich dabei selbst im +# Worker-Modus auf, einmal pro Interface). +# +# 2) MIT Interface-Namen als Parameter -> "Worker-Modus" +# ./traffic-peak-monitor.sh lan4 +# Ueberwacht genau dieses eine Interface dauerhaft. Wird normal- +# erweise nicht manuell aufgerufen, sondern automatisch vom +# Launcher-Modus gestartet. +# +# KONFIGURATION: +# conf/-.conf +# Erwartete Variablen darin (alle optional, siehe Defaults unten): +# INTERVAL - Messintervall in Sekunden +# LINE_SPEED_IN_MBIT - Download-Kapazitaet der Leitung in Mbit/s +# LINE_SPEED_OUT_MBIT - Upload-Kapazitaet der Leitung in Mbit/s +# +# LOGS: +# Liegen unter /var/log/traffic-peak-monitor/ (wird beim Start +# automatisch angelegt, falls nicht vorhanden). Pro Interface gibt es +# eine AKTIVE Log-Datei ohne Datumsendung, z.B.: +# /var/log/traffic-peak-monitor/traffic-peak-monitor-lan4.log +# Sobald ein neuer Kalendertag beginnt, wird diese Datei mit dem Datum +# des Tages, zu dem ihre Eintraege gehoeren, umbenannt (= "rotiert"), +# z.B. in: +# /var/log/traffic-peak-monitor/traffic-peak-monitor-lan4.log.2026-08-03 +# Danach wird wieder in eine frische, aktive Log-Datei geschrieben. +# +# BERECHTIGUNGEN: +# Das Script muss als root (UID 0) laufen. Ein "sudo" ist dafuer nicht +# noetig, wenn es bereits als root gestartet wird (z.B. per systemd +# oder root-Crontab). + +set -u + +# --- Grundlegende Pfade ermitteln -------------------------------------- +# realpath loest auch Symlinks auf, damit das Script von ueberall aus +# aufgerufen werden kann (z.B. per systemd mit vollem Pfad) und trotzdem +# sein eigenes Verzeichnis korrekt findet. +script_name="$(basename "$(realpath "$0")")" +working_dir="$(dirname "$(realpath "$0")")" +conf_dir="${working_dir}/conf" + +# Feste Log-Basis unter /var/log - unabhaengig vom Script-Verzeichnis, +# damit Logs an der ueblichen Stelle fuer Systemd-Dienste liegen und +# z.B. von logrotate/Backup-Tools leichter gefunden werden. +log_dir="/var/log/traffic-peak-monitor" + +# Basisname ohne Endung, z.B. "traffic-peak-monitor" aus +# "traffic-peak-monitor.sh" - wird fuer Config- und Log-Dateinamen +# gebraucht. +base_name="${script_name%%.*}" + +# Log-Verzeichnis anlegen, falls es noch nicht existiert (inkl. evtl. +# fehlender Elternverzeichnisse, "-p" macht das automatisch und meldet +# auch keinen Fehler, falls es schon existiert) +mkdir -p "$log_dir" + +# --- Sicherheitscheck: muss als root laufen ---------------------------- +if [ "$(id -u)" -ne 0 ]; then + echo "Fehler: Dieses Script muss als root (UID 0) laufen." >&2 + exit 1 +fi + +# ======================================================================== +# WORKER: ueberwacht genau ein Interface dauerhaft +# ======================================================================== +run_worker() { + local IFACE=$1 + local conf_file="${conf_dir}/${base_name}-${IFACE}.conf" + local log_file="${log_dir}/${base_name}-${IFACE}.log" + + # Default-Werte, falls in der Konfigurationsdatei nicht gesetzt + local INTERVAL=2 + local LINE_SPEED_IN_MBIT=100 + local LINE_SPEED_OUT_MBIT=100 + + # Konfigurationsdatei einlesen - ueberschreibt die obigen Defaults + # mit den Werten aus der .conf Datei (einfaches "source", da die + # Config-Datei nur Variablenzuweisungen wie in einem Bash-Script + # enthaelt, z.B. LINE_SPEED_IN_MBIT=250) + if [ -f "$conf_file" ]; then + # shellcheck disable=SC1090 + source "$conf_file" + else + echo "Warnung: Konfigurationsdatei $conf_file nicht gefunden, nutze Default-Werte." >&2 + fi + + # Leitungskapazitaet von Mbit/s in kbit/s umrechnen, damit die Einheiten + # beim spaeteren Prozent-Vergleich zusammenpassen + local LINE_SPEED_IN_KBIT=$(( LINE_SPEED_IN_MBIT * 1000 )) + local LINE_SPEED_OUT_KBIT=$(( LINE_SPEED_OUT_MBIT * 1000 )) + + # Erste Messung als Ausgangsbasis + local PREV_RX PREV_TX MAX_RX MAX_TX CUR_HOUR HOUR_START HOUR_END CUR_DAY + PREV_RX=$(cat "/sys/class/net/${IFACE}/statistics/rx_bytes") + PREV_TX=$(cat "/sys/class/net/${IFACE}/statistics/tx_bytes") + MAX_RX=0 + MAX_TX=0 + CUR_HOUR=$(date +%H) + HOUR_START=$(date '+%Y-%m-%d %H:00') + HOUR_END=$(date -d '+1 hour' '+%H:00') + + # CUR_DAY merkt sich, zu welchem Kalendertag die aktuell gesammelten + # Daten (und damit die aktive, noch nicht rotierte Log-Datei) gehoeren. + CUR_DAY=$(date '+%Y-%m-%d') + + # Haupt-Schleife: laeuft dauerhaft, bis der Prozess beendet wird + while true; do + sleep "$INTERVAL" + local RX TX RX_RATE TX_RATE NOW_HOUR + RX=$(cat "/sys/class/net/${IFACE}/statistics/rx_bytes") + TX=$(cat "/sys/class/net/${IFACE}/statistics/tx_bytes") + + # Differenz * 8 = Byte -> Bit, / INTERVAL = Bit pro Sekunde + RX_RATE=$(( (RX - PREV_RX) * 8 / INTERVAL )) + TX_RATE=$(( (TX - PREV_TX) * 8 / INTERVAL )) + + [ "$RX_RATE" -gt "$MAX_RX" ] && MAX_RX=$RX_RATE + [ "$TX_RATE" -gt "$MAX_TX" ] && MAX_TX=$TX_RATE + + NOW_HOUR=$(date +%H) + if [ "$NOW_HOUR" != "$CUR_HOUR" ]; then + local MAX_RX_KBIT MAX_TX_KBIT RX_PCT TX_PCT + MAX_RX_KBIT=$(( MAX_RX / 1000 )) + MAX_TX_KBIT=$(( MAX_TX / 1000 )) + RX_PCT=$(awk -v v="$MAX_RX_KBIT" -v c="$LINE_SPEED_IN_KBIT" 'BEGIN{printf "%.1f", (v/c)*100}') + TX_PCT=$(awk -v v="$MAX_TX_KBIT" -v c="$LINE_SPEED_OUT_KBIT" 'BEGIN{printf "%.1f", (v/c)*100}') + + # Eintrag fuer die gerade abgeschlossene Stunde in die AKTIVE + # Log-Datei schreiben. Wichtig: das muss VOR einer moeglichen + # Tages-Rotation passieren, da dieser Eintrag noch zu CUR_DAY + # gehoert (z.B. der Eintrag "23:00-00:00" gehoert noch zum alten Tag). + echo "${HOUR_START}-${HOUR_END} peak_rx=${MAX_RX_KBIT}kbit/s (${RX_PCT}%) peak_tx=${MAX_TX_KBIT}kbit/s (${TX_PCT}%)" >> "$log_file" + + MAX_RX=0 + MAX_TX=0 + CUR_HOUR=$NOW_HOUR + HOUR_START=$(date '+%Y-%m-%d %H:00') + HOUR_END=$(date -d '+1 hour' '+%H:00') + + # --- Tages-Rotation pruefen --------------------------------------- + # Hat sich der Kalendertag geaendert (z.B. Uebergang 23:xx -> 00:xx)? + # Falls ja: die bisherige aktive Log-Datei bekommt als Endung das + # Datum des Tages, dessen Eintraege sie enthaelt (CUR_DAY, also der + # ALTE Tag - nicht der gerade begonnene). Anschliessend beginnt eine + # neue, leere aktive Log-Datei fuer den neuen Tag. + local NEW_DAY + NEW_DAY=$(date '+%Y-%m-%d') + if [ "$NEW_DAY" != "$CUR_DAY" ]; then + if [ -f "$log_file" ]; then + mv "$log_file" "${log_file}.${CUR_DAY}" + fi + CUR_DAY=$NEW_DAY + fi + fi + + PREV_RX=$RX + PREV_TX=$TX + done +} + +# ======================================================================== +# LAUNCHER: kein Parameter angegeben -> alle Konfigurationen suchen +# und je einen Worker-Prozess starten +# ======================================================================== +if [ $# -eq 0 ]; then + # nullglob sorgt dafuer, dass das Array leer bleibt, wenn keine + # passenden Dateien existieren (statt des woertlichen Musters) + shopt -s nullglob + conf_files=("${conf_dir}/${base_name}"-*.conf) + shopt -u nullglob + + if [ ${#conf_files[@]} -eq 0 ]; then + echo "Keine Konfigurationsdateien in ${conf_dir} gefunden (erwartet: ${base_name}-.conf)" >&2 + exit 1 + fi + + pids=() + for cf in "${conf_files[@]}"; do + # Interface-Namen aus dem Dateinamen herausschneiden: + # aus "traffic-peak-monitor-lan4.11.conf" wird "lan4.11" + cf_base=$(basename "$cf" .conf) + iface="${cf_base#${base_name}-}" + + echo "Starte Monitoring fuer Interface '${iface}' (Konfiguration: ${cf})" + # Script ruft sich selbst im Worker-Modus auf, im Hintergrund. + # stdout/stderr des Worker-Prozesses gehen in eine eigene .out Datei, + # damit unerwartete Fehler (z.B. falscher Interface-Name) auffindbar sind. + "$0" "$iface" >> "${log_dir}/${base_name}-${iface}.out" 2>&1 & + pids+=("$!") + done + + echo "Gestartete Worker-Prozesse (PIDs): ${pids[*]}" + + # "wait" haelt den Launcher-Prozess am Leben, solange mindestens ein + # Worker laeuft. Das ist wichtig, falls der Launcher selbst z.B. per + # systemd ueberwacht werden soll (siehe systemd-Unit) - ohne "wait" + # wuerde der Launcher sofort beendet sein, obwohl die Worker im + # Hintergrund weiterlaufen. + wait + exit 0 +fi + +# ======================================================================== +# WORKER-MODUS: Interface als Parameter uebergeben +# ======================================================================== +run_worker "$1"