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
308 lines
10 KiB
Python
Vendored
308 lines
10 KiB
Python
Vendored
"""API Ingester Lambda - Fetches data from external API and writes to S3.
|
|
|
|
This Lambda:
|
|
1. Calls Mock External API to fetch data
|
|
2. Writes raw data to S3 landing bucket
|
|
3. S3 event automatically triggers Mock DAG Lambda
|
|
|
|
No Airflow triggering - using S3 events instead.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from contextlib import contextmanager
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
import boto3
|
|
import requests
|
|
|
|
s3_client = boto3.client("s3")
|
|
PIPELINE_NAME = "upstream_downstream_pipeline_lambda_ingester"
|
|
|
|
|
|
class _NoopSpan:
|
|
def set_attribute(self, *_args, **_kwargs) -> None:
|
|
return None
|
|
|
|
|
|
class _NoopTracer:
|
|
@contextmanager
|
|
def start_as_current_span(self, *_args, **_kwargs):
|
|
yield _NoopSpan()
|
|
|
|
|
|
def fetch_from_external_api(
|
|
api_url: str, inject_schema_change: bool = False, logger: logging.Logger = None
|
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
"""Fetch data from external API.
|
|
|
|
Args:
|
|
api_url: External API base URL
|
|
inject_schema_change: If True, configure API to inject schema change
|
|
|
|
Returns:
|
|
Tuple of (API response data, audit info with request/response details)
|
|
"""
|
|
audit_info = {"requests": []}
|
|
|
|
if inject_schema_change:
|
|
try:
|
|
config_response = requests.post(
|
|
f"{api_url}/config",
|
|
json={"inject_schema_change": True},
|
|
timeout=10,
|
|
)
|
|
logger.info("Configured external API to inject schema change")
|
|
audit_info["requests"].append(
|
|
{
|
|
"type": "POST",
|
|
"url": f"{api_url}/config",
|
|
"request_body": {"inject_schema_change": True},
|
|
"status_code": config_response.status_code,
|
|
"response_body": config_response.json() if config_response.ok else None,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Could not configure API: {e}")
|
|
|
|
response = requests.get(f"{api_url}/data", timeout=30)
|
|
response.raise_for_status()
|
|
|
|
result = response.json()
|
|
schema_version = result.get("meta", {}).get("schema_version", "unknown")
|
|
logger.info(f"Fetched from external API: schema_version={schema_version}")
|
|
|
|
# Log structured request/response for audit
|
|
audit_info["requests"].append(
|
|
{
|
|
"type": "GET",
|
|
"url": f"{api_url}/data",
|
|
"status_code": response.status_code,
|
|
"response_body": result,
|
|
"schema_version": schema_version,
|
|
}
|
|
)
|
|
logger.info(f"EXTERNAL_API_AUDIT: {json.dumps(audit_info)}")
|
|
|
|
return result, audit_info
|
|
|
|
|
|
def lambda_handler(event: dict, context: Any) -> dict:
|
|
"""Lambda handler for API ingestion.
|
|
|
|
Supports both direct Lambda invoke and API Gateway triggers.
|
|
|
|
Event parameters:
|
|
- inject_schema_change: bool - If true, API returns bad schema
|
|
- correlation_id: str - Optional correlation ID for tracing
|
|
|
|
Returns:
|
|
dict with s3_key, bucket, and status
|
|
"""
|
|
tracer = _NoopTracer()
|
|
logger = logging.getLogger(__name__)
|
|
logger.setLevel(logging.INFO) # Ensure logger level is set
|
|
|
|
# Handle API Gateway event format
|
|
if "body" in event and isinstance(event.get("body"), str):
|
|
try:
|
|
body = json.loads(event["body"])
|
|
inject_schema_change = body.get("inject_schema_change", False)
|
|
correlation_id = (
|
|
body.get("correlation_id") or f"ing-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}"
|
|
)
|
|
except json.JSONDecodeError:
|
|
inject_schema_change = False
|
|
correlation_id = f"ing-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}"
|
|
else:
|
|
# Direct Lambda invoke
|
|
inject_schema_change = event.get("inject_schema_change", False)
|
|
correlation_id = (
|
|
event.get("correlation_id") or f"ing-{datetime.utcnow().strftime('%Y%m%d-%H%M%S')}"
|
|
)
|
|
|
|
landing_bucket = os.environ.get("LANDING_BUCKET")
|
|
external_api_url = os.environ.get("EXTERNAL_API_URL")
|
|
|
|
missing_env = []
|
|
if not landing_bucket:
|
|
missing_env.append("LANDING_BUCKET")
|
|
if not external_api_url:
|
|
missing_env.append("EXTERNAL_API_URL")
|
|
if missing_env:
|
|
error_message = f"Missing required environment variables: {', '.join(missing_env)}"
|
|
logger.error(
|
|
json.dumps(
|
|
{
|
|
"event": "config_error",
|
|
"execution_run_id": correlation_id,
|
|
"correlation_id": correlation_id,
|
|
"missing_env": missing_env,
|
|
"error": error_message,
|
|
}
|
|
)
|
|
)
|
|
payload = {
|
|
"error": error_message,
|
|
"missing_env": missing_env,
|
|
"correlation_id": correlation_id,
|
|
"hint": "Ensure the Lambda environment includes LANDING_BUCKET and EXTERNAL_API_URL.",
|
|
}
|
|
if "body" in event:
|
|
return {
|
|
"statusCode": 500,
|
|
"headers": {"Content-Type": "application/json"},
|
|
"body": json.dumps(payload),
|
|
}
|
|
payload["statusCode"] = 500
|
|
return payload
|
|
|
|
execution_run_id = correlation_id
|
|
logger.info(
|
|
json.dumps(
|
|
{
|
|
"event": "lambda_invocation",
|
|
"execution_run_id": execution_run_id,
|
|
"correlation_id": correlation_id,
|
|
}
|
|
)
|
|
)
|
|
|
|
# Fetch data from external API
|
|
try:
|
|
with tracer.start_as_current_span("fetch_external_api") as span:
|
|
span.set_attribute("correlation_id", correlation_id)
|
|
span.set_attribute("execution.run_id", execution_run_id)
|
|
span.set_attribute("inject_schema_change", inject_schema_change)
|
|
api_response, audit_info = fetch_from_external_api(
|
|
external_api_url, inject_schema_change, logger
|
|
)
|
|
data = api_response.get("data", [])
|
|
api_meta = api_response.get("meta", {})
|
|
span.set_attribute("record_count", len(data))
|
|
logger.info(
|
|
json.dumps(
|
|
{
|
|
"event": "external_api_fetch_complete",
|
|
"execution_run_id": execution_run_id,
|
|
"record_count": len(data),
|
|
}
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
json.dumps(
|
|
{
|
|
"event": "external_api_error",
|
|
"execution_run_id": execution_run_id,
|
|
"error": str(e),
|
|
}
|
|
)
|
|
)
|
|
return {
|
|
"statusCode": 500,
|
|
"error": f"External API call failed: {str(e)}",
|
|
"correlation_id": correlation_id,
|
|
}
|
|
|
|
# Write to S3
|
|
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H-%M-%S")
|
|
s3_key = f"ingested/{timestamp}/data.json"
|
|
audit_key = f"audit/{correlation_id}.json"
|
|
|
|
try:
|
|
with tracer.start_as_current_span("write_s3_objects") as span:
|
|
span.set_attribute("s3.bucket", landing_bucket)
|
|
span.set_attribute("correlation_id", correlation_id)
|
|
span.set_attribute("execution.run_id", execution_run_id)
|
|
span.set_attribute("record_count", len(data))
|
|
|
|
# Write audit object with external API request/response details
|
|
audit_payload = {
|
|
"correlation_id": correlation_id,
|
|
"timestamp": timestamp,
|
|
"external_api_url": external_api_url,
|
|
"audit_info": audit_info,
|
|
}
|
|
s3_client.put_object(
|
|
Bucket=landing_bucket,
|
|
Key=audit_key,
|
|
Body=json.dumps(audit_payload, indent=2),
|
|
ContentType="application/json",
|
|
)
|
|
logger.info(
|
|
json.dumps(
|
|
{
|
|
"event": "s3_write_complete",
|
|
"execution_run_id": execution_run_id,
|
|
"audit_key": audit_key,
|
|
}
|
|
)
|
|
)
|
|
|
|
# Write main data with audit_key in metadata
|
|
s3_client.put_object(
|
|
Bucket=landing_bucket,
|
|
Key=s3_key,
|
|
Body=json.dumps(api_response, indent=2),
|
|
ContentType="application/json",
|
|
Metadata={
|
|
"correlation_id": correlation_id,
|
|
"source": "api_ingester_lambda",
|
|
"timestamp": timestamp,
|
|
"schema_version": api_meta.get("schema_version", "unknown"),
|
|
"schema_change_injected": str(inject_schema_change),
|
|
"audit_key": audit_key,
|
|
},
|
|
)
|
|
logger.info(
|
|
json.dumps(
|
|
{
|
|
"event": "s3_data_written",
|
|
"execution_run_id": execution_run_id,
|
|
"s3_key": s3_key,
|
|
"record_count": len(data),
|
|
"correlation_id": correlation_id,
|
|
"schema_version": api_meta.get("schema_version"),
|
|
}
|
|
)
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
json.dumps(
|
|
{
|
|
"event": "s3_write_error",
|
|
"execution_run_id": execution_run_id,
|
|
"error": str(e),
|
|
}
|
|
)
|
|
)
|
|
return {
|
|
"statusCode": 500,
|
|
"error": f"S3 write failed: {str(e)}",
|
|
"correlation_id": correlation_id,
|
|
}
|
|
|
|
result = {
|
|
"statusCode": 200,
|
|
"s3_key": s3_key,
|
|
"s3_bucket": landing_bucket,
|
|
"record_count": len(data),
|
|
"correlation_id": correlation_id,
|
|
"execution_run_id": execution_run_id,
|
|
"schema_version": api_meta.get("schema_version"),
|
|
"schema_change_injected": inject_schema_change,
|
|
}
|
|
|
|
# Format for API Gateway if called via HTTP
|
|
if "body" in event:
|
|
return {
|
|
"statusCode": 200,
|
|
"headers": {"Content-Type": "application/json"},
|
|
"body": json.dumps(result),
|
|
}
|
|
|
|
return result
|