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
239 lines
8.3 KiB
Python
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
|