Refactor basically everything
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from jinja2 import Environment, PackageLoader
|
||||
|
||||
from velping.config import Config, ServiceConfig, load_cfg
|
||||
|
||||
http = httpx.Client(
|
||||
headers={"User-Agent": "velping"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
app = FastAPI()
|
||||
app.mount("/static", StaticFiles(packages=["velping"]), name="static")
|
||||
uptime: dict[str, list[str]] = {}
|
||||
templates = Environment(loader=PackageLoader("velping", "templates"))
|
||||
cfg = load_cfg()
|
||||
|
||||
|
||||
def handle_service_status(
|
||||
service_name: str,
|
||||
down: bool = False,
|
||||
error: str | None = None,
|
||||
) -> None:
|
||||
"""Update service status and send notifications if configured."""
|
||||
last_status = uptime[service_name][-1]
|
||||
new_status = "O" if down else "I"
|
||||
|
||||
if cfg.ntfy.enabled and last_status != new_status:
|
||||
message = f"Service {service_name} is {'offline' if down else 'online'}"
|
||||
http.post(
|
||||
url=f"https://{cfg.ntfy.server}/{cfg.ntfy.topic}",
|
||||
data=message,
|
||||
)
|
||||
|
||||
uptime[service_name].pop(0)
|
||||
uptime[service_name].append(new_status)
|
||||
|
||||
if down:
|
||||
print(f"[E] Error pinging {service_name}: {error}")
|
||||
else:
|
||||
print(f"[I] Pinging {service_name} succeeded!")
|
||||
|
||||
|
||||
def tcp_ping(service: ServiceConfig) -> None:
|
||||
"""Continuously ping a service via TCP."""
|
||||
|
||||
socket_type = socket.AF_INET if service.ipv == 4 else socket.AF_INET6
|
||||
|
||||
while True:
|
||||
print(f"[I] Pinging {service.name}")
|
||||
|
||||
try:
|
||||
with socket.socket(family=socket_type, type=socket.SOCK_STREAM) as sock:
|
||||
sock.settimeout(service.timeout)
|
||||
sock.connect((service.hostname, service.port))
|
||||
|
||||
handle_service_status(service_name=service.name, down=False)
|
||||
|
||||
except socket.timeout:
|
||||
handle_service_status(
|
||||
service_name=service.name,
|
||||
down=True,
|
||||
error=f"Connection to {service.name} timed out",
|
||||
)
|
||||
except Exception as err:
|
||||
handle_service_status(
|
||||
service_name=service.name,
|
||||
down=True,
|
||||
error=str(err),
|
||||
)
|
||||
|
||||
time.sleep(cfg.pinging.rate)
|
||||
|
||||
|
||||
def http_ping(service: ServiceConfig) -> None:
|
||||
"""Continuously ping a service via HTTP."""
|
||||
while True:
|
||||
print(f"[I] Pinging {service.name}")
|
||||
|
||||
try:
|
||||
resp = http.get(url=service.url, timeout=service.timeout)
|
||||
resp.raise_for_status()
|
||||
handle_service_status(service_name=service.name, down=False)
|
||||
|
||||
except Exception as e:
|
||||
handle_service_status(
|
||||
service_name=service.name,
|
||||
down=True,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
time.sleep(cfg.pinging.rate)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root(cfg: Config) -> str:
|
||||
template = templates.get_template("index.xht")
|
||||
return template.render(
|
||||
cfg=cfg,
|
||||
services=cfg.services,
|
||||
uptime=uptime,
|
||||
)
|
||||
|
||||
|
||||
for service in cfg.services:
|
||||
uptime[service.name] = ["?"] * cfg.web.pings
|
||||
|
||||
ping_func = http_ping if service.type == "http" else tcp_ping
|
||||
threading.Thread(
|
||||
target=ping_func,
|
||||
args=(service,),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
uvicorn.run(app, host=cfg.web.addr, port=cfg.web.port)
|
||||
@@ -0,0 +1,64 @@
|
||||
import os
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
|
||||
class ServiceConfig(BaseModel):
|
||||
name: str
|
||||
type: Literal["http", "tcp"]
|
||||
timeout: int = 5
|
||||
|
||||
|
||||
class HTTPServiceConfig(ServiceConfig):
|
||||
url: str = Field(alias="address")
|
||||
|
||||
|
||||
class TCPServiceConfig(ServiceConfig):
|
||||
port: int
|
||||
hostname: str
|
||||
ipv: Literal[4, 6] = Field(alias="ip_version", default=4)
|
||||
|
||||
|
||||
class WebConfig(BaseModel):
|
||||
addr: str = Field(alias="address", default="::")
|
||||
port: int = 4200
|
||||
name: str = "velping"
|
||||
pings: int = 25
|
||||
|
||||
|
||||
class PingingConfig(BaseModel):
|
||||
rate: int = Field(alias="seconds_between_ping", default=60)
|
||||
|
||||
|
||||
class NtfyConfig(BaseModel):
|
||||
enabled: bool = False
|
||||
server: str = "ntfy.sh"
|
||||
topic: str
|
||||
|
||||
|
||||
Service = TCPServiceConfig | HTTPServiceConfig
|
||||
|
||||
|
||||
class Config(BaseModel):
|
||||
web: WebConfig = Field(alias="frontend", default={})
|
||||
ntfy: NtfyConfig = NtfyConfig(topic="")
|
||||
pinging: PingingConfig
|
||||
# NOTE: dict is legacy and likely to be removed in a future release.
|
||||
services: list[Service] | dict[str, Service] = []
|
||||
|
||||
|
||||
def load_cfg() -> Config:
|
||||
try:
|
||||
with Path(os.environ.get("VELPING_CONFIG", "config.toml")).open("rb") as f:
|
||||
cfg = Config(**tomllib.load(f))
|
||||
|
||||
if isinstance(cfg.services, dict):
|
||||
print("Your services config is using a dict, which is deprecated.")
|
||||
cfg.services = list(cfg.services.values())
|
||||
except ValidationError as e:
|
||||
raise SystemExit(str(e))
|
||||
|
||||
return cfg
|
||||
@@ -0,0 +1,53 @@
|
||||
::selection {
|
||||
background-color: #eeadb7;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background-color: #eeadb7;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Adwaita Mono', monospace;
|
||||
padding-left: 20px;
|
||||
padding-top: 10px;
|
||||
margin: 0;
|
||||
max-width: 1000px;
|
||||
background-color: #1e1e2e;
|
||||
color: #eeadb7;
|
||||
}
|
||||
|
||||
.green, .green::selection, .green::-moz-selection {
|
||||
color: #5cdd8b;
|
||||
}
|
||||
|
||||
.red, .red::selection, .red::-moz-selection {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.grey, .grey::selection, .grey::-moz-selection {
|
||||
color: #dadada;
|
||||
}
|
||||
|
||||
.service {
|
||||
width: 400px;
|
||||
height: 20px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.service p {
|
||||
width: 33%
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
width: 66%
|
||||
}
|
||||
|
||||
.status p {
|
||||
width: 9px;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html lang="en-gb" xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>{{ cfg.web.name }}</title>
|
||||
<link href="/static/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>{{ cfg.web.name }}</h2>
|
||||
|
||||
{% for service in services %}
|
||||
<div class="service">
|
||||
<p><b>{{ service.name }}</b></p>
|
||||
<div class="status">
|
||||
{% for ping in uptime[service.name] %}
|
||||
{% if ping == "I" %}
|
||||
<p class="green">█</p>
|
||||
{% elif ping == "O" %}
|
||||
<p class="red">█</p>
|
||||
{% else %}
|
||||
<p class="grey">█</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user