Files
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

469 lines
17 KiB
Python

"""PagerDuty REST API v2 client.
Wraps the PagerDuty API endpoints used for incident investigation, on-call
lookups, and service/escalation-policy discovery during RCA.
"""
from __future__ import annotations
import logging
from typing import Any
import httpx
from integrations.config_models import PagerDutyIntegrationConfig
from integrations.probes import ProbeResult
from platform.observability.errors.service import capture_service_error
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
PagerDutyConfig = PagerDutyIntegrationConfig
class PagerDutyClient:
"""Synchronous client for querying the PagerDuty REST API v2."""
def __init__(self, config: PagerDutyConfig) -> 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 PagerDuty credentials with a minimal incidents list call."""
if not self.is_configured:
return ProbeResult.missing("Missing API key.")
try:
with self:
resp = self._get_client().get("/incidents", params={"limit": 1})
resp.raise_for_status()
except httpx.HTTPStatusError as exc:
return ProbeResult.failed(f"Connection failed: {exc.response.status_code}")
except Exception as exc:
return ProbeResult.failed(f"Connection failed: {exc}")
return ProbeResult.passed(
"Connected to PagerDuty; API key accepted.",
)
def close(self) -> None:
if self._client is not None:
self._client.close()
self._client = None
def __enter__(self) -> PagerDutyClient:
return self
def __exit__(self, *_: object) -> None:
self.close()
# ------------------------------------------------------------------
# Incidents
# ------------------------------------------------------------------
def list_incidents(
self,
*,
statuses: list[str] | None = None,
urgencies: list[str] | None = None,
service_ids: list[str] | None = None,
since: str | None = None,
until: str | None = None,
limit: int = 25,
) -> dict[str, Any]:
"""List PagerDuty incidents, optionally filtered by status/urgency/service/time."""
params: dict[str, Any] = {"limit": min(limit, 100)}
if statuses:
params["statuses[]"] = statuses
if urgencies:
params["urgencies[]"] = urgencies
if service_ids:
params["service_ids[]"] = service_ids
if since:
params["since"] = since
if until:
params["until"] = until
try:
resp = self._get_client().get("/incidents", params=params)
resp.raise_for_status()
data = resp.json()
incidents = []
for inc in data.get("incidents", []):
incidents.append(
{
"id": inc.get("id", ""),
"incident_number": inc.get("incident_number"),
"title": inc.get("title", ""),
"status": inc.get("status", ""),
"urgency": inc.get("urgency", ""),
"priority": _extract_priority(inc),
"service": _extract_ref(inc.get("service")),
"escalation_policy": _extract_ref(inc.get("escalation_policy")),
"assigned_to": [
_extract_ref(a.get("assignee")) for a in inc.get("assignments", [])
],
"created_at": inc.get("created_at", ""),
"updated_at": inc.get("last_status_change_at", ""),
"html_url": inc.get("html_url", ""),
}
)
return {"success": True, "incidents": incidents, "total": len(incidents)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="list_incidents",
extras={"statuses": statuses, "service_ids": service_ids},
)
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="pagerduty",
method="list_incidents",
)
return {"success": False, "error": str(exc)}
def get_incident(self, incident_id: str) -> dict[str, Any]:
"""Fetch full details for a specific PagerDuty incident."""
try:
resp = self._get_client().get(f"/incidents/{incident_id}")
resp.raise_for_status()
data = resp.json().get("incident", {})
incident = {
"id": data.get("id", ""),
"incident_number": data.get("incident_number"),
"title": data.get("title", ""),
"description": data.get("description", ""),
"status": data.get("status", ""),
"urgency": data.get("urgency", ""),
"priority": _extract_priority(data),
"service": _extract_ref(data.get("service")),
"escalation_policy": _extract_ref(data.get("escalation_policy")),
"teams": [_extract_ref(t) for t in data.get("teams", [])],
"assigned_to": [
_extract_ref(a.get("assignee")) for a in data.get("assignments", [])
],
"acknowledgements": [
{
"acknowledger": _extract_ref(ack.get("acknowledger")),
"at": ack.get("at", ""),
}
for ack in data.get("acknowledgements", [])
],
"created_at": data.get("created_at", ""),
"updated_at": data.get("last_status_change_at", ""),
"resolved_at": data.get("resolved_at", ""),
"html_url": data.get("html_url", ""),
"alert_counts": data.get("alert_counts", {}),
}
return {"success": True, "incident": incident}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="get_incident",
extras={"incident_id": incident_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="pagerduty",
method="get_incident",
extras={"incident_id": incident_id},
)
return {"success": False, "error": str(exc)}
def list_incident_log_entries(self, incident_id: str, *, limit: int = 25) -> dict[str, Any]:
"""Fetch the activity log (timeline) for a specific PagerDuty incident."""
params: dict[str, Any] = {"limit": min(limit, 100)}
try:
resp = self._get_client().get(f"/incidents/{incident_id}/log_entries", params=params)
resp.raise_for_status()
data = resp.json()
log_entries = []
for entry in data.get("log_entries", []):
log_entries.append(
{
"id": entry.get("id", ""),
"type": entry.get("type", ""),
"summary": entry.get("summary", ""),
"created_at": entry.get("created_at", ""),
"agent": _extract_ref(entry.get("agent")),
"channel": entry.get("channel", {}),
}
)
return {"success": True, "log_entries": log_entries, "total": len(log_entries)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="list_incident_log_entries",
extras={"incident_id": incident_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="pagerduty",
method="list_incident_log_entries",
extras={"incident_id": incident_id},
)
return {"success": False, "error": str(exc)}
# ------------------------------------------------------------------
# On-call
# ------------------------------------------------------------------
def get_oncalls(
self,
*,
escalation_policy_ids: list[str] | None = None,
limit: int = 25,
) -> dict[str, Any]:
"""Fetch current on-call responders, optionally filtered by escalation policy."""
params: dict[str, Any] = {"limit": min(limit, 100)}
if escalation_policy_ids:
params["escalation_policy_ids[]"] = escalation_policy_ids
try:
resp = self._get_client().get("/oncalls", params=params)
resp.raise_for_status()
data = resp.json()
oncalls = []
for oc in data.get("oncalls", []):
oncalls.append(
{
"user": _extract_ref(oc.get("user")),
"escalation_policy": _extract_ref(oc.get("escalation_policy")),
"escalation_level": oc.get("escalation_level"),
"schedule": _extract_ref(oc.get("schedule")),
"start": oc.get("start", ""),
"end": oc.get("end", ""),
}
)
return {"success": True, "oncalls": oncalls, "total": len(oncalls)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="get_oncalls",
extras={"escalation_policy_ids": escalation_policy_ids},
)
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="pagerduty",
method="get_oncalls",
)
return {"success": False, "error": str(exc)}
# ------------------------------------------------------------------
# Services & escalation policies
# ------------------------------------------------------------------
def list_services(self, *, limit: int = 25) -> dict[str, Any]:
"""List PagerDuty services with their escalation policies."""
params: dict[str, Any] = {
"limit": min(limit, 100),
"include[]": ["escalation_policies", "integrations"],
}
try:
resp = self._get_client().get("/services", params=params)
resp.raise_for_status()
data = resp.json()
services = []
for svc in data.get("services", []):
services.append(
{
"id": svc.get("id", ""),
"name": svc.get("name", ""),
"description": svc.get("description", ""),
"status": svc.get("status", ""),
"escalation_policy": _extract_ref(svc.get("escalation_policy")),
"teams": [_extract_ref(t) for t in svc.get("teams", [])],
"alert_creation": svc.get("alert_creation", ""),
"integrations": [
{
"id": i.get("id", ""),
"name": i.get("name", ""),
"type": i.get("type", ""),
}
for i in svc.get("integrations", [])
],
"html_url": svc.get("html_url", ""),
}
)
return {"success": True, "services": services, "total": len(services)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="list_services",
)
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="pagerduty",
method="list_services",
)
return {"success": False, "error": str(exc)}
def get_service(self, service_id: str) -> dict[str, Any]:
"""Fetch full details for a specific PagerDuty service including routing rules."""
params: dict[str, Any] = {
"include[]": ["escalation_policies", "integrations"],
}
try:
resp = self._get_client().get(f"/services/{service_id}", params=params)
resp.raise_for_status()
data = resp.json().get("service", {})
service = {
"id": data.get("id", ""),
"name": data.get("name", ""),
"description": data.get("description", ""),
"status": data.get("status", ""),
"escalation_policy": _extract_ref(data.get("escalation_policy")),
"teams": [_extract_ref(t) for t in data.get("teams", [])],
"alert_creation": data.get("alert_creation", ""),
"incident_urgency_rule": data.get("incident_urgency_rule", {}),
"integrations": [
{
"id": i.get("id", ""),
"name": i.get("name", ""),
"type": i.get("type", ""),
"vendor": _extract_ref(i.get("vendor")),
}
for i in data.get("integrations", [])
],
"html_url": data.get("html_url", ""),
}
return {"success": True, "service": service}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="pagerduty",
method="get_service",
extras={"service_id": service_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="pagerduty",
method="get_service",
extras={"service_id": service_id},
)
return {"success": False, "error": str(exc)}
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _extract_ref(obj: dict[str, Any] | None) -> dict[str, str]:
"""Extract a compact {id, summary, type} reference from a PagerDuty object ref."""
if not obj:
return {}
return {
"id": obj.get("id", ""),
"summary": obj.get("summary", ""),
"type": obj.get("type", ""),
}
def _extract_priority(incident: dict[str, Any]) -> dict[str, str]:
"""Extract priority info from an incident, which may be null."""
priority = incident.get("priority")
if not priority:
return {}
return {
"id": priority.get("id", ""),
"name": priority.get("name", ""),
"summary": priority.get("summary", ""),
}
# ------------------------------------------------------------------
# Factory
# ------------------------------------------------------------------
def make_pagerduty_client(
api_key: str | None, base_url: str | None = None
) -> PagerDutyClient | None:
"""Create a PagerDutyClient if a valid API key is provided."""
token = (api_key or "").strip()
if not token:
return None
try:
config_kwargs: dict[str, Any] = {"api_key": token}
if base_url:
config_kwargs["base_url"] = base_url
return PagerDutyClient(PagerDutyConfig(**config_kwargs))
except Exception:
return None