Bladeren bron

Add files via upload

TheHuman 10 maanden geleden
bovenliggende
commit
3dd36f31b0
3 gewijzigde bestanden met toevoegingen van 704 en 0 verwijderingen
  1. 67 0
      nginx.md
  2. 500 0
      ntp-stat.sh
  3. 137 0
      readme.md

+ 67 - 0
nginx.md

@@ -0,0 +1,67 @@
+# Serve the HTML Report with Nginx
+
+You can use Nginx to serve the HTML report (index.html) either locally for quick access or online for remote monitoring.
+
+## Serve Localy with Nginx
+
+1. Install Start and Enable Nginx :
+```bash
+   sudo apt update
+   sudo apt install nginx
+   sudo systemctl start nginx
+   sudo systemctl enable nginx
+```
+2. Create and configure conf file of your site :
+```bash
+   sudo touch /etc/nginx/sites-available/ntp-stat
+```
+```bash
+   sudo nano /etc/nginx/sites-available/ntp-stat
+```
+```bash
+server {
+    listen 127.0.0.1:80;
+
+    root /var/www/ntp-stat;
+    index index.html;
+
+    location / {
+        try_files $uri $uri/ =404;
+    }
+}
+```
+3. Enable the site and reload nginx :
+```bash
+sudo ln -s /etc/nginx/sites-available/ntp-stat /etc/nginx/sites-enabled/
+sudo nginx -t 
+sudo systemctl reload nginx
+```
+## Serve Online with Nginx
+
+⚠️ The instructions are meant only to help you quickly serve the HTML report online. If you're planning to expose the report to the internet, you should follow proper best practices for :
+
+- Secure server configuration
+- HTTPS setup
+- Access control, firewalls, and authentication
+- ...
+
+
+1. Same like localy but : 
+```bash
+   sudo nano /etc/nginx/sites-available/ntp-stat
+```
+```bash
+server {
+    listen 80;
+    server_name HERE; # HERE PLACE YOUR PUBLIC IP OR DOMAIN NAME
+
+    root /var/www/ntp-stat;
+    index index.html;
+
+    location / {
+        try_files $uri $uri/ =404;
+    }
+}
+```
+
+

+ 500 - 0
ntp-stat.sh

