test: add go native production smoke
This commit is contained in:
217
tools/go_native_prod_smoke.py
Executable file
217
tools/go_native_prod_smoke.py
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""HTTP smoke checks for the Go native production vehicle ingest stack."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
SHANGHAI = dt.timezone(dt.timedelta(hours=8))
|
||||
DEFAULT_BASE_URL = "http://115.29.187.205:20210"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckSpec:
|
||||
name: str
|
||||
path: str
|
||||
params: dict[str, Any]
|
||||
minimum: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Check:
|
||||
name: str
|
||||
status: str
|
||||
count: int
|
||||
minimum: int
|
||||
message: str
|
||||
|
||||
|
||||
def shanghai_day_window(now: dt.datetime | None = None) -> tuple[str, str]:
|
||||
now = now or dt.datetime.now(tz=SHANGHAI)
|
||||
local = now.astimezone(SHANGHAI)
|
||||
start = dt.datetime(local.year, local.month, local.day, tzinfo=SHANGHAI)
|
||||
end = start + dt.timedelta(days=1)
|
||||
return start.isoformat(timespec="seconds"), end.isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def api_url(base_url: str, path: str, params: dict[str, Any]) -> str:
|
||||
return base_url.rstrip("/") + path + "?" + urllib.parse.urlencode(params)
|
||||
|
||||
|
||||
def query_json(base_url: str, path: str, params: dict[str, Any], timeout: float) -> dict[str, Any]:
|
||||
request = urllib.request.Request(
|
||||
api_url(base_url, path, params),
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
|
||||
def check_total(name: str, payload: dict[str, Any], minimum: int) -> Check:
|
||||
count = int(payload.get("total") or 0)
|
||||
sample = sample_summary(payload.get("items") or [])
|
||||
if count >= minimum:
|
||||
return Check(name, "pass", count, minimum, f"count={count}; sample={sample}")
|
||||
return Check(name, "fail", count, minimum, f"count={count}; expected >= {minimum}; sample={sample}")
|
||||
|
||||
|
||||
def sample_summary(items: list[Any]) -> str:
|
||||
if not items:
|
||||
return "{}"
|
||||
first = items[0]
|
||||
if not isinstance(first, dict):
|
||||
return json.dumps(first, ensure_ascii=False, sort_keys=True)
|
||||
keep = {
|
||||
key: first.get(key)
|
||||
for key in (
|
||||
"ts",
|
||||
"stat_date",
|
||||
"protocol",
|
||||
"vehicle_key",
|
||||
"vin",
|
||||
"phone",
|
||||
"message_id_hex",
|
||||
"metric_key",
|
||||
"metric_value",
|
||||
"total_mileage_km",
|
||||
"latest_total_mileage_km",
|
||||
)
|
||||
if first.get(key) not in (None, "")
|
||||
}
|
||||
return json.dumps(keep, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
|
||||
def overall_status(checks: list[Check]) -> str:
|
||||
return "fail" if any(check.status == "fail" for check in checks) else "pass"
|
||||
|
||||
|
||||
def build_check_specs(
|
||||
*,
|
||||
date_from: str,
|
||||
date_to: str,
|
||||
stat_date: str,
|
||||
min_raw: int,
|
||||
min_history: int,
|
||||
min_stat: int,
|
||||
) -> list[CheckSpec]:
|
||||
raw_params = {"dateFrom": date_from, "dateTo": date_to, "limit": 1}
|
||||
history_params = {"protocol": "JT808", "dateFrom": date_from, "dateTo": date_to, "limit": 1}
|
||||
return [
|
||||
CheckSpec("gb32960.raw", "/api/history/raw-frames", {**raw_params, "protocol": "GB32960"}, min_raw),
|
||||
CheckSpec("jt808.raw", "/api/history/raw-frames", {**raw_params, "protocol": "JT808"}, min_raw),
|
||||
CheckSpec("yutong_mqtt.raw", "/api/history/raw-frames", {**raw_params, "protocol": "YUTONG_MQTT"}, min_raw),
|
||||
CheckSpec("jt808.locations", "/api/history/locations", history_params, min_history),
|
||||
CheckSpec("jt808.mileage_points", "/api/history/mileage-points", history_params, min_history),
|
||||
CheckSpec(
|
||||
"gb32960.daily_mileage",
|
||||
"/api/stats/daily-metrics",
|
||||
{
|
||||
"protocol": "GB32960",
|
||||
"metricKey": "daily_mileage_km",
|
||||
"dateFrom": stat_date,
|
||||
"dateTo": stat_date,
|
||||
"limit": 1,
|
||||
},
|
||||
min_stat,
|
||||
),
|
||||
CheckSpec(
|
||||
"gb32960.daily_total_mileage",
|
||||
"/api/stats/daily-metrics",
|
||||
{
|
||||
"protocol": "GB32960",
|
||||
"metricKey": "daily_total_mileage_km",
|
||||
"dateFrom": stat_date,
|
||||
"dateTo": stat_date,
|
||||
"limit": 1,
|
||||
},
|
||||
min_stat,
|
||||
),
|
||||
CheckSpec(
|
||||
"jt808.daily_mileage",
|
||||
"/api/stats/daily-metrics",
|
||||
{
|
||||
"protocol": "JT808",
|
||||
"metricKey": "daily_mileage_km",
|
||||
"dateFrom": stat_date,
|
||||
"dateTo": stat_date,
|
||||
"limit": 1,
|
||||
},
|
||||
min_stat,
|
||||
),
|
||||
CheckSpec(
|
||||
"jt808.daily_total_mileage",
|
||||
"/api/stats/daily-metrics",
|
||||
{
|
||||
"protocol": "JT808",
|
||||
"metricKey": "daily_total_mileage_km",
|
||||
"dateFrom": stat_date,
|
||||
"dateTo": stat_date,
|
||||
"limit": 1,
|
||||
},
|
||||
min_stat,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def run_checks(base_url: str, specs: list[CheckSpec], timeout: float) -> list[Check]:
|
||||
checks: list[Check] = []
|
||||
for spec in specs:
|
||||
try:
|
||||
payload = query_json(base_url, spec.path, spec.params, timeout)
|
||||
checks.append(check_total(spec.name, payload, spec.minimum))
|
||||
except Exception as exc: # pragma: no cover - exercised by live failures.
|
||||
checks.append(Check(spec.name, "fail", 0, spec.minimum, f"request failed: {exc}"))
|
||||
return checks
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
||||
parser.add_argument("--date", help="Shanghai local date, YYYY-MM-DD. Defaults to today.")
|
||||
parser.add_argument("--timeout", type=float, default=5.0)
|
||||
parser.add_argument("--min-raw", type=int, default=1)
|
||||
parser.add_argument("--min-history", type=int, default=1)
|
||||
parser.add_argument("--min-stat", type=int, default=1)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
if args.date:
|
||||
day = dt.date.fromisoformat(args.date)
|
||||
start = dt.datetime(day.year, day.month, day.day, tzinfo=SHANGHAI)
|
||||
date_from, date_to = shanghai_day_window(start)
|
||||
stat_date = args.date
|
||||
else:
|
||||
date_from, date_to = shanghai_day_window()
|
||||
stat_date = date_from[:10]
|
||||
specs = build_check_specs(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
stat_date=stat_date,
|
||||
min_raw=args.min_raw,
|
||||
min_history=args.min_history,
|
||||
min_stat=args.min_stat,
|
||||
)
|
||||
checks = run_checks(args.base_url, specs, args.timeout)
|
||||
status = overall_status(checks)
|
||||
print(json.dumps({
|
||||
"status": status,
|
||||
"baseUrl": args.base_url,
|
||||
"dateFrom": date_from,
|
||||
"dateTo": date_to,
|
||||
"checks": [asdict(check) for check in checks],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0 if status == "pass" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user