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
433 lines
15 KiB
Python
433 lines
15 KiB
Python
"""Lambda client for function inspection and log retrieval."""
|
|
|
|
import base64
|
|
import json
|
|
import logging
|
|
from contextlib import suppress
|
|
from io import BytesIO
|
|
from typing import Any
|
|
from zipfile import ZipFile
|
|
|
|
from integrations.aws.env import make_boto3_client, require_aws_credentials
|
|
from platform.observability.errors.boundary import report_exception
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from botocore.exceptions import ClientError
|
|
except ImportError:
|
|
|
|
class ClientError(Exception): # type: ignore[no-redef]
|
|
"""Stub when botocore is not installed; prevents over-broad except clauses."""
|
|
|
|
|
|
def _get_lambda_client():
|
|
return make_boto3_client("lambda")
|
|
|
|
|
|
def _get_cloudwatch_logs_client():
|
|
return make_boto3_client("logs")
|
|
|
|
|
|
def get_function_configuration(function_name: str) -> dict[str, Any]:
|
|
"""
|
|
Get Lambda function configuration.
|
|
|
|
Args:
|
|
function_name: Lambda function name or ARN
|
|
|
|
Returns:
|
|
dict with function configuration
|
|
"""
|
|
client = _get_lambda_client()
|
|
if not client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(context="lambda_client.get_function_configuration")
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
try:
|
|
response = client.get_function_configuration(FunctionName=function_name)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"function_name": response.get("FunctionName"),
|
|
"function_arn": response.get("FunctionArn"),
|
|
"runtime": response.get("Runtime"),
|
|
"handler": response.get("Handler"),
|
|
"code_size": response.get("CodeSize"),
|
|
"timeout": response.get("Timeout"),
|
|
"memory_size": response.get("MemorySize"),
|
|
"last_modified": response.get("LastModified"),
|
|
"role": response.get("Role"),
|
|
"environment": response.get("Environment", {}).get("Variables", {}),
|
|
"description": response.get("Description"),
|
|
"version": response.get("Version"),
|
|
"state": response.get("State"),
|
|
"state_reason": response.get("StateReason"),
|
|
"layers": [
|
|
{"arn": layer.get("Arn"), "code_size": layer.get("CodeSize")}
|
|
for layer in response.get("Layers", [])
|
|
],
|
|
},
|
|
}
|
|
except ClientError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def get_function_code(
|
|
function_name: str,
|
|
extract_files: bool = True,
|
|
max_file_size: int = 10000,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get Lambda function deployment package.
|
|
|
|
Args:
|
|
function_name: Lambda function name or ARN
|
|
extract_files: If True, extract and return file contents
|
|
max_file_size: Maximum size in bytes for extracted files
|
|
|
|
Returns:
|
|
dict with function code location and optionally file contents
|
|
"""
|
|
client = _get_lambda_client()
|
|
if not client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(context="lambda_client.get_function_code")
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
try:
|
|
response = client.get_function(FunctionName=function_name)
|
|
|
|
code_location = response.get("Code", {}).get("Location")
|
|
code_size = response.get("Configuration", {}).get("CodeSize", 0)
|
|
repository_type = response.get("Code", {}).get("RepositoryType")
|
|
|
|
result: dict[str, Any] = {
|
|
"success": True,
|
|
"data": {
|
|
"function_name": function_name,
|
|
"code_location": code_location,
|
|
"code_size": code_size,
|
|
"repository_type": repository_type,
|
|
},
|
|
}
|
|
|
|
if extract_files and code_location and code_size < 5 * 1024 * 1024:
|
|
# Download and extract if code is under 5MB
|
|
import requests
|
|
|
|
zip_response = requests.get(code_location, timeout=30)
|
|
if zip_response.status_code == 200:
|
|
files: dict[str, Any] = {}
|
|
try:
|
|
with ZipFile(BytesIO(zip_response.content)) as zf:
|
|
for name in zf.namelist():
|
|
if name.endswith("/"):
|
|
continue
|
|
info = zf.getinfo(name)
|
|
if info.file_size <= max_file_size:
|
|
try:
|
|
content = zf.read(name).decode("utf-8")
|
|
files[name] = {
|
|
"size": info.file_size,
|
|
"content": content,
|
|
}
|
|
except UnicodeDecodeError:
|
|
files[name] = {
|
|
"size": info.file_size,
|
|
"binary": True,
|
|
}
|
|
else:
|
|
files[name] = {
|
|
"size": info.file_size,
|
|
"truncated": True,
|
|
}
|
|
result["data"]["files"] = files
|
|
result["data"]["file_count"] = len(files)
|
|
except Exception as e:
|
|
result["data"]["extract_error"] = str(e)
|
|
|
|
return result
|
|
except ClientError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def get_recent_invocations(
|
|
function_name: str,
|
|
limit: int = 50,
|
|
filter_pattern: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get recent Lambda invocation logs from CloudWatch.
|
|
|
|
Lambda logs are stored in /aws/lambda/{function_name}.
|
|
|
|
Args:
|
|
function_name: Lambda function name
|
|
limit: Maximum log events to return
|
|
filter_pattern: Optional CloudWatch filter pattern
|
|
|
|
Returns:
|
|
dict with invocation logs
|
|
"""
|
|
logs_client = _get_cloudwatch_logs_client()
|
|
if not logs_client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(context="lambda_client.get_recent_invocations")
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
log_group_name = f"/aws/lambda/{function_name}"
|
|
|
|
try:
|
|
kwargs = {
|
|
"logGroupName": log_group_name,
|
|
"limit": limit,
|
|
}
|
|
if filter_pattern:
|
|
kwargs["filterPattern"] = filter_pattern
|
|
|
|
response = logs_client.filter_log_events(**kwargs)
|
|
events = response.get("events", [])
|
|
|
|
# Parse events to identify invocations
|
|
invocations = []
|
|
current_invocation = None
|
|
|
|
for event in events:
|
|
message = event.get("message", "")
|
|
timestamp = event.get("timestamp")
|
|
|
|
if "START RequestId:" in message:
|
|
if current_invocation:
|
|
invocations.append(current_invocation)
|
|
request_id = (
|
|
message.split("RequestId: ")[1].split()[0] if "RequestId:" in message else None
|
|
)
|
|
current_invocation = {
|
|
"request_id": request_id,
|
|
"start_time": timestamp,
|
|
"logs": [message],
|
|
}
|
|
elif "END RequestId:" in message:
|
|
if current_invocation:
|
|
current_invocation["end_time"] = timestamp
|
|
current_invocation["logs"].append(message)
|
|
elif "REPORT RequestId:" in message:
|
|
if current_invocation:
|
|
current_invocation["logs"].append(message)
|
|
# Parse REPORT for duration and memory
|
|
if "Duration:" in message:
|
|
with suppress(IndexError, ValueError):
|
|
duration_part = message.split("Duration: ")[1].split()[0]
|
|
current_invocation["duration_ms"] = float(duration_part)
|
|
if "Memory Used:" in message:
|
|
with suppress(IndexError, ValueError):
|
|
memory_part = message.split("Memory Used: ")[1].split()[0]
|
|
current_invocation["memory_used_mb"] = int(memory_part)
|
|
invocations.append(current_invocation)
|
|
current_invocation = None
|
|
elif current_invocation:
|
|
current_invocation["logs"].append(message)
|
|
|
|
if current_invocation:
|
|
invocations.append(current_invocation)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"log_group": log_group_name,
|
|
"invocation_count": len(invocations),
|
|
"invocations": invocations[-10:], # Return last 10 invocations
|
|
"raw_event_count": len(events),
|
|
},
|
|
}
|
|
except ClientError as e:
|
|
error_code = e.response.get("Error", {}).get("Code", "")
|
|
if error_code == "ResourceNotFoundException":
|
|
return {
|
|
"success": False,
|
|
"error": f"Log group not found: {log_group_name}",
|
|
"log_group": log_group_name,
|
|
}
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def get_invocation_logs_by_request_id(
|
|
function_name: str,
|
|
request_id: str,
|
|
limit: int = 100,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get logs for a specific Lambda invocation by request ID.
|
|
|
|
Args:
|
|
function_name: Lambda function name
|
|
request_id: Lambda request ID
|
|
limit: Maximum log events to return
|
|
|
|
Returns:
|
|
dict with invocation logs
|
|
"""
|
|
logs_client = _get_cloudwatch_logs_client()
|
|
if not logs_client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(
|
|
context="lambda_client.get_invocation_logs_by_request_id"
|
|
)
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
log_group_name = f"/aws/lambda/{function_name}"
|
|
|
|
try:
|
|
response = logs_client.filter_log_events(
|
|
logGroupName=log_group_name,
|
|
filterPattern=f'"{request_id}"',
|
|
limit=limit,
|
|
)
|
|
|
|
events = response.get("events", [])
|
|
log_messages = [event.get("message", "") for event in events]
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"log_group": log_group_name,
|
|
"request_id": request_id,
|
|
"event_count": len(events),
|
|
"logs": log_messages,
|
|
},
|
|
}
|
|
except ClientError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def invoke_function(
|
|
function_name: str,
|
|
payload: dict[str, Any] | None = None,
|
|
invocation_type: str = "RequestResponse",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Invoke a Lambda function.
|
|
|
|
Args:
|
|
function_name: Lambda function name or ARN
|
|
payload: Optional payload dict
|
|
invocation_type: InvocationType (RequestResponse, Event, DryRun)
|
|
|
|
Returns:
|
|
dict with invocation result
|
|
"""
|
|
client = _get_lambda_client()
|
|
if not client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(context="lambda_client.invoke_function")
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
try:
|
|
kwargs = {
|
|
"FunctionName": function_name,
|
|
"InvocationType": invocation_type,
|
|
}
|
|
if payload:
|
|
kwargs["Payload"] = json.dumps(payload)
|
|
|
|
response = client.invoke(**kwargs)
|
|
|
|
result_payload = None
|
|
if "Payload" in response:
|
|
payload_bytes = response["Payload"].read()
|
|
try:
|
|
result_payload = json.loads(payload_bytes.decode())
|
|
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
report_exception(
|
|
exc,
|
|
logger=logger,
|
|
message="Lambda invocation returned non-JSON payload",
|
|
severity="warning",
|
|
tags={
|
|
"surface": "service_client",
|
|
"integration": "aws_lambda",
|
|
"component": "integrations.aws.lambda_client",
|
|
},
|
|
extras={
|
|
"function_name": function_name,
|
|
"payload_preview": payload_bytes[:200].decode("utf-8", "replace"),
|
|
},
|
|
)
|
|
result_payload = None
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"status_code": response.get("StatusCode"),
|
|
"function_error": response.get("FunctionError"),
|
|
"executed_version": response.get("ExecutedVersion"),
|
|
"log_result": base64.b64decode(response.get("LogResult", "")).decode()
|
|
if response.get("LogResult")
|
|
else None,
|
|
"payload": result_payload,
|
|
},
|
|
}
|
|
except ClientError as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def list_functions(
|
|
max_items: int = 50,
|
|
function_version: str = "ALL",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
List Lambda functions in the account.
|
|
|
|
Args:
|
|
max_items: Maximum functions to return
|
|
function_version: ALL or specific version
|
|
|
|
Returns:
|
|
dict with function list
|
|
"""
|
|
client = _get_lambda_client()
|
|
if not client:
|
|
return {"success": False, "error": "boto3 not available"}
|
|
credentials_error = require_aws_credentials(context="lambda_client.list_functions")
|
|
if credentials_error:
|
|
return credentials_error
|
|
|
|
try:
|
|
response = client.list_functions(
|
|
FunctionVersion=function_version,
|
|
MaxItems=max_items,
|
|
)
|
|
|
|
functions = []
|
|
for func in response.get("Functions", []):
|
|
functions.append(
|
|
{
|
|
"function_name": func.get("FunctionName"),
|
|
"function_arn": func.get("FunctionArn"),
|
|
"runtime": func.get("Runtime"),
|
|
"handler": func.get("Handler"),
|
|
"code_size": func.get("CodeSize"),
|
|
"timeout": func.get("Timeout"),
|
|
"memory_size": func.get("MemorySize"),
|
|
"last_modified": func.get("LastModified"),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"functions": functions,
|
|
"count": len(functions),
|
|
},
|
|
}
|
|
except ClientError as e:
|
|
return {"success": False, "error": str(e)}
|