Dodaj interwały dla monitorowanych hostów

This commit is contained in:
Wojtek
2026-10-14 11:38:49 +02:00
parent d9f0a69eae
commit 80cab9753e
2 changed files with 65 additions and 24 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ Uruchom:
python3 app.py python3 app.py
``` ```
Aplikacja pozwala dodawać, aktualizować i usuwać hosty, wybierać metodę `HTTP`/`ICMP`, ustawić interwał oraz timeout. Konfiguracja jest automatycznie zapisywana w pliku `hosts.json` obok aplikacji. Tabela pokazuje bieżący status, liczbę testów, liczbę odpowiedzi OK/FAIL, procent dostępności i średni czas odpowiedzi. Aplikacja pozwala dodawać, aktualizować i usuwać hosty, wybierać metodę `HTTP`/`ICMP` oraz ustawić osobny interwał i timeout. Kliknięcie hosta na liście uzupełnia formularz jego ustawieniami. Konfiguracja jest automatycznie zapisywana w pliku `hosts.json` obok aplikacji. Tabela pokazuje bieżący status, interwał, liczbę testów, liczbę odpowiedzi OK/FAIL, procent dostępności i średni czas odpowiedzi.
Przycisk „Uruchom monitoring” rozpoczyna cykliczne sprawdzanie wszystkich zapisanych hostów. Wyniki są zbierane równolegle, więc wolny host nie blokuje pozostałych. Przycisk „Uruchom monitoring” rozpoczyna cykliczne sprawdzanie wszystkich zapisanych hostów. Wyniki są zbierane równolegle, więc wolny host nie blokuje pozostałych.
+64 -23
View File
@@ -24,6 +24,7 @@ CONFIG_FILE = Path(__file__).with_name("hosts.json")
class Host: class Host:
target: str target: str
method: str = "http" method: str = "http"
interval: float = 10.0
class HostMonitorApp(tk.Tk): class HostMonitorApp(tk.Tk):
@@ -38,6 +39,7 @@ class HostMonitorApp(tk.Tk):
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10) self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10)
self.running = False self.running = False
self.after_id: str | None = None self.after_id: str | None = None
self.host_after_ids: dict[str, str] = {}
self.selected_target: str | None = None self.selected_target: str | None = None
self.worker_lock = threading.Lock() self.worker_lock = threading.Lock()
self._build_ui() self._build_ui()
@@ -70,10 +72,10 @@ class HostMonitorApp(tk.Tk):
table_frame = ttk.Frame(self, padding=(10, 0, 10, 10)) table_frame = ttk.Frame(self, padding=(10, 0, 10, 10))
table_frame.pack(fill="both", expand=True) table_frame.pack(fill="both", expand=True)
columns = ("target", "method", "status", "tests", "ok", "fail", "availability", "avg", "last") columns = ("target", "method", "interval", "status", "tests", "ok", "fail", "availability", "avg", "last")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="extended") 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"} headings = {"target": "Host / URL", "method": "Metoda", "interval": "Interwał [s]", "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} widths = {"target": 240, "method": 70, "interval": 85, "status": 75, "tests": 55, "ok": 45, "fail": 50, "availability": 90, "avg": 80, "last": 180}
for column in columns: for column in columns:
self.table.heading(column, text=headings[column]) self.table.heading(column, text=headings[column])
self.table.column(column, width=widths[column], anchor="center") self.table.column(column, width=widths[column], anchor="center")
@@ -103,6 +105,7 @@ class HostMonitorApp(tk.Tk):
self.selected_target = target self.selected_target = target
self.target_var.set(host.target) self.target_var.set(host.target)
self.method_var.set(host.method) self.method_var.set(host.method)
self.interval_var.set(str(host.interval).rstrip("0").rstrip("."))
def _add_host(self) -> None: def _add_host(self) -> None:
target = self.target_var.get().strip() target = self.target_var.get().strip()
@@ -110,6 +113,13 @@ class HostMonitorApp(tk.Tk):
if not target: if not target:
messagebox.showwarning("Brak hosta", "Wpisz adres hosta lub URL.") messagebox.showwarning("Brak hosta", "Wpisz adres hosta lub URL.")
return return
try:
interval = float(self.interval_var.get())
if interval <= 0:
raise ValueError
except ValueError:
messagebox.showerror("Błędny interwał", "Interwał musi być liczbą większą od zera.")
return
selected_host = next((host for host in self.hosts if host.target == self.selected_target), None) selected_host = next((host for host in self.hosts if host.target == self.selected_target), None)
duplicate = next((host for host in self.hosts if host.target == target and host is not selected_host), None) duplicate = next((host for host in self.hosts if host.target == target and host is not selected_host), None)
@@ -120,8 +130,13 @@ class HostMonitorApp(tk.Tk):
was_update = selected_host is not None was_update = selected_host is not None
if selected_host: if selected_host:
old_target = selected_host.target old_target = selected_host.target
if self.running:
old_after_id = self.host_after_ids.pop(old_target, None)
if old_after_id:
self.after_cancel(old_after_id)
selected_host.target = target selected_host.target = target
selected_host.method = method selected_host.method = method
selected_host.interval = interval
if old_target != target: if old_target != target:
if old_target in self.stats: if old_target in self.stats:
self.stats[target] = self.stats.pop(old_target) self.stats[target] = self.stats.pop(old_target)
@@ -130,13 +145,16 @@ class HostMonitorApp(tk.Tk):
existing = next((host for host in self.hosts if host.target == target), None) existing = next((host for host in self.hosts if host.target == target), None)
if existing: if existing:
existing.method = method existing.method = method
existing.interval = interval
self.selected_target = target self.selected_target = target
else: else:
self.hosts.append(Host(target, method)) self.hosts.append(Host(target, method, interval))
self.selected_target = target self.selected_target = target
if not was_update: if not was_update:
self.target_var.set("") self.target_var.set("")
self._refresh_table() self._refresh_table()
if was_update and self.running:
self._schedule_check(self.selected_target or target)
self._save_config(silent=True) self._save_config(silent=True)
def _remove_selected(self) -> None: def _remove_selected(self) -> None:
@@ -154,8 +172,9 @@ class HostMonitorApp(tk.Tk):
def _toggle_monitoring(self) -> None: def _toggle_monitoring(self) -> None:
if self.running: if self.running:
self.running = False self.running = False
if self.after_id: for after_id in self.host_after_ids.values():
self.after_cancel(self.after_id) self.after_cancel(after_id)
self.host_after_ids.clear()
self.start_button.configure(text="Uruchom monitoring") self.start_button.configure(text="Uruchom monitoring")
self.status_var.set("Monitoring zatrzymany.") self.status_var.set("Monitoring zatrzymany.")
return return
@@ -163,31 +182,39 @@ class HostMonitorApp(tk.Tk):
messagebox.showwarning("Brak hostów", "Dodaj co najmniej jeden host.") messagebox.showwarning("Brak hostów", "Dodaj co najmniej jeden host.")
return return
try: try:
float(self.interval_var.get())
float(self.timeout_var.get()) float(self.timeout_var.get())
if any(host.interval <= 0 for host in self.hosts):
raise ValueError
except ValueError: except ValueError:
messagebox.showerror("Błędne ustawienia", "Interwał i timeout muszą być liczbami.") messagebox.showerror("Błędne ustawienia", "Interwały hostów i timeout muszą być poprawnymi liczbami.")
return return
self.running = True self.running = True
self.start_button.configure(text="Zatrzymaj monitoring") self.start_button.configure(text="Zatrzymaj monitoring")
self._schedule_check(0) for host in self.hosts:
self._schedule_check(host.target, 0)
def _schedule_check(self, delay_ms: int | None = None) -> None: def _schedule_check(self, target: str, delay_ms: int | None = None) -> None:
if not self.running: if not self.running:
return return
if delay_ms is None: if delay_ms is None:
delay_ms = max(1, int(float(self.interval_var.get()) * 1000)) host = next((item for item in self.hosts if item.target == target), None)
self.after_id = self.after(delay_ms, self._start_check) if host is None:
return
delay_ms = max(1, int(host.interval * 1000))
self.host_after_ids[target] = self.after(delay_ms, lambda: self._start_check(target))
def _start_check(self) -> None: def _start_check(self, target: str) -> None:
if not self.running: if not self.running:
return return
timeout = float(self.timeout_var.get()) timeout = float(self.timeout_var.get())
host = next((item for item in self.hosts if item.target == target), None)
if host is None:
self.host_after_ids.pop(target, None)
return
with self.worker_lock: with self.worker_lock:
for host in self.hosts: future = self.executor.submit(check, host.target, host.method, timeout)
future = self.executor.submit(check, host.target, host.method, timeout) future.add_done_callback(lambda item, target=host.target: self._put_result(target, item))
future.add_done_callback(lambda item, target=host.target: self._put_result(target, item)) self._schedule_check(target)
self._schedule_check()
def _put_result(self, target: str, future: concurrent.futures.Future[CheckResult]) -> None: def _put_result(self, target: str, future: concurrent.futures.Future[CheckResult]) -> None:
try: try:
@@ -228,7 +255,7 @@ class HostMonitorApp(tk.Tk):
availability = f"{ok / tests * 100:.1f}%" if tests else "-" availability = f"{ok / tests * 100:.1f}%" if tests else "-"
avg = f"{float(stats.get('total_ms', 0)) / tests:.0f} ms" 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 "-" 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) values = (host.target, host.method.upper(), f"{host.interval:g}", "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") tag = "up" if stats.get("available") else ("down" if tests else "unknown")
if host.target in existing: if host.target in existing:
self.table.item(host.target, values=values, tags=(tag,)) self.table.item(host.target, values=values, tags=(tag,))
@@ -242,8 +269,17 @@ class HostMonitorApp(tk.Tk):
if CONFIG_FILE.exists(): if CONFIG_FILE.exists():
try: try:
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) 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")] default_interval = float(data.get("interval", 10))
self.interval_var.set(str(data.get("interval", 10))) self.hosts = [
Host(
str(item["target"]),
str(item.get("method", "http")),
float(item.get("interval", default_interval)),
)
for item in data.get("hosts", [])
if item.get("target")
]
self.interval_var.set(str(default_interval))
self.timeout_var.set(str(data.get("timeout", 3))) self.timeout_var.set(str(data.get("timeout", 3)))
self._refresh_table() self._refresh_table()
except (OSError, ValueError, KeyError, TypeError) as error: except (OSError, ValueError, KeyError, TypeError) as error:
@@ -251,7 +287,11 @@ class HostMonitorApp(tk.Tk):
def _save_config(self, silent: bool = False) -> None: def _save_config(self, silent: bool = False) -> None:
try: 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]} data = {
"interval": float(self.interval_var.get()),
"timeout": float(self.timeout_var.get()),
"hosts": [{"target": host.target, "method": host.method, "interval": host.interval} for host in self.hosts],
}
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if not silent: if not silent:
self.status_var.set(f"Zapisano konfigurację: {CONFIG_FILE.name}") self.status_var.set(f"Zapisano konfigurację: {CONFIG_FILE.name}")
@@ -260,8 +300,9 @@ class HostMonitorApp(tk.Tk):
def _close(self) -> None: def _close(self) -> None:
self.running = False self.running = False
if self.after_id: for after_id in self.host_after_ids.values():
self.after_cancel(self.after_id) self.after_cancel(after_id)
self.host_after_ids.clear()
self._save_config(silent=True) self._save_config(silent=True)
self.executor.shutdown(wait=False, cancel_futures=True) self.executor.shutdown(wait=False, cancel_futures=True)
self.destroy() self.destroy()