Files
ai-contract-platform/yuqei-infra/scripts/watch-deprecated-alias-usage.py
T
yuqei a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
release: freeze V2 release candidate
2026-07-12 15:32:24 +08:00

268 lines
10 KiB
Python

#!/usr/bin/env python3
"""Watch deprecated API alias usage over a short observation window."""
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple
REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_ARTIFACT_ROOT = REPO_ROOT / "project-docs" / "qa-artifacts" / "v2-deprecated-alias-observation"
def checked_at() -> str:
return datetime.now(timezone.utc).astimezone().isoformat()
def now_stamp() -> str:
return datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
def normalize_base_url(value: str) -> str:
return value.rstrip("/")
def build_metrics_url(base_url: str, api_prefix: str, alias_group: str, page_size: int) -> str:
query = urllib.parse.urlencode({"alias_group": alias_group, "page_size": page_size})
return f"{normalize_base_url(base_url)}{api_prefix.rstrip('/')}/metrics/deprecated-aliases?{query}"
def fetch_json(url: str, timeout_seconds: float) -> Dict[str, Any]:
request = urllib.request.Request(
url,
headers={
"Accept": "application/json",
"User-Agent": "yuqei-v2-deprecated-alias-watch/1.0",
},
method="GET",
)
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
payload = response.read().decode("utf-8")
data = json.loads(payload)
if not isinstance(data, dict):
raise ValueError("deprecated alias metrics response must be a JSON object")
return data
def usage_items(data: Dict[str, Any]) -> List[Dict[str, Any]]:
items = data.get("items", [])
if not isinstance(items, list):
raise ValueError("deprecated alias metrics response items must be a list")
return [item for item in items if isinstance(item, dict)]
def usage_total(data: Dict[str, Any]) -> int:
total = data.get("total", 0)
if isinstance(total, int):
return total
return int(total)
def filter_ignored_items(
items: List[Dict[str, Any]],
*,
ignored_trace_ids: Set[str],
ignored_trace_prefixes: Tuple[str, ...],
) -> List[Dict[str, Any]]:
filtered = [] # type: List[Dict[str, Any]]
for item in items:
trace_id = str(item.get("trace_id") or "")
if trace_id in ignored_trace_ids:
continue
if ignored_trace_prefixes and trace_id.startswith(ignored_trace_prefixes):
continue
filtered.append(item)
return filtered
def collect_new_items(
data: Dict[str, Any],
*,
baseline_ids: Set[str],
ignored_trace_ids: Set[str],
ignored_trace_prefixes: Tuple[str, ...],
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
items = usage_items(data)
new_items = [item for item in items if item.get("id") and str(item["id"]) not in baseline_ids]
relevant_new_items = filter_ignored_items(
new_items,
ignored_trace_ids=ignored_trace_ids,
ignored_trace_prefixes=ignored_trace_prefixes,
)
return new_items, relevant_new_items
def redact_item(item: Dict[str, Any]) -> Dict[str, Any]:
allowed_keys = {
"id",
"alias_group",
"fields",
"target_field",
"compatibility_status",
"contract_id",
"actor_name_present",
"endpoint",
"method",
"trace_id",
"tenant_id",
"timestamp",
}
return {key: item.get(key) for key in allowed_keys if key in item}
def write_artifact(artifact_dir: Path, report: Dict[str, Any]) -> Path:
artifact_dir.mkdir(parents=True, exist_ok=True)
path = artifact_dir / f"deprecated-alias-observation-{now_stamp()}.json"
path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
return path
def print_progress(message: str, *, quiet: bool) -> None:
if not quiet:
print(message, flush=True)
def main() -> int:
parser = argparse.ArgumentParser(description="Watch deprecated V2 API alias usage.")
parser.add_argument("--base-url", default="http://localhost:8100", help="Contract API base URL.")
parser.add_argument("--api-prefix", default="/api/v1", help="Contract API prefix.")
parser.add_argument("--alias-group", default="contract_update_action", help="Deprecated alias group to observe.")
parser.add_argument("--duration-seconds", type=float, default=300.0, help="Observation window length.")
parser.add_argument("--interval-seconds", type=float, default=30.0, help="Polling interval.")
parser.add_argument("--timeout-seconds", type=float, default=20.0, help="HTTP timeout per poll.")
parser.add_argument("--page-size", type=int, default=100, help="Deprecated alias metric page size.")
parser.add_argument("--max-allowed-new", type=int, default=0, help="Maximum allowed new usages during window.")
parser.add_argument("--ignore-trace-id", action="append", default=[], help="Trace id to ignore.")
parser.add_argument("--ignore-trace-prefix", action="append", default=[], help="Trace id prefix to ignore.")
parser.add_argument("--artifact-dir", default=str(DEFAULT_ARTIFACT_ROOT), help="Directory for JSON report.")
parser.add_argument("--no-artifact", action="store_true", help="Do not write a JSON report.")
parser.add_argument("--quiet", action="store_true", help="Only print the final report.")
args = parser.parse_args()
duration_seconds = max(args.duration_seconds, 0.0)
interval_seconds = max(args.interval_seconds, 0.1)
page_size = max(args.page_size, 1)
max_allowed_new = max(args.max_allowed_new, 0)
ignored_trace_ids = {str(value) for value in args.ignore_trace_id}
ignored_trace_prefixes = tuple(str(value) for value in args.ignore_trace_prefix)
url = build_metrics_url(args.base_url, args.api_prefix, args.alias_group, page_size)
started_at = checked_at()
started = time.monotonic()
print_progress(
"DEPRECATED_ALIAS_OBSERVATION_CONNECT "
f"alias_group={args.alias_group} duration_seconds={duration_seconds:g} "
f"interval_seconds={interval_seconds:g} url={url}",
quiet=args.quiet,
)
try:
baseline = fetch_json(url, args.timeout_seconds)
except Exception as exc:
print(f"DEPRECATED_ALIAS_OBSERVATION_ERROR stage=baseline url={url} error={exc}", file=sys.stderr)
return 2
baseline_total = usage_total(baseline)
baseline_ids = {str(item.get("id")) for item in usage_items(baseline) if item.get("id")}
final = baseline
samples = 1
print_progress(
"DEPRECATED_ALIAS_OBSERVATION_START "
f"alias_group={args.alias_group} baseline_total={baseline_total} "
f"baseline_ids={len(baseline_ids)} max_allowed_new={max_allowed_new}",
quiet=args.quiet,
)
while True:
elapsed = time.monotonic() - started
remaining = duration_seconds - elapsed
if remaining <= 0:
break
time.sleep(min(interval_seconds, remaining))
try:
final = fetch_json(url, args.timeout_seconds)
except Exception as exc:
print(f"DEPRECATED_ALIAS_OBSERVATION_ERROR stage=poll url={url} error={exc}", file=sys.stderr)
return 2
samples += 1
final_total_for_progress = usage_total(final)
progress_new_items, progress_relevant_items = collect_new_items(
final,
baseline_ids=baseline_ids,
ignored_trace_ids=ignored_trace_ids,
ignored_trace_prefixes=ignored_trace_prefixes,
)
elapsed_after_poll = time.monotonic() - started
remaining_after_poll = max(duration_seconds - elapsed_after_poll, 0.0)
observed_delta_for_progress = max(final_total_for_progress - baseline_total, len(progress_new_items))
print_progress(
"DEPRECATED_ALIAS_OBSERVATION_POLL "
f"sample={samples} elapsed_seconds={elapsed_after_poll:.1f} "
f"remaining_seconds={remaining_after_poll:.1f} baseline_total={baseline_total} "
f"final_total={final_total_for_progress} observed_delta={observed_delta_for_progress} "
f"relevant_new_count={len(progress_relevant_items)} max_allowed_new={max_allowed_new}",
quiet=args.quiet,
)
final_total = usage_total(final)
new_items, relevant_new_items = collect_new_items(
final,
baseline_ids=baseline_ids,
ignored_trace_ids=ignored_trace_ids,
ignored_trace_prefixes=ignored_trace_prefixes,
)
reset_detected = final_total < baseline_total
observed_delta = max(final_total - baseline_total, len(new_items))
relevant_new_count = len(relevant_new_items)
passed = relevant_new_count <= max_allowed_new
report = {
"status": "pass" if passed else "fail",
"checked_at": checked_at(),
"started_at": started_at,
"duration_seconds": round(time.monotonic() - started, 3),
"samples": samples,
"url": url,
"alias_group": args.alias_group,
"baseline_total": baseline_total,
"final_total": final_total,
"observed_delta": observed_delta,
"relevant_new_count": relevant_new_count,
"max_allowed_new": max_allowed_new,
"reset_detected": reset_detected,
"ignored_trace_ids": sorted(ignored_trace_ids),
"ignored_trace_prefixes": list(ignored_trace_prefixes),
"new_items": [redact_item(item) for item in relevant_new_items],
}
if not args.no_artifact:
report["artifact_path"] = str(write_artifact(Path(args.artifact_dir), report))
print(json.dumps(report, ensure_ascii=False, indent=2))
if passed:
print(
"DEPRECATED_ALIAS_OBSERVATION_PASS "
f"alias_group={args.alias_group} relevant_new_count={relevant_new_count} "
f"baseline_total={baseline_total} final_total={final_total}"
)
return 0
print(
"DEPRECATED_ALIAS_OBSERVATION_FAIL "
f"alias_group={args.alias_group} relevant_new_count={relevant_new_count} "
f"max_allowed_new={max_allowed_new}",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
raise SystemExit(main())