feat: módulo Python importável health_check/ e docs de payload

- 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
This commit is contained in:
SantosFC 2026-06-08 11:41:12 -03:00 committed by GitHub
parent b27403dd60
commit 64925ffe5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 210 additions and 61 deletions

106
docs/ping-payload.md Normal file
View file

@ -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/<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
- **Method:** `POST`
- **URL:** `https://hc-ping.com/<uuid>`
## Headers
| Header | Value |
|---|---|
| `User-Agent` | `health-check/<version>` |
| `Content-Type` | `application/json` |
## Body
```json
{
"user": "<agent-name>",
"device": "<device-name>",
"ips": ["<ip1>", "<ip2>"],
"uptime": "<Xd Yh Zm>"
}
```
| 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 `<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
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

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