Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions DOCKER_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![Docker Image Size](https://img.shields.io/docker/image-size/syntaxsorcerer7/internet-monitor)
![Docker Stars](https://img.shields.io/docker/stars/syntaxsorcerer7/internet-monitor)

A containerized internet monitoring tool that continuously tracks your internet connection availability and visualizes it in a clear web interface with professional charts.
A containerized internet monitoring tool that continuously tracks your internet connection availability and ping latency and visualizes it in a clear web interface with professional charts.

## 🚀 Quick Start

Expand All @@ -22,9 +22,9 @@ open http://localhost:8000
## 📊 Features

### 📈 Three Monitoring Levels
- **📍 Detailed History**: Minute-by-minute recording of all connectivity tests
- **⏰ 24-Hour Overview**: Hourly aggregation with availability percentages
- **📅 30-Day Overview**: Daily long-term trends for SLA monitoring
- **📍 Detailed History**: Minute-by-minute recording of all connectivity tests including ping latency
- **⏰ 24-Hour Overview**: Hourly aggregation with availability percentages plus average and 99th percentile ping
- **📅 30-Day Overview**: Daily long-term trends including average and 99th percentile ping for SLA monitoring

### 🎯 Professional Availability Levels
- 🟢 **Excellent** (≥99.9%): Enterprise-grade availability
Expand All @@ -35,9 +35,11 @@ open http://localhost:8000
### ⚡ Technical Highlights
- **Real-time Updates**: Automatic chart refresh every 60 seconds
- **Responsive Design**: Optimized for desktop, tablet, and mobile
- **Toggleable Charts**: Switch between availability and ping views
- **Persistent Storage**: SQLite database with automatic cleanup
- **Multi-Platform**: Supports AMD64 and ARM64 (Raspberry Pi, Apple Silicon)
- **High Performance**: Minimal resource usage through Alpine Linux
- **Ping Latency Measurement**: ICMP ping to capture latency and display in dashboard

## ⚙️ Configuration

Expand Down
56 changes: 32 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Internet-Verbindungsmonitor 🌐

Ein containerisiertes Internet-Monitoring-Tool, das die Verfügbarkeit Ihrer Internetverbindung kontinuierlich überwacht und in einer übersichtlichen Web-Oberfläche mit professionellen Diagrammen visualisiert.
Ein containerisiertes Internet-Monitoring-Tool, das die Verfügbarkeit Ihrer Internetverbindung sowie die Ping-Latenz kontinuierlich überwacht und in einer übersichtlichen Web-Oberfläche mit professionellen Diagrammen visualisiert.

![Internet Monitor Dashboard](https://img.shields.io/badge/Status-Production%20Ready-green)
![Container](https://img.shields.io/badge/Container-Podman%2FDocker-blue)
Expand Down Expand Up @@ -38,9 +38,9 @@ cd internet-check

### 📈 Drei Monitoring-Ebenen

- **📍 Detaillierter Verlauf**: Minutengenaue Aufzeichnung aller Connectivity-Tests
- **⏰ 24-Stunden-Übersicht**: Stündliche Aggregation mit Verfügbarkeitsprozenten
- **📅 30-Tage-Übersicht**: Tägliche Langzeittrends für SLA-Monitoring
- **📍 Detaillierter Verlauf**: Minutengenaue Aufzeichnung aller Connectivity-Tests inklusive Ping-Latenz
- **⏰ 24-Stunden-Übersicht**: Stündliche Aggregation mit Verfügbarkeitsprozenten sowie durchschnittlicher und 99%-Perzentil-Ping
- **📅 30-Tage-Übersicht**: Tägliche Langzeittrends inklusive durchschnittlicher und 99%-Perzentil-Ping-Latenz für SLA-Monitoring

### 🎯 Professionelle Verfügbarkeitsstufen

Expand All @@ -53,9 +53,11 @@ cd internet-check

- **Echtzeit-Updates**: Automatische Diagramm-Aktualisierung alle 60 Sekunden
- **Responsive Design**: Optimiert für Desktop, Tablet und Mobile
- **Umschaltbare Diagramme**: Verfügbarkeit und Ping lassen sich getrennt anzeigen
- **Persistente Speicherung**: SQLite-Datenbank mit automatischem Cleanup
- **Container-Ready**: Podman/Docker-basiert ohne komplexe Abhängigkeiten
- **Hochperformant**: Minimaler Ressourcenverbrauch durch Alpine Linux
- **Ping-Latenzmessung**: ICMP-Ping zur Erfassung der aktuellen Latenz und Anzeige im Dashboard

## ⚙️ Konfiguration

Expand Down Expand Up @@ -161,25 +163,29 @@ Das Projekt ist jetzt modular strukturiert:

### 📊 Datenmodell

Die Status-Tabelle speichert neben dem Onlinestatus auch die gemessene Ping-Latenz:

```sql
CREATE TABLE status (
ts INTEGER PRIMARY KEY, -- Unix-Timestamp (Sekunden seit 1970)
up INTEGER -- 1=online, 0=offline
ts INTEGER PRIMARY KEY, -- Unix-Timestamp (Sekunden seit 1970)
up INTEGER, -- 1 = online, 0 = offline
ping REAL -- gemessene Ping-Latenz in Millisekunden
);
```

### 🔗 API-Endpunkte

- `GET /` - Responsive Web-Dashboard
- `GET /data` - JSON-API mit Roh- und Aggregationsdaten
- `GET /data` - JSON-API mit Roh- und Aggregationsdaten inklusive Ping-Latenzen (`pings`)

### Monitoring-Algorithmus
### 🛰️ Monitoring-Algorithmus

1. **HTTP-Test**: GET-Request an konfigurierte URL
2. **Timeout-Handling**: 5 Sekunden maximale Wartezeit
3. **Bewertung**: HTTP 2xx = Online | Timeout/Error = Offline
4. **Speicherung**: Timestamp + Status in SQLite
5. **Retention**: Automatisches Cleanup alter Daten
1. **Ping-Messung**: ICMP-Ping zur Ermittlung der Latenz
2. **HTTP-Test**: GET-Request an die konfigurierte URL
3. **Timeout-Handling**: 5 Sekunden maximale Wartezeit
4. **Bewertung**: HTTP 2xx = Online, sonst Offline
5. **Speicherung**: Timestamp + Status + Ping in SQLite
6. **Retention**: Automatisches Cleanup alter Daten

## 💡 Anwendungsfälle

Expand Down Expand Up @@ -214,19 +220,21 @@ podman exec monitor-primary cp /app/data.db /tmp/
podman cp monitor-primary:/tmp/data.db ./backup_$(date +%Y%m%d).db

# CSV-Export für Excel-Analyse
podman exec monitor-primary sqlite3 /app/data.db \
"SELECT datetime(ts, 'unixepoch') as timestamp,
CASE up WHEN 1 THEN 'Online' ELSE 'Offline' END as status
FROM status ORDER BY ts;" \
-header -csv > connectivity_report.csv
podman exec monitor-primary sqlite3 /app/data.db \
"SELECT datetime(ts, 'unixepoch') as timestamp,
CASE up WHEN 1 THEN 'Online' ELSE 'Offline' END as status,
ping
FROM status ORDER BY ts;" \
-header -csv > connectivity_report.csv

# Statistiken abrufen
podman exec monitor-primary sqlite3 /app/data.db \
"SELECT
COUNT(*) as total_checks,
SUM(up) as successful_checks,
ROUND(SUM(up) * 100.0 / COUNT(*), 2) as uptime_percentage
FROM status WHERE ts >= strftime('%s', 'now', '-24 hours');"
podman exec monitor-primary sqlite3 /app/data.db \
"SELECT
COUNT(*) as total_checks,
SUM(up) as successful_checks,
ROUND(SUM(up) * 100.0 / COUNT(*), 2) as uptime_percentage,
ROUND(AVG(ping), 2) as avg_ping_ms
FROM status WHERE ts >= strftime('%s', 'now', '-24 hours');"
```

## 📊 SLA-Referenztabelle
Expand Down
82 changes: 60 additions & 22 deletions monitor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os, time, threading, sqlite3, datetime as dt, requests
import subprocess, urllib.parse, re, math
from flask import Flask, jsonify, Response, render_template

# ────────── Konfiguration via ENV ──────────
Expand All @@ -11,27 +12,45 @@ def get_utc_timestamp():
"""UTC-Timestamp zurückgeben (immer UTC, unabhängig von Container-Zeitzone)"""
return int(dt.datetime.now(dt.timezone.utc).timestamp())

def ping_host():
"""Einmaliges Ping auf das TEST_URL-Host ausführen und Laufzeit in ms zurückgeben."""
host = urllib.parse.urlparse(TEST_URL).hostname or TEST_URL
try:
r = subprocess.run(["ping", "-c", "1", "-W", "1", host], capture_output=True, text=True)
if r.returncode == 0:
m = re.search(r"time=([0-9.]+) ms", r.stdout)
if m:
return float(m.group(1))
except Exception:
pass
return None

# ────────── Backend ──────────

app = Flask(__name__)

def init_db():
"""Tabelle anlegen, falls sie noch nicht existiert."""
with sqlite3.connect(DB_PATH) as c:
c.execute("CREATE TABLE IF NOT EXISTS status(ts INTEGER PRIMARY KEY, up INTEGER)")
c.execute("CREATE TABLE IF NOT EXISTS status(ts INTEGER PRIMARY KEY, up INTEGER, ping REAL)")
try:
c.execute("ALTER TABLE status ADD COLUMN ping REAL")
except sqlite3.OperationalError:
pass

def write_sample():
"""Einen Connectivity-Datensatz schreiben und alte Daten trimmen."""
ping_ms = ping_host() # Ping immer messen, unabhängig vom HTTP-Test
up = 1
try:
try:
requests.get(TEST_URL, timeout=5)
except requests.RequestException:
except requests.RequestException:
up = 0

now = get_utc_timestamp() # UTC-Timestamp verwenden
with sqlite3.connect(DB_PATH) as c:
# INSERT OR REPLACE verwenden um Duplikate zu vermeiden
c.execute("INSERT OR REPLACE INTO status VALUES(?,?)", (now, up))
c.execute("INSERT OR REPLACE INTO status(ts,up,ping) VALUES(?,?,?)", (now, up, ping_ms))
# Alte Daten löschen - aber nur sehr alte (älter als RETENTION)
cutoff = now - RETENTION*86400
deleted = c.execute("DELETE FROM status WHERE ts < ?", (cutoff,)).rowcount
Expand All @@ -52,7 +71,7 @@ def index():
def data():
# Alle Daten holen (ohne since Filter) um das Problem zu debuggen
with sqlite3.connect(DB_PATH) as c:
rows = c.execute("SELECT ts,up FROM status ORDER BY ts").fetchall()
rows = c.execute("SELECT ts,up,ping FROM status ORDER BY ts").fetchall()
total_count = c.execute("SELECT COUNT(*) FROM status").fetchone()[0]

print(f"DEBUG: Total records: {total_count}, All records: {len(rows)}")
Expand All @@ -73,30 +92,40 @@ def data():
# Basis-Daten für detaillierten Verlauf - nur letzten 12 Stunden
twelve_hours_ago = now - (12 * 3600) # 12 Stunden in Sekunden
detail_rows = [row for row in rows if row[0] >= twelve_hours_ago]

result = {
"labels":[t for t,_ in detail_rows], # Raw UTC timestamps der letzten 12h senden
"values":[u for _,u in detail_rows]
"labels":[t for t,_,_ in detail_rows], # Raw UTC timestamps der letzten 12h senden
"values":[u for _,u,_ in detail_rows],
"pings":[p for _,_,p in detail_rows]
}

# 24-Stunden-Aggregation (letzte 24h)
current_hour_start = (now // 3600) * 3600 # Aktuelle Stunde, auf den Stundenanfang gerundet
hourly_data = []

for h in range(24):
# Von der aktuellen Stunde 23 Stunden zurückgehen
hour_start = current_hour_start - (h * 3600)
hour_end = hour_start + 3600
hour_rows = [up for ts, up in rows if hour_start <= ts < hour_end]
hour_rows = [(up, ping) for ts, up, ping in rows if hour_start <= ts < hour_end]

if hour_rows:
uptime = sum(hour_rows) / len(hour_rows)
ups = [u for u, _ in hour_rows]
pings = [p for _, p in hour_rows if p is not None]
uptime = sum(ups) / len(ups)
ping_avg = sum(pings) / len(pings) if pings else None
if pings:
k = max(0, min(len(pings)-1, math.ceil(len(pings) * 0.99) - 1))
ping_p99 = sorted(pings)[k]
else:
ping_p99 = None
else:
uptime = -1 # Keine Daten verfügbar

ping_avg = ping_p99 = None

# Nur die Stunde des Tages (0-23) senden
hour_of_day = dt.datetime.fromtimestamp(hour_start, dt.timezone.utc).hour
hourly_data.insert(0, {"hour": hour_of_day, "uptime": uptime})
hourly_data.insert(0, {"hour": hour_of_day, "uptime": uptime, "ping_avg": ping_avg, "ping_p99": ping_p99})

# Tages-Aggregation (letzte 30 Tage) - korrekte Kalendertage verwenden
daily_data = []
Expand All @@ -105,23 +134,32 @@ def data():
for d in range(30):
# Kalendertag berechnen (d Tage zurück vom heutigen Tag)
target_date = current_utc_date - dt.timedelta(days=d)

# Start und Ende des Kalendertages in UTC
day_start_dt = dt.datetime.combine(target_date, dt.time.min).replace(tzinfo=dt.timezone.utc)
day_end_dt = dt.datetime.combine(target_date, dt.time.max).replace(tzinfo=dt.timezone.utc)

day_start = int(day_start_dt.timestamp())
day_end = int(day_end_dt.timestamp())
day_rows = [up for ts, up in rows if day_start <= ts <= day_end]

day_rows = [(up, ping) for ts, up, ping in rows if day_start <= ts <= day_end]

if day_rows:
uptime = sum(day_rows) / len(day_rows)
ups = [u for u, _ in day_rows]
pings = [p for _, p in day_rows if p is not None]
uptime = sum(ups) / len(ups)
ping_avg = sum(pings) / len(pings) if pings else None
if pings:
k = max(0, min(len(pings)-1, math.ceil(len(pings) * 0.99) - 1))
ping_p99 = sorted(pings)[k]
else:
ping_p99 = None
else:
uptime = -1 # Keine Daten verfügbar

ping_avg = ping_p99 = None

# UTC-Timestamp für Tagesbeginn senden
daily_data.insert(0, {"date": day_start, "uptime": uptime})
daily_data.insert(0, {"date": day_start, "uptime": uptime, "ping_avg": ping_avg, "ping_p99": ping_p99})

result["hourly"] = hourly_data
result["daily"] = daily_data
Expand Down
Loading