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
300 lines
9.9 KiB
Python
300 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
|
"""End-to-end agent investigation test for Flink ECS pipeline.
|
|
|
|
Tests if the agent can trace a schema validation failure through:
|
|
1. Flink task logs (ECS CloudWatch)
|
|
2. S3 input data
|
|
3. S3 metadata/audit trail
|
|
4. Trigger Lambda
|
|
5. External Vendor API
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
from datetime import UTC, datetime
|
|
|
|
import boto3
|
|
import requests
|
|
|
|
from platform.observability.trace.hook import traceable
|
|
from surfaces.cli.investigation import run_investigation_cli
|
|
from tests.shared.e2e_rca_checks import (
|
|
audit_key_mentioned,
|
|
investigation_text_blob,
|
|
s3_key_mentioned,
|
|
)
|
|
from tests.shared.stack_config import get_flink_config
|
|
from tests.utils.alert_factory import create_alert
|
|
|
|
# Configuration loaded dynamically from CloudFormation
|
|
CONFIG = get_flink_config()
|
|
|
|
|
|
def trigger_pipeline_failure() -> dict:
|
|
"""Trigger the Flink pipeline with error injection."""
|
|
print("=" * 60)
|
|
print("Triggering Flink Pipeline Failure")
|
|
print("=" * 60)
|
|
|
|
if not CONFIG["trigger_api_url"]:
|
|
print("ERROR: TRIGGER_API_URL not configured")
|
|
return None
|
|
|
|
# Trigger with error injection
|
|
url = f"{CONFIG['trigger_api_url']}trigger?inject_error=true"
|
|
print(f"\nPOST {url}")
|
|
|
|
response = requests.post(url, timeout=60)
|
|
if not response.ok:
|
|
print(f"ERROR: Trigger failed with status {response.status_code}")
|
|
return None
|
|
|
|
result = response.json()
|
|
print(f"Trigger response: {json.dumps(result, indent=2)}")
|
|
|
|
correlation_id = result.get("correlation_id")
|
|
s3_key = result.get("s3_key")
|
|
audit_key = result.get("audit_key")
|
|
task_arn = result.get("task_arn")
|
|
|
|
print(f"\nCorrelation ID: {correlation_id}")
|
|
print(f"S3 Key: {s3_key}")
|
|
print(f"Task ARN: {task_arn}")
|
|
|
|
# Wait for ECS task to complete (and fail)
|
|
print("\nWaiting for ECS task to complete...")
|
|
time.sleep(30) # Give task time to start and fail
|
|
|
|
return {
|
|
"correlation_id": correlation_id,
|
|
"s3_key": s3_key,
|
|
"audit_key": audit_key,
|
|
"task_arn": task_arn,
|
|
"bucket": CONFIG["landing_bucket"],
|
|
}
|
|
|
|
|
|
def get_failure_details(failure_data: dict) -> dict:
|
|
"""Get error details from CloudWatch logs."""
|
|
print("\n" + "=" * 60)
|
|
print("Retrieving Failure Details from CloudWatch")
|
|
print("=" * 60)
|
|
|
|
logs_client = boto3.client("logs", region_name="us-east-1")
|
|
correlation_id = failure_data["correlation_id"]
|
|
|
|
try:
|
|
response = logs_client.filter_log_events(
|
|
logGroupName=CONFIG["log_group"],
|
|
startTime=int((time.time() - 3600) * 1000), # Last hour
|
|
filterPattern=correlation_id,
|
|
)
|
|
|
|
error_message = "Schema validation failed"
|
|
for event in response.get("events", []):
|
|
message = event["message"]
|
|
if "[FLINK][ERROR]" in message and "Schema validation failed" in message:
|
|
error_message = message.split("[FLINK][ERROR]")[-1].strip()
|
|
break
|
|
|
|
print(f"Found error in logs: {error_message}")
|
|
failure_data["error_message"] = error_message
|
|
failure_data["log_group"] = CONFIG["log_group"]
|
|
|
|
except Exception as e:
|
|
print(f"Warning: Could not fetch CloudWatch logs: {e}")
|
|
failure_data["error_message"] = "Schema validation failed: Missing fields ['customer_id']"
|
|
failure_data["log_group"] = CONFIG["log_group"]
|
|
|
|
return failure_data
|
|
|
|
|
|
def test_agent_investigation(failure_data: dict):
|
|
"""Test agent can investigate the Flink pipeline failure."""
|
|
print("\n" + "=" * 60)
|
|
print("Running Agent Investigation")
|
|
print("=" * 60)
|
|
|
|
# Create alert with Flink task information
|
|
alert = create_alert(
|
|
pipeline_name="tracer_flink_ml_feature_pipeline",
|
|
run_name=failure_data.get("task_arn", "flink-task"),
|
|
status="failed",
|
|
timestamp=datetime.now(UTC).isoformat(),
|
|
severity="critical",
|
|
alert_name=f"Flink ML Task Failed: {failure_data['correlation_id']}",
|
|
annotations={
|
|
"cloudwatch_log_group": failure_data["log_group"],
|
|
"ecs_cluster": CONFIG["ecs_cluster"],
|
|
"task_arn": failure_data.get("task_arn", ""),
|
|
"landing_bucket": failure_data["bucket"],
|
|
"s3_key": failure_data["s3_key"],
|
|
"audit_key": failure_data.get("audit_key", ""),
|
|
"processed_bucket": CONFIG["processed_bucket"],
|
|
"correlation_id": failure_data["correlation_id"],
|
|
"error_message": failure_data["error_message"],
|
|
"trigger_function": CONFIG["trigger_lambda"],
|
|
"mock_api_function": CONFIG["mock_api_lambda"],
|
|
"mock_api_url": CONFIG["mock_api_url"],
|
|
"context_sources": "s3,cloudwatch,ecs,lambda",
|
|
},
|
|
)
|
|
|
|
print("\nAlert created:")
|
|
print(f" Pipeline: {alert.get('labels', {}).get('alertname', 'unknown')}")
|
|
print(f" Correlation ID: {failure_data['correlation_id']}")
|
|
print(f" Log Group: {failure_data['log_group']}")
|
|
print(f" S3 Data: s3://{failure_data['bucket']}/{failure_data['s3_key']}")
|
|
if failure_data.get("audit_key"):
|
|
print(f" S3 Audit: s3://{failure_data['bucket']}/{failure_data['audit_key']}")
|
|
|
|
print("\nStarting investigation agent...")
|
|
print("-" * 60)
|
|
|
|
# Run investigation with traceable metadata
|
|
@traceable(
|
|
run_type="chain",
|
|
name=f"test_flink_ml - {alert['alert_id'][:8]}",
|
|
metadata={
|
|
"alert_id": alert["alert_id"],
|
|
"pipeline_name": "tracer_flink_ml_feature_pipeline",
|
|
"correlation_id": failure_data["correlation_id"],
|
|
"s3_key": failure_data["s3_key"],
|
|
"ecs_cluster": CONFIG["ecs_cluster"],
|
|
"log_group": failure_data["log_group"],
|
|
"task_arn": failure_data.get("task_arn"),
|
|
},
|
|
)
|
|
def run_investigation():
|
|
return run_investigation_cli(raw_alert=alert)
|
|
|
|
result = run_investigation()
|
|
|
|
print("-" * 60)
|
|
print("\nInvestigation Results:")
|
|
print(f" Status: {result.get('status', 'unknown')}")
|
|
|
|
# Analyze investigation output
|
|
investigation = result.get("investigation", {})
|
|
root_cause = result.get("root_cause_analysis", {})
|
|
|
|
print("\nInvestigation Summary:")
|
|
if investigation:
|
|
print(f" Context gathered: {len(investigation)} items")
|
|
for key, value in investigation.items():
|
|
if isinstance(value, dict):
|
|
print(f" - {key}: {len(value)} entries")
|
|
elif isinstance(value, list):
|
|
print(f" - {key}: {len(value)} items")
|
|
|
|
print("\nRoot Cause Analysis:")
|
|
if root_cause:
|
|
print(json.dumps(root_cause, indent=2))
|
|
|
|
# Check if agent identified the key components
|
|
success_checks = {
|
|
"Flink logs retrieved": False,
|
|
"S3 input data inspected": False,
|
|
"Audit trail traced": False,
|
|
"External API identified": False,
|
|
"Schema change detected": False,
|
|
}
|
|
|
|
investigation_text = investigation_text_blob(result)
|
|
|
|
if (
|
|
"cloudwatch" in investigation_text
|
|
or "flink" in investigation_text
|
|
or "/ecs/" in investigation_text
|
|
):
|
|
success_checks["Flink logs retrieved"] = True
|
|
|
|
if s3_key_mentioned(investigation_text, failure_data["s3_key"]):
|
|
success_checks["S3 input data inspected"] = True
|
|
|
|
audit_key = (failure_data.get("audit_key") or "").strip()
|
|
if audit_key_mentioned(investigation_text, audit_key):
|
|
success_checks["Audit trail traced"] = True
|
|
|
|
if (
|
|
(
|
|
"external" in investigation_text
|
|
and ("api" in investigation_text or "vendor" in investigation_text)
|
|
)
|
|
or "mock_api" in investigation_text
|
|
or "execute-api" in investigation_text
|
|
):
|
|
success_checks["External API identified"] = True
|
|
|
|
if (
|
|
"event_id" in investigation_text
|
|
or "customer_id" in investigation_text
|
|
or "schema" in investigation_text
|
|
or "missing fields" in investigation_text
|
|
or "validation failed" in investigation_text
|
|
):
|
|
success_checks["Schema change detected"] = True
|
|
|
|
print("\nSuccess Checks:")
|
|
passed_count = 0
|
|
for check, passed in success_checks.items():
|
|
status = "PASS" if passed else "FAIL"
|
|
print(f" [{status}] {check}")
|
|
if passed:
|
|
passed_count += 1
|
|
|
|
# Require at least 4/5 checks to pass (schema change detection is critical)
|
|
min_required = 4
|
|
if passed_count < min_required:
|
|
print(
|
|
f"\nFailed: Agent passed {passed_count}/{len(success_checks)} checks, need {min_required}"
|
|
)
|
|
return False
|
|
if not success_checks["Schema change detected"]:
|
|
print("\nFailed: Agent must detect schema change as root cause")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Run the end-to-end test."""
|
|
print("\n" + "=" * 60)
|
|
print("FLINK ECS E2E INVESTIGATION TEST")
|
|
print("=" * 60)
|
|
|
|
# Trigger failure
|
|
failure_data = trigger_pipeline_failure()
|
|
if not failure_data:
|
|
print("\nERROR: Could not trigger pipeline failure")
|
|
return False
|
|
|
|
# Get failure details from logs
|
|
failure_data = get_failure_details(failure_data)
|
|
|
|
# Run agent investigation
|
|
success = test_agent_investigation(failure_data)
|
|
|
|
print("\n" + "=" * 60)
|
|
if success:
|
|
print("TEST PASSED: Agent successfully traced the failure")
|
|
print(" and detected the schema change as root cause")
|
|
else:
|
|
print("TEST FAILED: Agent could not complete full trace")
|
|
print("=" * 60 + "\n")
|
|
|
|
return success
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Load configuration from environment or CDK outputs
|
|
import os
|
|
|
|
CONFIG["trigger_api_url"] = os.environ.get("TRIGGER_API_URL", CONFIG["trigger_api_url"])
|
|
CONFIG["landing_bucket"] = os.environ.get("LANDING_BUCKET", CONFIG["landing_bucket"])
|
|
CONFIG["processed_bucket"] = os.environ.get("PROCESSED_BUCKET", CONFIG["processed_bucket"])
|
|
|
|
success = main()
|
|
sys.exit(0 if success else 1)
|