Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

579 lines
21 KiB
Python

"""Datadog API client for querying logs, events, and monitors.
Uses the Datadog REST API directly via httpx (no SDK dependency).
Credentials come from the user's Datadog integration stored in the Tracer web app DB.
"""
from __future__ import annotations
import asyncio
import logging
import time
from datetime import UTC, datetime, timedelta
from typing import Any
import httpx
from integrations.config_models import DatadogIntegrationConfig
from integrations.probes import ProbeResult
from platform.observability.errors.service import capture_service_error
logger = logging.getLogger(__name__)
_DEFAULT_TIMEOUT = 30
DatadogConfig = DatadogIntegrationConfig
class DatadogClient:
"""Synchronous client for querying Datadog logs, events, and monitors."""
def __init__(self, config: DatadogConfig) -> 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 and self.config.app_key)
def probe_access(self) -> ProbeResult:
"""Validate Datadog credentials with a lightweight monitor list call."""
if not self.is_configured:
return ProbeResult.missing("Missing API key or application key.")
result = self.list_monitors()
if not result.get("success"):
return ProbeResult.failed(
f"Monitor API check failed: {result.get('error', 'unknown error')}",
site=self.config.site,
)
total = int(result.get("total", 0) or 0)
return ProbeResult.passed(
f"Connected to api.{self.config.site} and listed {total} monitors.",
site=self.config.site,
total=total,
)
def search_logs(
self,
query: str,
time_range_minutes: int = 60,
limit: int = 50,
*,
start: datetime | None = None,
end: datetime | None = None,
) -> dict[str, Any]:
"""Search Datadog logs using the Log Search API (v2)."""
now = end or datetime.now(UTC)
from_ts = start or (now - timedelta(minutes=time_range_minutes))
payload = {
"filter": {
"query": query,
"from": from_ts.isoformat(),
"to": now.isoformat(),
},
"sort": "-timestamp",
"page": {"limit": min(limit, 1000)},
}
try:
resp = self._get_client().post("/api/v2/logs/events/search", json=payload)
resp.raise_for_status()
data = resp.json()
logs = []
for event in data.get("data", []):
attrs = event.get("attributes", {})
custom = attrs.get("attributes", {}) or {}
log = {
"timestamp": attrs.get("timestamp", ""),
"message": attrs.get("message", ""),
"status": attrs.get("status", ""),
"service": attrs.get("service", ""),
"host": attrs.get("host", ""),
"tags": attrs.get("tags", []),
}
# Merge custom JSON attributes so pod/node fields are top-level
log.update({k: v for k, v in custom.items() if isinstance(k, str)})
logs.append(log)
return {"success": True, "logs": logs, "total": len(logs)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="search_logs",
extras={"query": query, "time_range_minutes": time_range_minutes},
)
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="datadog",
method="search_logs",
extras={"query": query, "time_range_minutes": time_range_minutes},
)
return {"success": False, "error": str(exc)}
def query_metrics(
self,
query: str,
*,
start: datetime,
end: datetime,
) -> dict[str, Any]:
"""Query Datadog metrics (v1 query API) for a bounded time range."""
params: dict[str, str | int] = {
"from": int(start.timestamp()),
"to": int(end.timestamp()),
"query": query,
}
try:
resp = self._get_client().get("/api/v1/query", params=params)
resp.raise_for_status()
payload = resp.json()
series_list = payload.get("series") or []
if not isinstance(series_list, list) or not series_list:
return {"success": True, "timestamps": [], "values": []}
first = series_list[0] if isinstance(series_list[0], dict) else {}
pointlist = first.get("pointlist") or []
timestamps: list[str] = []
values: list[float] = []
for point in pointlist:
if not (isinstance(point, list | tuple) and len(point) >= 2):
continue
ts_ms, value = point[0], point[1]
if value is None:
continue
try:
ts = datetime.fromtimestamp(float(ts_ms) / 1000.0, tz=UTC)
timestamps.append(ts.isoformat().replace("+00:00", "Z"))
values.append(float(value))
except Exception:
continue
return {"success": True, "timestamps": timestamps, "values": values}
except httpx.HTTPStatusError as e:
logger.warning(
"[datadog] Metrics query HTTP failure status=%s query=%r",
e.response.status_code,
query,
)
return {
"success": False,
"error": f"HTTP {e.response.status_code}: {e.response.text[:200]}",
}
except Exception as e:
logger.warning(
"[datadog] Metrics query request error type=%s detail=%s",
type(e).__name__,
e,
)
return {"success": False, "error": str(e)}
def list_monitors(
self,
query: str | None = None,
) -> dict[str, Any]:
"""List Datadog monitors, optionally filtered by query."""
params: dict[str, Any] = {"page": 0, "page_size": 50}
if query:
params["query"] = query
try:
resp = self._get_client().get("/api/v1/monitor", params=params)
resp.raise_for_status()
monitors = resp.json()
results = []
for m in monitors if isinstance(monitors, list) else []:
results.append(
{
"id": m.get("id"),
"name": m.get("name", ""),
"type": m.get("type", ""),
"query": m.get("query", ""),
"message": m.get("message", ""),
"overall_state": m.get("overall_state", ""),
"tags": m.get("tags", []),
}
)
return {"success": True, "monitors": results, "total": len(results)}
except httpx.HTTPStatusError as exc:
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="list_monitors",
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="datadog",
method="list_monitors",
extras={"query": query},
)
return {"success": False, "error": str(exc)}
def get_pods_on_node(
self,
node_ip: str,
time_range_minutes: int = 60,
limit: int = 200,
) -> dict[str, Any]:
"""Query Datadog Infrastructure API for all pods running on a given node IP.
Uses the Datadog log search API to find pod telemetry tagged with the node IP,
then returns the unique set of pods with their last-seen status.
Args:
node_ip: The node's IP address (from an alert, e.g. "10.0.1.42")
time_range_minutes: How far back to look for pod activity
limit: Max log events to scan for pod tags
Returns:
dict with pods list, each entry containing pod_name, namespace, container,
node_ip, node_name, status/exit_code if available.
"""
query = f"host:{node_ip} OR node_ip:{node_ip}"
result = self.search_logs(query, time_range_minutes=time_range_minutes, limit=limit)
if not result.get("success"):
return {"success": False, "error": result.get("error", "Unknown error"), "pods": []}
seen: set[str] = set()
pods: list[dict[str, Any]] = []
for log in result.get("logs", []):
pod_name = container_name = kube_namespace = exit_code = node_name = found_node_ip = (
None
)
for tag in log.get("tags", []):
if not isinstance(tag, str) or ":" not in tag:
continue
k, _, v = tag.partition(":")
if k == "pod_name":
pod_name = v
elif k == "container_name":
container_name = v
elif k == "kube_namespace":
kube_namespace = v
elif k == "exit_code":
exit_code = v
elif k == "node_name":
node_name = v
elif k == "node_ip":
found_node_ip = v
pod_name = pod_name or log.get("pod_name")
container_name = container_name or log.get("container_name")
kube_namespace = kube_namespace or log.get("kube_namespace")
node_name = node_name or log.get("node_name")
found_node_ip = found_node_ip or log.get("node_ip", node_ip)
if exit_code is None and log.get("exit_code") is not None:
exit_code = str(log["exit_code"])
if pod_name and pod_name not in seen:
seen.add(pod_name)
pods.append(
{
"pod_name": pod_name,
"namespace": kube_namespace,
"container": container_name,
"node_ip": found_node_ip or node_ip,
"node_name": node_name,
"exit_code": exit_code,
"status": "failed" if exit_code and exit_code != "0" else "running",
}
)
return {"success": True, "pods": pods, "total": len(pods), "node_ip": node_ip}
def get_events(
self,
query: str | None = None,
time_range_minutes: int = 60,
) -> dict[str, Any]:
"""Query Datadog events (v2 API)."""
now = datetime.now(UTC)
from_ts = now - timedelta(minutes=time_range_minutes)
payload: dict[str, Any] = {
"filter": {
"from": from_ts.isoformat(),
"to": now.isoformat(),
},
"sort": "-timestamp",
"page": {"limit": 50},
}
if query:
payload["filter"]["query"] = query
try:
resp = self._get_client().post("/api/v2/events/search", json=payload)
resp.raise_for_status()
data = resp.json()
events = []
for event in data.get("data", []):
attrs = event.get("attributes", {})
events.append(
{
"timestamp": attrs.get("timestamp", ""),
"title": attrs.get("title", ""),
"message": attrs.get("message", attrs.get("text", "")),
"tags": attrs.get("tags", []),
"source": attrs.get("source", ""),
}
)
return {"success": True, "events": events, "total": len(events)}
except httpx.HTTPStatusError as exc:
capture_service_error(exc, logger=logger, integration="datadog", method="get_events")
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="datadog", method="get_events")
return {"success": False, "error": str(exc)}
class DatadogAsyncClient:
"""Async client that fetches logs, monitors, and events in parallel."""
def __init__(self, config: DatadogConfig) -> None:
self.config = config
@property
def is_configured(self) -> bool:
return bool(self.config.api_key and self.config.app_key)
async def _search_logs(
self,
client: httpx.AsyncClient,
query: str,
time_range_minutes: int,
limit: int,
) -> dict[str, Any]:
now = datetime.now(UTC)
from_ts = now - timedelta(minutes=time_range_minutes)
payload = {
"filter": {
"query": query,
"from": from_ts.isoformat(),
"to": now.isoformat(),
},
"sort": "-timestamp",
"page": {"limit": min(limit, 1000)},
}
t0 = time.monotonic()
try:
resp = await client.post("/api/v2/logs/events/search", json=payload)
resp.raise_for_status()
data = resp.json()
duration_ms = int((time.monotonic() - t0) * 1000)
logs = []
for event in data.get("data", []):
attrs = event.get("attributes", {})
custom = attrs.get("attributes", {}) or {}
log = {
"timestamp": attrs.get("timestamp", ""),
"message": attrs.get("message", ""),
"status": attrs.get("status", ""),
"service": attrs.get("service", ""),
"host": attrs.get("host", ""),
"tags": attrs.get("tags", []),
}
log.update({k: v for k, v in custom.items() if isinstance(k, str)})
logs.append(log)
return {"success": True, "logs": logs, "total": len(logs), "duration_ms": duration_ms}
except httpx.HTTPStatusError as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="_search_logs",
extras={"query": query, "time_range_minutes": time_range_minutes},
)
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
"duration_ms": duration_ms,
}
except Exception as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="_search_logs",
extras={"query": query, "time_range_minutes": time_range_minutes},
)
return {"success": False, "error": str(exc), "duration_ms": duration_ms}
async def _list_monitors(
self,
client: httpx.AsyncClient,
query: str | None,
) -> dict[str, Any]:
params: dict[str, Any] = {"page": 0, "page_size": 50}
if query:
params["query"] = query
t0 = time.monotonic()
try:
resp = await client.get("/api/v1/monitor", params=params)
resp.raise_for_status()
monitors = resp.json()
duration_ms = int((time.monotonic() - t0) * 1000)
results = []
for m in monitors if isinstance(monitors, list) else []:
results.append(
{
"id": m.get("id"),
"name": m.get("name", ""),
"type": m.get("type", ""),
"query": m.get("query", ""),
"message": m.get("message", ""),
"overall_state": m.get("overall_state", ""),
"tags": m.get("tags", []),
}
)
return {
"success": True,
"monitors": results,
"total": len(results),
"duration_ms": duration_ms,
}
except httpx.HTTPStatusError as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="_list_monitors",
extras={"query": query},
)
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
"duration_ms": duration_ms,
}
except Exception as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(
exc,
logger=logger,
integration="datadog",
method="_list_monitors",
extras={"query": query},
)
return {"success": False, "error": str(exc), "duration_ms": duration_ms}
async def _get_events(
self,
client: httpx.AsyncClient,
query: str | None,
time_range_minutes: int,
) -> dict[str, Any]:
now = datetime.now(UTC)
from_ts = now - timedelta(minutes=time_range_minutes)
payload: dict[str, Any] = {
"filter": {
"from": from_ts.isoformat(),
"to": now.isoformat(),
},
"sort": "-timestamp",
"page": {"limit": 50},
}
if query:
payload["filter"]["query"] = query
t0 = time.monotonic()
try:
resp = await client.post("/api/v2/events/search", json=payload)
resp.raise_for_status()
data = resp.json()
duration_ms = int((time.monotonic() - t0) * 1000)
events = []
for event in data.get("data", []):
attrs = event.get("attributes", {})
events.append(
{
"timestamp": attrs.get("timestamp", ""),
"title": attrs.get("title", ""),
"message": attrs.get("message", attrs.get("text", "")),
"tags": attrs.get("tags", []),
"source": attrs.get("source", ""),
}
)
return {
"success": True,
"events": events,
"total": len(events),
"duration_ms": duration_ms,
}
except httpx.HTTPStatusError as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(exc, logger=logger, integration="datadog", method="_get_events")
return {
"success": False,
"error": f"HTTP {exc.response.status_code}: {exc.response.text[:200]}",
"duration_ms": duration_ms,
}
except Exception as exc:
duration_ms = int((time.monotonic() - t0) * 1000)
capture_service_error(exc, logger=logger, integration="datadog", method="_get_events")
return {"success": False, "error": str(exc), "duration_ms": duration_ms}
async def fetch_all(
self,
logs_query: str,
time_range_minutes: int,
logs_limit: int,
monitor_query: str | None,
events_query: str | None,
) -> dict[str, Any]:
"""Fetch logs, monitors, and events in parallel. Returns combined results with per-source timing."""
async with httpx.AsyncClient(
base_url=self.config.base_url,
headers=self.config.headers,
timeout=_DEFAULT_TIMEOUT,
) as client:
logs_result, monitors_result, events_result = await asyncio.gather(
self._search_logs(client, logs_query, time_range_minutes, logs_limit),
self._list_monitors(client, monitor_query),
self._get_events(client, events_query, time_range_minutes),
return_exceptions=False,
)
return {
"logs": logs_result,
"monitors": monitors_result,
"events": events_result,
}