@@ -0,0 +1,500 @@
+#!/bin/bash
+set -e
+
+#### Configuration ####
+INTERFACE="eth0"
+
+PAGE_TITLE="Network Traffic and NTP Statistics for ${INTERFACE}"
+OUTPUT_DIR="/var/www/ntp-stat"
+HTML_FILENAME="index.html"
+
+ENABLE_LOGGING="yes"
+LOG_FILE="/var/log/ntp-stat.log"
+RRD_DIR="/var/lib/chrony-rrd"
+RRD_FILE="$RRD_DIR/chrony.rrd"
+WIDTH=800
+HEIGHT=300
+TIMEOUT_SECONDS=5
+#######################
+
+log_message() {
+    local level="$1"
+    local message="$2"
+    if [[ "$ENABLE_LOGGING" == "yes" ]]; then
+    	echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE"
+    fi
+    	echo "[$level] $message"
+}
+
+validate_numeric() {
+    local value="$1"
+    local name="$2"
+    if ! [[ "$value" =~ ^[0-9]+$ ]]; then
+        log_message "ERROR" "Invalid $name: $value. Must be numeric."
+        exit 1
+    fi
+}
+
+check_commands() {
+    local commands=("vnstati" "rrdtool" "chronyc" "sudo" "timeout")
+    for cmd in "${commands[@]}"; do
+        if ! command -v "$cmd" &>/dev/null; then
+            log_message "ERROR" "Command '$cmd' not found in PATH."
+            exit 1
+        fi
+    done
+}
+
+setup_directories() {
+    log_message "INFO" "Checking and preparing directories..."
+    for dir in "$OUTPUT_DIR" "$RRD_DIR"; do
+        mkdir -p "$dir" || {
+            log_message "ERROR" "Failed to create directory: $dir"
+            exit 1
+        }
+        if [ ! -w "$dir" ]; then
+            log_message "ERROR" "Directory '$dir' is not writable."
+            exit 1
+        fi
+    done
+}
+
+generate_vnstat_images() {
+    log_message "INFO" "Generating vnStat images for interface '$INTERFACE'..."
+    local modes=("s" "d" "t" "h" "m" "y")
+    for mode in "${modes[@]}"; do
+        vnstati -"$mode" -i "$INTERFACE" -o "$OUTPUT_DIR/vnstat_${mode}.png" || {
+            log_message "ERROR" "Failed to generate vnstat image for mode $mode"
+            exit 1
+        }
+    done
+}
+
+collect_chrony_data() {
+    log_message "INFO" "Collecting Chrony data..."
+    get_html() {
+        timeout "$TIMEOUT_SECONDS"s sudo chronyc "$1" -v 2>&1 | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/' || {
+            log_message "ERROR" "Failed to collect chronyc $1 data"
+            return 1
+        }
+    }
+
+    RAW_TRACKING=$(timeout "$TIMEOUT_SECONDS"s sudo chronyc tracking) || {
+        log_message "ERROR" "Failed to collect chronyc tracking data"
+        exit 1
+    }
+    CHRONYC_TRACKING_HTML=$(echo "$RAW_TRACKING" | sed 's/&/\&/g;s/</\</g;s/>/\>/g;s/$/<br>/')
+    CHRONYC_SOURCES=$(get_html sources) || exit 1
+    CHRONYC_SOURCESTATS=$(get_html sourcestats) || exit 1
+    CHRONYC_SELECTDATA=$(get_html selectdata) || exit 1
+}
+
+extract_chronyc_values() {
+    extract_val() {
+        echo "$RAW_TRACKING" | awk "/$1/ {print \$$2}" | grep -E '^[-+]?[0-9.]+$' || echo "U"
+    }
+
+    OFFSET=$(extract_val "Last offset" "NF-1")
+    FREQ=$(extract_val "Frequency" "NF-2")
+    RESID_FREQ=$(extract_val "Residual freq" "NF-1")
+    SKEW=$(extract_val "Skew" "NF-1")
+    DELAY=$(extract_val "Root delay" "NF-1")
+    DISPERSION=$(extract_val "Root dispersion" "NF-1")
+    STRATUM=$(extract_val "Stratum" "3")
+
+    RAW_STATS=$(LC_ALL=C sudo chronyc serverstats) || {
+        log_message "ERROR" "Failed to collect chronyc serverstats"
+        exit 1
+    }
+    get_stat() {
+        echo "$RAW_STATS" | awk -F'[[:space:]]*:[[:space:]]*' "/$1/ {print \$2}" | grep -E '^[0-9]+$' || echo "U"
+    }
+    PKTS_RECV=$(get_stat "NTP packets received")
+    PKTS_DROP=$(get_stat "NTP packets dropped")
+    CMD_RECV=$(get_stat "Command packets received")
+    CMD_DROP=$(get_stat "Command packets dropped")
+    LOG_DROP=$(get_stat "Client log records dropped")
+    NTS_KE_ACC=$(get_stat "NTS-KE connections accepted")
+    NTS_KE_DROP=$(get_stat "NTS-KE connections dropped")
+    AUTH_PKTS=$(get_stat "Authenticated NTP packets")
+    INTERLEAVED=$(get_stat "Interleaved NTP packets")
+    TS_HELD=$(get_stat "NTP timestamps held")
+}
+
+create_rrd_database() {
+    if [ ! -f "$RRD_FILE" ]; then
+        log_message "INFO" "Creating new RRD file: $RRD_FILE"
+        LC_ALL=C rrdtool create "$RRD_FILE" --step 300 \
+            DS:offset:GAUGE:600:U:U DS:frequency:GAUGE:600:U:U DS:resid_freq:GAUGE:600:U:U DS:skew:GAUGE:600:U:U \
+            DS:delay:GAUGE:600:U:U DS:dispersion:GAUGE:600:U:U DS:stratum:GAUGE:600:0:16 \
+            DS:pkts_recv:COUNTER:600:0:U DS:pkts_drop:COUNTER:600:0:U DS:cmd_recv:COUNTER:600:0:U \
+            DS:cmd_drop:COUNTER:600:0:U DS:log_drop:COUNTER:600:0:U DS:nts_ke_acc:COUNTER:600:0:U \
+            DS:nts_ke_drop:COUNTER:600:0:U DS:auth_pkts:COUNTER:600:0:U DS:interleaved:COUNTER:600:0:U \
+            DS:ts_held:GAUGE:600:0:U \
+            RRA:AVERAGE:0.5:1:576 RRA:AVERAGE:0.5:6:672 RRA:AVERAGE:0.5:24:732 RRA:AVERAGE:0.5:288:730 \
+            RRA:MAX:0.5:1:576 RRA:MAX:0.5:6:672 RRA:MAX:0.5:24:732 RRA:MAX:0.5:288:730 \
+            RRA:MIN:0.5:1:576 RRA:MIN:0.5:6:672 RRA:MIN:0.5:24:732 RRA:MIN:0.5:288:730 || {
+                log_message "ERROR" "Failed to create RRD database"
+                exit 1
+            }
+    fi
+}
+
+update_rrd_database() {
+    log_message "INFO" "Updating RRD database..."
+    UPDATE_STRING="N:$OFFSET:$FREQ:$RESID_FREQ:$SKEW:$DELAY:$DISPERSION:$STRATUM:$PKTS_RECV:$PKTS_DROP:$CMD_RECV:$CMD_DROP:$LOG_DROP:$NTS_KE_ACC:$NTS_KE_DROP:$AUTH_PKTS:$INTERLEAVED:$TS_HELD"
+    LC_ALL=C rrdtool update "$RRD_FILE" "$UPDATE_STRING" || {
+        log_message "ERROR" "Failed to update RRD database"
+        exit 1
+    }
+}
+
+generate_graphs() {
+    log_message "INFO" "Generating graphs..."
+    local END_TIME=$(date +%s)
+    local START_TIME=$((END_TIME - 86400))
+    declare -A graphs=(
+        ["chrony_serverstats"]="--title 'Chrony Server Statistics - by day' --vertical-label 'Packets/second' \
+            --lower-limit 0 --units-exponent 0 \
+            DEF:pkts_recv='$RRD_FILE':pkts_recv:AVERAGE \
+            DEF:pkts_drop='$RRD_FILE':pkts_drop:AVERAGE \
+            DEF:cmd_recv='$RRD_FILE':cmd_recv:AVERAGE \
+            DEF:cmd_drop='$RRD_FILE':cmd_drop:AVERAGE \
+            DEF:log_drop='$RRD_FILE':log_drop:AVERAGE \
+            DEF:nts_ke_acc='$RRD_FILE':nts_ke_acc:AVERAGE \
+            DEF:nts_ke_drop='$RRD_FILE':nts_ke_drop:AVERAGE \
+            DEF:auth_pkts='$RRD_FILE':auth_pkts:AVERAGE \
+            'COMMENT: \l' \
+            'AREA:pkts_recv#C4FFC4:Packets received          ' \
+            'LINE1:pkts_recv#00E000:' \
+            'GPRINT:pkts_recv:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:pkts_recv:MIN:Min\: %5.2lf%s' \
+            'GPRINT:pkts_recv:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:pkts_recv:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:pkts_drop#FF8C00:Packets dropped          ' \
+            'GPRINT:pkts_drop:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:pkts_drop:MIN:Min\: %5.2lf%s' \
+            'GPRINT:pkts_drop:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:pkts_drop:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:cmd_recv#4169E1:Command packets received ' \
+            'GPRINT:cmd_recv:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:cmd_recv:MIN:Min\: %5.2lf%s' \
+            'GPRINT:cmd_recv:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:cmd_recv:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:cmd_drop#FFD700:Command packets dropped ' \
+            'GPRINT:cmd_drop:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:cmd_drop:MIN:Min\: %5.2lf%s' \
+            'GPRINT:cmd_drop:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:cmd_drop:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:log_drop#9400D3:Client log records dropped' \
+            'GPRINT:log_drop:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:log_drop:MIN:Min\: %5.2lf%s' \
+            'GPRINT:log_drop:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:log_drop:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:nts_ke_acc#8A2BE2:NTS-KE connections accepted' \
+            'GPRINT:nts_ke_acc:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:nts_ke_acc:MIN:Min\: %5.2lf%s' \
+            'GPRINT:nts_ke_acc:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:nts_ke_acc:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:nts_ke_drop#9370DB:NTS-KE connections dropped' \
+            'GPRINT:nts_ke_drop:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:nts_ke_drop:MIN:Min\: %5.2lf%s' \
+            'GPRINT:nts_ke_drop:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:nts_ke_drop:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:auth_pkts#FF0000:Authenticated NTP packets' \
+            'GPRINT:auth_pkts:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:auth_pkts:MIN:Min\: %5.2lf%s' \
+            'GPRINT:auth_pkts:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:auth_pkts:MAX:Max\: %5.2lf%s\l'"
+        ["chrony_tracking"]="--title 'Chrony Tracking Stats - by day' --vertical-label '(seconds,ppm)' \
+            --units-exponent 0 \
+            DEF:stratum='$RRD_FILE':stratum:AVERAGE \
+            DEF:offset='$RRD_FILE':offset:AVERAGE \
+            DEF:freq='$RRD_FILE':frequency:AVERAGE \
+            DEF:resid_freq='$RRD_FILE':resid_freq:AVERAGE \
+            DEF:skew='$RRD_FILE':skew:AVERAGE \
+            DEF:delay='$RRD_FILE':delay:AVERAGE \
+            DEF:dispersion='$RRD_FILE':dispersion:AVERAGE \
+            CDEF:offset_scaled=offset,1000,* \
+            CDEF:resfreq_scaled=resid_freq,100,* \
+            CDEF:skew_scaled=skew,100,* \
+            CDEF:delay_scaled=delay,1000,* \
+            CDEF:disp_scaled=dispersion,1000,* \
+            'COMMENT: \l' \
+            'LINE1:stratum#00E000:Stratum                    ' \
+            'GPRINT:stratum:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:stratum:MIN:Min\: %5.2lf%s' \
+            'GPRINT:stratum:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:stratum:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:offset_scaled#0000FF:System Time (x1000)        ' \
+            'GPRINT:offset_scaled:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:offset_scaled:MIN:Min\: %5.2lf%s' \
+            'GPRINT:offset_scaled:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:offset_scaled:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:freq#FFC300:Frequency (ppm)            ' \
+            'GPRINT:freq:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:freq:MIN:Min\: %5.2lf%s' \
+            'GPRINT:freq:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:freq:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:resfreq_scaled#FF69B4:Residual Freq (ppm, x100)  ' \
+            'GPRINT:resfreq_scaled:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:resfreq_scaled:MIN:Min\: %5.2lf%s' \
+            'GPRINT:resfreq_scaled:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:resfreq_scaled:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:skew_scaled#9400D3:Skew (ppm, x100)            ' \
+            'GPRINT:skew_scaled:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:skew_scaled:MIN:Min\: %5.2lf%s' \
+            'GPRINT:skew_scaled:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:skew_scaled:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:delay_scaled#00BFFF:Root delay (seconds, x1000) ' \
+            'GPRINT:delay_scaled:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:delay_scaled:MIN:Min\: %5.2lf%s' \
+            'GPRINT:delay_scaled:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:delay_scaled:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:disp_scaled#FFFF00:Root dispersion (sec, x1000)' \
+            'GPRINT:disp_scaled:LAST:  Cur\: %5.2lf%s' \
+            'GPRINT:disp_scaled:MIN:Min\: %5.2lf%s' \
+            'GPRINT:disp_scaled:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:disp_scaled:MAX:Max\: %5.2lf%s\l'"
+        ["chrony_offset"]="--title 'Chrony System Time Offset - by day' --vertical-label 'seconds' \
+            DEF:offset='$RRD_FILE':offset:AVERAGE \
+            CDEF:offset_ms=offset,1000,* \
+            LINE2:offset_ms#00ff00:'System time offset to NTP time' \
+            GPRINT:offset_ms:LAST:'Cur\: %5.2lf%sms\n'"
+        ["chrony_delay"]="--title 'Chrony Network Delay - by day' --vertical-label 'seconds' \
+            DEF:delay='$RRD_FILE':delay:AVERAGE \
+            CDEF:delay_ms=delay,1000,* \
+            LINE2:delay_ms#00ff00:'Network path delay' \
+            GPRINT:delay_ms:LAST:'Cur\: %5.2lf%sms\n'"
+        ["chrony_frequency"]="--title 'Chrony Clock Frequency Error - by day' --vertical-label 'ppm' \
+            DEF:freq='$RRD_FILE':frequency:AVERAGE \
+            LINE2:freq#00ff00:'Local clock frequency error' \
+            GPRINT:freq:LAST:'Cur\: %5.2lf%sppm\n'"
+        ["chrony_drift"]="--title 'Chrony Drift - by day' --vertical-label 'Parts Per Million' \
+            --units-exponent 0 \
+            DEF:freq='$RRD_FILE':frequency:AVERAGE \
+            DEF:skew='$RRD_FILE':skew:AVERAGE \
+            'COMMENT: \l' \
+            'LINE1:freq#32CD32:System Clock Gain/Loss Rate' \
+            'GPRINT:freq:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:freq:MIN:Min\: %5.2lf%s' \
+            'GPRINT:freq:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:freq:MAX:Max\: %5.2lf%s\l' \
+            'LINE1:skew#4169E1:Estimate of Error Bound    ' \
+            'GPRINT:skew:LAST:Cur\: %5.2lf%s' \
+            'GPRINT:skew:MIN:Min\: %5.2lf%s' \
+            'GPRINT:skew:AVERAGE:Avg\: %5.2lf%s' \
+            'GPRINT:skew:MAX:Max\: %5.2lf%s\l'"
+    )
+
+    for graph in "${!graphs[@]}"; do
+        local cmd="LC_ALL=C rrdtool graph '$OUTPUT_DIR/$graph.png' --width '$WIDTH' --height '$HEIGHT' --start end-1d --end now-180s ${graphs[$graph]}"
+        eval "$cmd" || {
+            log_message "ERROR" "Failed to generate graph: $graph"
+            exit 1
+        }
+    done
+}
+
+generate_html() {
+    log_message "INFO" "Generating HTML report..."
+    local GENERATED_TIMESTAMP=$(date)
+    cat >"$OUTPUT_DIR/$HTML_FILENAME" <<EOF
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>${PAGE_TITLE} - Server Status</title>
+    <style>
+        :root {
+            --primary-text: #212529;
+            --secondary-text: #6c757d;
+            --background-color: #f8f9fa;
+            --content-background: #ffffff;
+            --border-color: #dee2e6;
+            --code-background: #282c34;
+            --code-text: #abb2bf;
+        }
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            margin: 0;
+            padding: 20px;
+            background-color: var(--background-color);
+            color: var(--primary-text);
+            line-height: 1.6;
+        }
+        .container {
+            max-width: 1200px;
+            margin: 0 auto;
+            background-color: var(--content-background);
+            padding: 20px 40px;
+            border-radius: 8px;
+            box-shadow: 0 4px 8px rgba(0,0,0,0.05);
+        }
+        header {
+            text-align: center;
+            border-bottom: 1px solid var(--border-color);
+            padding-bottom: 20px;
+            margin-bottom: 30px;
+        }
+        header h1 {
+            margin: 0;
+            font-size: 2.5em;
+            color: var(--primary-text);
+        }
+        section {
+            margin-bottom: 40px;
+        }
+        h2 {
+            font-size: 1.8em;
+            color: var(--primary-text);
+            border-bottom: 1px solid var(--border-color);
+            padding-bottom: 10px;
+            margin-top: 0;
+            margin-bottom: 20px;
+        }
+        h3 {
+            font-size: 1.3em;
+            color: var(--primary-text);
+            margin-top: 25px;
+        }
+        .graph-grid {
+            display: grid;
+            grid-template-columns: 1fr;
+            gap: 10px;
+            text-align: center;
+        }
+        @media (min-width: 768px) {
+            .graph-grid {
+                grid-template-columns: repeat(2, 1fr);
+            }
+        }
+        figure {
+            margin: 0;
+            padding: 0;
+        }
+        img {
+            max-width: 100%;
+            height: auto;
+            border: 1px solid var(--border-color);
+            border-radius: 4px;
+            box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+        }
+        pre {
+            background-color: var(--code-background);
+            color: var(--code-text);
+            padding: 10px;
+            border: 1px solid #444;
+            border-radius: 4px;
+            overflow-x: auto;
+            white-space: pre-wrap;
+            word-wrap: break-word;
+            font-size: 0.8em;
+        }
+        footer {
+            text-align: center;
+            margin-top: 40px;
+            padding-top: 20px;
+            border-top: 1px solid var(--border-color);
+            font-size: 0.9em;
+            color: var(--secondary-text);
+        }
+    </style>
+</head>
+<body>
+    <div class="container">
+	<main>
+            <section id="chrony-graphs">
+                <h2>Chrony Graphs</h2>
+                <div class="graph-grid">
+                    <figure>
+                        <img src="chrony_serverstats.png" alt="Chrony server statistics graph">
+                    </figure>
+                    <figure>
+                        <img src="chrony_offset.png" alt="Chrony system clock offset graph">
+                    </figure>
+                    <figure>
+                        <img src="chrony_tracking.png" alt="Chrony system clock tracking graph">
+                    </figure>
+                    <figure>
+                        <img src="chrony_delay.png" alt="Chrony sync delay graph">
+                    </figure>
+                    <figure>
+                        <img src="chrony_frequency.png" alt="Chrony clock frequency graph">
+                    </figure>
+                    <figure>
+                        <img src="chrony_drift.png" alt="Chrony clock frequency drift graph">
+                    </figure>
+                </div>
+            </section>
+
+            <section id="vnstat-graphs">
+                <h2>vnStati  Graphs</h2>
+                <div class="graph-grid">
+                    <figure>
+                        <img src="vnstat_s.png" alt="vnStat live traffic graph">
+                    </figure>
+                    <figure>
+                        <img src="vnstat_h.png" alt="vnStat hourly traffic graph">
+                     </figure>
+                    <figure>
+                        <img src="vnstat_d.png" alt="vnStat daily traffic graph">
+                     </figure>
+                    <figure>
+                        <img src="vnstat_m.png" alt="vnStat monthly traffic graph">
+                    </figure>
+                     <figure>
+                        <img src="vnstat_y.png" alt="vnStat yearly traffic graph">
+                    </figure>
+                    <figure>
+                        <img src="vnstat_t.png" alt="vnStat top 10 days traffic graph">
+                    </figure>
+                </div>
+            </section>
+
+            <section id="chrony-stats">
+                <h2>Chrony - NTP Statistics</h2>
+
+                <h3>Command: <code>chronyc sources -v</code></h3>
+                <pre><code>${CHRONYC_SOURCES}</code></pre>
+
+                <h3>Command: <code>chronyc selectdata -v</code></h3>
+                <pre><code>${CHRONYC_SELECTDATA}</code></pre>
+
+                <h3>Command: <code>chronyc sourcestats -v</code></h3>
+                <pre><code>${CHRONYC_SOURCESTATS}</code></pre>
+
+                <h3>Command: <code>chronyc tracking</code></h3>
+                <pre><code>${CHRONYC_TRACKING_HTML}</code></pre>
+            </section>
+        </main>
+
+        <footer>
+            <p>Page generated on: ${GENERATED_TIMESTAMP}</p>
+        </footer>
+    </div>
+</body>
+</html>
+EOF
+}
+
+main() {
+    log_message "INFO" "Starting vnstati script..."
+    validate_numeric "$WIDTH" "WIDTH"
+    validate_numeric "$HEIGHT" "HEIGHT"
+    validate_numeric "$TIMEOUT_SECONDS" "TIMEOUT_SECONDS"
+    check_commands
+    setup_directories
+    generate_vnstat_images
+    collect_chrony_data
+    extract_chronyc_values
+    create_rrd_database
+    update_rrd_database
+    generate_graphs
+    generate_html
+    log_message "INFO" "HTML page and graphs generated in: $OUTPUT_DIR/$HTML_FILENAME"
+    echo "✅ Successfully generated report"
+}
+
+main

