# CloudWatch Alert trigger lambda
# Lambda will parse CW input and take time when Alert is triggered
# Get logs from S3 and parse them
# Put to CloudWatch Log Group log stream with errors
# Create SQS
# Configure proper IAM permissions for this Lambda
# This lambda will invoke twice if first iteration will return “No logs matching {error_pattern}XX found in the processed files”
# After 1st iteration it will create SQS queue and SQS will trigger this lambda again after 1 minute.
import json
import boto3
import gzip
from datetime import datetime, timedelta
import logging
# AWS clients
s3 = boto3.client('s3')
logs = boto3.client('logs')
sqs = boto3.client('sqs')
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Config
BUCKET = 'BUCKET_WITH_ALB_LOGS'
BASE_PREFIX = 'BUCKET_WITH_ALB_LOGS/AWSLogs/AWS_ACCOUND_ID/elasticloadbalancing/REGION/'
LOG_GROUP = 'CLOUDWATCH_LOG_GROUP_NAME'
MAX_FILES_TO_PROCESS = 6
SQS_QUEUE_URL = 'SQS_URL'
RETRY_DELAY_SECONDS = 60 # Time where this lambda will invoke again
def ensure_log_group_and_stream_exists(log_group, log_stream):
"""Ensure the log group and log stream exist, creating them if necessary."""
try:
response = logs.describe_log_groups(logGroupNamePrefix=log_group)
log_groups = [lg for lg in response['logGroups'] if lg['logGroupName'] == log_group]
if not log_groups:
logger.info(f"Log group {log_group} not found, creating it...")
logs.create_log_group(logGroupName=log_group)
logger.info(f"Log group {log_group} created successfully")
else:
logger.info(f"Log group {log_group} already exists")
except Exception as e:
logger.error(f"Error checking/creating log group {log_group}: {str(e)}")
raise
try:
response = logs.describe_log_streams(
logGroupName=log_group,
logStreamNamePrefix=log_stream,
limit=1
)
streams = [s for s in response['logStreams'] if s['logStreamName'] == log_stream]
if not streams:
logger.info(f"Log stream {log_stream} not found, creating it...")
logs.create_log_stream(logGroupName=log_group, logStreamName=log_stream)
logger.info(f"Log stream {log_stream} created successfully")
else:
logger.info(f"Log stream {log_stream} already exists")
except Exception as e:
logger.error(f"Error checking/creating log stream {log_stream}: {str(e)}")
raise
def schedule_retry(event):
"""Schedule a retry by sending the event to SQS with a delay."""
logger.info(f"Scheduling retry after {RETRY_DELAY_SECONDS} seconds via SQS")
new_event = event.copy()
new_event['is_retry'] = True
try:
sqs.send_message(
QueueUrl=SQS_QUEUE_URL,
MessageBody=json.dumps(new_event),
DelaySeconds=RETRY_DELAY_SECONDS
)
logger.info("Successfully scheduled retry via SQS")
except Exception as e:
logger.error(f"Failed to schedule retry via SQS: {str(e)}")
raise
def lambda_handler(event, context):
logger.info(f"Event: {json.dumps(event)}")
# Handle SQS event structure
if 'Records' in event and event['Records']:
# Extract the body from the SQS message and parse it
sqs_body = event['Records'][0]['body']
try:
event = json.loads(sqs_body)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse SQS message body: {str(e)}")
return {'status': 'error', 'message': f"Invalid SQS message body: {str(e)}"}
# Now process the event (either direct or from SQS)
is_retry = event.get('is_retry', False)
logger.info(f"Is retry: {is_retry}")
alarm_data = event.get('alarmData', {})
state_data = alarm_data.get('state', {})
metric_data = alarm_data.get('configuration', {}).get('metrics', [{}])[0]
try:
timestamp_str = state_data.get('timestamp', '')
if not timestamp_str:
logger.error("No timestamp provided in alarm state data")
return {'status': 'error', 'message': 'Missing timestamp in alarm state data'}
alarm_time = datetime.strptime(timestamp_str.split('.')[0], '%Y-%m-%dT%H:%M:%S')
logger.info(f"Alarm triggered at: {alarm_time}")
metric_name = metric_data.get('metricStat', {}).get('metric', {}).get('name', '')
if '4XX' in metric_name:
error_pattern = '4'
elif '5XX' in metric_name:
error_pattern = '5'
elif '3XX' in metric_name:
error_pattern = '3'
else:
logger.error(f"Unknown metric name: {metric_name}")
return {'status': 'error', 'message': 'Unknown metric name'}
logger.info(f"Filtering for status codes starting with: {error_pattern}")
log_stream = metric_name
logger.info(f"Using log stream name: {log_stream}")
date_prefix = alarm_time.strftime('%Y/%m/%d/')
s3_prefix = f"{BASE_PREFIX}{date_prefix}"
logger.info(f"S3 prefix: {s3_prefix}")
s3_objects = []
continuation_token = None
while True:
list_kwargs = {'Bucket': BUCKET, 'Prefix': s3_prefix}
if continuation_token:
list_kwargs['ContinuationToken'] = continuation_token
response = s3.list_objects_v2(**list_kwargs)
s3_objects.extend(response.get('Contents', []))
if not response.get('IsTruncated', False):
break
continuation_token = response.get('NextContinuationToken')
if not s3_objects:
logger.warning(f"No log files found under prefix {s3_prefix}")
return {'status': 'success', 'processed': 0}
log_files = []
for obj in s3_objects:
key = obj['Key']
try:
file_time_str = key.split('_')[-3]
file_time = datetime.strptime(file_time_str, '%Y%m%dT%H%MZ')
log_files.append({'key': key, 'file_time': file_time})
except Exception as e:
logger.error(f"Error parsing timestamp for {key}: {str(e)}")
continue
log_files.sort(key=lambda x: x['file_time'], reverse=True)
logger.info(f"Found {len(log_files)} log files")
relevant_files = []
for log_file in log_files:
file_time = log_file['file_time']
end_time = file_time + timedelta(minutes=10)
if file_time <= alarm_time < end_time:
relevant_files.append(log_file)
if not relevant_files:
logger.warning(f"No log files found covering alarm time {alarm_time}")
return {'status': 'success', 'processed': 0}
files_to_process = []
for log_file in relevant_files[:MAX_FILES_TO_PROCESS]:
key = log_file['key']
file_time = log_file['file_time']
log_start_time = file_time
log_end_time = file_time + timedelta(minutes=10)
files_to_process.append((key, log_start_time, log_end_time))
logger.info(f"Selected {key} - covers [{log_start_time}, {log_end_time})")
filtered_logs = []
for key, log_start_time, log_end_time in files_to_process:
try:
s3_obj = s3.get_object(Bucket=BUCKET, Key=key)
with gzip.GzipFile(fileobj=s3_obj['Body']) as gz:
for line in gz.read().decode('utf-8').splitlines():
fields = line.split()
if len(fields) < 12:
continue
log_time = datetime.strptime(fields[1][:19], '%Y-%m-%dT%H:%M:%S')
elb_status = fields[8]
target_status = fields[9]
logger.info(f"Processing log: time={log_time}, elb_status={elb_status}, target_status={target_status}")
status_to_check = elb_status if 'ELB' in metric_name else target_status
if status_to_check.startswith(error_pattern):
try:
status_code = int(status_to_check)
if (error_pattern == '3' and 300 <= status_code <= 399) or \
(error_pattern == '4' and 400 <= status_code <= 499) or \
(error_pattern == '5' and 500 <= status_code <= 599):
log_timestamp = int(log_time.timestamp() * 1000)
filtered_logs.append({
'timestamp': log_timestamp,
'message': line
})
logger.info(f"Filtered log: {line}")
except ValueError:
logger.warning(f"Invalid status code format: {status_to_check}, skipping log: {line}")
except Exception as e:
logger.error(f"Error processing {key}: {str(e)}")
continue
if filtered_logs:
filtered_logs.sort(key=lambda x: x['timestamp'])
log_events = [{'timestamp': e['timestamp'], 'message': e['message']} for e in filtered_logs]
ensure_log_group_and_stream_exists(LOG_GROUP, log_stream)
try:
response = logs.put_log_events(
logGroupName=LOG_GROUP,
logStreamName=log_stream,
logEvents=log_events
)
logger.info(f"Sent {len(log_events)} logs to CloudWatch: {response}")
except logs.exceptions.InvalidSequenceTokenException as e:
logger.info("Handling InvalidSequenceTokenException...")
stream_info = logs.describe_log_streams(
logGroupName=LOG_GROUP,
logStreamNamePrefix=log_stream
)['logStreams'][0]
response = logs.put_log_events(
logGroupName=LOG_GROUP,
logStreamName=log_stream,
logEvents=log_events,
sequenceToken=stream_info.get('uploadSequenceToken')
)
logger.info(f"Sent {len(log_events)} logs to CloudWatch with sequence token: {response}")
except Exception as e:
logger.error(f"Failed to send logs to CloudWatch: {str(e)}")
raise
else:
logger.info(f"No logs matching {error_pattern}XX found in the processed files")
if not is_retry:
schedule_retry(event)
return {'status': 'retry_scheduled', 'processed': 0}
return {'status': 'success', 'processed': len(filtered_logs), 'is_retry': is_retry}
except Exception as e:
logger.error(f"Error processing alarm: {str(e)}")
return {'status': 'error', 'message': str(e)}
