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