Skip to content
Draft
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
7 changes: 7 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Options:
--targets TARGETS Comma-separated ping targets (e.g. 1.1.1.1,8.8.8.8)
--poll SECONDS Poll interval in seconds (default: 2)
--threshold N Consecutive failures to declare an outage (default: 4)
--lat-warn MS High-latency warning threshold in milliseconds (default: 100)
--enable-dns Force-enable DNS health checks (overrides saved config)
--disable-dns Disable DNS health checks
--dns-target HOST Hostname to resolve for DNS checks (default: google.com)
--web-port PORT Web dashboard port (default: 8080)
```

Expand All @@ -64,6 +68,9 @@ python3 -m connectivity_monitor --headless --targets 1.1.1.1,8.8.8.8 --web-port

# Adjust poll interval and threshold
python3 -m connectivity_monitor --headless --poll 5 --threshold 3

# Tune latency alerts and DNS checks
python3 -m connectivity_monitor --headless --lat-warn 80 --disable-dns --targets 1.1.1.1,9.9.9.9
```

## Web Dashboard & API
Expand Down
30 changes: 29 additions & 1 deletion python/connectivity_monitor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import argparse
import sys

from .config import load_config, interactive_setup, headless_config, ensure_dirs
from .config import (
load_config,
interactive_setup,
headless_config,
ensure_dirs,
validate_config,
)
from .monitor import run_monitor


Expand Down Expand Up @@ -35,6 +41,23 @@ def main():
"--threshold", type=int, default=None,
help="Consecutive failures to declare an outage (default: 4)",
)
parser.add_argument(
"--lat-warn", type=int, dest="lat_warn", default=None,
help="Latency threshold in ms for warning/high-latency events (default: 100)",
)
dns_group = parser.add_mutually_exclusive_group()
dns_group.add_argument(
"--enable-dns", dest="enable_dns", action="store_true", default=None,
help="Force-enable DNS health checks (overrides saved config)",
)
dns_group.add_argument(
"--disable-dns", dest="enable_dns", action="store_false",
help="Disable DNS health checks",
)
parser.add_argument(
"--dns-target", type=str, dest="dns_target", default=None,
help="Hostname to resolve for DNS checks (default: google.com)",
)
parser.add_argument(
"--web-port", type=int, default=None, dest="web_port",
help="Web dashboard port (default: 8080)",
Expand All @@ -52,6 +75,11 @@ def main():
cfg = interactive_setup(saved)
cfg["headless"] = False

try:
cfg = validate_config(cfg)
except ValueError as exc:
parser.error(str(exc))

run_monitor(cfg)


Expand Down
70 changes: 70 additions & 0 deletions python/connectivity_monitor/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@
}


def _normalize_targets(targets):
"""Split and clean a comma-separated targets string."""
if not targets:
return []
return [t.strip() for t in str(targets).split(",") if t.strip()]


def _require_positive_int(value, label, minimum=1, maximum=None):
"""Validate integer bounds and return the coerced value."""
try:
num = int(value)
except (TypeError, ValueError):
raise ValueError("{} must be an integer.".format(label))
if num < minimum:
raise ValueError("{} must be at least {}.".format(label, minimum))
if maximum is not None and num > maximum:
raise ValueError("{} must be at most {}.".format(label, maximum))
return num


def get_base_dir():
"""Return base directory for logs/reports/config."""
home = os.path.expanduser("~")
Expand Down Expand Up @@ -134,7 +154,57 @@ def headless_config(args):
cfg["poll"] = args.poll
if args.threshold:
cfg["threshold"] = args.threshold
if args.lat_warn is not None:
cfg["lat_warn"] = args.lat_warn
if args.enable_dns is not None:
cfg["enable_dns"] = args.enable_dns
if args.dns_target:
cfg["dns_target"] = args.dns_target
if args.web_port is not None:
cfg["web_port"] = args.web_port

return cfg


def validate_config(cfg):
"""
Validate and normalize configuration.

Ensures numeric fields are positive, port is within range, targets are
present, and DNS settings are consistent. Returns the validated config
(mutating the original dict).
"""
cfg["poll"] = _require_positive_int(
cfg.get("poll", DEFAULTS["poll"]), "Poll interval (seconds)"
)
cfg["threshold"] = _require_positive_int(
cfg.get("threshold", DEFAULTS["threshold"]),
"Failure threshold",
)
cfg["lat_warn"] = _require_positive_int(
cfg.get("lat_warn", DEFAULTS["lat_warn"]),
"Latency warning threshold (ms)",
)
cfg["web_port"] = _require_positive_int(
cfg.get("web_port", DEFAULTS["web_port"]),
"Web dashboard port",
maximum=65535,
)

targets = _normalize_targets(cfg.get("targets", DEFAULTS["targets"]))
if not targets:
raise ValueError("At least one ping target is required (e.g. 1.1.1.1).")
cfg["targets"] = ",".join(targets)

enable_dns = cfg.get("enable_dns", True)
if isinstance(enable_dns, str):
enable_dns = enable_dns.lower() not in ("false", "0", "no", "off")
cfg["enable_dns"] = bool(enable_dns)

if cfg["enable_dns"]:
dns_target = cfg.get("dns_target", DEFAULTS["dns_target"])
if not dns_target or not str(dns_target).strip():
raise ValueError("DNS health check is enabled but no DNS hostname is set.")
cfg["dns_target"] = str(dns_target).strip()

return cfg