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
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Capture a real Datadog alert + evidence fixture for the K8s RCA feedback test.
|
|
|
|
Queries the Datadog API for logs and monitors matching the K8s test case,
|
|
validates response shapes against the DatadogClient contract, and writes
|
|
the fixture to fixtures/datadog_k8s_alert.json.
|
|
|
|
Prerequisites:
|
|
- K8s error path has been triggered (test_datadog.py or trigger_alert.py)
|
|
- DD_API_KEY and DD_APP_KEY environment variables set
|
|
|
|
Usage (from project root):
|
|
python -m tests.e2e.kubernetes.capture_fixture
|
|
python -m tests.e2e.kubernetes.capture_fixture --time-range 120
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from tests.utils.alert_factory import create_alert
|
|
|
|
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "datadog_k8s_alert.json"
|
|
|
|
LOG_QUERY = "PIPELINE_ERROR kube_namespace:tracer-test"
|
|
MONITOR_TAG = "managed_by:tracer-agent"
|
|
|
|
LOG_ENTRY_SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
"timestamp": str,
|
|
"message": str,
|
|
"status": str,
|
|
"service": str,
|
|
"host": str,
|
|
"tags": list,
|
|
}
|
|
|
|
MONITOR_SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
"id": (int, type(None)),
|
|
"name": str,
|
|
"type": str,
|
|
"query": str,
|
|
"message": str,
|
|
"overall_state": str,
|
|
"tags": list,
|
|
}
|
|
|
|
|
|
def _validate_shape(entry: dict, schema: dict[str, type | tuple[type, ...]], label: str) -> None:
|
|
for key, expected_type in schema.items():
|
|
if key not in entry:
|
|
raise ValueError(f"{label} missing required key: {key}")
|
|
if not isinstance(entry[key], expected_type):
|
|
raise TypeError(
|
|
f"{label}.{key}: expected {expected_type}, got {type(entry[key]).__name__}"
|
|
)
|
|
|
|
|
|
def _extract_k8s_tags(logs: list[dict]) -> dict[str, str]:
|
|
"""Extract K8s metadata from log tags for the alert payload."""
|
|
k8s = {}
|
|
for log in logs:
|
|
for tag in log.get("tags", []):
|
|
if isinstance(tag, str) and ":" in tag:
|
|
key, _, val = tag.partition(":")
|
|
if key.startswith("kube_") and key not in k8s:
|
|
k8s[key] = val
|
|
return k8s
|
|
|
|
|
|
def capture(time_range_minutes: int = 60) -> dict:
|
|
from integrations.datadog.client import DatadogClient, DatadogConfig
|
|
|
|
api_key = os.environ.get("DD_API_KEY", "")
|
|
app_key = os.environ.get("DD_APP_KEY", "")
|
|
site = os.environ.get("DD_SITE", "datadoghq.com")
|
|
|
|
if not api_key or not app_key:
|
|
print("DD_API_KEY and DD_APP_KEY are required")
|
|
sys.exit(1)
|
|
|
|
client = DatadogClient(DatadogConfig(api_key=api_key, app_key=app_key, site=site))
|
|
|
|
print(f"Querying Datadog logs: {LOG_QUERY} (last {time_range_minutes}min)...")
|
|
log_result = client.search_logs(LOG_QUERY, time_range_minutes=time_range_minutes, limit=50)
|
|
if not log_result.get("success"):
|
|
print(f"Log query failed: {log_result.get('error')}")
|
|
sys.exit(1)
|
|
|
|
logs = log_result.get("logs", [])
|
|
if not logs:
|
|
print("No logs found. Has the K8s error path been triggered recently?")
|
|
sys.exit(1)
|
|
|
|
print(f" Found {len(logs)} log entries")
|
|
for i, log in enumerate(logs):
|
|
_validate_shape(log, LOG_ENTRY_SCHEMA, f"log[{i}]")
|
|
print(" All log entries pass schema validation")
|
|
|
|
error_keywords = ("error", "fail", "exception", "traceback", "pipeline_error")
|
|
error_logs = [
|
|
log for log in logs if any(kw in log.get("message", "").lower() for kw in error_keywords)
|
|
]
|
|
print(f" {len(error_logs)} error logs")
|
|
|
|
print(f"\nQuerying Datadog monitors: tag:{MONITOR_TAG}...")
|
|
monitor_result = client.list_monitors(query=f"tag:{MONITOR_TAG}")
|
|
if not monitor_result.get("success"):
|
|
print(f"Monitor query failed: {monitor_result.get('error')}")
|
|
sys.exit(1)
|
|
|
|
monitors = monitor_result.get("monitors", [])
|
|
print(f" Found {len(monitors)} monitors")
|
|
for i, mon in enumerate(monitors):
|
|
_validate_shape(mon, MONITOR_SCHEMA, f"monitor[{i}]")
|
|
print(" All monitors pass schema validation")
|
|
|
|
k8s_tags = _extract_k8s_tags(logs)
|
|
kube_namespace = k8s_tags.get("kube_namespace", "tracer-test")
|
|
kube_job = k8s_tags.get("kube_job", "etl-transform-error")
|
|
|
|
alert = create_alert(
|
|
pipeline_name="kubernetes_etl_pipeline",
|
|
run_name=kube_job,
|
|
status="failed",
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
severity="critical",
|
|
alert_name="KubernetesJobFailed",
|
|
environment="test",
|
|
annotations={
|
|
"summary": f"Kubernetes job {kube_job} failed in namespace {kube_namespace}",
|
|
"kube_namespace": kube_namespace,
|
|
"kube_job": kube_job,
|
|
},
|
|
)
|
|
|
|
fixture = {
|
|
"_meta": {
|
|
"captured_at": datetime.now(UTC).isoformat(),
|
|
"source": "capture_fixture.py",
|
|
"datadog_site": site,
|
|
"schema_version": 1,
|
|
},
|
|
"alert": alert,
|
|
"evidence": {
|
|
"datadog_logs": logs,
|
|
"datadog_error_logs": error_logs,
|
|
"datadog_monitors": monitors,
|
|
},
|
|
}
|
|
|
|
FIXTURE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(FIXTURE_PATH, "w") as f:
|
|
json.dump(fixture, f, indent=2)
|
|
|
|
print(f"\nFixture written to {FIXTURE_PATH}")
|
|
print(f" Logs: {len(logs)}, Error logs: {len(error_logs)}, Monitors: {len(monitors)}")
|
|
return fixture
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Capture Datadog K8s fixture")
|
|
parser.add_argument(
|
|
"--time-range",
|
|
type=int,
|
|
default=60,
|
|
help="How far back to search logs (minutes, default 60)",
|
|
)
|
|
args = parser.parse_args()
|
|
capture(time_range_minutes=args.time_range)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|