Dodaj interwały dla monitorowanych hostów
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
self._schedule_check(target)
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user