Capturing Detailed Amazon SES Email Logs in CloudWatch Logs

I'll explore the available SES event destinations and provide three practical options to capture and store detailed SES event logs in CloudWatch Logs.

Amazon Simple Email Service (SES) provides a robust platform for sending emails. Still, it comes with one limitation: SES does not generate traditional log files with detailed per-transaction data (such as individual source and destination email addresses). Instead, SES aggregates metrics (like sends, bounces, and complaints) that you can monitor via CloudWatch Metrics. This means you won't find an out-of-the-box option to send detailed logs directly to CloudWatch Logs.

One of our customers at DoiT wanted to generate and analyze the logs of emails sent from an application running on EC2 that uses the SMTP endpoint deployed by SES. As explained above, this is not a native functionality of SES. However, there are several workarounds to capture the granular details you need. In this post, I'll explore the available SES event destinations and provide three practical options to capture and store detailed SES event logs in CloudWatch Logs —complete with relative costs, an assessment of relative complexity, and example commands for testing using the API/SDK and an SMTP tool.

Understanding SES Event Destinations

Amazon SES allows you to use Configuration Sets with Event Destinations to capture more granular details about your email transactions. The supported destinations include:

Amazon CloudWatch (Metrics only) – Only aggregated metrics are published.

Amazon EventBridge

Amazon SNS

Amazon Data Firehose

Amazon Pinpoint

For detailed per-message logs sent to CloudWatch Logs, you need to leverage one of the above options with an intermediary solution to forward the information to a storage service such as CloudWatch Logs. Note: I will not discuss using Pinpoint in this article.

Three Ways to Send SES Logs to CloudWatch Logs

Option 1: EventBridge to CloudWatch Logs

You can capture SES event data by leveraging Amazon EventBridge to forward events directly into CloudWatch Logs. This method creates an EventBridge rule that matches SES events and sends them to a CloudWatch Logs group.

Workflow:

  1. Publish SES Events to EventBridge: Configure your SES Configuration Set to send events (all events, or only a subset, such as sends, bounces,...) to EventBridge.
  2. Create a CloudWatch Logs group: I'll use the name /aws/ses/ses-to-eventbridge-to-cwlogs for this example.
  3. Create an EventBridge Rule: Set up an EventBridge rule named, for instance, eventbridge-ses-to-cwlogs-rule with an event pattern to capture SES events.

For example, an event pattern might look like this:

{
"source": ["aws.ses"],
"detail-type": ["Email Sent", "Email Bounced"]
}
  1. Configure the Target: Associate the CloudWatch Logs group (in this example, /aws/ses/ses-to-eventbridge-to-cwlogs) as the target for the rule.

Estimated Costs & Complexity:

Cost: Low. EventBridge pricing is based on the number of events ingested, and CloudWatch Logs pricing is based on data ingestion and storage.

Complexity: Low. No coding is required; the setup is limited to configuring the rule filter and the target.

Option 2: SNS to Lambda and then to CloudWatch Logs

This option involves publishing SES events to an Amazon SNS topic and then using a Lambda function subscribed to that topic to write event details into CloudWatch Logs.

Workflow:

  1. Publish SES Events to SNS: Configure your SES Configuration Set to send events (all events, or only a subset, such as sends, bounces,...) to an SNS topic. The topic can be created while creating the destination, or you can use an existing one. In this example, I'll create the SNS topic SES-Destination-Topic.
  2. Deploy a Lambda Function:
    1. Create a Lambda function with the provided Python code (see below).
    2. Attach an IAM role that includes permissions for:
      1. CloudWatch Logs actions to the final CloudWatch Logs log group (e.g., logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents).
  3. Subscribe the Lambda Function to the SNS topic: The Lambda function, triggered by SNS notifications, processes the event messages sent to SES-Destination-Topic and writes them to a specified CloudWatch Logs group and stream. The Lambda function will create (if not existing) and use the log group /aws/ses/es-to-sns-to-cwlogs and log stream ses-to-sns-to-cwlogs-stream to store the ingested logs.

