From 64925ffe5bab5bed88b770f9f9ae69a6817222a5 Mon Sep 17 00:00:00 2001 From: SantosFC <33733406+SantosFC@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:41:12 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20m=C3=B3dulo=20Python=20import=C3=A1vel?= =?UTF-8?q?=20health=5Fcheck/=20e=20docs=20de=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cria health_check/ com __init__.py, _ping.py e _config.py - healthcheck.py vira CLI fino que usa o pacote - pyproject.toml completo com build system (hatchling) e metadados - docs/ping-payload.md com instruções de uso como biblioteca --- docs/ping-payload.md | 106 +++++++++++++++++++++++++++++++++++++++ health_check/__init__.py | 7 +++ health_check/_config.py | 17 +++++++ health_check/_ping.py | 50 ++++++++++++++++++ healthcheck.py | 71 +++++--------------------- pyproject.toml | 20 +++++++- 6 files changed, 210 insertions(+), 61 deletions(-) create mode 100644 docs/ping-payload.md create mode 100644 health_check/__init__.py create mode 100644 health_check/_config.py create mode 100644 health_check/_ping.py diff --git a/docs/ping-payload.md b/docs/ping-payload.md new file mode 100644 index 0000000..d3df316 --- /dev/null +++ b/docs/ping-payload.md @@ -0,0 +1,106 @@ +# Ping Payload Standard + +This document defines the standard payload for all health check pings sent to Healthchecks.io across all agents (scripts, bots, services). + +## Why not use an existing library? + +The [`healthchecks-io`](https://pypi.org/project/healthchecks-io/) package on PyPI focuses on the management API and does not send a custom body payload. This library fills that gap with an opinionated, consistent payload that identifies the agent, device, IPs, and uptime on every ping. + +--- + +## Using as a Python module + +Install directly from the repository: + +```bash +pip install git+https://github.com/SantosFC/health-check.git +``` + +### Basic usage + +```python +from health_check import ping, ping_fail + +url = "https://hc-ping.com/" + +try: + status = ping(url, agent="my-telegram-bot", device="my-server") + if 200 <= status < 300: + print("Ping OK") + else: + ping_fail(url, agent="my-telegram-bot", device="my-server") +except Exception as exc: + ping_fail(url, agent="my-telegram-bot", device="my-server") + raise +``` + +### Loading config from file + +```python +from health_check import load_config, ping + +config = load_config() # reads ~/.config/health-check +ping(config["HEALTHCHECK_URL"], agent="my-bot", device=config["DEVICE_NAME"]) +``` + +### Custom config path + +```python +from pathlib import Path +from health_check import load_config + +config = load_config(config_file=Path("/etc/my-bot/config")) +``` + +### Timeout + +```python +ping(url, agent="my-bot", device="server", timeout=5) +``` + +--- + +## HTTP Request + +- **Method:** `POST` +- **URL:** `https://hc-ping.com/` + +## Headers + +| Header | Value | +|---|---| +| `User-Agent` | `health-check/` | +| `Content-Type` | `application/json` | + +## Body + +```json +{ + "user": "", + "device": "", + "ips": ["", ""], + "uptime": "" +} +``` + +| Field | Type | Description | +|---|---|---| +| `user` | `string` | Name of the agent sending the ping (script, bot, service) | +| `device` | `string` | Name of the device or host where the agent runs | +| `ips` | `array of strings` | List of IP addresses of the device | +| `uptime` | `string` | Device uptime formatted as `Xd Yh Zm` | + +## Example + +```json +{ + "user": "my-telegram-bot", + "device": "my-server", + "ips": ["192.168.1.10", "10.0.0.5"], + "uptime": "3d 14h 22m" +} +``` + +## Failure Ping + +On error, agents must send a POST to `/fail` with the same body. The `ping_fail` function handles this automatically. diff --git a/health_check/__init__.py b/health_check/__init__.py new file mode 100644 index 0000000..1598366 --- /dev/null +++ b/health_check/__init__.py @@ -0,0 +1,7 @@ +"""health-check: lightweight Healthchecks.io ping client.""" + +from health_check._config import load_config +from health_check._ping import ping, ping_fail + +__version__ = "1.0.2" +__all__ = ["ping", "ping_fail", "load_config"] diff --git a/health_check/_config.py b/health_check/_config.py new file mode 100644 index 0000000..c554fb8 --- /dev/null +++ b/health_check/_config.py @@ -0,0 +1,17 @@ +import os +from pathlib import Path + + +def load_config(config_file: Path | None = None) -> dict: + """Load configuration from file, falling back to environment variables.""" + path = config_file or Path.home() / ".config" / "health-check" + config = {} + if path.exists(): + for line in path.read_text().splitlines(): + if "=" in line: + key, value = line.split("=", 1) + config[key.strip()] = value.strip() + + config.setdefault("HEALTHCHECK_URL", os.environ.get("HEALTHCHECK_URL", "")) + config.setdefault("DEVICE_NAME", os.environ.get("DEVICE_NAME", "unknown")) + return config diff --git a/health_check/_ping.py b/health_check/_ping.py new file mode 100644 index 0000000..b0971e4 --- /dev/null +++ b/health_check/_ping.py @@ -0,0 +1,50 @@ +import json +import subprocess +import urllib.error +import urllib.request +from pathlib import Path + +_USER_AGENT = "health-check/1.0.2" + + +def _build_body(agent: str, device: str) -> bytes: + try: + ips = subprocess.check_output(["hostname", "-I"], timeout=5).decode().split() + except Exception: + ips = [] + + try: + seconds = int(float(Path("/proc/uptime").read_text().split()[0])) + d, rem = divmod(seconds, 86400) + h, rem = divmod(rem, 3600) + m = rem // 60 + uptime = f"{d}d {h}h {m}m" + except Exception: + uptime = "unknown" + + return json.dumps({"user": agent, "device": device, "ips": ips, "uptime": uptime}).encode() + + +def ping(url: str, agent: str, device: str, timeout: int = 10) -> int: + """Send a success ping to Healthchecks.io. Returns the HTTP status code.""" + body = _build_body(agent, device) + req = urllib.request.Request( + url, data=body, method="POST", + headers={"User-Agent": _USER_AGENT, "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=timeout) as response: + return response.getcode() + + +def ping_fail(url: str, agent: str, device: str, timeout: int = 10) -> None: + """Send a failure ping to Healthchecks.io (/fail).""" + body = _build_body(agent, device) + fail_url = url.rstrip("/") + "/fail" + try: + req = urllib.request.Request( + fail_url, data=body, method="POST", + headers={"User-Agent": _USER_AGENT, "Content-Type": "application/json"}, + ) + urllib.request.urlopen(req, timeout=timeout) + except Exception: + pass diff --git a/healthcheck.py b/healthcheck.py index 5bfbfda..a09b138 100644 --- a/healthcheck.py +++ b/healthcheck.py @@ -1,49 +1,17 @@ #!/usr/bin/env python3 -import json +"""CLI entry point for health-check.""" + import os -import subprocess import sys -import urllib.request -import urllib.error from datetime import datetime, timezone -from pathlib import Path -USER_NAME = "Ronaldo Freitas Dias" +import urllib.error +from health_check import ping, ping_fail, load_config + +AGENT = "Ronaldo Freitas Dias" -def load_config(): - config_file = Path.home() / ".config" / "health-check" - config = {} - if config_file.exists(): - for line in config_file.read_text().splitlines(): - if "=" in line: - key, value = line.split("=", 1) - config[key.strip()] = value.strip() - return config - - -def ping(url: str, body: bytes, timeout: int) -> int: - req = urllib.request.Request( - url, data=body, method="POST", - headers={"User-Agent": "health-check/1.0.2", "Content-Type": "application/json"} - ) - with urllib.request.urlopen(req, timeout=timeout) as response: - return response.getcode() - - -def ping_fail(url: str, body: bytes, timeout: int) -> None: - fail_url = url.rstrip("/") + "/fail" - try: - req = urllib.request.Request( - fail_url, data=body, method="POST", - headers={"User-Agent": "health-check/1.0.2", "Content-Type": "application/json"} - ) - urllib.request.urlopen(req, timeout=timeout) - except Exception: - pass - - -def main(): +def main() -> int: config = load_config() url = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("HEALTHCHECK_URL") or config.get("HEALTHCHECK_URL") if not url: @@ -52,43 +20,28 @@ def main(): device = os.environ.get("DEVICE_NAME") or config.get("DEVICE_NAME", "unknown") timeout = int(os.environ.get("HEALTHCHECK_TIMEOUT", 10)) - try: - ips = subprocess.check_output(["hostname", "-I"], timeout=5).decode().split() - except Exception: - ips = [] try: - seconds = int(float(Path("/proc/uptime").read_text().split()[0])) - d, rem = divmod(seconds, 86400) - h, rem = divmod(rem, 3600) - m = rem // 60 - uptime = f"{d}d {h}h {m}m" - except Exception: - uptime = "unknown" - - body = json.dumps({"user": USER_NAME, "device": device, "ips": ips, "uptime": uptime}).encode() - - try: - status = ping(url, body, timeout) + status = ping(url, agent=AGENT, device=device, timeout=timeout) if 200 <= status < 300: print(f"{datetime.now(timezone.utc).isoformat()} status={status} device={device} url={url}") return 0 print(f"ERROR: HTTP {status} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 2 except urllib.error.HTTPError as exc: print(f"HTTPError {exc.code} {exc.reason} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 3 except urllib.error.URLError as exc: print(f"URLError {exc.reason} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 4 except Exception as exc: print(f"ERROR {exc} url={url}", file=sys.stderr) - ping_fail(url, body, timeout) + ping_fail(url, agent=AGENT, device=device, timeout=timeout) return 5 diff --git a/pyproject.toml b/pyproject.toml index bb1773c..b088fab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,19 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "health-check" +version = "1.0.2" +description = "Lightweight Healthchecks.io ping client with opinionated payload (agent, device, ips, uptime)" +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } +dependencies = [] + +[project.scripts] +health-check = "healthcheck:main" + [tool.ruff] target-version = "py39" line-length = 110 @@ -13,8 +29,8 @@ select = [ "SIM", # flake8-simplify ] ignore = [ - "B904", # raise from exc — não obrigatório em scripts simples + "B904", ] [tool.ruff.lint.isort] -known-first-party = ["healthcheck"] +known-first-party = ["health_check"]