"""OpsGenie REST API client. Wraps the OpsGenie Alert API endpoints used for alert investigation and triage. Credentials come from the user's OpsGenie integration stored locally or via env vars. """ from __future__ import annotations import logging from typing import Any import httpx from integrations.config_models import OpsGenieIntegrationConfig from integrations.probes import ProbeResult from platform.observability.errors.service import capture_service_error logger = logging.getLogger(__name__) _DEFAULT_TIMEOUT = 30 OpsGenieConfig = OpsGenieIntegrationConfig class OpsGenieClient: """Synchronous client for querying the OpsGenie Alert API.""" def __init__(self, config: OpsGenieConfig) -> 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, timeout=_DEFAULT_TIMEOUT, ) return self._client @property def is_configured(self) -> bool: return bool(self.config.api_key) def probe_access(self) -> ProbeResult: """Validate OpsGenie credentials with a minimal alert list call.""" if not self.is_configured: return ProbeResult.missing("Missing API key.") with self: result = self.list_alerts(limit=1) if not result.get("success"): return ProbeResult.failed( f"Alert list check failed: {result.get('error', 'unknown error')}", region=self.config.region, ) return ProbeResult.passed( f"Connected to OpsGenie ({self.config.region.upper()} region); API key accepted.", region=self.config.region, ) def close(self) -> None: if self._client is not None: self._client.close() self._client = None def __enter__(self) -> OpsGenieClient: return self def __exit__(self, *_: object) -> None: self.close() def list_alerts( self, query: str = "", limit: int = 20, ) -> dict[str, Any]: """List OpsGenie alerts, optionally filtered by search query.""" params: dict[str, Any] = {"limit": min(limit, 100)} if query: params["query"] = query try: resp = self._get_client().get("/v2/alerts", params=params) resp.raise_for_status() data = resp.json() alerts = [] for a in data.get("data", []): alerts.append( { "id": a.get("id", ""), "tiny_id": a.get("tinyId", ""), "message": a.get("message", ""), "status": a.get("status", ""), "acknowledged": a.get("acknowledged", False), "is_seen": a.get("isSeen", False), "priority": a.get("priority", ""), "source": a.get("source", ""), "tags": a.get("tags", []), "created_at": a.get("createdAt", ""), "updated_at": a.get("updatedAt", ""), "owner": a.get("owner", ""), "integration_type": a.get("integration", {}).get("type", ""), } ) return {"success": True, "alerts": alerts, "total": len(alerts)} except httpx.HTTPStatusError as exc: capture_service_error( exc, logger=logger, integration="opsgenie", method="list_alerts", extras={"query": query}, ) 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="opsgenie", method="list_alerts", extras={"query": query}, ) return {"success": False, "error": str(exc)} def get_alert(self, alert_id: str) -> dict[str, Any]: """Fetch full details for a specific OpsGenie alert.""" try: resp = self._get_client().get(f"/v2/alerts/{alert_id}") resp.raise_for_status() data = resp.json().get("data", {}) alert = { "id": data.get("id", ""), "tiny_id": data.get("tinyId", ""), "message": data.get("message", ""), "description": data.get("description", ""), "status": data.get("status", ""), "acknowledged": data.get("acknowledged", False), "is_seen": data.get("isSeen", False), "priority": data.get("priority", ""), "source": data.get("source", ""), "tags": data.get("tags", []), "teams": [t.get("id", "") for t in data.get("teams", [])], "responders": [ {"type": r.get("type", ""), "id": r.get("id", "")} for r in data.get("responders", []) ], "actions": data.get("actions", []), "details": data.get("details", {}), "alias": data.get("alias", ""), "entity": data.get("entity", ""), "created_at": data.get("createdAt", ""), "updated_at": data.get("updatedAt", ""), "count": data.get("count", 0), "owner": data.get("owner", ""), "integration_type": data.get("integration", {}).get("type", ""), } return {"success": True, "alert": alert} except httpx.HTTPStatusError as exc: capture_service_error( exc, logger=logger, integration="opsgenie", method="get_alert", extras={"alert_id": alert_id}, ) 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="opsgenie", method="get_alert", extras={"alert_id": alert_id}, ) return {"success": False, "error": str(exc)} def get_alert_logs(self, alert_id: str, limit: int = 20) -> dict[str, Any]: """Fetch the activity log for a specific OpsGenie alert.""" params: dict[str, Any] = {"limit": min(limit, 100)} try: resp = self._get_client().get(f"/v2/alerts/{alert_id}/logs", params=params) resp.raise_for_status() data = resp.json() logs = [] for entry in data.get("data", []): logs.append( { "log": entry.get("log", ""), "type": entry.get("type", ""), "owner": entry.get("owner", ""), "created_at": entry.get("createdAt", ""), "offset": entry.get("offset", ""), } ) return {"success": True, "logs": logs, "total": len(logs)} except httpx.HTTPStatusError as exc: capture_service_error( exc, logger=logger, integration="opsgenie", method="get_alert_logs", extras={"alert_id": alert_id}, ) 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="opsgenie", method="get_alert_logs", extras={"alert_id": alert_id}, ) return {"success": False, "error": str(exc)} def add_note(self, alert_id: str, note: str) -> dict[str, Any]: """Add a note to an OpsGenie alert (findings write-back).""" try: resp = self._get_client().post( f"/v2/alerts/{alert_id}/notes", json={"body": note}, ) resp.raise_for_status() return {"success": True} except httpx.HTTPStatusError as exc: capture_service_error( exc, logger=logger, integration="opsgenie", method="add_note", extras={"alert_id": alert_id}, ) 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="opsgenie", method="add_note", extras={"alert_id": alert_id}, ) return {"success": False, "error": str(exc)} def make_opsgenie_client(api_key: str | None, region: str | None = None) -> OpsGenieClient | None: """Create an OpsGenieClient if a valid API key is provided.""" token = (api_key or "").strip() if not token: return None try: return OpsGenieClient(OpsGenieConfig(api_key=token, region=region or "us")) except Exception: return None