Below is a sample Lambda function that processes SNS messages containing SES events:

import boto3
import json
import time

# Set the target log group and stream names.
LOG_GROUP = '/aws/ses/ses-to-sns-to-cwlogs'
LOG_STREAM = 'ses-to-sns-to-cwlogs-stream'

logs_client = boto3.client('logs')

def ensure_log_group_and_stream():
    """
    Ensure that the log group and log stream exist.
    Returns the current sequence token for the log stream if available.
    """
    # Check for the existence of the log group.
    groups_response = logs_client.describe_log_groups(logGroupNamePrefix=LOG_GROUP)
    groups = groups_response.get('logGroups', [])
    if not any(group['logGroupName'] == LOG_GROUP for group in groups):
        logs_client.create_log_group(logGroupName=LOG_GROUP)
        print(f"Created log group: {LOG_GROUP}")

    # Check if the log stream exists.
    streams_response = logs_client.describe_log_streams(
        logGroupName=LOG_GROUP,
        logStreamNamePrefix=LOG_STREAM
    )
    streams = streams_response.get('logStreams', [])
    if not any(stream['logStreamName'] == LOG_STREAM for stream in streams):
        logs_client.create_log_stream(logGroupName=LOG_GROUP, logStreamName=LOG_STREAM)
        print(f"Created log stream: {LOG_STREAM}")
        return None
    else:
        # Return the uploadSequenceToken if available.
        return streams[0].get('uploadSequenceToken')

def lambda_handler(event, context):
    # Ensure log group and stream exist.
    sequence_token = ensure_log_group_and_stream()
    
    log_events = []
    current_time = int(time.time() * 1000)
    
    # Process each SNS record.
    for record in event.get('Records', []):
        sns_message = record.get('Sns', {}).get('Message', '')
        # Prepare a log event.
        log_events.append({
            'timestamp': current_time,
            'message': sns_message
        })
    
    # Prepare parameters for put_log_events call.
    params = {
        'logGroupName': LOG_GROUP,
        'logStreamName': LOG_STREAM,
        'logEvents': log_events
    }
    if sequence_token:
        params['sequenceToken'] = sequence_token

    # Write the log events to CloudWatch Logs.
    response = logs_client.put_log_events(**params)
    print("Put log events response:", response)
    
    return {
        'statusCode': 200,
        'body': json.dumps('SNS messages logged successfully.')
    }

Estimated Costs & Complexity:

Cost: Low. SNS charges per request (cents per million requests), Lambda costs are minimal for low to moderate event volumes, and CloudWatch Logs pricing is based on data ingestion and storage.

Complexity: Moderate. It requires writing and deploying a Lambda function, setting up proper SNS subscriptions, and managing IAM permissions.

Option 3: Kinesis Data Firehose to S3, then to Lambda, and then to CloudWatch Logs

Suppose you also want to store your logs as S3 objects for later analysis and archival in addition to sending them to CloudWatch Logs. In that case, you can leverage Kinesis Data Firehose to deliver SES events to Amazon S3 and then to CloudWatch Logs using a Lambda function. This option provides long-term archival storage and the ability to process logs later using tools like AWS Athena or Amazon Redshift.

