Lambda Loadout

Gem Version License: MIT Ruby

AWS Lambda Powertools for Ruby — A developer toolkit to implement serverless best practices and increase developer velocity.

Lambda Loadout provides structured logging, CloudWatch metrics via Embedded Metric Format (EMF), error handling, error notifications via SNS, and alerting for AWS Lambda functions. Inspired by AWS Lambda Powertools for Python.

Features

  • CloudWatch Metrics (EMF) — Custom metrics via CloudWatch Embedded Metric Format (zero API calls, zero latency overhead)
  • Structured Logging — JSON structured logging with automatic Lambda context enrichment
  • Error Notifications — Detailed error alerts via SNS with CloudWatch deep links and event source detection
  • Error Handling & Alerting — Automatic error capture with CloudWatch alarm configuration helpers (CloudFormation & Terraform)
  • Cold Start Tracking — Built-in cold start metric capture
  • Middleware Pattern — Clean with_observability wrapper and Handler DSL for Ruby Lambda handlers
  • Global Configuration — Module-level API for shared logger/metrics instances
  • Lambda Layer Support — Build and publish as a Lambda layer via Rake tasks

Installation

Add to your Lambda function's Gemfile:

gem 'lambda_loadout'

Or install directly:

gem install lambda_loadout

Dependencies

  • json (~> 2.0) — JSON serialization (Ruby stdlib)
  • aws-sdk-sns (~> 1.0) — SNS error notifications

Quick Start

Basic Usage

require 'lambda_loadout'

LOGGER = LambdaLoadout::Logger.new(service: "payment")
METRICS = LambdaLoadout::Metrics.new(namespace: "MyApp", service: "payment")

def lambda_handler(event:, context:)
  LambdaLoadout.with_logging_and_metrics(LOGGER, METRICS, context) do
    LOGGER.info("Processing payment", order_id: event['orderId'])
    METRICS.add_metric(name: "PaymentProcessed", unit: "Count", value: 1)
    METRICS.add_dimension(name: "payment_type", value: event['type'])

    { statusCode: 200, body: "Payment processed" }
  end
end

With Error Notifications

def lambda_handler(event:, context:)
  LambdaLoadout.with_logging_and_metrics(
    LOGGER, METRICS, context,
    event: event,
    error_notification_config: { sns_topic_arn: ENV['ERROR_NOTIFICATION_TOPIC_ARN'] }
  ) do
    process_payment(event)
    { statusCode: 200, body: "Success" }
  end
end

When an error occurs, the ErrorNotifier sends a detailed SNS message including:

  • Error class, message, and stack trace (first 15 lines)
  • Lambda request context (function name, request ID, memory, remaining time)
  • Event source detection (API Gateway, SQS, SNS, DynamoDB Streams, EventBridge)
  • Deep links to CloudWatch Logs Insights (pre-filtered query) and log streams
  • JSON-formatted error data for programmatic consumption

Notification failures are caught and logged — they won't break your Lambda execution.

Using Middleware Pattern

require 'lambda_loadout'

class PaymentHandler
  include LambdaLoadout::Middleware

  def initialize
    @logger = LambdaLoadout::Logger.new(service: "payment")
    @metrics = LambdaLoadout::Metrics.new(namespace: "MyApp", service: "payment")
  end

  def call(event:, context:)
    with_observability(context) do
      @logger.info("Processing payment", event: event)
      result = process_payment(event)
      @metrics.add_metric(name: "PaymentProcessed", unit: "Count", value: 1)
      { statusCode: 200, body: result.to_json }
    end
  end
end

HANDLER = PaymentHandler.new

def lambda_handler(event:, context:)
  HANDLER.call(event: event, context: context)
end

Using Handler DSL

require 'lambda_loadout'

LambdaLoadout::Handler.configure do |config|
  config.service = "payment"
  config.namespace = "MyApp"
  config.log_level = :info
  config.capture_cold_start = true
end

def lambda_handler(event:, context:)
  LambdaLoadout::Handler.call(event, context) do |logger, metrics|
    logger.info("Processing event", event_type: event['type'])
    metrics.add_metric(name: "EventProcessed", unit: "Count", value: 1)
    { statusCode: 200, body: "OK" }
  end
end

Global Configuration

require 'lambda_loadout'

LambdaLoadout.configure do |config|
  config.service = "payment-api"
  config.namespace = "MyApp"
  config.log_level = :debug
end

# Access global instances anywhere
LambdaLoadout.logger.info("Starting up")
LambdaLoadout.metrics.add_metric(name: "Boot", unit: "Count", value: 1)

CloudWatch Metrics (EMF)

Metrics are published via CloudWatch Embedded Metric Format — structured JSON written to stdout that CloudWatch automatically extracts. No PutMetricData API calls, no additional latency.

