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

832 lines
27 KiB
Python

"""
Datadog Demo Orchestrator (local Kubernetes) — bioinformatics variant pipeline.
Runs 5 real Kubernetes Jobs on docker-desktop (no EKS needed):
- Job 0: sample-batch-a → ingest succeeds (2 valid records)
- Job 1: sample-batch-b → ingest succeeds (3 valid records)
- Job 2: sample-batch-c → validate FAILS (S003 missing chromosome + quality_score)
- Job 3: sample-batch-d → validate FAILS (S007 missing ref_allele + alt_allele)
- Job 4: sample-batch-e → validate FAILS (S009 missing gene + chromosome)
Failing jobs use backoffLimit=2 so k8s retries 3 times total → BackoffLimitExceeded
(shows as multiple pod restarts in kubectl describe).
Real container stdout/stderr is captured via kubectl logs and shipped to Datadog
Logs Intake with correct kube-style tags so the log monitor fires → Slack.
Prerequisites:
Docker Desktop running with Kubernetes enabled
DD_API_KEY + DD_APP_KEY in .env
Run with:
make datadog-demo
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
import urllib.request
import uuid
from datetime import UTC, datetime
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
from tests.e2e.kubernetes.infrastructure_sdk.local import (
create_or_update_monitor,
load_monitor_definitions,
)
from tests.utils.conftest import get_test_config
BASE_DIR = Path(__file__).parent
PIPELINE_DIR = BASE_DIR / "pipeline_code"
MONITOR_DEFS = str(BASE_DIR / "k8s_manifests" / "datadog-monitors.yaml")
NAMESPACE = "tracer-dd"
CLUSTER = "tracer-dd-demo"
PIPELINE_NAME = "bioinformatics_variant_pipeline"
IMAGE_TAG = "tracer-dd-pipeline:latest"
KUBE_CONTEXT = "docker-desktop"
# ---------------------------------------------------------------------------
# 5 sample batches: 2 succeed, 3 fail with distinct real validation errors
# ---------------------------------------------------------------------------
# Each entry: (batch_id, stage, records, backoff_limit)
# stage="ingest" → always writes data, always succeeds → backoff_limit=0
# stage="validate" → fails if required field missing → backoff_limit=2 (3 attempts)
_BATCHES = [
# Job 0 — succeeds: 2 fully valid records
(
"sample-batch-a",
"ingest",
[
{
"sample_id": "S001",
"gene": "BRCA1",
"chromosome": "17",
"position": 43044295,
"ref_allele": "A",
"alt_allele": "G",
"quality_score": 99.2,
},
{
"sample_id": "S002",
"gene": "TP53",
"chromosome": "17",
"position": 7674220,
"ref_allele": "C",
"alt_allele": "T",
"quality_score": 87.5,
},
],
0,
),
# Job 1 — succeeds: 3 valid records
(
"sample-batch-b",
"ingest",
[
{
"sample_id": "S005",
"gene": "PTEN",
"chromosome": "10",
"position": 89692905,
"ref_allele": "G",
"alt_allele": "A",
"quality_score": 92.1,
},
{
"sample_id": "S006",
"gene": "RB1",
"chromosome": "13",
"position": 48941756,
"ref_allele": "C",
"alt_allele": "T",
"quality_score": 78.3,
},
{
"sample_id": "S010",
"gene": "APC",
"chromosome": "5",
"position": 112707498,
"ref_allele": "T",
"alt_allele": "C",
"quality_score": 95.0,
},
],
0,
),
# Job 2 — FAILS: S003 missing chromosome + quality_score → 3 attempts (backoff=2)
(
"sample-batch-c",
"validate",
[
{
"sample_id": "S001",
"gene": "BRCA1",
"chromosome": "17",
"position": 43044295,
"ref_allele": "A",
"alt_allele": "G",
"quality_score": 99.2,
},
{
"sample_id": "S003",
"gene": "EGFR",
"position": 55174772,
"ref_allele": "G",
"alt_allele": "A",
}, # missing: chromosome, quality_score
],
2,
),
# Job 3 — FAILS: S007 missing ref_allele + alt_allele → 3 attempts (backoff=2)
(
"sample-batch-d",
"validate",
[
{
"sample_id": "S005",
"gene": "PTEN",
"chromosome": "10",
"position": 89692905,
"ref_allele": "G",
"alt_allele": "A",
"quality_score": 92.1,
},
{
"sample_id": "S007",
"gene": "VHL",
"chromosome": "3",
"position": 10183671,
"quality_score": 61.4,
}, # missing: ref_allele, alt_allele
],
2,
),
# Job 4 — FAILS: S009 missing gene + chromosome → 3 attempts (backoff=2)
(
"sample-batch-e",
"validate",
[
{
"sample_id": "S006",
"gene": "RB1",
"chromosome": "13",
"position": 48941756,
"ref_allele": "C",
"alt_allele": "T",
"quality_score": 78.3,
},
{
"sample_id": "S009",
"position": 7577120,
"ref_allele": "T",
"alt_allele": "C",
"quality_score": 55.0,
}, # missing: gene, chromosome
],
2,
),
]
# ---------------------------------------------------------------------------
# Datadog API helper
# ---------------------------------------------------------------------------
def _dd(method: str, path: str, body: object = None, *, intake: bool = False) -> dict:
api_key = os.environ["DD_API_KEY"]
site = os.environ.get("DD_SITE", "datadoghq.com")
app_key = os.environ.get("DD_APP_KEY", "")
host = f"https://http-intake.logs.{site}" if intake else f"https://api.{site}"
headers: dict[str, str] = {"DD-API-KEY": api_key, "Content-Type": "application/json"}
if not intake:
headers["DD-APPLICATION-KEY"] = app_key
req = urllib.request.Request(
host + path,
data=json.dumps(body).encode() if body is not None else None,
headers=headers,
method=method,
)
with urllib.request.urlopen(req, timeout=20) as resp:
raw = resp.read()
return json.loads(raw) if raw.strip() else {}
# ---------------------------------------------------------------------------
# kubectl helpers
# ---------------------------------------------------------------------------
def _kubectl(*args: str, check: bool = True, capture: bool = True) -> subprocess.CompletedProcess:
cmd = ["kubectl", "--context", KUBE_CONTEXT, *args]
return subprocess.run(cmd, check=check, capture_output=capture, text=True)
def _ensure_namespace() -> None:
result = _kubectl("get", "namespace", NAMESPACE, check=False)
if result.returncode != 0:
_kubectl("create", "namespace", NAMESPACE)
print(f" Created namespace {NAMESPACE}")
else:
print(f" Namespace {NAMESPACE} exists")
def _delete_old_jobs(run_id: str) -> None:
"""Delete any leftover jobs from previous runs to avoid name conflicts."""
for batch_id, _, _, _ in _BATCHES:
_kubectl("delete", "job", batch_id, "-n", NAMESPACE, "--ignore-not-found", check=False)
def _create_job(
batch_id: str, stage: str, records: list[dict], backoff_limit: int, run_id: str
) -> None:
"""Create a k8s Job for one pipeline batch. Records are passed via env var JSON."""
restart_policy = "Never" # Always Never so failed pods stay alive for log collection
manifest = {
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": batch_id,
"namespace": NAMESPACE,
"labels": {
"pipeline": PIPELINE_NAME,
"run_id": run_id,
"stage": stage,
"cluster": CLUSTER,
},
},
"spec": {
"backoffLimit": backoff_limit,
"template": {
"metadata": {
"labels": {
"pipeline": PIPELINE_NAME,
"run_id": run_id,
"stage": stage,
"batch_id": batch_id,
},
},
"spec": {
"restartPolicy": restart_policy,
"containers": [
{
"name": stage,
"image": IMAGE_TAG,
"imagePullPolicy": "IfNotPresent",
"env": [
{"name": "PIPELINE_STAGE", "value": stage},
{"name": "PIPELINE_NAME", "value": PIPELINE_NAME},
{"name": "PIPELINE_RUN_ID", "value": batch_id},
{"name": "RECORDS_JSON", "value": json.dumps(records)},
],
}
],
},
},
},
}
proc = subprocess.run(
["kubectl", "--context", KUBE_CONTEXT, "apply", "-f", "-", "-n", NAMESPACE],
input=json.dumps(manifest),
capture_output=True,
text=True,
check=True,
)
_ = proc # applied
def _wait_for_jobs(timeout_seconds: int = 180) -> dict[str, dict]:
"""Wait until all jobs are complete (succeeded or failed). Returns status per job."""
batch_ids = [b[0] for b in _BATCHES]
deadline = time.time() + timeout_seconds
statuses: dict[str, dict] = {}
print(f" Waiting up to {timeout_seconds}s for {len(batch_ids)} jobs...")
while time.time() < deadline:
result = _kubectl(
"get",
"jobs",
"-n",
NAMESPACE,
"-o",
"json",
"--selector",
f"pipeline={PIPELINE_NAME}",
check=False,
)
if result.returncode != 0:
time.sleep(3)
continue
items = json.loads(result.stdout).get("items", [])
done = 0
statuses = {}
for item in items:
name = item["metadata"]["name"]
conds = item.get("status", {}).get("conditions", [])
succeeded = item["status"].get("succeeded", 0)
failed = item["status"].get("failed", 0)
active = item["status"].get("active", 0)
complete = any(c["type"] == "Complete" and c["status"] == "True" for c in conds)
job_failed = any(c["type"] == "Failed" and c["status"] == "True" for c in conds)
statuses[name] = {
"succeeded": complete,
"failed": job_failed,
"active": active,
"success_count": succeeded,
"fail_count": failed,
}
if complete or job_failed:
done += 1
if done >= len(batch_ids):
break
pending = [n for n, s in statuses.items() if not s["succeeded"] and not s["failed"]]
print(f" Still running: {pending} ...")
time.sleep(5)
return statuses
def _get_pod_logs(batch_id: str, stage: str) -> tuple[str, str, list[str]]:
"""Get stdout+stderr from all pods of a job. Returns (stdout, stderr, pod_names)."""
pods_result = _kubectl(
"get",
"pods",
"-n",
NAMESPACE,
"-l",
f"batch_id={batch_id}",
"-o",
"jsonpath={.items[*].metadata.name}",
check=False,
)
pod_names = pods_result.stdout.strip().split() if pods_result.stdout.strip() else []
all_stdout: list[str] = []
all_stderr: list[str] = []
for pod_name in pod_names:
logs_result = _kubectl(
"logs",
pod_name,
"-n",
NAMESPACE,
"--all-containers",
"--previous=false",
check=False,
)
if logs_result.stdout:
all_stdout.extend(logs_result.stdout.splitlines())
if logs_result.stderr:
all_stderr.extend(logs_result.stderr.splitlines())
prev_result = _kubectl(
"logs",
pod_name,
"-n",
NAMESPACE,
"--previous",
check=False,
)
if prev_result.stdout:
all_stdout.extend(f"[prev] {line}" for line in prev_result.stdout.splitlines())
return "\n".join(all_stdout), "\n".join(all_stderr), pod_names
# ---------------------------------------------------------------------------
# Ship real container logs to Datadog
# ---------------------------------------------------------------------------
def _ship_job_logs(
batch_id: str,
stage: str,
stdout: str,
stderr: str,
pod_names: list[str],
job_status: dict,
run_id: str,
) -> None:
"""Send real kubectl logs from k8s pods to Datadog Logs Intake."""
succeeded = job_status.get("succeeded", False)
fail_count = job_status.get("fail_count", 0)
entries: list[dict] = []
def _base(pod_name: str) -> dict:
return {
"ddsource": "kubernetes",
"ddtags": (
f"kube_namespace:{NAMESPACE},"
f"pod_name:{pod_name},"
f"container_name:{stage},"
f"kube_job:{batch_id},"
f"cluster:{CLUSTER},"
f"pipeline:{PIPELINE_NAME},"
f"run_id:{run_id},"
f"stage:{stage}"
),
"hostname": f"{CLUSTER}-control-plane",
"service": "variant-pipeline",
# Top-level JSON attributes — required for {{@field}} template vars in monitor messages
"pod_name": pod_name,
"container_name": stage,
"kube_job": batch_id,
"kube_namespace": NAMESPACE,
"cluster": CLUSTER,
"pipeline": PIPELINE_NAME,
"run_id": run_id,
}
primary_pod = pod_names[0] if pod_names else f"{batch_id}-pod"
for line in stdout.splitlines():
entries.append({**_base(primary_pod), "message": line, "status": "info"})
for line in stderr.splitlines():
entries.append({**_base(primary_pod), "message": line, "status": "error"})
for pod_name in pod_names or [primary_pod]:
status_str = "succeeded" if succeeded else f"failed (attempts={fail_count})"
entries.append(
{
**_base(pod_name),
"message": (
f"[pod-lifecycle] pod={pod_name} job={batch_id} stage={stage} "
f"status={status_str} run_id={run_id}"
),
"status": "info" if succeeded else "error",
}
)
if entries:
_dd("POST", "/api/v2/logs", entries, intake=True)
# ---------------------------------------------------------------------------
# Build Docker image + load into k8s
# ---------------------------------------------------------------------------
def _build_image() -> None:
subprocess.run(
["docker", "build", "-t", IMAGE_TAG, str(PIPELINE_DIR)],
check=True,
capture_output=True,
)
def _load_image_into_k8s() -> None:
"""Import the Docker image into Docker Desktop k8s containerd via docker save | ctr import."""
nodes_result = _kubectl(
"get",
"nodes",
"-o",
"jsonpath={.items[*].metadata.name}",
check=False,
)
nodes = nodes_result.stdout.strip().split() if nodes_result.stdout.strip() else []
if not nodes:
print(" No nodes found — skipping image load")
return
save_proc = subprocess.run(
["docker", "save", IMAGE_TAG],
capture_output=True,
check=True,
)
image_tar = save_proc.stdout
for node in nodes:
# Use kubectl debug (ephemeral container) to stream the tar into ctr import
result = subprocess.run(
[
"docker",
"exec",
node, # works when cluster is kind; for Docker Desktop use host containerd
"ctr",
"--namespace",
"k8s.io",
"images",
"import",
"-",
],
input=image_tar,
capture_output=True,
check=False,
)
if result.returncode == 0:
print(f" Loaded image into node {node} via ctr")
else:
# Docker Desktop k8s: nodes are VM-internal — fall back to kind load if available
kind_result = subprocess.run(
["kind", "load", "docker-image", IMAGE_TAG],
capture_output=True,
check=False,
)
if kind_result.returncode == 0:
print(" Loaded image via kind load")
else:
print(
f" Warning: could not load image into node {node} — imagePullPolicy=IfNotPresent will use cached copy"
)
break
# ---------------------------------------------------------------------------
# Patch pipeline code to accept RECORDS_JSON env var
# ---------------------------------------------------------------------------
def _patch_pipeline_for_k8s() -> None:
"""Ensure stages read RECORDS_JSON env var so the orchestrator controls records."""
ingest_path = PIPELINE_DIR / "stages" / "ingest.py"
current = ingest_path.read_text()
if "RECORDS_JSON" in current:
return
patched = '''"""Ingest stage: read variant records from RECORDS_JSON env var (k8s) or defaults."""
import json
import os
import sys
from config import PIPELINE_NAME, PIPELINE_RUN_ID
_STAGING_PATH = "/tmp/staging"
_DEFAULT_RECORDS = [
{"sample_id": "S001", "gene": "BRCA1", "chromosome": "17", "position": 43044295,
"ref_allele": "A", "alt_allele": "G", "quality_score": 99.2},
{"sample_id": "S002", "gene": "TP53", "chromosome": "17", "position": 7674220,
"ref_allele": "C", "alt_allele": "T", "quality_score": 87.5},
]
def main() -> None:
os.makedirs(_STAGING_PATH, exist_ok=True)
output = os.path.join(_STAGING_PATH, f"{PIPELINE_RUN_ID}_raw.json")
records_env = os.environ.get("RECORDS_JSON")
if records_env:
try:
records = json.loads(records_env)
except json.JSONDecodeError as e:
print(f"PIPELINE_ERROR: Invalid RECORDS_JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
records = _DEFAULT_RECORDS
with open(output, "w") as f:
json.dump({"pipeline": PIPELINE_NAME, "run_id": PIPELINE_RUN_ID, "records": records}, f)
print(json.dumps({
"stage": "ingest",
"status": "success",
"pipeline": PIPELINE_NAME,
"run_id": PIPELINE_RUN_ID,
"record_count": len(records),
"output": output,
}))
'''
ingest_path.write_text(patched)
def _patch_validate_for_k8s() -> None:
"""Ensure validate stage seeds staging dir from RECORDS_JSON when no raw file exists."""
validate_path = PIPELINE_DIR / "stages" / "validate.py"
current = validate_path.read_text()
if "RECORDS_JSON" in current:
return
patched = '''"""Validate stage: enforce schema on ingested variant records. Fails on bad data."""
import json
import os
import sys
from config import PIPELINE_NAME, PIPELINE_RUN_ID, REQUIRED_FIELDS
from errors import ValidationError
_STAGING_PATH = "/tmp/staging"
def _load_records() -> list[dict]:
path = f"{_STAGING_PATH}/{PIPELINE_RUN_ID}_raw.json"
if not os.path.exists(path):
# In k8s, records are passed via RECORDS_JSON — seed staging dir
records_env = os.environ.get("RECORDS_JSON")
if not records_env:
raise FileNotFoundError(f"No staging file at {path} and no RECORDS_JSON env var")
os.makedirs(_STAGING_PATH, exist_ok=True)
records = json.loads(records_env)
with open(path, "w") as f:
json.dump({"pipeline": PIPELINE_NAME, "run_id": PIPELINE_RUN_ID, "records": records}, f)
return records
with open(path) as f:
return json.load(f)["records"]
def _validate(records: list[dict]) -> None:
for i, record in enumerate(records):
missing = [f for f in REQUIRED_FIELDS if f not in record]
if missing:
raise ValidationError(
f"PIPELINE_ERROR: Schema validation failed for record {i} "
f"(sample_id={record.get(\'sample_id\', \'?\')}): missing fields {missing}"
)
def main() -> None:
records = _load_records()
print(f"[validate] Checking {len(records)} records against schema {REQUIRED_FIELDS}")
try:
_validate(records)
except ValidationError as e:
print(str(e), file=sys.stderr)
print(json.dumps({
"stage": "validate",
"status": "failed",
"pipeline": PIPELINE_NAME,
"run_id": PIPELINE_RUN_ID,
"error": str(e),
}))
sys.exit(1)
print(json.dumps({
"stage": "validate",
"status": "success",
"pipeline": PIPELINE_NAME,
"run_id": PIPELINE_RUN_ID,
"record_count": len(records),
}))
'''
validate_path.write_text(patched)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
get_test_config()
if not os.environ.get("DD_API_KEY") or not os.environ.get("DD_APP_KEY"):
print("DD_API_KEY and DD_APP_KEY must be set in .env")
return 1
run_id = f"dd-{uuid.uuid4().hex[:8]}"
dd_site = os.environ.get("DD_SITE", "datadoghq.com")
print(f"Run ID: {run_id} [{datetime.now(UTC).strftime('%H:%M:%S')} UTC]")
# 1. Upsert Datadog monitors
print("\n[1/5] Upserting Datadog monitors...")
defs = load_monitor_definitions(MONITOR_DEFS)
monitor_ids: dict[str, int] = {}
for d in defs:
result = create_or_update_monitor(d)
mid = result.get("id") or result.get("monitor", {}).get("id")
monitor_ids[d["name"]] = mid
print(f" [{mid}] {d['name']}")
# 2. Patch pipeline code + build Docker image + load into k8s
print(f"\n[2/5] Patching pipeline code and building image {IMAGE_TAG}...")
_patch_pipeline_for_k8s()
_patch_validate_for_k8s()
_build_image()
_load_image_into_k8s()
print(" Image ready")
# 3. Create k8s namespace + Jobs
print(f"\n[3/5] Creating {len(_BATCHES)} Kubernetes Jobs in namespace {NAMESPACE}...")
_ensure_namespace()
_delete_old_jobs(run_id)
for batch_id, stage, records, backoff_limit in _BATCHES:
_create_job(batch_id, stage, records, backoff_limit, run_id)
retry_note = (
f"backoff={backoff_limit} (up to {backoff_limit + 1} attempts)"
if backoff_limit > 0
else "no retry"
)
print(f" Created job/{batch_id} stage={stage} {retry_note}")
# 4. Wait for all jobs to complete or fail
print("\n[4/5] Waiting for jobs to complete...")
job_statuses = _wait_for_jobs(timeout_seconds=300)
succeeded_jobs = [n for n, s in job_statuses.items() if s["succeeded"]]
failed_jobs = [n for n, s in job_statuses.items() if s["failed"]]
for name, status in job_statuses.items():
icon = "✓" if status["succeeded"] else "✗"
if status["succeeded"]:
batch = next((b for b in _BATCHES if b[0] == name), None)
stage_name = batch[1] if batch else "?"
outcome = f"stage={stage_name} exit=0"
else:
batch = next((b for b in _BATCHES if b[0] == name), None)
stage_name = batch[1] if batch else "?"
outcome = f"stage={stage_name} exit=1 ({status['fail_count']} attempts)"
print(f" {icon} {name} {outcome}")
print(f"\n {len(succeeded_jobs)} succeeded, {len(failed_jobs)} failed")
# 5. Collect real pod logs + ship to Datadog
print("\n[5/5] Collecting pod logs and shipping to Datadog...")
for batch_id, stage, _, _ in _BATCHES:
status = job_statuses.get(batch_id, {})
stdout, stderr, pod_names = _get_pod_logs(batch_id, stage)
_ship_job_logs(batch_id, stage, stdout, stderr, pod_names, status, run_id)
line_count = len(stdout.splitlines()) + len(stderr.splitlines())
print(f" Shipped {batch_id} ({len(pod_names)} pods, {line_count} log lines)")
# Post a summary event for all failures
failed_details = "\n".join(
f" {b} (stage={s}): BackoffLimitExceeded after {job_statuses.get(b, {}).get('fail_count', 0)} attempts"
for b, s, _, backoff in _BATCHES
if backoff > 0
)
_dd(
"POST",
"/api/v1/events",
{
"title": f"[tracer-dd] {len(failed_jobs)}/5 jobs failed: {PIPELINE_NAME} ({run_id})",
"text": (
f"Run ID: {run_id}\n"
f"Cluster: {CLUSTER} Namespace: {NAMESPACE}\n"
f"Context: {KUBE_CONTEXT}\n"
f"Succeeded: {', '.join(succeeded_jobs) or 'none'}\n"
f"Failed ({len(failed_jobs)}) — BackoffLimitExceeded:\n{failed_details}\n\n"
f"Log query: PIPELINE_ERROR kube_namespace:{NAMESPACE}"
),
"alert_type": "error",
"priority": "normal",
"tags": [
f"cluster:{CLUSTER}",
f"kube_namespace:{NAMESPACE}",
f"pipeline:{PIPELINE_NAME}",
f"run_id:{run_id}",
f"failed_jobs:{len(failed_jobs)}",
"source:tracer-agent",
"env:local",
"team:devops",
],
},
)
print("\n" + "=" * 60)
print(f"DONE — {len(failed_jobs)}/5 jobs failed, logs in Datadog")
print("=" * 60)
q_all = f"kube_namespace:{NAMESPACE} run_id:{run_id}"
print(
f"\nAll pods: https://app.{dd_site}/logs?query={q_all.replace(' ', '+').replace(':', '%3A')}"
)
print("\nFailed jobs:")
for name in failed_jobs:
q = f"kube_namespace:{NAMESPACE} kube_job:{name}"
url = f"https://app.{dd_site}/logs?query={q.replace(' ', '+').replace(':', '%3A')}"
print(f" {name}{url}")
print("\nkubectl status:")
print(f" kubectl --context {KUBE_CONTEXT} get jobs -n {NAMESPACE}")
print(f" kubectl --context {KUBE_CONTEXT} get pods -n {NAMESPACE}")
print(f" kubectl --context {KUBE_CONTEXT} describe job <name> -n {NAMESPACE}")
print("\nMonitors:")
for _name, mid in monitor_ids.items():
print(f" [{mid}] https://app.{dd_site}/monitors/{mid}")
print("\nSlack #devs-alerts notified by Datadog within ~5 min")
print("=" * 60)
return 0
if __name__ == "__main__":
sys.exit(main())