first commit
This commit is contained in:
commit
0f7da18e95
|
|
@ -0,0 +1,72 @@
|
||||||
|
# ===============================
|
||||||
|
# Python
|
||||||
|
# ===============================
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# FastAPI / Uvicorn
|
||||||
|
# ===============================
|
||||||
|
*.log
|
||||||
|
uvicorn.log
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Environment variables
|
||||||
|
# ===============================
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# IDE / Editor
|
||||||
|
# ===============================
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# OS
|
||||||
|
# ===============================
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Test / Coverage
|
||||||
|
# ===============================
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Build / Distribution
|
||||||
|
# ===============================
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Database / Runtime files
|
||||||
|
# ===============================
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# Cache / Temp
|
||||||
|
# ===============================
|
||||||
|
.cache/
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
service/*
|
||||||
|
!service/default.yaml
|
||||||
|
*.db
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
procps \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy source code vào image
|
||||||
|
COPY app ./app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
from app.checks import http, shell
|
||||||
|
|
||||||
|
CHECK_MODULES = {
|
||||||
|
"http": http,
|
||||||
|
"shell": shell,
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_check_module(check_type: str):
|
||||||
|
if check_type not in CHECK_MODULES:
|
||||||
|
raise ValueError(f"Unsupported check type: {check_type}")
|
||||||
|
return CHECK_MODULES[check_type]
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import time
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
import jsonpath_ng.ext as jp
|
||||||
|
|
||||||
|
from app.utils.template import render, render_headers
|
||||||
|
from app.utils.security import mask_sensitive
|
||||||
|
|
||||||
|
MAX_BODY = 500
|
||||||
|
|
||||||
|
|
||||||
|
def safe_body(resp: httpx.Response):
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
return json.dumps(mask_sensitive(data))[:MAX_BODY]
|
||||||
|
except Exception:
|
||||||
|
return resp.text[:MAX_BODY]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_cookies(resp: httpx.Response):
|
||||||
|
return {k: v for k, v in resp.cookies.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def build_cookies(context: dict):
|
||||||
|
"""
|
||||||
|
Ưu tiên:
|
||||||
|
1. context["cookies"] (save toàn bộ)
|
||||||
|
2. các key có chứa token
|
||||||
|
"""
|
||||||
|
if isinstance(context.get("cookies"), dict):
|
||||||
|
return context["cookies"]
|
||||||
|
|
||||||
|
cookies = {}
|
||||||
|
for k, v in context.items():
|
||||||
|
if "token" in k.lower():
|
||||||
|
cookies[k] = v
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
|
||||||
|
async def run(check, context):
|
||||||
|
start = time.monotonic()
|
||||||
|
|
||||||
|
method = check.get("method", "GET").upper()
|
||||||
|
|
||||||
|
# --- BASE URL ---
|
||||||
|
base_url = context.get("base_url") or check.get("base_url")
|
||||||
|
url = check.get("url", "")
|
||||||
|
if base_url and url.startswith("/"):
|
||||||
|
url = base_url.rstrip("/") + url
|
||||||
|
url = render(url, context)
|
||||||
|
|
||||||
|
# --- PARAMS ---
|
||||||
|
params = check.get("params")
|
||||||
|
if params:
|
||||||
|
params = {k: render(v, context) for k, v in params.items()}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=check.get("timeout", 5),
|
||||||
|
cookies=build_cookies(context),
|
||||||
|
) as client:
|
||||||
|
|
||||||
|
r = await client.request(
|
||||||
|
method=method,
|
||||||
|
url=url,
|
||||||
|
json=check.get("body"),
|
||||||
|
headers=render_headers(check.get("headers", {}), context),
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
latency = int((time.monotonic() - start) * 1000)
|
||||||
|
ok = True
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
# ---------- EXPECT ----------
|
||||||
|
if "expect" in check:
|
||||||
|
exp = check["expect"]
|
||||||
|
|
||||||
|
if "status_code" in exp and r.status_code != exp["status_code"]:
|
||||||
|
ok = False
|
||||||
|
reason = f"status_code != {exp['status_code']}"
|
||||||
|
|
||||||
|
if ok and "json" in exp:
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
reason = "response is not json"
|
||||||
|
else:
|
||||||
|
for _, expr in exp["json"].items():
|
||||||
|
if not jp.parse(expr).find(data):
|
||||||
|
ok = False
|
||||||
|
reason = f"jsonpath mismatch: {expr}"
|
||||||
|
break
|
||||||
|
|
||||||
|
# ---------- SAVE ----------
|
||||||
|
if ok and "save" in check:
|
||||||
|
try:
|
||||||
|
data = r.json()
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
for key, expr in check["save"].items():
|
||||||
|
|
||||||
|
# save toàn bộ cookies
|
||||||
|
if expr == "__cookies__":
|
||||||
|
context[key] = extract_cookies(r)
|
||||||
|
|
||||||
|
# save cookie cụ thể
|
||||||
|
elif isinstance(expr, str) and expr.startswith("cookie:"):
|
||||||
|
cookie_name = expr.split(":", 1)[1]
|
||||||
|
if cookie_name in r.cookies:
|
||||||
|
context[key] = r.cookies[cookie_name]
|
||||||
|
|
||||||
|
# save từ json
|
||||||
|
else:
|
||||||
|
matches = jp.parse(expr).find(data)
|
||||||
|
if matches:
|
||||||
|
context[key] = matches[0].value
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": check["name"],
|
||||||
|
"type": "http",
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"params": params,
|
||||||
|
"ok": ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": latency,
|
||||||
|
"reason": reason,
|
||||||
|
"response": {
|
||||||
|
"body": safe_body(r)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"name": check["name"],
|
||||||
|
"type": "http",
|
||||||
|
"method": method,
|
||||||
|
"url": url,
|
||||||
|
"params": params,
|
||||||
|
"ok": False,
|
||||||
|
"error": str(e),
|
||||||
|
"latency_ms": int((time.monotonic() - start) * 1000),
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
async def run(check):
|
||||||
|
result = subprocess.run(
|
||||||
|
check["command"],
|
||||||
|
shell=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
ok = result.returncode == 0
|
||||||
|
return {
|
||||||
|
"status": "PASS" if ok else "FAIL",
|
||||||
|
"stdout": result.stdout.decode()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
from app.checks import load_check_module
|
||||||
|
|
||||||
|
async def run_checks(service_cfg):
|
||||||
|
context = {
|
||||||
|
"base_url": service_cfg.get("base_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
executed = []
|
||||||
|
healthy = True
|
||||||
|
|
||||||
|
checks = service_cfg["checks"]
|
||||||
|
|
||||||
|
# index theo name
|
||||||
|
check_map = {c["name"]: c for c in checks}
|
||||||
|
|
||||||
|
for check in checks:
|
||||||
|
name = check["name"]
|
||||||
|
deps = check.get("depends_on", [])
|
||||||
|
|
||||||
|
# ---------- CHECK DEPENDENCY ----------
|
||||||
|
unmet = [
|
||||||
|
d for d in deps
|
||||||
|
if d not in results or results[d]["ok"] is False
|
||||||
|
]
|
||||||
|
|
||||||
|
if unmet:
|
||||||
|
results[name] = {
|
||||||
|
"name": name,
|
||||||
|
"type": check["type"],
|
||||||
|
"ok": False,
|
||||||
|
"skipped": True,
|
||||||
|
"reason": f"depends_on failed: {unmet}",
|
||||||
|
}
|
||||||
|
healthy = False
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---------- RUN CHECK ----------
|
||||||
|
module = load_check_module(check["type"])
|
||||||
|
result = await module.run(check, context)
|
||||||
|
|
||||||
|
results[name] = result
|
||||||
|
executed.append(name)
|
||||||
|
|
||||||
|
if not result["ok"]:
|
||||||
|
healthy = False
|
||||||
|
|
||||||
|
return healthy, list(results.values())
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
SERVICES_DIR = "services"
|
||||||
|
|
||||||
|
def load_services():
|
||||||
|
services = []
|
||||||
|
|
||||||
|
for filename in os.listdir(SERVICES_DIR):
|
||||||
|
if not filename.endswith(".yaml"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = os.path.join(SERVICES_DIR, filename)
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
cfg = yaml.safe_load(f)
|
||||||
|
|
||||||
|
if not cfg.get("enabled", True):
|
||||||
|
continue
|
||||||
|
|
||||||
|
services.append(cfg)
|
||||||
|
|
||||||
|
return services
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import asyncio
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from app.loader import load_services
|
||||||
|
from app.store import all, get, history, init_db
|
||||||
|
from app.scheduler import run_service
|
||||||
|
from app.utils.schema import load_schema, render_response
|
||||||
|
|
||||||
|
services = load_services()
|
||||||
|
TASKS = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# ===== STARTUP =====
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
for svc in services:
|
||||||
|
task = asyncio.create_task(run_service(svc))
|
||||||
|
TASKS.append(task)
|
||||||
|
|
||||||
|
yield # 👉 App chạy ở đây
|
||||||
|
|
||||||
|
# ===== SHUTDOWN =====
|
||||||
|
for task in TASKS:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Health Agent",
|
||||||
|
lifespan=lifespan
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
health_schema = load_schema("health_response")
|
||||||
|
|
||||||
|
services = []
|
||||||
|
for name, data in all().items():
|
||||||
|
checks = data.get("checks", [])
|
||||||
|
failed = sum(1 for c in checks if not c.get("ok"))
|
||||||
|
|
||||||
|
services.append({
|
||||||
|
"message": f"Service {name} is {data.get('status')}",
|
||||||
|
"name": name,
|
||||||
|
"status": data.get("status") == "HEALTHY",
|
||||||
|
"total_checks": len(checks),
|
||||||
|
"failed_checks": failed,
|
||||||
|
"last_updated": data.get("last_updated"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
print(services)
|
||||||
|
|
||||||
|
return render_response(health_schema, services)
|
||||||
|
|
||||||
|
@app.get("/services")
|
||||||
|
def services_list():
|
||||||
|
return all()
|
||||||
|
|
||||||
|
@app.get("/services/{name}")
|
||||||
|
def service_detail(name: str):
|
||||||
|
return get(name)
|
||||||
|
|
||||||
|
@app.get("/services/{name}/history")
|
||||||
|
def service_history(name: str, limit: int = 20):
|
||||||
|
return history(name, limit)
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from app.engine import run_checks
|
||||||
|
from app.store import update
|
||||||
|
|
||||||
|
logger = logging.getLogger("health-agent")
|
||||||
|
|
||||||
|
async def run_service(service_cfg):
|
||||||
|
interval = service_cfg.get("interval", 30)
|
||||||
|
service_name = service_cfg.get("service", "unknown")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
healthy, checks = await run_checks(service_cfg)
|
||||||
|
|
||||||
|
# 🔹 Health result (PASS / FAIL) → dữ liệu hợp lệ
|
||||||
|
update(
|
||||||
|
service_name,
|
||||||
|
{
|
||||||
|
"status": "HEALTHY" if healthy else "UNHEALTHY",
|
||||||
|
"checks": checks,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not healthy:
|
||||||
|
logger.warning(
|
||||||
|
"Service %s is UNHEALTHY", service_name
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
# 🔹 App shutdown → task bị cancel
|
||||||
|
logger.info(
|
||||||
|
"Health check task stopped for service %s",
|
||||||
|
service_name
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# ❗ BUG của agent / engine (KHÔNG phải health fail)
|
||||||
|
update(
|
||||||
|
service_name,
|
||||||
|
{
|
||||||
|
"status": "UNHEALTHY",
|
||||||
|
"checks": [],
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"Agent error while checking service %s: %s",
|
||||||
|
service_name,
|
||||||
|
e,
|
||||||
|
exc_info=True, # stacktrace chỉ dùng cho BUG
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
type: array # 🔥 response là mảng
|
||||||
|
|
||||||
|
item:
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
type: string # tên service
|
||||||
|
|
||||||
|
status:
|
||||||
|
type: boolean # HEALTHY | UNHEALTHY
|
||||||
|
|
||||||
|
message: string
|
||||||
|
|
||||||
|
meta:
|
||||||
|
description: Health status của từng service
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
DB_PATH = "health.db"
|
||||||
|
|
||||||
|
RESULTS = {}
|
||||||
|
LOCK = Lock()
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS service_results (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
service TEXT,
|
||||||
|
status TEXT,
|
||||||
|
checked_at TEXT,
|
||||||
|
details TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def update(service, data):
|
||||||
|
now = datetime.utcnow().isoformat()
|
||||||
|
|
||||||
|
with LOCK:
|
||||||
|
# RAM
|
||||||
|
RESULTS[service] = {
|
||||||
|
**data,
|
||||||
|
"last_check": now
|
||||||
|
}
|
||||||
|
|
||||||
|
# SQLite
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO service_results (service, status, checked_at, details) VALUES (?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
service,
|
||||||
|
data["status"],
|
||||||
|
now,
|
||||||
|
json.dumps(data)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def history(service, limit=20):
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("""
|
||||||
|
SELECT status, checked_at, details
|
||||||
|
FROM service_results
|
||||||
|
WHERE service = ?
|
||||||
|
ORDER BY checked_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
""", (service, limit))
|
||||||
|
rows = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"status": r[0],
|
||||||
|
"checked_at": r[1],
|
||||||
|
"details": json.loads(r[2])
|
||||||
|
}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
def all():
|
||||||
|
return RESULTS
|
||||||
|
|
||||||
|
def get(service):
|
||||||
|
return RESULTS.get(service)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import yaml
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema(name: str) -> dict:
|
||||||
|
"""
|
||||||
|
Load schema YAML từ app/schemas/<name>.yaml
|
||||||
|
"""
|
||||||
|
path = BASE_DIR / "schemas" / f"{name}.yaml"
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def render_response(schema: dict, data):
|
||||||
|
schema_type = schema.get("type")
|
||||||
|
|
||||||
|
# ---------- ARRAY ----------
|
||||||
|
if schema_type == "array":
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
item_schema = schema.get("item", {})
|
||||||
|
return [
|
||||||
|
render_response(
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"fields": item_schema.get("fields", {})
|
||||||
|
},
|
||||||
|
item
|
||||||
|
)
|
||||||
|
for item in data
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------- OBJECT ----------
|
||||||
|
if schema_type == "object":
|
||||||
|
fields = schema.get("fields", {})
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, field_cfg in fields.items():
|
||||||
|
if key in data:
|
||||||
|
result[key] = data[key]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ---------- FALLBACK ----------
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
SENSITIVE_KEYS = {
|
||||||
|
"password",
|
||||||
|
"token",
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
"authorization",
|
||||||
|
}
|
||||||
|
|
||||||
|
def mask_sensitive(data):
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return {
|
||||||
|
k: (
|
||||||
|
"***"
|
||||||
|
if k.lower() in SENSITIVE_KEYS
|
||||||
|
else mask_sensitive(v)
|
||||||
|
)
|
||||||
|
for k, v in data.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [mask_sensitive(v) for v in data]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
_PATTERN = re.compile(r"\{\{(\w+)\}\}")
|
||||||
|
|
||||||
|
def render(value, context: dict):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def replace(match):
|
||||||
|
key = match.group(1)
|
||||||
|
return str(context.get(key, ""))
|
||||||
|
|
||||||
|
return _PATTERN.sub(replace, value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_headers(headers: dict, context: dict):
|
||||||
|
return {
|
||||||
|
k: render(v, context)
|
||||||
|
for k, v in headers.items()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
health-agent:
|
||||||
|
build: .
|
||||||
|
container_name: health-agent
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# 🔥 mount folder services
|
||||||
|
- ./services:/app/services
|
||||||
|
|
||||||
|
environment:
|
||||||
|
- SERVICES_DIR=/app/services
|
||||||
|
- TZ=Asia/Ho_Chi_Minh
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
pyyaml
|
||||||
|
httpx
|
||||||
|
jsonpath-ng
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
service: bids-service
|
||||||
|
enabled: true
|
||||||
|
interval: 30
|
||||||
|
base_url: http://localhost:4000/api/v1/admin
|
||||||
|
|
||||||
|
checks:
|
||||||
|
- type: http
|
||||||
|
name: login
|
||||||
|
url: /auth/login
|
||||||
|
method: POST
|
||||||
|
body:
|
||||||
|
username: admin
|
||||||
|
password: Admin@123
|
||||||
|
expect:
|
||||||
|
status_code: 201
|
||||||
|
save:
|
||||||
|
cookies: __cookies__
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: me
|
||||||
|
depends_on: [login]
|
||||||
|
url: /auth/me
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: webs
|
||||||
|
depends_on: [login]
|
||||||
|
url: /web-bids
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: create-webs
|
||||||
|
depends_on: [login]
|
||||||
|
body:
|
||||||
|
origin_url: http://abc.com
|
||||||
|
method: POST
|
||||||
|
url: /web-bids
|
||||||
|
expect:
|
||||||
|
status_code: 400
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: update-webs
|
||||||
|
depends_on: [login]
|
||||||
|
body:
|
||||||
|
origin_url: http://abc.com
|
||||||
|
method: PUT
|
||||||
|
url: /web-bids/0
|
||||||
|
expect:
|
||||||
|
status_code: 404
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: delete-webs
|
||||||
|
depends_on: [login]
|
||||||
|
method: DELETE
|
||||||
|
url: /web-bids/0
|
||||||
|
expect:
|
||||||
|
status_code: 404
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: admins
|
||||||
|
depends_on: [login]
|
||||||
|
url: /admins
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: admins
|
||||||
|
depends_on: [login]
|
||||||
|
url: /admins
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: admins
|
||||||
|
depends_on: [login]
|
||||||
|
url: /admins
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: bids
|
||||||
|
depends_on: [login]
|
||||||
|
url: /bids
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: create-bids
|
||||||
|
depends_on: [login]
|
||||||
|
body:
|
||||||
|
url: http://abc.com
|
||||||
|
method: POST
|
||||||
|
url: /bids
|
||||||
|
expect:
|
||||||
|
status_code: 400
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: update-bids
|
||||||
|
depends_on: [login]
|
||||||
|
body:
|
||||||
|
url: http://abc.com
|
||||||
|
method: PUT
|
||||||
|
url: /bids/0
|
||||||
|
expect:
|
||||||
|
status_code: 400
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: delete-bids
|
||||||
|
depends_on: [login]
|
||||||
|
method: DELETE
|
||||||
|
url: /bids/0
|
||||||
|
expect:
|
||||||
|
status_code: 404
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: out-bid-logs
|
||||||
|
depends_on: [login]
|
||||||
|
url: /out-bid-logs
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
- type: http
|
||||||
|
name: send-message-histories
|
||||||
|
depends_on: [login]
|
||||||
|
url: /send-message-histories
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# ================================
|
||||||
|
# SERVICE CONFIG
|
||||||
|
# ================================
|
||||||
|
|
||||||
|
service: default-service # Tên service (duy nhất trong toàn hệ thống)
|
||||||
|
enabled: false # true = bật check, false = bỏ qua service này
|
||||||
|
interval: 30 # Chu kỳ chạy health check (giây)
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# CHECK LIST
|
||||||
|
# ================================
|
||||||
|
checks:
|
||||||
|
# ------------------------------------------------
|
||||||
|
# 1️⃣ LOGIN CHECK (TOKEN TRONG RESPONSE BODY)
|
||||||
|
# - API trả access_token trong JSON
|
||||||
|
# - Token sẽ được save vào context
|
||||||
|
# ------------------------------------------------
|
||||||
|
- type: http # Loại check: http | shell
|
||||||
|
name: login-token # Tên check
|
||||||
|
|
||||||
|
url: http://localhost:4000/api/v1/admin/auth/login
|
||||||
|
method: POST
|
||||||
|
timeout: 5
|
||||||
|
|
||||||
|
body:
|
||||||
|
email: admin@example.com
|
||||||
|
password: Admin@1234
|
||||||
|
|
||||||
|
# Điều kiện pass
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
json:
|
||||||
|
access_token: $.data.access_token
|
||||||
|
# ↑ JSONPath phải tồn tại
|
||||||
|
|
||||||
|
# Lưu token vào context
|
||||||
|
save:
|
||||||
|
token: $.data.access_token
|
||||||
|
# context["token"] = response.data.access_token
|
||||||
|
# cookies: __cookies__ => Trường hợp dùng cho cần save cookie
|
||||||
|
|
||||||
|
# 👉 Check này BLOCKING
|
||||||
|
# 👉 Fail là các check phụ thuộc không chạy
|
||||||
|
|
||||||
|
# ------------------------------------------------
|
||||||
|
# 2️⃣ ME CHECK (DÙNG TOKEN TỪ CONTEXT)
|
||||||
|
# - Authorization header dùng {{token}}
|
||||||
|
# ------------------------------------------------
|
||||||
|
- type: http
|
||||||
|
name: me
|
||||||
|
|
||||||
|
url: http://localhost:4000/api/v1/admin/me
|
||||||
|
method: GET
|
||||||
|
timeout: 3
|
||||||
|
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{token}}"
|
||||||
|
# ↑ token lấy từ context của service này
|
||||||
|
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
# ------------------------------------------------
|
||||||
|
# 3️⃣ HEALTH CHECK (ĐỘC LẬP)
|
||||||
|
# - Không cần login
|
||||||
|
# - Login fail vẫn chạy
|
||||||
|
# ------------------------------------------------
|
||||||
|
- type: http
|
||||||
|
name: health
|
||||||
|
|
||||||
|
url: http://localhost:4000/health
|
||||||
|
method: GET
|
||||||
|
timeout: 2
|
||||||
|
|
||||||
|
expect:
|
||||||
|
status_code: 200
|
||||||
|
|
||||||
|
independent: true # 🔥 Không phụ thuộc login
|
||||||
|
|
||||||
|
# ------------------------------------------------
|
||||||
|
# 4️⃣ PROCESS CHECK (SHELL)
|
||||||
|
# - Kiểm tra process OS
|
||||||
|
# -> Chưa test nào dùng thì viết tiếp
|
||||||
|
# ------------------------------------------------
|
||||||
|
- type: shell
|
||||||
|
name: process
|
||||||
|
|
||||||
|
command: "pgrep -f auth-service"
|
||||||
|
|
||||||
|
independent: true
|
||||||
Loading…
Reference in New Issue