| """Database sandbox service.""" |
|
|
| from __future__ import annotations |
|
|
| import time |
| from collections import deque |
| from typing import Any |
|
|
| from fastapi import FastAPI |
| from pydantic import BaseModel |
| from starlette.responses import PlainTextResponse |
|
|
| app = FastAPI(title="sandbox-database", version="0.1.0") |
|
|
| STARTED = time.time() |
| ACTIVE_FAILURE: str | None = None |
| LOGS: deque[str] = deque(maxlen=500) |
| DATA: dict[str, dict[str, Any]] = {"txn-001": {"status": "ok", "amount": 42.0}} |
|
|
|
|
| class ChaosInjectRequest(BaseModel): |
| failure_mode: str |
| severity: str = "high" |
|
|
|
|
| class QueryRequest(BaseModel): |
| key: str |
|
|
|
|
| def _now() -> str: |
| return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) |
|
|
|
|
| def _log(level: str, message: str, **extra: Any) -> None: |
| payload = {"ts": _now(), "service": "database", "level": level, "message": message, **extra} |
| LOGS.append(str(payload)) |
|
|
|
|
| def _status() -> str: |
| if ACTIVE_FAILURE in {"oom", "query_timeout"}: |
| return "down" |
| if ACTIVE_FAILURE in {"connection_pool_exhaustion", "replication_lag"}: |
| return "degraded" |
| return "healthy" |
|
|
|
|
| @app.get("/health") |
| def health() -> dict[str, Any]: |
| return {"service": "database", "status": _status(), "failure_mode": ACTIVE_FAILURE, "uptime_s": int(time.time() - STARTED)} |
|
|
|
|
| @app.get("/metrics", response_class=PlainTextResponse) |
| def metrics() -> str: |
| status = _status() |
| health_value = {"healthy": 1.0, "degraded": 0.5, "down": 0.0}[status] |
| error_rate = 0.01 if status == "healthy" else (0.3 if status == "degraded" else 0.98) |
| latency = 0.03 if status == "healthy" else (0.45 if status == "degraded" else 3.5) |
| return ( |
| f'service_health{{service="database"}} {health_value}\n' |
| f'service_error_rate{{service="database"}} {error_rate}\n' |
| f'service_latency_seconds{{service="database",quantile="0.99"}} {latency}\n' |
| ) |
|
|
|
|
| @app.get("/logs") |
| def logs(lines: int = 50) -> dict[str, Any]: |
| size = max(1, min(lines, 500)) |
| return {"service": "database", "entries": list(LOGS)[-size:]} |
|
|
|
|
| @app.post("/chaos/inject") |
| def inject(req: ChaosInjectRequest) -> dict[str, Any]: |
| global ACTIVE_FAILURE |
| ACTIVE_FAILURE = req.failure_mode |
| _log("WARN", "failure injected", failure_mode=req.failure_mode, severity=req.severity) |
| return {"status": "ok", "service": "database", "failure_mode": ACTIVE_FAILURE} |
|
|
|
|
| @app.post("/chaos/clear") |
| def clear() -> dict[str, Any]: |
| global ACTIVE_FAILURE |
| ACTIVE_FAILURE = None |
| _log("INFO", "failure cleared") |
| return {"status": "ok", "service": "database", "failure_mode": ACTIVE_FAILURE} |
|
|
|
|
| @app.post("/query") |
| def query(req: QueryRequest) -> dict[str, Any]: |
| if _status() == "down": |
| _log("ERROR", "query failed", reason="database down") |
| return {"ok": False, "error": "database_down"} |
| if ACTIVE_FAILURE == "replication_lag": |
| _log("WARN", "serving stale record", key=req.key) |
| return {"ok": True, "stale": True, "record": DATA.get(req.key)} |
| return {"ok": True, "record": DATA.get(req.key)} |
|
|
|
|