Dodaj monitor dostępności hostów
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Graficzna aplikacja do monitorowania dostępności hostów."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
from host_checker import CheckResult, check
|
||||
|
||||
|
||||
CONFIG_FILE = Path(__file__).with_name("hosts.json")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Host:
|
||||
target: str
|
||||
method: str = "http"
|
||||
|
||||
|
||||
class HostMonitorApp(tk.Tk):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.title("Monitor dostępności hostów")
|
||||
self.geometry("1120x620")
|
||||
self.minsize(850, 450)
|
||||
self.hosts: list[Host] = []
|
||||
self.stats: dict[str, dict[str, float | int | bool | str]] = {}
|
||||
self.results: queue.Queue[tuple[str, CheckResult]] = queue.Queue()
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
|
||||
self.running = False
|
||||
self.after_id: str | None = None
|
||||
self.worker_lock = threading.Lock()
|
||||
self._build_ui()
|
||||
self._load_config()
|
||||
self.protocol("WM_DELETE_WINDOW", self._close)
|
||||
self.after(200, self._consume_results)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
controls = ttk.Frame(self, padding=10)
|
||||
controls.pack(fill="x")
|
||||
|
||||
ttk.Label(controls, text="Host / URL:").grid(row=0, column=0, sticky="w")
|
||||
self.target_var = tk.StringVar()
|
||||
ttk.Entry(controls, textvariable=self.target_var, width=38).grid(row=0, column=1, padx=5)
|
||||
ttk.Label(controls, text="Metoda:").grid(row=0, column=2, sticky="e")
|
||||
self.method_var = tk.StringVar(value="http")
|
||||
ttk.Combobox(controls, textvariable=self.method_var, values=("http", "icmp"), state="readonly", width=8).grid(row=0, column=3, padx=5)
|
||||
ttk.Button(controls, text="Dodaj / aktualizuj", command=self._add_host).grid(row=0, column=4, padx=5)
|
||||
ttk.Button(controls, text="Usuń zaznaczone", command=self._remove_selected).grid(row=0, column=5, padx=5)
|
||||
|
||||
ttk.Label(controls, text="Interwał [s]:").grid(row=1, column=0, sticky="w", pady=(8, 0))
|
||||
self.interval_var = tk.StringVar(value="10")
|
||||
ttk.Spinbox(controls, from_=1, to=86400, textvariable=self.interval_var, width=8).grid(row=1, column=1, sticky="w", padx=5, pady=(8, 0))
|
||||
ttk.Label(controls, text="Timeout [s]:").grid(row=1, column=2, sticky="e", pady=(8, 0))
|
||||
self.timeout_var = tk.StringVar(value="3")
|
||||
ttk.Spinbox(controls, from_=0.1, to=300, increment=0.5, textvariable=self.timeout_var, width=8).grid(row=1, column=3, sticky="w", padx=5, pady=(8, 0))
|
||||
self.start_button = ttk.Button(controls, text="Uruchom monitoring", command=self._toggle_monitoring)
|
||||
self.start_button.grid(row=1, column=4, padx=5, pady=(8, 0))
|
||||
ttk.Button(controls, text="Zapisz konfigurację", command=self._save_config).grid(row=1, column=5, padx=5, pady=(8, 0))
|
||||
|
||||
table_frame = ttk.Frame(self, padding=(10, 0, 10, 10))
|
||||
table_frame.pack(fill="both", expand=True)
|
||||
columns = ("target", "method", "status", "tests", "ok", "fail", "availability", "avg", "last")
|
||||
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="extended")
|
||||
headings = {"target": "Host / URL", "method": "Metoda", "status": "Status", "tests": "Testy", "ok": "OK", "fail": "FAIL", "availability": "Dostępność", "avg": "Śr. czas", "last": "Ostatni test"}
|
||||
widths = {"target": 260, "method": 70, "status": 75, "tests": 55, "ok": 45, "fail": 50, "availability": 90, "avg": 80, "last": 180}
|
||||
for column in columns:
|
||||
self.table.heading(column, text=headings[column])
|
||||
self.table.column(column, width=widths[column], anchor="center")
|
||||
self.table.column("target", anchor="w")
|
||||
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
|
||||
self.table.configure(yscrollcommand=scrollbar.set)
|
||||
self.table.pack(side="left", fill="both", expand=True)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
self.table.tag_configure("up", foreground="#16803c")
|
||||
self.table.tag_configure("down", foreground="#b42318")
|
||||
self.table.tag_configure("unknown", foreground="#666666")
|
||||
|
||||
self.status_var = tk.StringVar(value="Gotowy. Dodaj hosty i uruchom monitoring.")
|
||||
ttk.Label(self, textvariable=self.status_var, relief="sunken", anchor="w", padding=5).pack(fill="x", side="bottom")
|
||||
|
||||
def _add_host(self) -> None:
|
||||
target = self.target_var.get().strip()
|
||||
method = self.method_var.get()
|
||||
if not target:
|
||||
messagebox.showwarning("Brak hosta", "Wpisz adres hosta lub URL.")
|
||||
return
|
||||
existing = next((host for host in self.hosts if host.target == target), None)
|
||||
if existing:
|
||||
existing.method = method
|
||||
else:
|
||||
self.hosts.append(Host(target, method))
|
||||
self.target_var.set("")
|
||||
self._refresh_table()
|
||||
self._save_config(silent=True)
|
||||
|
||||
def _remove_selected(self) -> None:
|
||||
selected = set(self.table.selection())
|
||||
if not selected:
|
||||
return
|
||||
self.hosts = [host for host in self.hosts if host.target not in selected]
|
||||
for target in selected:
|
||||
self.stats.pop(target, None)
|
||||
self._refresh_table()
|
||||
self._save_config(silent=True)
|
||||
|
||||
def _toggle_monitoring(self) -> None:
|
||||
if self.running:
|
||||
self.running = False
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id)
|
||||
self.start_button.configure(text="Uruchom monitoring")
|
||||
self.status_var.set("Monitoring zatrzymany.")
|
||||
return
|
||||
if not self.hosts:
|
||||
messagebox.showwarning("Brak hostów", "Dodaj co najmniej jeden host.")
|
||||
return
|
||||
try:
|
||||
float(self.interval_var.get())
|
||||
float(self.timeout_var.get())
|
||||
except ValueError:
|
||||
messagebox.showerror("Błędne ustawienia", "Interwał i timeout muszą być liczbami.")
|
||||
return
|
||||
self.running = True
|
||||
self.start_button.configure(text="Zatrzymaj monitoring")
|
||||
self._schedule_check(0)
|
||||
|
||||
def _schedule_check(self, delay_ms: int | None = None) -> None:
|
||||
if not self.running:
|
||||
return
|
||||
if delay_ms is None:
|
||||
delay_ms = max(1, int(float(self.interval_var.get()) * 1000))
|
||||
self.after_id = self.after(delay_ms, self._start_check)
|
||||
|
||||
def _start_check(self) -> None:
|
||||
if not self.running:
|
||||
return
|
||||
timeout = float(self.timeout_var.get())
|
||||
with self.worker_lock:
|
||||
for host in self.hosts:
|
||||
future = self.executor.submit(check, host.target, host.method, timeout)
|
||||
future.add_done_callback(lambda item, target=host.target: self._put_result(target, item))
|
||||
self._schedule_check()
|
||||
|
||||
def _put_result(self, target: str, future: concurrent.futures.Future[CheckResult]) -> None:
|
||||
try:
|
||||
self.results.put((target, future.result()))
|
||||
except Exception as error: # nie przerywaj monitorowania przez pojedynczy host
|
||||
self.results.put((target, CheckResult(target, "?", False, str(error), 0)))
|
||||
|
||||
def _consume_results(self) -> None:
|
||||
changed = False
|
||||
while True:
|
||||
try:
|
||||
target, result = self.results.get_nowait()
|
||||
except queue.Empty:
|
||||
break
|
||||
stats = self.stats.setdefault(target, {"tests": 0, "ok": 0, "fail": 0, "total_ms": 0.0, "available": False, "detail": "", "last": ""})
|
||||
stats["tests"] = int(stats["tests"]) + 1
|
||||
stats["ok"] = int(stats["ok"]) + int(result.available)
|
||||
stats["fail"] = int(stats["fail"]) + int(not result.available)
|
||||
stats["total_ms"] = float(stats["total_ms"]) + result.elapsed_ms
|
||||
stats["available"] = result.available
|
||||
stats["detail"] = result.detail
|
||||
stats["last"] = datetime.now().strftime("%H:%M:%S")
|
||||
changed = True
|
||||
if changed:
|
||||
self._refresh_table()
|
||||
self.after(200, self._consume_results)
|
||||
|
||||
def _refresh_table(self) -> None:
|
||||
existing = set(self.table.get_children())
|
||||
current = {host.target for host in self.hosts}
|
||||
for target in existing - current:
|
||||
self.table.delete(target)
|
||||
for host in self.hosts:
|
||||
stats = self.stats.get(host.target, {})
|
||||
tests = int(stats.get("tests", 0))
|
||||
ok = int(stats.get("ok", 0))
|
||||
fail = int(stats.get("fail", 0))
|
||||
availability = f"{ok / tests * 100:.1f}%" if tests else "-"
|
||||
avg = f"{float(stats.get('total_ms', 0)) / tests:.0f} ms" if tests else "-"
|
||||
last = f"{stats.get('last', '-')} ({stats.get('detail', '-')})" if tests else "-"
|
||||
values = (host.target, host.method.upper(), "OK" if stats.get("available") else ("FAIL" if tests else "-"), tests, ok, fail, availability, avg, last)
|
||||
tag = "up" if stats.get("available") else ("down" if tests else "unknown")
|
||||
if host.target in existing:
|
||||
self.table.item(host.target, values=values, tags=(tag,))
|
||||
else:
|
||||
self.table.insert("", "end", iid=host.target, values=values, tags=(tag,))
|
||||
|
||||
def _load_config(self) -> None:
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
||||
self.hosts = [Host(str(item["target"]), str(item.get("method", "http"))) for item in data.get("hosts", []) if item.get("target")]
|
||||
self.interval_var.set(str(data.get("interval", 10)))
|
||||
self.timeout_var.set(str(data.get("timeout", 3)))
|
||||
self._refresh_table()
|
||||
except (OSError, ValueError, KeyError, TypeError) as error:
|
||||
messagebox.showwarning("Błąd konfiguracji", f"Nie udało się odczytać hosts.json: {error}")
|
||||
|
||||
def _save_config(self, silent: bool = False) -> None:
|
||||
try:
|
||||
data = {"interval": float(self.interval_var.get()), "timeout": float(self.timeout_var.get()), "hosts": [{"target": host.target, "method": host.method} for host in self.hosts]}
|
||||
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
if not silent:
|
||||
self.status_var.set(f"Zapisano konfigurację: {CONFIG_FILE.name}")
|
||||
except (OSError, ValueError) as error:
|
||||
messagebox.showerror("Błąd zapisu", str(error))
|
||||
|
||||
def _close(self) -> None:
|
||||
self.running = False
|
||||
if self.after_id:
|
||||
self.after_cancel(self.after_id)
|
||||
self._save_config(silent=True)
|
||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||
self.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
HostMonitorApp().mainloop()
|
||||
Reference in New Issue
Block a user