mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
207 lines
5.7 KiB
Python
207 lines
5.7 KiB
Python
"""Service health checks for Hanzo K8s CLI.
|
|
|
|
Runs concurrent HTTP health checks against known domains and displays
|
|
results in a rich table with color-coded status.
|
|
|
|
Usage:
|
|
hanzo k8s health # Check default cluster domains
|
|
hanzo k8s health --cluster adnexus # Check a specific cluster
|
|
hanzo k8s health --timeout 10 # Custom timeout
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
|
|
import click
|
|
import httpx
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
console = Console()
|
|
|
|
# Known domains per cluster
|
|
CLUSTER_DOMAINS: dict[str, list[str]] = {
|
|
"hanzo": [
|
|
"billing.hanzo.ai",
|
|
"console.hanzo.ai",
|
|
"api.hanzo.ai",
|
|
"hanzo.chat",
|
|
"kms.hanzo.ai",
|
|
"models.hanzo.ai",
|
|
"platform.hanzo.ai",
|
|
"pricing.hanzo.ai",
|
|
"status.hanzo.ai",
|
|
"analytics.hanzo.ai",
|
|
"hanzo.id",
|
|
"hanzo.bot",
|
|
"mpc.hanzo.ai",
|
|
"cloud.hanzo.ai",
|
|
"s3.hanzo.ai",
|
|
"search.hanzo.ai",
|
|
"gateway.hanzo.ai",
|
|
"s3.hanzo.ai",
|
|
],
|
|
"adnexus": [
|
|
"ad.nexus",
|
|
"api.ad.nexus",
|
|
"api-ai.ad.nexus",
|
|
"dsp.ad.nexus",
|
|
"ssp.ad.nexus",
|
|
"docs.ad.nexus",
|
|
"grafana.ad.nexus",
|
|
"id.ad.nexus",
|
|
],
|
|
"lux": [
|
|
"lux.network",
|
|
"lux.id",
|
|
],
|
|
"zoo": [
|
|
"zoo.ngo",
|
|
"zips.zoo.ngo",
|
|
],
|
|
}
|
|
|
|
|
|
async def _check_domain(
|
|
client: httpx.AsyncClient, domain: str
|
|
) -> tuple[str, int, float, str]:
|
|
"""Check a single domain. Returns (domain, status, latency_ms, error)."""
|
|
url = f"https://{domain}/"
|
|
t0 = time.monotonic()
|
|
try:
|
|
resp = await client.get(url, follow_redirects=True)
|
|
latency = (time.monotonic() - t0) * 1000
|
|
return domain, resp.status_code, latency, ""
|
|
except httpx.TimeoutException:
|
|
latency = (time.monotonic() - t0) * 1000
|
|
return domain, 0, latency, "timeout"
|
|
except httpx.ConnectError as e:
|
|
latency = (time.monotonic() - t0) * 1000
|
|
return domain, 0, latency, f"connect: {e}"
|
|
except Exception as e:
|
|
latency = (time.monotonic() - t0) * 1000
|
|
return domain, 0, latency, str(e)
|
|
|
|
|
|
async def _run_checks(
|
|
domains: list[str], timeout: float
|
|
) -> list[tuple[str, int, float, str]]:
|
|
"""Run health checks concurrently against all domains."""
|
|
async with httpx.AsyncClient(
|
|
timeout=httpx.Timeout(timeout),
|
|
verify=False, # Some services may have cert issues
|
|
headers={"User-Agent": "hanzo-cli/health"},
|
|
) as client:
|
|
tasks = [_check_domain(client, d) for d in domains]
|
|
return await asyncio.gather(*tasks)
|
|
|
|
|
|
def _status_style(code: int, error: str) -> str:
|
|
"""Return rich style string for a status code."""
|
|
if error:
|
|
return "bold red"
|
|
if 200 <= code < 300:
|
|
return "bold green"
|
|
if 300 <= code < 400:
|
|
return "yellow"
|
|
if 400 <= code < 500:
|
|
return "yellow"
|
|
return "bold red"
|
|
|
|
|
|
def _status_text(code: int, error: str) -> str:
|
|
"""Return display text for status."""
|
|
if error:
|
|
return error[:30]
|
|
return str(code)
|
|
|
|
|
|
@click.command("health")
|
|
@click.option(
|
|
"--cluster",
|
|
"-c",
|
|
default=None,
|
|
help="Cluster name (default: current cluster).",
|
|
)
|
|
@click.option(
|
|
"--timeout",
|
|
"-t",
|
|
default=15.0,
|
|
type=float,
|
|
help="HTTP timeout in seconds (default: 15).",
|
|
)
|
|
def health(cluster: str | None, timeout: float) -> None:
|
|
"""Run HTTP health checks against cluster services.
|
|
|
|
Checks all known domains for the specified cluster (or the current
|
|
active cluster) and displays a table with status codes and latency.
|
|
"""
|
|
# Resolve cluster name
|
|
if cluster is None:
|
|
from hanzo_cli.k8s.clusters import get_current_cluster
|
|
|
|
cluster, _ = get_current_cluster()
|
|
|
|
domains = CLUSTER_DOMAINS.get(cluster, [])
|
|
if not domains:
|
|
available = ", ".join(sorted(CLUSTER_DOMAINS.keys()))
|
|
console.print(
|
|
f"[yellow]No domains configured for cluster '{cluster}'.[/yellow]"
|
|
)
|
|
console.print(f"Known clusters with domains: {available}")
|
|
return
|
|
|
|
console.print(
|
|
f"Checking [cyan]{len(domains)}[/cyan] domains for "
|
|
f"[bold]{cluster}[/bold] (timeout: {timeout}s)...\n"
|
|
)
|
|
|
|
# Run async checks
|
|
results = asyncio.run(_run_checks(domains, timeout))
|
|
|
|
# Sort: errors first, then by status code descending, then by domain
|
|
results.sort(key=lambda r: (r[3] == "", r[1], r[0]))
|
|
|
|
# Build table
|
|
table = Table(title=f"Health — {cluster}")
|
|
table.add_column("Domain", style="cyan", min_width=25)
|
|
table.add_column("Status", justify="center", min_width=8)
|
|
table.add_column("Latency", justify="right", min_width=10)
|
|
table.add_column("Note", style="dim")
|
|
|
|
ok_count = 0
|
|
for domain, code, latency_ms, error in results:
|
|
style = _status_style(code, error)
|
|
status_text = _status_text(code, error)
|
|
|
|
if latency_ms >= 5000:
|
|
latency_str = f"[red]{latency_ms:.0f}ms[/red]"
|
|
elif latency_ms >= 1000:
|
|
latency_str = f"[yellow]{latency_ms:.0f}ms[/yellow]"
|
|
else:
|
|
latency_str = f"{latency_ms:.0f}ms"
|
|
|
|
note = ""
|
|
if error:
|
|
note = error[:50]
|
|
elif code >= 400:
|
|
note = "client/server error"
|
|
|
|
if not error and 200 <= code < 400:
|
|
ok_count += 1
|
|
|
|
table.add_row(domain, f"[{style}]{status_text}[/{style}]", latency_str, note)
|
|
|
|
console.print(table)
|
|
|
|
total = len(results)
|
|
if ok_count == total:
|
|
console.print(f"\n[bold green]All {total} services healthy.[/bold green]")
|
|
else:
|
|
console.print(
|
|
f"\n[bold]{ok_count}/{total}[/bold] healthy, "
|
|
f"[red]{total - ok_count} issues[/red]"
|
|
)
|