AWS Lambda for parsing ALB logs stored in S3 after CloudWatch Alert triggered

devops-incident-response

# 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)}

official docs

Leave a Reply

Your email address will not be published. Required fields are marked *

โ˜• Support us ยท ๐Ÿ’ณ Monobank