An AWS Lambda function written in Rust that forwards CloudWatch Logs to OpenTelemetry-compatible backends. This function receives CloudWatch Logs events via subscription filters, transforms them into OpenTelemetry log format, and exports them using the Rotel agent. This is built on the existing Rotel OpenTelemetry data plane, so logs can be exported to any supported exporter.
- OpenTelemetry Native: Transforms all logs to OpenTelemetry format
- Multiple Export Targets: Supports OTLP HTTP/gRPC and other exporters via Rotel
- Automatic parsing: Support for JSON and key=value parsing, with automatic detection
- Log stream parser mapping: Pre-built parser rules for known AWS CW log groups/streams
- AWS Resource Attributes: Automatically enriches logs with AWS Lambda and CloudWatch log group tags
- Reliable delivery: Ensures logs are delivered successfully before acknowledging request
The following services have been tested to work via CloudWatch Logs and include additional custom handling. Unlisted services that log via CloudWatch will likely work, but may be missing custom processing. This list will expand as we verify support for additional services.
| Source | Location |
|---|---|
| CodeBuild Logs | CloudWatch |
| CloudWatch Logs | CloudWatch |
| CloudTrail Logs | CloudWatch |
| EKS Control Plane Logs | CloudWatch |
| Lambda Logs | CloudWatch |
At the moment these only support deploying in the us-east-1 AWS region. See the section "Manual Deployment" for multi-region
instructions.
Launch this stack to export CloudWatch logs to any OTLP compatible endpoint.
| Region | x86_64 | arm64 |
|---|---|---|
us-east-1 |
![]() |
![]() |
Launch this stack to export CloudWatch logs to ClickHouse.
| Region | x86_64 | arm64 |
|---|---|---|
us-east-1 |
![]() |
![]() |
For production deployments, follow these steps to manually deploy the Lambda function using pre-built artifacts.
You can download pre-built deployment Lambda .zip files for x86_64 and arm64 architectures from the Releases page, or from the following links:
| Region | x86_64 | arm64 |
|---|---|---|
| us-east-1 | Download | Download |
The following us-east-1 S3 bucket contains pre-built files for any given release and architecture:
s3://rotel-lambda-forwarder/rotel-lambda-forwarder/v{version}/{arch}/rotel-lambda-forwarder.zip
for the latest release, use the URL:
s3://rotel-lambda-forwarder/rotel-lambda-forwarder/latest/{arch}/rotel-lambda-forwarder.zip
NOTE: These are located in the AWS us-east-1 region, so you can only create Lambda functions in that same region. If
you need to create the function in a different region, you'll need to copy the rotel-lambda-forwarder.zip to a different
bucket in the same region as the function.
Create an IAM role with the necessary permissions for the Lambda function:
aws iam create-role \
--role-name rotel-lambda-forwarder-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'Attach the basic Lambda execution policy:
aws iam attach-role-policy \
--role-name rotel-lambda-forwarder-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRoleIf your function needs additional AWS service access (e.g., S3, DynamoDB), attach those policies as well.
Required Permissions for Log Group Tag Enrichment
The Lambda function needs permissions to:
- List tags on CloudWatch Logs log groups
- Read and write to the S3 bucket for cache persistence
Create and attach a custom policy for tag caching:
aws iam create-policy \
--policy-name rotel-lambda-forwarder-tags-policy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:ListTagsForResource"
],
"Resource": "arn:aws:logs:*:*:log-group:*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/rotel-lambda-forwarder/cache/log-groups/*"
},
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME"
}
]
}'
aws iam attach-role-policy \
--role-name rotel-lambda-forwarder-role \
--policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/rotel-lambda-forwarder-tags-policyReplace YOUR_BUCKET_NAME with your S3 bucket name and YOUR_ACCOUNT_ID with your AWS account ID.
Note the role ARN from the output for the next step.
Create the Lambda function using the AWS CLI:
aws lambda create-function \
--function-name rotel-lambda-forwarder \
--runtime provided.al2023 \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/rotel-lambda-forwarder-role \
--handler bootstrap \
--code S3Bucket=rotel-lambda-forwarder,S3Key=rotel-lambda-forwarder/latest/x86_64/rotel-lambda-forwarder.zip \
--timeout 30 \
--memory-size 256 \
--architectures x86_64 \
--region REGION \
--environment Variables="{
ROTEL_OTLP_EXPORTER_ENDPOINT=https://your-otlp-endpoint.com,
FORWARDER_S3_BUCKET=your-cache-bucket-name
}"Important parameters:
--runtime: Useprovided.al2023for Amazon Linux 2023 custom runtime--architectures: Must match your build target (x86_64_--timeout: Adjust based on your log volume (recommended: 30 seconds)--memory-size: Adjust based on log volume (recommended: 256-512 MB)
To update an existing function with the latest version:
# Update function code
aws lambda update-function-code \
--function-name rotel-lambda-forwarder \
--s3-bucket rotel-lambda-forwarder \
--s3-key rotel-lambda-forwarder/latest/x86_64/rotel-lambda-forwarder.zipUpdate environment variables:
aws lambda update-function-configuration \
--function-name rotel-lambda-forwarder \
--environment Variables="{
ROTEL_OTLP_EXPORTER_ENDPOINT=https://your-otlp-endpoint.com,
FORWARDER_S3_BUCKET=your-cache-bucket-name
}"The Lambda forwarder automatically retrieves and caches tags associated with CloudWatch log groups. Tags are added as resource attributes in the format cloudwatch.log.tags.<tag-key>.
Note: When deploying via CloudFormation, an S3 bucket for tag caching is automatically created with the name rotel-lambda-forwarder-<stack-name>-<account-id>. For manual deployments, you need to create an S3 bucket and set the FORWARDER_S3_BUCKET environment variable.
- First Request: When logs are received from a log group for the first time, the forwarder calls the CloudWatch Logs
ListTagsForResourceAPI to fetch tags. - Caching: Tags are cached in memory with a 15-minute TTL (from the time they were fetched) to avoid repeated API calls.
- S3 Persistence: If configured with an S3 bucket, the tag cache is persisted to S3 for durability across Lambda cold starts.
- Automatic Updates: The cache is automatically updated when new log groups are encountered or when cached entries expire.
CloudFormation Deployments: The S3 bucket is automatically created and configured.
Manual Deployments: Set the FORWARDER_S3_BUCKET environment variable:
FORWARDER_S3_BUCKET=your-cache-bucket-nameThe cache file is stored at: s3://<bucket>/rotel-lambda-forwarder/cache/log-groups/tags.json.gz
Note: If you don't need tag enrichment, you can omit the logs:ListTagsForResource permission from the Lambda's IAM role. A circuit breaker will activate on the first log batch and the forwarder will continue processing logs without tags.
If a log group has tags env=production and team=platform, the resource attributes will include:
cloudwatch.log.tags.env=productioncloudwatch.log.tags.team=platform
The forwarder will attempt to map attributes from a log line to top-level properties of the OTLP log record when possible. This may vary by log source.
trace_idandspan_id: if they are present and these match the OTLP format they'll be promoted to the log record fieldsseverity_textandseverity_number: attempts to maplevelfield to severity fields if possibletimestamp: will be pulled from log fields if possible, or fallback to the observed time when it arrived at the lambda forwarderbody: will look for log, msg, or message fields. Some log sources may construct an appropriate body text from custom attribute fields
If a log record can not be parsed into structured fields, the body of the log record will be set to the full string
contents of the log.
The remaining attributes that are not mapped to top-level log record fields are persisted and stored in the attributes field of the log record. The attribute names are kept identical to their original values in the incoming CloudWatch log record. The forwarder does not remap these field names by default.
This is for several reasons:
- Translation loss: it makes it challenging to translate concepts from AWS documentation if field names are not mapped the same, requiring a guide to map between contexts
- Schema versioning: changes to the mapping require versioning or migration in order to not break existing queries
The forwarder will add an cloudwatch.id field to the log record attributes that represents
the unique ID of the log record in CloudWatch. This can be used to dedup unique log records.
The forwarder supports stripping message fields before they are converted to OTLP log record attributes. This allows sensitive, verbose, or otherwise uncessary fields to be removed before shipping them off to an exporter.
This is hard-coded at the moment and is based on the platform the log record is received from. We plan to make this configurable in the future.
- Strips
responseElements.credentials.sessionToken
The Lambda function is configured via the same environment variables supported by Rotel, as documented in the docs.
TODO
The Lambda Forwarder relies on the at-least-once reliability built into Rotel to ensure that an incoming request is successfully exported before returning success. This means that the lambda function duration will include the time to export to all configured exporters. If an incoming logs request can not be exported during the duration of the function, the Lambda Forwarder will return a failure. CloudWatch is stated to retry up to 24 hours for any unsuccesful subscription Lambda request.
Given that failures will be retried by CloudWatch, it makes little sense for Rotel to retry export requests past the maximum function duration. Therefore, you should set the following environment variable to prevent requests retrying past the function duration. This should be set to the function's maximum timeout.
ROTEL_EXPORTER_RETRY_MAX_ELAPSED_TIME=30sTo forward logs from CloudWatch Logs to the Lambda function, create a subscription filter:
The Lambda function must have permission to be invoked by CloudWatch Logs for each CloudWatch log group you want to collect logs from.
Add this permission:
aws lambda add-permission \
--function-name rotel-lambda-forwarder \
--statement-id cloudwatch-logs-invoke \
--action "lambda:InvokeFunction" \
--principal logs.amazonaws.com \
--source-arn "arn:aws:logs:REGION:ACCOUNT_ID:log-group:/YOUR_LOG_GROUP:*"Replace REGION, ACCOUNT_ID, and YOUR_LOG_GROUP with appropriate values.
Create a subscription filter to forward logs from a CloudWatch Logs log group:
aws logs put-subscription-filter \
--log-group-name YOUR_LOG_GROUP \
--filter-name forward-to-rotel \
--filter-pattern "" \
--destination-arn arn:aws:lambda:REGION:ACCOUNT_ID:function:rotel-lambda-forwarderParameters:
--log-group-name: The CloudWatch Logs log group to forward from--filter-name: A name for the subscription filter--filter-pattern: Log filter pattern (use""to forward all logs, or specify a pattern)--destination-arn: ARN of the rotel-lambda-forwarder function
- Each log group can have a maximum of 2 subscription filters
- The Lambda function must be in the same region as the log group
- Monitor Lambda invocations and errors in CloudWatch Metrics after setup
Want to chat about this project, share feedback, or suggest improvements? Join our Discord server! Whether you're a user of this project or not, we'd love to hear your thoughts and ideas. See you there! 🚀
See DEVELOPING for developer instructions.
See RELEASING.md for release process instructions.
Quick Start:
- GitHub Actions (Recommended): Go to Actions → Bump Version and click "Run workflow"
After merging the version bump PR:
- Tag is automatically created (
auto-tag.yml) - GitHub release is created (
auto-release.yml) - Artifacts are built and uploaded (
release.yml)
Built with ❤️ by Streamfold.