+ 137 - 0
readme.md

@@ -0,0 +1,137 @@
+# Chrony and network monitoring script
+
+Bash script designed to monitor network traffic and Chrony statistics, generating visual graphs and an HTML report for easy monitoring.
+It’s lightweight, requires minimal system resources, and is ideal for low-resource servers
+
+Demo here : [https://thehuman00.github.io/demo-chrony-stats.github.io/](https://thehuman00.github.io/demo-chrony-stats.github.io/)
+
+## Features
+
+- **HTML report**: Creates a styled HTML page with embedded graphs and raw `chronyc` command outputs.
+- **Network  monitoring**
+- **Chrony statistics**
+- **Lightweight**: Designed to be resource-efficient, with minimal overhead and no heavy dependencies.
+
+## Requirements
+
+- **Dependencies**:
+  - `vnstat` + `vnstati` : For network traffic monitoring + graph generation.
+  - `chrony` : For NTP statistics collection.
+  - `rrdtool` : For creating and managing the Round-Robin Database.
+  - `timeout` (part of `coreutils`) : For setting timeouts on `chronyc` commands.
+
+**Install dependencies** (on Debian/Ubuntu-based systems):
+   ```bash
+   sudo apt update
+   sudo apt install vnstat vnstati rrdtool chrony coreutils
+   ```
+**Configure vnStat**:
+   Ensure `vnstat` is monitoring the correct network interface (e.g., `eth0`):
+   ```bash
+   sudo vnstat -u -i YOUR-INTERFACE
+   ```
+   Replace `YOUR-INTERFACE` with your network interface. Find here :
+   ```bash
+   vnstat --iflist
+   ```
+   ⚠️ Change your network interface (if not eth0) in [Configuration](#configuration) ⚠️
+
+
+## Installation
+
+1. **Download the script**:
+   ```bash
+   wget https://raw.githubusercontent.com/TheHuman00/chrony-stats/master/ntp-stat.sh -O /usr/local/bin/ntp-stat.sh
+   ```
+
+2. **Make the script executable**:
+   ```bash
+   chmod +x /usr/local/bin/ntp-stat.sh
+   ```
+
+3. **Test the script**:
+   Run the script manually to ensure it works:
+   ```bash
+   sudo /usr/local/bin/ntp-stat.sh
+   ```
+   Check the output in `/var/www/ntp-stat/index.html` and/or verify logs in `/var/log/ntp-stat.log`.
+
+## Configuration
+
+   ```bash
+   sudo nano /usr/local/bin/ntp-stat.sh
+   ```
+
+The script includes a configuration section at the top of `ntp-stat.sh`. Modify these variables as needed:
+
+   ```bash
+   [...]
+   #### Configuration ####
+
+   # ⚠️ IMPORTANT: Replace "eth0" with your actual interface 
+   #    (e.g., ens33, enp0s3, wlan0, ...)
+   INTERFACE="eth0"
+
+   PAGE_TITLE="Network Traffic and NTP Statistics for ${INTERFACE}"
+   OUTPUT_DIR="/var/www/ntp-stat"
+   HTML_FILENAME="index.html"
+
+   ENABLE_LOGGING="yes"
+   LOG_FILE="/var/log/ntp-stat.log"
+   RRD_DIR="/var/lib/chrony-rrd"
+   RRD_FILE="$RRD_DIR/chrony.rrd"
+   WIDTH=800
+   HEIGHT=300
+   TIMEOUT_SECONDS=5
+   #########################
+   [...]
+   ```
+
+
+## Setting up a crontab (Run every 5 minutes)
+
+To run the script every 5 minutes with `sudo` privileges, configure the root crontab :
+
+1. **Edit the root crontab**:
+   ```bash
+   sudo crontab -e
+   ```
+
+2. **Add the Following Line**:
+   ```bash
+   */5 * * * * /usr/local/bin/ntp-stat.sh
+   ```
+   This schedules the script to run every 5 minutes.
+
+3. **Verify Crontab**:
+   Check the crontab entry:
+   ```bash
+   sudo crontab -l
+   ```
+
+## Usage
+
+1. **Run the Script**:
+   ```bash
+   sudo /usr/local/bin/ntp-stat.sh
+   ```
+
+2. **View the Output**:
+   - The HTML report is generated at `/var/www/ntp-stat/index.html`.
+   - Open this file in a web browser or serve it via a web server (e.g., Apache or Nginx).
+
+   [See here how to serve via nginx in localhost](nginx.md)
+
+3. **Monitor Logs**:
+   Check `/var/log/ntp-stat.log` for execution details and errors.
+
+## License
+
+This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+
+## Acknowledgments
+
+Built with `vnstat`, `vnstati`, `rrdtool`, and `chrony`.
+- https://humdi.net/vnstat/
+- https://rrdtool.org/rrdtool/index.en.html
+- https://chrony-project.org/