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:
parent
23b41b37c1
commit
16d6fd92f6
6 changed files with 164 additions and 70 deletions
|
|
@ -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
7
health_check/__init__.py
Normal 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
17
health_check/_config.py
Normal 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
50
health_check/_ping.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue