Files
tracer-cloud--opensre/integrations/opsgenie/client.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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