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

747 lines
25 KiB
Python

# ======== from tools/datadog_context_tool/ ========
"""Datadog investigation tool — fetches logs, monitors, and events concurrently."""
from __future__ import annotations
import asyncio
import concurrent.futures
import re
from typing import Any
from core.tool_framework.tool_decorator import tool
from integrations.datadog._client import make_async_client
from platform.common.evidence_compaction import compact_logs, summarize_counts
def _run_in_thread(coro: Any) -> Any:
"""Run a coroutine safely regardless of whether an event loop is already running."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(asyncio.run, coro)
return future.result()
return asyncio.run(coro)
def _extract_pod_from_logs(logs: list[dict]) -> tuple[str | None, str | None, str | None]:
for log in logs:
if not isinstance(log, dict):
continue
pod_name = container_name = kube_namespace = 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
if pod_name:
return pod_name, container_name, kube_namespace
return None, None, None
def _parse_oom_details(message: str) -> dict[str, Any]:
details: dict[str, Any] = {}
msg_lower = message.lower()
if "oom" not in msg_lower and "memory limit" not in msg_lower:
return details
m = re.search(r"[Rr]equested[=:\s]+([0-9]+\s*[GMKBgmkb]i?)", message)
if m:
details["memory_requested"] = m.group(1).strip()
m = re.search(r"[Ll]imit[=:\s]+([0-9]+\s*[GMKBgmkb]i?)", message)
if m:
details["memory_limit"] = m.group(1).strip()
m = re.search(r"attempt[=:\s]+(\d+)", message)
if m:
details["attempt"] = m.group(1)
return details
def _collect_failed_pods(logs: list[dict]) -> list[dict]:
seen: set[str] = set()
pods: list[dict] = []
for log in logs:
if not isinstance(log, dict):
continue
pod_name = container_name = kube_namespace = exit_code = kube_job = cluster = None
node_name = 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 == "kube_job":
kube_job = v
elif k == "cluster":
cluster = v
elif k == "node_name":
node_name = v
elif k == "node_ip":
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")
if exit_code is None and log.get("exit_code") is not None:
exit_code = str(log["exit_code"])
kube_job = kube_job or log.get("kube_job")
cluster = cluster or log.get("cluster")
node_name = node_name or log.get("node_name")
node_ip = node_ip or log.get("node_ip")
if pod_name and pod_name not in seen:
seen.add(pod_name)
entry: dict[str, Any] = {
"pod_name": pod_name,
"container": container_name,
"namespace": kube_namespace,
"exit_code": exit_code,
}
if kube_job:
entry["kube_job"] = kube_job
if cluster:
entry["cluster"] = cluster
if node_name:
entry["node_name"] = node_name
if node_ip:
entry["node_ip"] = node_ip
msg = log.get("message", "")
if msg and any(kw in msg.lower() for kw in _ERROR_KEYWORDS):
entry["error"] = msg[:200]
oom = _parse_oom_details(msg)
if oom:
entry.update(oom)
pods.append(entry)
pod_index = {p["pod_name"]: p for p in pods}
for log in logs:
if not isinstance(log, dict):
continue
msg = log.get("message", "")
if not msg:
continue
oom = _parse_oom_details(msg)
if not oom:
continue
lp = log.get("pod_name")
if not lp:
for tag in log.get("tags", []):
if isinstance(tag, str) and tag.startswith("pod_name:"):
lp = tag.partition(":")[2]
break
if lp and lp in pod_index:
pod_index[lp].update({k: v for k, v in oom.items() if k not in pod_index[lp]})
return pods
def _context_is_available(sources: dict[str, dict]) -> bool:
return bool(sources.get("datadog", {}).get("connection_verified"))
def _context_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"query": dd.get("default_query", ""),
"time_range_minutes": dd.get("time_range_minutes", 60),
"limit": 75,
"monitor_query": dd.get("monitor_query"),
"kube_namespace": (dd.get("kubernetes_context") or {}).get("namespace"),
"api_key": dd.get("api_key"),
"app_key": dd.get("app_key"),
"site": dd.get("site", "datadoghq.com"),
}
@tool(
name="query_datadog_all",
display_name="Datadog",
source="datadog",
description="Fetch Datadog logs, monitors, and events in parallel for fast investigation.",
use_cases=[
"Full Datadog context in a single fast operation",
"Kubernetes pod failure investigation (logs + monitors + events together)",
"Getting the complete picture for root cause analysis",
],
requires=[],
input_schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Datadog log search query"},
"time_range_minutes": {"type": "integer", "default": 60},
"limit": {"type": "integer", "default": 75},
"monitor_query": {"type": "string"},
"kube_namespace": {"type": "string"},
"api_key": {"type": "string"},
"app_key": {"type": "string"},
"site": {"type": "string", "default": "datadoghq.com"},
},
"required": ["query"],
},
is_available=_context_is_available,
extract_params=_context_extract_params,
)
def fetch_datadog_context(
query: str,
time_range_minutes: int = 60,
limit: int = 75,
monitor_query: str | None = None,
kube_namespace: str | None = None,
api_key: str | None = None,
app_key: str | None = None,
site: str = "datadoghq.com",
**_kwargs: Any,
) -> dict[str, Any]:
"""Fetch Datadog logs, monitors, and events in parallel for fast investigation."""
client = make_async_client(api_key, app_key, site)
if not client or not client.is_configured:
return {
"source": "datadog_investigate",
"available": False,
"error": "Datadog integration not configured",
"logs": [],
"error_logs": [],
"monitors": [],
"events": [],
}
events_query = query
if kube_namespace and kube_namespace not in (query or ""):
events_query = f"kube_namespace:{kube_namespace}"
raw = _run_in_thread(
client.fetch_all(
logs_query=query,
time_range_minutes=time_range_minutes,
logs_limit=limit,
monitor_query=monitor_query,
events_query=events_query,
)
)
logs_raw = raw.get("logs", {})
monitors_raw = raw.get("monitors", {})
events_raw = raw.get("events", {})
fetch_duration_ms: dict[str, int] = {
"logs": logs_raw.get("duration_ms", 0),
"monitors": monitors_raw.get("duration_ms", 0),
"events": events_raw.get("duration_ms", 0),
}
logs = logs_raw.get("logs", []) if logs_raw.get("success") else []
monitors = monitors_raw.get("monitors", []) if monitors_raw.get("success") else []
events = events_raw.get("events", []) if events_raw.get("success") else []
error_logs = [
log for log in logs if any(kw in log.get("message", "").lower() for kw in _ERROR_KEYWORDS)
]
pod_name, container_name, detected_namespace = _extract_pod_from_logs(error_logs or logs)
failed_pods = _collect_failed_pods(logs)
# Compact logs to stay within prompt limits
compacted_logs = compact_logs(logs, limit=75)
compacted_error_logs = compact_logs(error_logs, limit=30)
errors: dict[str, str] = {}
if not logs_raw.get("success") and logs_raw.get("error"):
errors["logs"] = logs_raw["error"]
if not monitors_raw.get("success") and monitors_raw.get("error"):
errors["monitors"] = monitors_raw["error"]
if not events_raw.get("success") and events_raw.get("error"):
errors["events"] = events_raw["error"]
result_data = {
"source": "datadog_investigate",
"available": True,
"logs": compacted_logs,
"error_logs": compacted_error_logs,
"total": logs_raw.get("total", len(logs)),
"query": query,
"monitors": monitors,
"events": events,
"fetch_duration_ms": fetch_duration_ms,
"pod_name": pod_name,
"container_name": container_name,
"kube_namespace": detected_namespace or kube_namespace,
"failed_pods": failed_pods,
"errors": errors,
}
summary = summarize_counts(logs_raw.get("total", len(logs)), len(compacted_logs), "logs")
if summary:
result_data["truncation_note"] = summary
return result_data
# ======== from tools/datadog_events_tool/ ========
"""Datadog events query tool."""
from core.tool_framework.tool_decorator import tool
from integrations.datadog._client import make_client, unavailable
def _events_is_available(sources: dict[str, dict]) -> bool:
return bool(sources.get("datadog", {}).get("connection_verified"))
def _events_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"query": dd.get("default_query"),
"time_range_minutes": dd.get("time_range_minutes", 60),
**_dd_creds(dd),
}
@tool(
name="query_datadog_events",
display_name="Datadog events",
source="datadog",
description="Query Datadog events for deployments, alerts, and system changes.",
use_cases=[
"Finding recent deployment events that may correlate with failures",
"Reviewing alert trigger/resolve events",
"Checking for infrastructure changes around the time of an incident",
],
requires=[],
input_schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Event search query"},
"time_range_minutes": {"type": "integer", "default": 60},
"api_key": {"type": "string"},
"app_key": {"type": "string"},
"site": {"type": "string", "default": "datadoghq.com"},
},
"required": [],
},
is_available=_events_is_available,
extract_params=_events_extract_params,
)
def query_datadog_events(
query: str | None = None,
time_range_minutes: int = 60,
api_key: str | None = None,
app_key: str | None = None,
site: str = "datadoghq.com",
**_kwargs: Any,
) -> dict[str, Any]:
"""Query Datadog events for deployments, alerts, and system changes."""
client = make_client(api_key, app_key, site)
if not client:
return unavailable("datadog_events", "events", "Datadog integration not configured")
result = client.get_events(query=query, time_range_minutes=time_range_minutes)
if not result.get("success"):
return unavailable("datadog_events", "events", result.get("error", "Unknown error"))
return {
"source": "datadog_events",
"available": True,
"events": result.get("events", []),
"total": result.get("total", 0),
"query": query,
}
# ======== from tools/datadog_logs_tool/ ========
"""Datadog log search tool."""
from typing import cast
from core.tool_framework.tool_decorator import tool
from integrations.datadog.availability import datadog_available_or_backend
_ERROR_KEYWORDS = (
"error",
"fail",
"exception",
"traceback",
"pipeline_error",
"critical",
"killed",
"oomkilled",
"crash",
"panic",
"timeout",
)
def _dd_creds(dd: dict) -> dict:
return {
"api_key": dd.get("api_key"),
"app_key": dd.get("app_key"),
"site": dd.get("site", "datadoghq.com"),
}
def _logs_is_available(sources: dict[str, dict]) -> bool:
return datadog_available_or_backend(sources)
def _logs_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"query": dd.get("default_query", ""),
"time_range_minutes": dd.get("time_range_minutes", 60),
"limit": 50,
"datadog_backend": dd.get("_backend"),
**_dd_creds(dd),
}
@tool(
name="query_datadog_logs",
display_name="Datadog logs",
source="datadog",
tags=("logs", "observability"),
description="Search Datadog logs for pipeline errors, exceptions, and application events.",
use_cases=[
"Investigating pipeline errors reported by Datadog monitors",
"Finding error logs in Kubernetes namespaces",
"Searching for PIPELINE_ERROR patterns and ETL failures",
"Correlating log events with Datadog alerts",
],
requires=[],
input_schema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "Datadog log search query"},
"time_range_minutes": {"type": "integer", "default": 60},
"limit": {"type": "integer", "default": 50},
"api_key": {"type": "string"},
"app_key": {"type": "string"},
"site": {"type": "string", "default": "datadoghq.com"},
},
"required": ["query"],
},
is_available=_logs_is_available,
extract_params=_logs_extract_params,
)
def query_datadog_logs(
query: str,
time_range_minutes: int = 60,
limit: int = 50,
api_key: str | None = None,
app_key: str | None = None,
site: str = "datadoghq.com",
datadog_backend: Any = None,
**_kwargs: Any,
) -> dict[str, Any]:
"""Search Datadog logs for pipeline errors, exceptions, and application events.
When ``datadog_backend`` is provided (e.g. a FixtureDatadogBackend from the
synthetic harness) the call short-circuits and returns the backend's response
directly.
"""
if datadog_backend is not None:
return cast("dict[str, Any]", datadog_backend.query_logs(query=query))
client = make_client(api_key, app_key, site)
if not client:
return unavailable("datadog_logs", "logs", "Datadog integration not configured")
result = client.search_logs(query, time_range_minutes=time_range_minutes, limit=limit)
if not result.get("success"):
return unavailable("datadog_logs", "logs", result.get("error", "Unknown error"))
logs = result.get("logs", [])
error_logs = [
log for log in logs if any(kw in log.get("message", "").lower() for kw in _ERROR_KEYWORDS)
]
# Compact logs to stay within prompt limits
compacted_logs = compact_logs(logs, limit=50)
compacted_error_logs = compact_logs(error_logs, limit=30)
result_data = {
"source": "datadog_logs",
"available": True,
"logs": compacted_logs,
"error_logs": compacted_error_logs,
"total": result.get("total", 0),
"query": query,
}
summary = summarize_counts(result.get("total", 0), len(compacted_logs), "logs")
if summary:
result_data["truncation_note"] = summary
return result_data
# ======== from tools/datadog_metrics_tool/ ========
"""Datadog metrics query tool (stub — implementation pending)."""
from pydantic import BaseModel, Field
from core.tool_framework.tool_decorator import tool
class QueryDatadogMetricsInput(BaseModel):
metric_name: str = Field(
description="Datadog metric name to query, for example `system.cpu.user`."
)
time_range_minutes: int = Field(
default=60,
description="Lookback window in minutes for metric retrieval.",
)
query: str | None = Field(
default=None,
description="Optional full Datadog metrics query string override.",
)
class QueryDatadogMetricsOutput(BaseModel):
source: str = Field(description="Evidence source label.")
available: bool = Field(description="Whether Datadog metrics query is available.")
metric_name: str = Field(description="Metric name requested.")
metrics: list[dict[str, Any]] = Field(default_factory=list, description="Returned metric data.")
error: str | None = Field(default=None, description="Error details when unavailable.")
def _metrics_is_available(_sources: dict[str, dict]) -> bool:
# Hidden from the planner until the Metrics API v2 implementation lands (see #669).
# Flip back to `bool(sources.get("datadog", {}).get("connection_verified"))` once
# the stub body below is replaced with a real request.
return False
def _metrics_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"metric_name": dd.get("metric_name", ""),
"time_range_minutes": dd.get("time_range_minutes", 60),
"api_key": dd.get("api_key"),
"app_key": dd.get("app_key"),
"site": dd.get("site", "datadoghq.com"),
}
@tool(
name="query_datadog_metrics",
source="datadog",
description="Query Datadog metrics for infrastructure and application performance data.",
use_cases=[
"Investigating CPU or memory spikes correlated with an alert",
"Reviewing custom pipeline throughput metrics over time",
"Checking host resource utilisation trends",
],
requires=[],
source_id="datadog_metrics_api",
evidence_type="metrics",
side_effect_level="read_only",
examples=[
"Check `system.cpu.user` around incident window for saturation patterns.",
"Run a custom metrics query string for service-specific error-rate metrics.",
],
anti_examples=["Use this tool for log content or deployment timeline evidence."],
input_model=QueryDatadogMetricsInput,
output_model=QueryDatadogMetricsOutput,
injected_params=("api_key", "app_key", "site"),
is_available=_metrics_is_available,
extract_params=_metrics_extract_params,
)
def query_datadog_metrics(
metric_name: str,
time_range_minutes: int = 60,
query: str | None = None,
**_kwargs: Any,
) -> dict[str, Any]:
"""Query Datadog metrics for infrastructure and application performance data.
NOTE: This tool is a stub. A full implementation will query the Datadog
Metrics API (v2) to retrieve time-series data for pipeline performance,
host resource utilisation, and custom business metrics.
"""
return {
"source": "datadog_metrics",
"available": False,
"error": "DataDogMetricsTool is not yet implemented.",
"metric_name": metric_name,
"time_range_minutes": time_range_minutes,
"query": query,
"metrics": [],
}
# ======== from tools/datadog_monitors_tool/ ========
"""Datadog monitor listing tool."""
from core.tool_framework.tool_decorator import tool
def _monitors_is_available(sources: dict[str, dict]) -> bool:
return datadog_available_or_backend(sources)
def _monitors_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"query": dd.get("monitor_query"),
"datadog_backend": dd.get("_backend"),
**_dd_creds(dd),
}
@tool(
name="query_datadog_monitors",
display_name="Datadog monitors",
source="datadog",
description="List Datadog monitors to understand alerting configuration and current states.",
use_cases=[
"Understanding which monitors triggered an alert",
"Finding the exact query behind a Datadog alert",
"Checking monitor states (OK, Alert, Warn, No Data)",
"Reviewing monitor configuration for pipeline monitoring",
],
requires=[],
input_schema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Optional monitor filter (e.g., 'tag:pipeline:tracer-ai-agent')",
},
"api_key": {"type": "string"},
"app_key": {"type": "string"},
"site": {"type": "string", "default": "datadoghq.com"},
},
"required": [],
},
is_available=_monitors_is_available,
extract_params=_monitors_extract_params,
)
def query_datadog_monitors(
query: str | None = None,
api_key: str | None = None,
app_key: str | None = None,
site: str = "datadoghq.com",
datadog_backend: Any = None,
**_kwargs: Any,
) -> dict[str, Any]:
"""List Datadog monitors to understand alerting configuration and current states.
When ``datadog_backend`` is provided (e.g. a FixtureDatadogBackend from the
synthetic harness) the call short-circuits and returns the backend's response
directly.
"""
if datadog_backend is not None:
return cast("dict[str, Any]", datadog_backend.query_monitors(query=query))
client = make_client(api_key, app_key, site)
if not client:
return unavailable("datadog_monitors", "monitors", "Datadog integration not configured")
result = client.list_monitors(query=query)
if not result.get("success"):
return unavailable("datadog_monitors", "monitors", result.get("error", "Unknown error"))
return {
"source": "datadog_monitors",
"available": True,
"monitors": result.get("monitors", []),
"total": result.get("total", 0),
"query_filter": query,
}
# ======== from tools/datadog_node_pods_tool/ ========
"""Datadog tool: resolve a node IP to the pods running on that node."""
from core.tool_framework.tool_decorator import tool
def _node_pods_is_available(sources: dict[str, dict]) -> bool:
dd = sources.get("datadog", {})
return bool(dd.get("connection_verified") and dd.get("node_ip"))
def _node_pods_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
dd = sources["datadog"]
return {
"node_ip": dd.get("node_ip", ""),
"time_range_minutes": dd.get("time_range_minutes", 60),
**_dd_creds(dd),
}
@tool(
name="get_pods_on_node",
source="datadog",
description="Resolve a node IP address to all pods running on that node via Datadog.",
use_cases=[
"Mapping a node IP from an infrastructure alert to specific pods",
"Discovering what pods were running on a failed node",
"Feeding pod names into log retrieval tools for further investigation",
],
requires=[],
input_schema={
"type": "object",
"properties": {
"node_ip": {
"type": "string",
"description": "The IP address of the node (e.g. '10.0.1.42')",
},
"time_range_minutes": {"type": "integer", "default": 60},
"limit": {"type": "integer", "default": 200},
"api_key": {"type": "string"},
"app_key": {"type": "string"},
"site": {"type": "string", "default": "datadoghq.com"},
},
"required": ["node_ip"],
},
is_available=_node_pods_is_available,
extract_params=_node_pods_extract_params,
)
def get_pods_on_node(
node_ip: str,
time_range_minutes: int = 60,
limit: int = 200,
api_key: str | None = None,
app_key: str | None = None,
site: str = "datadoghq.com",
**_kwargs: Any,
) -> dict[str, Any]:
"""Resolve a node IP address to all pods running on that node via Datadog."""
if not node_ip or not node_ip.strip():
return unavailable("datadog_node_ip_to_pods", "pods", "node_ip is required")
client = make_client(api_key, app_key, site)
if not client:
return unavailable("datadog_node_ip_to_pods", "pods", "Datadog integration not configured")
result = client.get_pods_on_node(
node_ip=node_ip, time_range_minutes=time_range_minutes, limit=limit
)
if not result.get("success"):
return unavailable(
"datadog_node_ip_to_pods", "pods", result.get("error", "Unknown error"), node_ip=node_ip
)
return {
"source": "datadog_node_ip_to_pods",
"available": True,
"node_ip": node_ip,
"pods": result.get("pods", []),
"total": result.get("total", 0),
}