Workflow:

  1. Publish SES Events to Kinesis Data Firehose: Configure your SES Configuration Set to send events (all events or only a subset, such as sends, bounces, etc.) to a Kinesis Data Firehose delivery stream.
  2. Store in S3: Set up Kinesis Data Firehose to deliver log files to an S3 bucket (for instance, called test-seslogsbucket).
  3. Deploy the Lambda Function:
    1. Create the Lambda function with the provided Python code (see below).
    2. Attach an IAM role that includes permissions for:
      1. s3:GetObject on test-seslogsbucket.
      2. CloudWatch Logs actions to the final CloudWatch Logs log group (e.g., logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents).
  4. Configure S3 Bucket Notifications:
    1. In the AWS Management Console, navigate to the S3 bucket test-seslogsbucket.
    2. Under Properties > Event notifications, add a new event notification:
      1. Event types: PUT (or All object create events).
      2. Prefix/Suffix: (Optional, if you want to filter specific files).
      3. Destination: Choose Lambda function and select your Lambda function.
    3. The Lambda function will create (if not existing) and use the log group /aws/ses/ses-to-fh-to-s3-to-cwlogs and log stream ses-to-fh-to-s3-to-cwlogs-stream to store the ingested logs.

Below is a sample Lambda function that processes S3 objects containing SES events:

import boto3
import json
import gzip
import time
from urllib.parse import unquote_plus

logs_client = boto3.client('logs')
s3_client = boto3.client('s3')

# Define CloudWatch Logs group and stream names
LOG_GROUP = '/aws/ses/ses-to-fh-to-s3-to-cwlogs'
LOG_STREAM = 'ses-to-fh-to-s3-to-cwlogs-stream'

def ensure_log_group_and_stream():
    """
    Ensure that the CloudWatch Logs group and log stream exist.
    Returns the current sequence token for the log stream if available.
    """
    try:
        logs_client.create_log_group(logGroupName=LOG_GROUP)
    except logs_client.exceptions.ResourceAlreadyExistsException:
        pass

    try:
        logs_client.create_log_stream(logGroupName=LOG_GROUP, logStreamName=LOG_STREAM)
    except logs_client.exceptions.ResourceAlreadyExistsException:
        pass

    # Retrieve the upload sequence token.
    response = logs_client.describe_log_streams(
        logGroupName=LOG_GROUP,
        logStreamNamePrefix=LOG_STREAM
    )
    streams = response.get('logStreams', [])
    token = streams[0].get('uploadSequenceToken') if streams else None
    return token

def lambda_handler(event, context):
    """
    Process S3 event notifications, ingest the stored data from the S3 bucket,
    and forward each JSON record to CloudWatch Logs.
    """
    sequence_token = ensure_log_group_and_stream()
    log_events = []
    current_time = int(time.time() * 1000)
    
    # Process each record from the S3 event notification.
    for record in event.get('Records', []):
        bucket = record['s3']['bucket']['name']
        key = unquote_plus(record['s3']['object']['key'])
        try:
            response = s3_client.get_object(Bucket=bucket, Key=key)
            content = response['Body'].read()
            
            # Attempt to decompress if the file is gzipped.
            try:
                content = gzip.decompress(content)
            except Exception:
                pass  # If not gzipped, continue as is.
            
            # Process each line as a separate JSON record.
            content_str = content.decode('utf-8')
            for line in content_str.splitlines():
                if line.strip():
                    try:
                        data = json.loads(line)
                        log_message = json.dumps(data)
                        log_events.append({
                            'timestamp': current_time,
                            'message': log_message
                        })
                    except Exception as e:
                        print(f"Error processing line in object {key} from bucket {bucket}: {e}")
        except Exception as e:
            print(f"Error processing object {key} from bucket {bucket}: {e}")
    
    # If there are log events to send, write them to CloudWatch Logs.
    if log_events:
        params = {
            'logGroupName': LOG_GROUP,
            'logStreamName': LOG_STREAM,
            'logEvents': log_events
        }
        if sequence_token:
            params['sequenceToken'] = sequence_token
        
        response = logs_client.put_log_events(**params)
        print("Put log events response:", response)
    
    return {
        'statusCode': 200,
        'body': json.dumps('S3 events ingested into CloudWatch Logs successfully.')
    }

Estimated Costs & Complexity:

Cost: Low to moderate. Firehose pricing is based on the volume of data ingested and delivered, while S3 storage costs are very low, especially for archival purposes. Lambda costs are minimal for low to moderate event volumes, and CloudWatch Logs pricing is based on data ingestion and storage.

