Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

239 lines
8.3 KiB
Python

"""Alertmanager REST API client.
Wraps the Alertmanager v2 API endpoints used for alert investigation and context enrichment.
Credentials come from the user's Alertmanager integration stored locally or via env vars.
Supports three auth modes:
- No auth (common for internal/intranet deployments)
- Bearer token (via a reverse proxy that adds auth)
- HTTP Basic auth (username + password)
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from integrations.config_models import AlertmanagerIntegrationConfig
from integrations.probes import ProbeResult
from platform.observability.errors.service import capture_service_error
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
AlertmanagerConfig = AlertmanagerIntegrationConfig
class AlertmanagerClient:
"""Synchronous client for querying the Alertmanager v2 API."""
def __init__(self, config: AlertmanagerConfig) -> None:
self.config = config
self._client: httpx.Client | None = None
def _get_client(self) -> httpx.Client:
if self._client is None:
self._client = httpx.Client(
base_url=self.config.base_url,
headers=self.config.headers,
auth=self.config.basic_auth,
timeout=_DEFAULT_TIMEOUT,
)
return self._client
@property
def is_configured(self) -> bool:
return bool(self.config.base_url)
def probe_access(self) -> ProbeResult:
"""Validate Alertmanager connectivity via the status endpoint."""
if not self.is_configured:
return ProbeResult.missing("Missing base_url.")
result = self.get_status()
if not result.get("success"):
return ProbeResult.failed(
f"Status check failed: {result.get('error', 'unknown error')}"
)
status_data = result.get("status", {})
cluster_status = (
status_data.get("cluster", {}).get("status", "unknown")
if isinstance(status_data, dict)
else "ok"
)
return ProbeResult.passed(
f"Connected to Alertmanager at {self.config.base_url}; cluster status: {cluster_status}.",
cluster_status=cluster_status,
)
def close(self) -> None:
if self._client is not None:
self._client.close()
self._client = None
def __enter__(self) -> AlertmanagerClient:
return self
def __exit__(self, *_: object) -> None:
self.close()
def get_status(self) -> dict[str, Any]:
"""Fetch Alertmanager status — used as a health/connectivity check."""
try:
resp = self._get_client().get("/api/v2/status")
resp.raise_for_status()
data = resp.json()
return {"success": True, "status": data}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="get_status"
)
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
}
except Exception as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="get_status"
)
return {"success": False, "error": str(exc)}
def list_alerts(
self,
active: bool = True,
silenced: bool = False,
inhibited: bool = False,
filter_labels: list[str] | None = None,
limit: int = 100,
) -> dict[str, Any]:
"""List alerts from Alertmanager.
Args:
active: Include active (firing) alerts.
silenced: Include silenced alerts.
inhibited: Include inhibited alerts.
filter_labels: Optional label matchers (e.g. ['alertname="HighErrorRate"']).
limit: Maximum number of alerts to return.
"""
params: dict[str, Any] = {
"active": str(active).lower(),
"silenced": str(silenced).lower(),
"inhibited": str(inhibited).lower(),
}
if filter_labels:
params["filter"] = filter_labels
try:
resp = self._get_client().get("/api/v2/alerts", params=params)
resp.raise_for_status()
data = resp.json()
if not isinstance(data, list):
return {"success": False, "error": "Unexpected response format from /api/v2/alerts"}
alerts = []
for a in data[:limit]:
alerts.append(
{
"fingerprint": a.get("fingerprint", ""),
"status": a.get("status", {}).get("state", "unknown"),
"inhibited_by": a.get("status", {}).get("inhibitedBy", []),
"silenced_by": a.get("status", {}).get("silencedBy", []),
"labels": a.get("labels", {}),
"annotations": a.get("annotations", {}),
"starts_at": a.get("startsAt", ""),
"ends_at": a.get("endsAt", ""),
"generator_url": a.get("generatorURL", ""),
}
)
return {"success": True, "alerts": alerts, "total": len(alerts)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="list_alerts"
)
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
}
except Exception as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="list_alerts"
)
return {"success": False, "error": str(exc)}
def list_silences(self, limit: int = 50) -> dict[str, Any]:
"""List silences from Alertmanager."""
try:
resp = self._get_client().get("/api/v2/silences")
resp.raise_for_status()
data = resp.json()
if not isinstance(data, list):
return {
"success": False,
"error": "Unexpected response format from /api/v2/silences",
}
silences = []
for s in data[:limit]:
silences.append(
{
"id": s.get("id", ""),
"status": s.get("status", {}).get("state", "unknown"),
"matchers": s.get("matchers", []),
"comment": s.get("comment", ""),
"created_by": s.get("createdBy", ""),
"starts_at": s.get("startsAt", ""),
"ends_at": s.get("endsAt", ""),
}
)
active = [s for s in silences if s["status"] == "active"]
return {
"success": True,
"silences": silences,
"active_silences": active,
"total": len(silences),
}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="list_silences"
)
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
}
except Exception as exc:
capture_service_error(
exc, logger=logger, integration="alertmanager", method="list_silences"
)
return {"success": False, "error": str(exc)}
def make_alertmanager_client(
base_url: str | None,
bearer_token: str | None = None,
username: str | None = None,
password: str | None = None,
) -> AlertmanagerClient | None:
"""Create an AlertmanagerClient if a valid base_url is provided."""
url = (base_url or "").strip().rstrip("/")
if not url:
return None
try:
return AlertmanagerClient(
AlertmanagerConfig(
base_url=url,
bearer_token=bearer_token or "",
username=username or "",
password=password or "",
)
)
except Exception:
return None