4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
273 lines
9.6 KiB
Python
273 lines
9.6 KiB
Python
"""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
|