Complexity: Moderate. The setup involves configuring Kinesis Data Firehose, setting up an S3 bucket, ensuring proper data formatting and partitioning if needed, writing and deploying a Lambda function, setting up proper S3 notification trigger, and managing IAM permissions.

Associating the Configuration Set with Your Emails

Regardless of your option, you must associate your SES Configuration Set with the emails you send. There are two primary methods:

Using SMTP (Simple Mail Transfer Protocol)

For a quick test, and assuming you create the required SMTP credentials, you can use swaks (Swiss Army Knife for SMTP):

swaks --to recipient@example.com --from sender@example.com --server email-smtp.eu-west-1.amazonaws.com:587 --auth-user SES_SMTP_USERNAME --auth-password SES_SMTP_PASSWORD --auth LOGIN --tls --header "X-SES-CONFIGURATION-SET: myconfigset"

Using API/SDK

When sending emails programmatically, specify the configuration set by including the ConfigurationSetName parameter in your SendEmail or SendRawEmail API call.

For example, using the AWS CLI:

aws ses send-email \
  --from "sender@example.com" \
  --destination "ToAddresses=recipient@example.com" \
  --message "Subject={Data=Test Email-42,Charset=utf-8},Body={Text={Data=This is a sample email sent using SES CLI with configuration set myconfigset.,Charset=utf-8}}" \
  --configuration-set-name myconfigset

If you would like more details, please take a look at the Amazon SES Developer Guide.

Verifying the events in CloudWatch Logs

The contents may differ slightly depending on the actual origin (EventBridge, SNS or S3) of the event written to the CloudWatch Logs.

Option 1: EventBridge directly to CloudWatch Logs

A single event stream will be generated for each event. For instance, if a message sent is bounced, there will be one stream for the "Email Sent" event and one for the "Email Bounced" event.

Their contents will look like this:

