Files
monitoring/traffic-peak-monitor.sh
T

244 lines
10 KiB
Bash
Executable File

#!/bin/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/<scriptname ohne Endung>-<interface>.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
# Locale fuer Zahlenformate fest auf "C" setzen: printf's %f-Format
# richtet sich sonst nach der System-Locale und erwartet dort z.B. bei
# deutschen Locales ein Komma statt eines Punkts als Dezimaltrennzeichen.
# awk liefert die Prozentwerte weiter unten aber immer mit Punkt - ohne
# dieses LC_NUMERIC=C wuerde printf das mit "Ungueltige Zahl" ablehnen.
export LC_NUMERIC=C
# --- 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).
#
# Feste Feldbreiten fuer eine sauber ausgerichtete Ausgabe:
# %7d -> kbit/s-Wert, rechtsbuendig, 7 Stellen breit.
# 7 Stellen reichen bis 9.999.999 kbit/s (= 9999 Mbit/s),
# deckt also auch 1000-Mbit/s-Leitungen (max. 1.000.000
# kbit/s = 7-stellig) komfortabel ab.
# %5.1f -> Prozentwert, rechtsbuendig, 1 Nachkommastelle,
# 5 Stellen breit (deckt bis "100.0" ab).
# Durch die feste Breite des peak_rx-Blocks beginnt "peak_tx="
# in jeder Zeile automatisch an derselben Spalte (linksbuendig),
# unabhaengig davon wie gross die einzelnen Zahlen sind.
printf '%s peak_rx=%7dkbit/s (%5.1f%%) peak_tx=%7dkbit/s (%5.1f%%)\n' \
"${HOUR_START}-${HOUR_END}" "$MAX_RX_KBIT" "$RX_PCT" "$MAX_TX_KBIT" "$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}-<interface>.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"