feat: reestruturar como módulo Python importável (health_check/)

- 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 atualizado com instruções de uso como biblioteca
This commit is contained in:
Claude 2026-06-08 14:31:45 +00:00
parent 23b41b37c1
commit 16d6fd92f6
No known key found for this signature in database
6 changed files with 164 additions and 70 deletions

View file

@ -2,6 +2,64 @@
This document defines the standard payload for all health check pings sent to Healthchecks.io across all agents (scripts, bots, services). 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/<your-uuid>"
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 ## HTTP Request
- **Method:** `POST` - **Method:** `POST`
@ -11,16 +69,9 @@ This document defines the standard payload for all health check pings sent to He
| Header | Value | | Header | Value |
|---|---| |---|---|
| `User-Agent` | `<service-name>/<version>` | | `User-Agent` | `health-check/<version>` |
| `Content-Type` | `application/json` | | `Content-Type` | `application/json` |
The `User-Agent` must identify the agent and its version. Examples:
```
health-check/1.0.2
telegram-bot-alerts/2.1.0
```
## Body ## Body
```json ```json
@ -52,4 +103,4 @@ telegram-bot-alerts/2.1.0
## Failure Ping ## Failure Ping
On error, agents must send a POST to `<uuid>/fail` with the same body. On error, agents must send a POST to `<uuid>/fail` with the same body. The `ping_fail` function handles this automatically.

7
health_check/__init__.py Normal file
View file

@ -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"]

17
health_check/_config.py Normal file
View file

@ -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

50
health_check/_ping.py Normal file
View file

@ -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 (<uuid>/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

View file

@ -1,49 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json """CLI entry point for health-check."""
import os import os
import subprocess
import sys import sys
import urllib.request
import urllib.error
from datetime import datetime, timezone 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(): def main() -> int:
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():
config = load_config() config = load_config()
url = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("HEALTHCHECK_URL") or config.get("HEALTHCHECK_URL") url = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("HEALTHCHECK_URL") or config.get("HEALTHCHECK_URL")
if not url: if not url:
@ -52,43 +20,28 @@ def main():
device = os.environ.get("DEVICE_NAME") or config.get("DEVICE_NAME", "unknown") device = os.environ.get("DEVICE_NAME") or config.get("DEVICE_NAME", "unknown")
timeout = int(os.environ.get("HEALTHCHECK_TIMEOUT", 10)) timeout = int(os.environ.get("HEALTHCHECK_TIMEOUT", 10))
try:
ips = subprocess.check_output(["hostname", "-I"], timeout=5).decode().split()
except Exception:
ips = []
try: try:
seconds = int(float(Path("/proc/uptime").read_text().split()[0])) status = ping(url, agent=AGENT, device=device, timeout=timeout)
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)
if 200 <= status < 300: if 200 <= status < 300:
print(f"{datetime.now(timezone.utc).isoformat()} status={status} device={device} url={url}") print(f"{datetime.now(timezone.utc).isoformat()} status={status} device={device} url={url}")
return 0 return 0
print(f"ERROR: HTTP {status} url={url}", file=sys.stderr) 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 return 2
except urllib.error.HTTPError as exc: except urllib.error.HTTPError as exc:
print(f"HTTPError {exc.code} {exc.reason} url={url}", file=sys.stderr) 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 return 3
except urllib.error.URLError as exc: except urllib.error.URLError as exc:
print(f"URLError {exc.reason} url={url}", file=sys.stderr) 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 return 4
except Exception as exc: except Exception as exc:
print(f"ERROR {exc} url={url}", file=sys.stderr) 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 return 5

View file

@ -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] [tool.ruff]
target-version = "py39" target-version = "py39"
line-length = 110 line-length = 110
@ -13,8 +29,8 @@ select = [
"SIM", # flake8-simplify "SIM", # flake8-simplify
] ]
ignore = [ ignore = [
"B904", # raise from exc — não obrigatório em scripts simples "B904",
] ]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["healthcheck"] known-first-party = ["health_check"]