{
    "version": "0",
    "id": "2b93dbad-1f8d-71e7-589a-dbdb73b36cdf",
    "detail-type": "Email Bounced",
    "source": "aws.ses",
    "account": "123412341234",
    "time": "2025-02-24T15:26:12Z",
    "region": "eu-west-1",
    "resources": [
        "arn:aws:ses:eu-west-1:123412341234:configuration-set/myconfigset"
    ],
    "detail": {
        "eventType": "Bounce",
        "bounce": {
            "feedbackId": "0102019557770283-cb236fec-2833-423c-8757-cc04ad779388-000000",
            "bounceType": "Transient",
            "bounceSubType": "General",
            "bouncedRecipients": [
                {
                    "emailAddress": "sender@example.com",
                    "action": "failed",
                    "status": "5.7.26",
                    "diagnosticCode": "smtp; 550-5.7.26 Unauthenticated email from example.com is not accepted due to domain's\n550-5.7.26 DMARC policy. Please contact the administrator of example.com domain if\n550-5.7.26 this was a legitimate mail. To learn about the DMARC initiative, go\n550-5.7.26 to\n550 5.7.26  https://support.google.com/mail/?p=DmarcRejection 5b1f17b1804b1-43bbe038ffesi6861245e9.46 - gsmtp"
                }
            ],
            "timestamp": "2025-02-24T15:26:12.148Z",
            "remoteMtaIp": "209.85.202.26",
            "reportingMTA": "dns; a7-20.smtp-out.eu-west-1.amazonses.com"
        },
        "mail": {
            "timestamp": "2025-02-24T15:26:11.394Z",
            "source": "sender@example.com",
            "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
            "sendingAccountId": "123412341234",
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "destination": [
                "recipient@example.com"
            ],
            "headersTruncated": false,
            "headers": [
                {
                    "name": "From",
                    "value": "sender@example.com"
                },
                {
                    "name": "From",
                    "value": "recipient@example.com"
                },
                {
                    "name": "Subject",
                    "value": "Test Email 42"
                },
                {
                    "name": "MIME-Version",
                    "value": "1.0"
                },
                {
                    "name": "Content-Type",
                    "value": "text/plain; charset=utf-8"
                },
                {
                    "name": "Content-Transfer-Encoding",
                    "value": "7bit"
                }
            ],
            "commonHeaders": {
                "from": [
                    "sender@example.com"
                ],
                "to": [
                    "recipient@example.com"
                ],
                "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
                "subject": "Test Email 42"
            },
            "tags": {
                "ses:source-tls-version": [
                    "TLSv1.3"
                ],
                "ses:operation": [
                    "SendEmail"
                ],
                "ses:configuration-set": [
                    "myconfigset"
                ],
                "ses:source-ip": [
                    "93.156.205.152"
                ],
                "ses:from-domain": [
                    "example.com"
                ],
                "ses:caller-identity": [
                    "javier"
                ]
            }
        }
    }
}
{
    "version": "0",
    "id": "c7b3dc29-3101-6ef7-8711-22f85b401658",
    "detail-type": "Email Sent",
    "source": "aws.ses",
    "account": "123412341234",
    "time": "2025-02-24T15:26:11Z",
    "region": "eu-west-1",
    "resources": [
        "arn:aws:ses:eu-west-1:123412341234:configuration-set/myconfigset"
    ],
    "detail": {
        "eventType": "Send",
        "mail": {
            "timestamp": "2025-02-24T15:26:11.394Z",
            "source": "sender@example.com",
            "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
            "sendingAccountId": "123412341234",
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "destination": [
                "recipient@example.com"
            ],
            "headersTruncated": false,
            "headers": [
                {
                    "name": "From",
                    "value": "sender@example.com"
                },
                {
                    "name": "From",
                    "value": "recipient@example.com"
                },
                {
                    "name": "Subject",
                    "value": "Test Email 42"
                },
                {
                    "name": "MIME-Version",
                    "value": "1.0"
                },
                {
                    "name": "Content-Type",
                    "value": "text/plain; charset=utf-8"
                },
                {
                    "name": "Content-Transfer-Encoding",
                    "value": "7bit"
                }
            ],
            "commonHeaders": {
                "from": [
                    "sender@example.com"
                ],
                "to": [
                    "recipient@example.com"
                ],
                "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
                "subject": "Test Email 42"
            },
            "tags": {
                "ses:source-tls-version": [
                    "TLSv1.3"
                ],
                "ses:operation": [
                    "SendEmail"
                ],
                "ses:configuration-set": [
                    "myconfigset"
                ],
                "ses:source-ip": [
                    "93.156.205.152"
                ],
                "ses:from-domain": [
                    "example.com"
                ],
                "ses:caller-identity": [
                    "javier"
                ]
            }
        },
        "send": {}
    }
}

Option 2: SNS to CloudWatch Logs through Lambda

Multiple events are written to the same CloudWatch Logs stream by the Lambda function.

Their contents will look like this:

