Files
atc-agents/api/pipeline_ops.py
T

166 lines
6.0 KiB
Python
Raw Normal View History

"""Pipeline / data-generation control for the Command Center.
Triggers per-database Airflow DAGs (light, configurable row counts), reports
run status, and returns live "in sync" counts via Trino so the UI can show the
whole data flow being pulsed and recognised downstream.
"""
from __future__ import annotations
import os
import time
from typing import Any
import httpx
from fastapi import APIRouter, Body, Query
from fastapi.responses import JSONResponse
AIRFLOW_URL = os.getenv("AIRFLOW_URL", "http://10.0.21.55:8080").rstrip("/")
AIRFLOW_USER = os.getenv("AIRFLOW_USER", "admin")
AIRFLOW_PASSWORD = os.getenv("AIRFLOW_PASSWORD", "")
TRINO_URL = os.getenv("TRINO_URL", "http://10.0.21.50:8089").rstrip("/")
TRINO_USER = os.getenv("TRINO_USER", "mo")
router = APIRouter(prefix="/api/pipeline", tags=["pipeline"])
# UI source key -> Airflow DAG id
SOURCE_DAG = {
"postgres": "gen_postgres",
"mysql": "gen_mysql",
"mongodb": "gen_mongodb",
"cassandra": "gen_cassandra",
"neo4j": "gen_neo4j",
"all": "generate_data_all_databases",
"hadoop": "gen_hadoop_history",
}
# UI source key -> Trino fully-qualified table for live row counts
SOURCE_COUNT_SQL = {
"postgres": "SELECT count(*) FROM postgres_sales.public.sales_orders",
"mysql": "SELECT count(*) FROM mysql_hr.hr.employee_events",
"mongodb": "SELECT count(*) FROM mongodb_supplychain.supplychain.events",
"cassandra": "SELECT count(*) FROM cassandra_telemetry.telemetry.device_metrics",
}
_token_cache: dict[str, Any] = {"token": None, "exp": 0.0}
async def _airflow_token(client: httpx.AsyncClient) -> str:
now = time.time()
if _token_cache["token"] and _token_cache["exp"] > now + 30:
return _token_cache["token"]
r = await client.post(
f"{AIRFLOW_URL}/auth/token",
json={"username": AIRFLOW_USER, "password": AIRFLOW_PASSWORD},
timeout=10,
)
r.raise_for_status()
tok = r.json()["access_token"]
_token_cache["token"] = tok
_token_cache["exp"] = now + 20 * 60 # tokens last ~24h; refresh well before
return tok
async def _trino_scalar(sql: str, timeout: float = 12.0) -> int | None:
try:
async with httpx.AsyncClient(timeout=timeout) as client:
r = await client.post(
f"{TRINO_URL}/v1/statement",
content=sql.encode(),
headers={"X-Trino-User": TRINO_USER},
)
data = r.json()
rows: list[Any] = []
nxt = data.get("nextUri")
if data.get("data"):
rows += data["data"]
while nxt:
rr = await client.get(nxt)
d = rr.json()
if d.get("data"):
rows += d["data"]
if d.get("error"):
return None
nxt = d.get("nextUri")
if rows:
return int(rows[0][0])
except Exception:
return None
return None
@router.post("/generate/{source}")
async def generate(source: str, body: dict[str, Any] = Body(default={})) -> JSONResponse:
dag_id = SOURCE_DAG.get(source)
if not dag_id:
return JSONResponse({"ok": False, "error": f"Unknown source '{source}'"}, status_code=400)
rows = body.get("rows")
conf: dict[str, Any] = {}
if rows is not None:
try:
conf["rows"] = max(1, min(int(rows), 2_000_000))
except (TypeError, ValueError):
return JSONResponse({"ok": False, "error": "rows must be an integer"}, status_code=400)
try:
async with httpx.AsyncClient() as client:
tok = await _airflow_token(client)
r = await client.post(
f"{AIRFLOW_URL}/api/v2/dags/{dag_id}/dagRuns",
headers={"Authorization": f"Bearer {tok}"},
json={"logical_date": None, "conf": conf},
timeout=15,
)
if r.status_code >= 400:
return JSONResponse({"ok": False, "error": f"Airflow {r.status_code}: {r.text[:300]}"}, status_code=200)
j = r.json()
return JSONResponse({
"ok": True,
"source": source,
"dag_id": dag_id,
"run_id": j.get("dag_run_id"),
"state": j.get("state"),
"rows": conf.get("rows"),
})
except Exception as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=200)
@router.get("/runs/{source}")
async def runs(source: str, limit: int = Query(5)) -> JSONResponse:
dag_id = SOURCE_DAG.get(source)
if not dag_id:
return JSONResponse({"ok": False, "error": f"Unknown source '{source}'"}, status_code=400)
try:
async with httpx.AsyncClient() as client:
tok = await _airflow_token(client)
headers = {"Authorization": f"Bearer {tok}"}
r = await client.get(
f"{AIRFLOW_URL}/api/v2/dags/{dag_id}/dagRuns",
headers=headers,
params={"order_by": "-run_after", "limit": limit},
timeout=12,
)
runs_list = r.json().get("dag_runs", [])
out = []
for run in runs_list:
out.append({
"run_id": run.get("dag_run_id"),
"state": run.get("state"),
"start": run.get("start_date"),
"end": run.get("end_date"),
"conf": run.get("conf"),
})
return JSONResponse({"ok": True, "source": source, "dag_id": dag_id, "runs": out})
except Exception as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=200)
@router.get("/sync")
async def sync(source: str | None = Query(None)) -> JSONResponse:
"""Live row counts per source (via Trino) for the 'in sync' display."""
targets = [source] if source and source in SOURCE_COUNT_SQL else list(SOURCE_COUNT_SQL.keys())
counts: dict[str, Any] = {}
for src in targets:
counts[src] = await _trino_scalar(SOURCE_COUNT_SQL[src])
return JSONResponse({"ok": True, "counts": counts})