Creating Metrics

metrics = LambdaLoadout::Metrics.new(namespace: "MyApp", service: "payment")

metrics.add_metric(name: "BookingConfirmation", unit: "Count", value: 1)
metrics.add_metric(name: "ResponseTime", unit: "Milliseconds", value: 145.5)
metrics.add_dimension(name: "environment", value: "production")
metrics.(key: "booking_id", value: "abc-123")
metrics.flush

High-Resolution Metrics

metrics.add_metric(name: "HighPrecisionMetric", unit: "Count", value: 1, resolution: 1)

Default Dimensions

metrics.set_default_dimensions(environment: "production", region: "us-east-1")
# These persist across flushes

Custom Timestamps

metrics.set_timestamp(Time.now)           # Time object
metrics.set_timestamp(1699876543000)      # Epoch milliseconds
# Validates within CloudWatch limits (14 days past, 2 hours future)

Auto-Flush

Metrics automatically flush when the 100-metric limit is reached. You can also call metrics.flush manually or rely on the with_logging_and_metrics / with_observability wrappers to flush in their ensure block.

Supported Units

Count, Seconds, Milliseconds, Microseconds, Bytes, Kilobytes, Megabytes, Gigabytes, Terabytes, Bits, Kilobits, Megabits, Gigabits, Terabits, Percent, Count/Second, Bytes/Second, Kilobytes/Second, Megabytes/Second, Gigabytes/Second, Terabytes/Second, Bits/Second, Kilobits/Second, Megabits/Second, Gigabits/Second, Terabits/Second, None

EMF Output Example

{
  "_aws": {
    "Timestamp": 1699876543000,
    "CloudWatchMetrics": [{
      "Namespace": "MyApp",
      "Dimensions": [["service", "environment"]],
      "Metrics": [
        {"Name": "PaymentProcessed", "Unit": "Count"},
        {"Name": "ResponseTime", "Unit": "Milliseconds"}
      ]
    }]
  },
  "service": "payment",
  "environment": "production",
  "PaymentProcessed": 1.0,
  "ResponseTime": 145.5,
  "booking_id": "abc-123"
}

Structured Logging

Basic Logging

logger = LambdaLoadout::Logger.new(service: "payment", level: :info)

logger.info("Payment processed", order_id: "12345", amount: 99.99)
# => {"level":"INFO","timestamp":"2025-11-13T12:00:00.000Z","message":"Payment processed","service":"payment","order_id":"12345","amount":99.99}

Lambda Context Injection

def lambda_handler(event:, context:)
  logger.inject_lambda_context(context)
  logger.info("Processing request")
  # Output includes: function_name, function_version, function_request_id, function_memory_size, cold_start
end

Exception Logging

begin
  risky_operation()
rescue StandardError => e
  logger.error("Operation failed", e, order_id: order_id)
  # Automatically includes error message, error_class, and backtrace (first 10 lines)

  # Or use the dedicated exception method
  logger.exception(e, message: "Operation failed", order_id: order_id)
end

Persistent Fields

logger.append_keys(correlation_id: "abc-123", tenant_id: "tenant-1")
logger.info("Event 1")  # Includes correlation_id and tenant_id

logger.remove_keys(:tenant_id)
logger.info("Event 2")  # Only includes correlation_id

Debug Log Sampling

logger = LambdaLoadout::Logger.new(service: "payment", level: :debug, sampling_rate: 0.1)
# Only 10% of debug logs will be emitted

Log Levels

debug, info, warn, error, fatal

Error Notifications via SNS

Sends rich error alerts when Lambda functions fail.

Standalone Usage

notifier = LambdaLoadout::ErrorNotifier.new(
  sns_topic_arn: ENV['ERROR_NOTIFICATION_TOPIC_ARN'],
  logger: logger,
  region: 'us-east-1'  # optional, defaults to AWS_REGION env var
)

notifier.notify(error: e, context: context, event: event)

Integrated Usage

Pass error_notification_config to with_logging_and_metrics (see Quick Start above). Notifications are sent automatically on unhandled exceptions.

What's Included in Each Notification

  • Error class, message, and stack trace (first 15 lines)
  • Lambda request context (function name, request ID, memory, remaining time)
  • Event source detection (API Gateway, SQS, SNS, DynamoDB Streams, EventBridge)
  • Deep links to CloudWatch Logs Insights (pre-filtered query) and log streams
  • JSON-formatted error data for programmatic consumption

Error Handling & Alarms

Automatic Error Capture

handler = LambdaLoadout::ErrorHandler.new(
  logger: logger,
  metrics: metrics,
  capture_stack_trace: true,
  error_metric_name: "LambdaError"  # default
)

def lambda_handler(event:, context:)
  handler.handle(context) do
    process_event(event)
  end
end

CloudWatch Alarm Configuration

Generate CloudFormation or Terraform for alarms:

alarm = LambdaLoadout::AlarmConfig.new(
  metric_name: "LambdaError",
  namespace: "MyApp",
  threshold: 1,
  evaluation_periods: 1,
  period: 60,
  statistic: "Sum",
  comparison_operator: "GreaterThanOrEqualToThreshold"
)

puts alarm.to_cloudformation(
  alarm_name: "PaymentErrorAlarm",
  sns_topic_arn: "arn:aws:sns:us-east-1:123456789012:alerts",
  dimensions: { service: "payment" }
)

puts alarm.to_terraform(
  resource_name: "payment_error_alarm",
  sns_topic_arn: "arn:aws:sns:us-east-1:123456789012:alerts",
  dimensions: { service: "payment" }
)

Lambda Layer

Lambda Loadout can be published as a Lambda layer so your functions don't need to bundle it in their deployment packages. This is the recommended approach for teams sharing the gem across multiple Lambda functions.

Rake Tasks

# Build the layer zip
bundle exec rake layer:build

# Build and publish to AWS (requires configured AWS CLI)
bundle exec rake layer:publish

# Clean build artifacts
bundle exec rake layer:clean

The layer targets the ruby3.4 runtime. After publishing, add the returned LayerVersionArn to your Lambda function configuration.

Shell Scripts (alternative)

./scripts/build_layer.sh      # Build only
./scripts/publish_layer.sh    # Publish (requires prior build)
./scripts/release.sh          # Build + publish interactively

Using the Layer in Your Lambda

Terraform:

resource "aws_lambda_function" "my_function" {
  # ...
  layers = ["arn:aws:lambda:us-east-1:ACCOUNT:layer:lambda-loadout:VERSION"]
}

CloudFormation / SAM:

MyFunction:
  Type: AWS::Lambda::Function
  Properties:
    Layers:
      - arn:aws:lambda:us-east-1:ACCOUNT:layer:lambda-loadout:VERSION

Environment Variables

Variable Description
POWERTOOLS_SERVICE_NAME Default service name for Logger and Metrics
POWERTOOLS_METRICS_NAMESPACE Default metrics namespace
ERROR_NOTIFICATION_TOPIC_ARN SNS topic ARN for error notifications
ENVIRONMENT / STAGE Environment name (included in error notifications)
AWS_REGION AWS region (used by ErrorNotifier, defaults to us-east-1)

Testing

Testing with Metrics

output = StringIO.new
metrics = LambdaLoadout::Metrics.new(namespace: "MyApp", output: output)

metrics.add_metric(name: "TestMetric", unit: "Count", value: 1)
metrics.flush

emf_output = JSON.parse(output.string)
expect(emf_output['TestMetric']).to eq(1)

Testing with Logger

output = StringIO.new
logger = LambdaLoadout::Logger.new(service: "test", output: output)

logger.info("Test message", data: "value")

log_entry = JSON.parse(output.string)
expect(log_entry['message']).to eq("Test message")

Both Logger and Metrics accept an output: parameter, making it easy to capture and assert on output in tests without mocking stdout.

Development

# Install dependencies
bundle install

# Run tests
bundle exec rake spec

# Run linter
bundle exec rake rubocop

# Run both (default task)
bundle exec rake

# List all available tasks
bundle exec rake -T

Project Structure

lambda-loadout/
├── lib/lambda_loadout/
│   ├── version.rb          # Gem version
│   ├── logger.rb           # Structured JSON logging
│   ├── metrics.rb          # CloudWatch EMF metrics
│   ├── errors.rb           # ErrorHandler, AlarmConfig, custom error classes
│   ├── error_notifier.rb   # SNS error notifications with CloudWatch deep links
│   ├── middleware.rb        # Middleware module + Handler DSL
│   └── global.rb           # Module-level API & global config
├── examples/               # Working Lambda handler examples
├── spec/                   # RSpec test suite
├── scripts/                # Layer build & publish scripts
└── docs/                   # Integration guide, alarm templates

Roadmap

  • [x] CloudWatch Metrics (EMF)
  • [x] Structured Logging
  • [x] Error Handling & Alarm Config (CloudFormation + Terraform)
  • [x] Cold Start Tracking
  • [x] Error Notifications via SNS
  • [x] Middleware Pattern & Handler DSL
  • [x] Global Configuration API
  • [ ] X-Ray Tracing Integration
  • [ ] Parameter Store / Secrets Manager Integration
  • [ ] Idempotency Support
  • [ ] Batch Processing Utilities
  • [ ] Event Source Data Classes

License

MIT — see LICENSE.txt for details.