{
    "eventType": "Bounce",
    "bounce": {
        "feedbackId": "0102019557770283-cb236fec-2833-423c-8757-cc04ad779388-000000",
        "bounceType": "Transient",
        "bounceSubType": "General",
        "bouncedRecipients": [
            {
                "emailAddress": "sender@example.com",
                "action": "failed",
                "status": "5.7.26",
                "diagnosticCode": "smtp; 550-5.7.26 Unauthenticated email from example.com is not accepted due to domain's\n550-5.7.26 DMARC policy. Please contact the administrator of example.com domain if\n550-5.7.26 this was a legitimate mail. To learn about the DMARC initiative, go\n550-5.7.26 to\n550 5.7.26  https://support.google.com/mail/?p=DmarcRejection 5b1f17b1804b1-43bbe038ffesi6861245e9.46 - gsmtp"
            }
        ],
        "timestamp": "2025-02-24T15:26:12.148Z",
        "remoteMtaIp": "209.85.202.26",
        "reportingMTA": "dns; a7-20.smtp-out.eu-west-1.amazonses.com"
    },
    "mail": {
        "timestamp": "2025-02-24T15:26:11.394Z",
        "source": "sender@example.com",
        "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
        "sendingAccountId": "123412341234",
        "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
        "destination": [
            "recipient@example.com"
        ],
        "headersTruncated": false,
        "headers": [
            {
                "name": "From",
                "value": "sender@example.com"
            },
            {
                "name": "To",
                "value": "recipient@example.com"
            },
            {
                "name": "Subject",
                "value": "Test Email 42"
            },
            {
                "name": "MIME-Version",
                "value": "1.0"
            },
            {
                "name": "Content-Type",
                "value": "text/plain; charset=utf-8"
            },
            {
                "name": "Content-Transfer-Encoding",
                "value": "7bit"
            }
        ],
        "commonHeaders": {
            "from": [
                "sender@example.com"
            ],
            "to": [
                "recipient@example.com"
            ],
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "subject": "Test Email 42"
        },
        "tags": {
            "ses:source-tls-version": [
                "TLSv1.3"
            ],
            "ses:operation": [
                "SendEmail"
            ],
            "ses:configuration-set": [
                "myconfigset"
            ],
            "ses:source-ip": [
                "93.156.205.152"
            ],
            "ses:from-domain": [
                "example.com"
            ],
            "ses:caller-identity": [
                "javier"
            ]
        }
    }
}
{
    "eventType": "Send",
    "mail": {
        "timestamp": "2025-02-24T15:26:11.394Z",
        "source": "sender@example.com",
        "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
        "sendingAccountId": "123412341234",
        "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
        "destination": [
            "recipient@example.com"
        ],
        "headersTruncated": false,
        "headers": [
            {
                "name": "From",
                "value": "sender@example.com"
            },
            {
                "name": "To",
                "value": "recipient@example.com"
            },
            {
                "name": "Subject",
                "value": "Test Email 42"
            },
            {
                "name": "MIME-Version",
                "value": "1.0"
            },
            {
                "name": "Content-Type",
                "value": "text/plain; charset=utf-8"
            },
            {
                "name": "Content-Transfer-Encoding",
                "value": "7bit"
            }
        ],
        "commonHeaders": {
            "from": [
                "sender@example.com"
            ],
            "to": [
                "recipient@example.com"
            ],
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "subject": "Test Email 42"
        },
        "tags": {
            "ses:source-tls-version": [
                "TLSv1.3"
            ],
            "ses:operation": [
                "SendEmail"
            ],
            "ses:configuration-set": [
                "myconfigset"
            ],
            "ses:source-ip": [
                "93.156.205.152"
            ],
            "ses:from-domain": [
                "example.com"
            ],
            "ses:caller-identity": [
                "javier"
            ]
        }
    },
    "send": {}
}

Option 3: S3 (via Kinesis Data Firehose) to CloudWatch Logs through Lambda

Multiple events are written to the same CloudWatch Logs stream by the Lambda function.

Their contents will look like this:

{
    "eventType": "Bounce",
    "bounce": {
        "feedbackId": "0102019557770283-cb236fec-2833-423c-8757-cc04ad779388-000000",
        "bounceType": "Transient",
        "bounceSubType": "General",
        "bouncedRecipients": [
            {
                "emailAddress": "sender@example.com",
                "action": "failed",
                "status": "5.7.26",
                "diagnosticCode": "smtp; 550-5.7.26 Unauthenticated email from example.com is not accepted due to domain's\n550-5.7.26 DMARC policy. Please contact the administrator of example.com domain if\n550-5.7.26 this was a legitimate mail. To learn about the DMARC initiative, go\n550-5.7.26 to\n550 5.7.26  https://support.google.com/mail/?p=DmarcRejection 5b1f17b1804b1-43bbe038ffesi6861245e9.46 - gsmtp"
            }
        ],
        "timestamp": "2025-02-24T15:26:12.148Z",
        "remoteMtaIp": "209.85.202.26",
        "reportingMTA": "dns; a7-20.smtp-out.eu-west-1.amazonses.com"
    },
    "mail": {
        "timestamp": "2025-02-24T15:26:11.394Z",
        "source": "sender@example.com",
        "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
        "sendingAccountId": "123412341234",
        "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
        "destination": [
            "recipient@example.com"
        ],
        "headersTruncated": false,
        "headers": [
            {
                "name": "From",
                "value": "sender@example.com"
            },
            {
                "name": "To",
                "value": "recipient@example.com"
            },
            {
                "name": "Subject",
                "value": "Test Email 42"
            },
            {
                "name": "MIME-Version",
                "value": "1.0"
            },
            {
                "name": "Content-Type",
                "value": "text/plain; charset=utf-8"
            },
            {
                "name": "Content-Transfer-Encoding",
                "value": "7bit"
            }
        ],
        "commonHeaders": {
            "from": [
                "sender@example.com"
            ],
            "to": [
                "recipient@example.com"
            ],
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "subject": "Test Email 42"
        },
        "tags": {
            "ses:source-tls-version": [
                "TLSv1.3"
            ],
            "ses:operation": [
                "SendEmail"
            ],
            "ses:configuration-set": [
                "myconfigset"
            ],
            "ses:source-ip": [
                "93.156.205.152"
            ],
            "ses:from-domain": [
                "example.com"
            ],
            "ses:caller-identity": [
                "javier"
            ]
        }
    }
}
{
    "eventType": "Send",
    "mail": {
        "timestamp": "2025-02-24T15:26:11.394Z",
        "source": "sender@example.com",
        "sourceArn": "arn:aws:ses:eu-west-1:123412341234:identity/sender@example.com",
        "sendingAccountId": "123412341234",
        "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
        "destination": [
            "recipient@example.com"
        ],
        "headersTruncated": false,
        "headers": [
            {
                "name": "From",
                "value": "sender@example.com"
            },
            {
                "name": "To",
                "value": "recipient@example.com"
            },
            {
                "name": "Subject",
                "value": "Test Email 42"
            },
            {
                "name": "MIME-Version",
                "value": "1.0"
            },
            {
                "name": "Content-Type",
                "value": "text/plain; charset=utf-8"
            },
            {
                "name": "Content-Transfer-Encoding",
                "value": "7bit"
            }
        ],
        "commonHeaders": {
            "from": [
                "sender@example.com"
            ],
            "to": [
                "recipient@example.com"
            ],
            "messageId": "010201955776ffc2-8a1a4d9c-f484-46ac-955f-cd92d48de6bf-000000",
            "subject": "Test Email 42"
        },
        "tags": {
            "ses:source-tls-version": [
                "TLSv1.3"
            ],
            "ses:operation": [
                "SendEmail"
            ],
            "ses:configuration-set": [
                "myconfigset"
            ],
            "ses:source-ip": [
                "93.156.205.152"
            ],
            "ses:from-domain": [
                "example.com"
            ],
            "ses:caller-identity": [
                "javier"
            ]
        }
    },
    "send": {}
}

Please notice the slight difference in the JSON contents in option 1.

Conclusion

In summary, while Amazon SES aggregates key metrics, it doesn’t provide per-transaction logs out of the box. To capture detailed email event data, you can integrate SES with services like EventBridge, SNS with Lambda, or Kinesis Data Firehose combined with S3 and Lambda. Each approach offers a balance of cost, complexity, and functionality—allowing you to choose the best fit for your monitoring and archival needs without relying on native detailed logging.

Subscribe to Javier in the Cloud

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe