AWS Lambda now supports advanced logging controls that enable functions to emit logs in JSON structured format and control log level granularity. The Swift AWS Lambda Runtime should support these capabilities to provide developers with enhanced logging, filtering, and observability features.
Currently, the Swift runtime emits logs in plaintext (unstructured) format. Adding support for JSON structured logging would enable:
- Easier searching, filtering, and analysis of logs
- Automated log analysis and dashboard creation
- Compliance with OpenTelemetry (OTel) Logs Data Model
- Dynamic log level control without code changes
Current Behavior
- Lambda functions emit logs in plaintext format
- No native support for JSON structured logging
- Log level control requires code changes
- Difficult to query and filter logs programmatically
Expected Behavior
The Swift runtime should support the logging configuration provided by Lambda through environment variables:
- Log Format: Support both
Text (default) and JSON formats via AWS_LAMBDA_LOG_FORMAT
- Application Log Level: Control granularity of application logs (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) via
AWS_LAMBDA_LOG_LEVEL
Note: Custom runtimes are NOT responsible for emitting system logs (START, END, REPORT). The Lambda service handles these automatically.
JSON Log Format Structure
When JSON format is enabled, application logs should follow this structure:
{
"timestamp": "2024-01-16T10:30:45.586Z",
"level": "INFO",
"message": "Processing request",
"requestId": "79b4f56e-95b1-4643-9700-2807f4e68189"
}
Additional fields can be included based on the logging context:
logger: The name/source of the logger
- Custom metadata fields from the application
Implementation Considerations
1. Configuration via Environment Variables
Lambda provides logging configuration through environment variables that custom runtimes should read:
AWS_LAMBDA_LOG_FORMAT: Text or JSON (default: Text)
AWS_LAMBDA_LOG_LEVEL: Application log level - TRACE, DEBUG, INFO, WARN, ERROR, FATAL (default: INFO)
LOG_LEVEL: Legacy environment variable for log level (backward compatibility)
Log Level Precedence:
The runtime should support both AWS_LAMBDA_LOG_LEVEL (new) and LOG_LEVEL (existing) with the following precedence:
-
Text format (default):
- If both
LOG_LEVEL and AWS_LAMBDA_LOG_LEVEL are set: Use LOG_LEVEL (backward compatibility)
- If only
AWS_LAMBDA_LOG_LEVEL is set: Use it
- If only
LOG_LEVEL is set: Use it (existing behavior)
- If neither is set: Use default log level
-
JSON format:
- If both
LOG_LEVEL and AWS_LAMBDA_LOG_LEVEL are set: Use AWS_LAMBDA_LOG_LEVEL and emit a warning
- If only
AWS_LAMBDA_LOG_LEVEL is set: Use it
- If only
LOG_LEVEL is set: Use it but emit a warning recommending AWS_LAMBDA_LOG_LEVEL
- If neither is set: Use default log level
Important: When AWS_LAMBDA_LOG_FORMAT=Text (or not set), the runtime should continue working exactly as it does today with no changes. The implementation considerations below only apply when AWS_LAMBDA_LOG_FORMAT=JSON.
From the AWS documentation:
"When using a custom runtime, you can integrate Lambda's advanced logging controls by checking the value of the AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL environment variables and configuring your runtime's loggers accordingly."
2. Integration with swift-log
The Swift runtime uses the swift-log library for logging. When AWS_LAMBDA_LOG_FORMAT=JSON, we need to:
- Create a custom
LogHandler that supports JSON output
- Respect the configured log level from
AWS_LAMBDA_LOG_LEVEL
- Include Lambda-specific metadata (requestId, traceId, etc.)
- Format logs according to the expected structure
When AWS_LAMBDA_LOG_FORMAT=Text (default), continue using the existing logging implementation.
Logger Initialization Strategy:
The logger initialization should follow a two-phase approach:
-
Runtime Initialization (once per runtime instance):
- Read
AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL environment variables
- Create a
LoggingConfiguration object
- Store configuration for use during invocations
-
Per-Request Logger Creation (once per invocation):
- In the run loop, after receiving invocation metadata
- Create a new
Logger instance with request-specific metadata (requestID, traceID)
- Use the appropriate
LogHandler based on configuration (JSON or Text)
- Pass this logger to
LambdaContext
Why create a new Logger per invocation?
Logger is a value type (struct) - copying is cheap
- The
LogHandler is immutable - cannot be swapped after Logger creation
- For JSON format, request metadata must be in the handler constructor (not added as metadata)
- Clean separation between runtime-level and request-level logging
- No metadata cleanup needed after invocation
This approach ensures:
- Configuration is read once (performance)
- Each invocation gets a logger with correct request metadata
- The logger is automatically propagated through
LambdaContext to all components and user handlers
- No changes needed to user code - they access
context.logger as they do today
3. Structured Logging API
Provide a developer-friendly API for structured logging:
import AWSLambdaRuntime
import Logging
struct MyLambda: SimpleLambdaHandler {
func handle(_ event: String, context: LambdaContext) async throws -> String {
// Automatic JSON formatting when AWS_LAMBDA_LOG_FORMAT=JSON
context.logger.info("Processing event", metadata: [
"eventSize": "\(event.count)",
"userId": "user123"
])
context.logger.debug("Detailed debug information")
context.logger.error("Error occurred", metadata: [
"errorCode": "E001"
])
return "Success"
}
}
4. Log Level Filtering
When AWS_LAMBDA_LOG_LEVEL is set, implement efficient log level filtering:
// Only emit logs at or above the configured level
if logLevel >= configuredLevel {
emitLog(message)
}
This allows operators to control log verbosity without code changes, regardless of whether the format is Text or JSON.
5. Supported Runtimes and Logging Methods
According to AWS documentation, for managed runtimes to support JSON application logs, they must use specific logging methods. For custom runtimes like Swift:
- The runtime must check
AWS_LAMBDA_LOG_FORMAT and format logs accordingly
- The runtime must respect
AWS_LAMBDA_LOG_LEVEL for filtering
- Application logs must be written to stdout/stderr
- JSON logs must include at minimum:
timestamp, level, message, requestId
Proposed Changes
Files to Create/Modify
-
Sources/AWSLambdaRuntime/Logging/JSONLogHandler.swift (new)
- Custom
LogHandler implementation for JSON format
- Include Lambda context metadata (requestId, traceId)
- Format according to expected structure
-
Sources/AWSLambdaRuntime/Logging/LoggingConfiguration.swift (new)
- Read
AWS_LAMBDA_LOG_FORMAT, AWS_LAMBDA_LOG_LEVEL, and LOG_LEVEL from environment
- Implement log level precedence rules (see above)
- Emit warnings when both log level env vars are set
- Validate and parse log levels
- Provide configuration to runtime
-
Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift (modify)
- Initialize
LoggingConfiguration once during runtime initialization
- Store configuration as instance variable
-
Sources/AWSLambdaRuntime/Lambda.swift (modify)
- In
runLoop, create per-request logger using LoggingConfiguration.makeLogger()
- Pass request-specific logger to
LambdaContext
- Remove manual metadata manipulation (requestID) - now handled by LogHandler
-
Sources/AWSLambdaRuntime/Docs.docc/ (modify)
- Add documentation for structured logging
- Provide examples of JSON logging usage
- Document log level configuration
Actual Implementation
Implementation Status: ✅ Complete and tested on both macOS and Linux (Amazon Linux 2)
The implementation has been completed in the feature/structured-json-logging branch with the following files:
1. Sources/AWSLambdaRuntime/Logging/LoggingConfiguration.swift
Complete implementation with log level precedence rules. See the actual file for full details.
Key features:
- Reads
AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL environment variables
- Supports legacy
LOG_LEVEL for backward compatibility
- Implements precedence rules with appropriate warnings
- Provides
makeLogger() factory method for per-request logger creation
2. Sources/AWSLambdaRuntime/Logging/JSONLogHandler.swift
Complete LogHandler implementation for JSON format. See the actual file for full details.
Key features:
- Uses
JSONEncoder with .iso8601 date encoding strategy
- Includes request metadata (requestID, traceID) in every log entry
- Maps swift-log levels to AWS Lambda levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Outputs compact JSON (no pretty printing)
- Uses conditional Foundation imports for cross-platform compatibility:
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
Important Implementation Details:
-
Date Formatting: Uses JSONEncoder.dateEncodingStrategy = .iso8601 which is available and works correctly on both macOS and Linux (Amazon Linux 2). This produces RFC 3339 compliant timestamps like 2024-01-16T10:30:45.586Z.
-
Cross-Platform Compatibility: Uses conditional imports to support both FoundationEssentials (Linux) and Foundation (macOS):
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
-
Per-Request Logger Creation: Each invocation gets a new Logger instance with request-specific metadata. This is efficient because:
Logger is a struct (value type) - copying is cheap
LogHandler is immutable - cannot be swapped after creation
- Request metadata (requestID, traceID) must be in the handler constructor for JSON format
- No metadata cleanup needed after invocation
-
Compilation Verified:
- ✅ macOS build successful
- ✅ Linux (Amazon Linux 2) build successful via Docker
- ✅ All 98 tests pass
Integration Points
In LambdaRuntime.swift:
public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLambdaHandler {
let handlerStorage: SendingStorage<Handler>
let logger: Logger
let eventLoop: EventLoop
let loggingConfiguration: LoggingConfiguration // NEW
public init(
handler: sending Handler,
eventLoop: EventLoop = Lambda.defaultEventLoop,
logger: Logger = Logger(label: "LambdaRuntime")
) {
self.handlerStorage = SendingStorage(handler)
self.eventLoop = eventLoop
// Initialize logger for runtime-level logging (before reading config)
var log = logger
log.logLevel = Lambda.env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? logger.logLevel
self.logger = log
// NEW: Read logging configuration (may emit warnings via logger)
self.loggingConfiguration = LoggingConfiguration(logger: self.logger)
}
}
In Lambda.swift run loop:
public static func runLoop<RuntimeClient: LambdaRuntimeClientProtocol, Handler>(
runtimeClient: RuntimeClient,
handler: Handler,
loggingConfiguration: LoggingConfiguration, // NEW parameter
logger: Logger // Keep for runtime-level logging
) async throws where Handler: StreamingLambdaHandler {
var handler = handler
while !Task.isCancelled {
logger.trace("Waiting for next invocation")
let (invocation, writer) = try await runtimeClient.nextInvocation()
// NEW: Create per-request logger with request metadata
let requestLogger = loggingConfiguration.makeLogger(
label: "Lambda",
requestID: invocation.metadata.requestID,
traceID: invocation.metadata.traceID
)
// Pass request-specific logger to context
try await handler.handle(
invocation.event,
responseWriter: writer,
context: LambdaContext(
requestID: invocation.metadata.requestID,
traceID: invocation.metadata.traceID,
tenantID: invocation.metadata.tenantID,
invokedFunctionARN: invocation.metadata.invokedFunctionARN,
deadline: LambdaClock.Instant(
millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch
),
logger: requestLogger // NEW: Per-request logger
)
)
}
}
SAM/CloudFormation Configuration Example
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .build/plugins/AWSLambdaPackager/outputs/MyLambda/MyLambda.zip
Handler: bootstrap
Runtime: provided.al2
LoggingConfig:
LogFormat: JSON
ApplicationLogLevel: DEBUG
Testing Requirements
Completed Tests
Remaining Tests
Benefits
- Improved Observability: Structured logs are easier to query and analyze
- Better Debugging: Dynamic log level control without redeployment
- Cost Optimization: Reduce log volume in production with higher log levels
- CloudWatch Integration: Better integration with CloudWatch Logs Insights
- Standards Alignment: Follows AWS Lambda logging conventions
- Lambda Managed Instances Support: Required for LMI functions (JSON only)
Migration Considerations
- Default behavior remains unchanged (text format)
- Opt-in via Lambda configuration (
LoggingConfig)
- Existing functions continue to work without changes
LOG_LEVEL environment variable continues to work for backward compatibility
- When using JSON format, prefer
AWS_LAMBDA_LOG_LEVEL over LOG_LEVEL
- Runtime emits warnings when both log level env vars are set to help with migration
- Breaking change warning: Switching to JSON may affect existing log parsing pipelines
References
Related Issues
Labels
enhancement
logging
observability
cloudwatch
Priority
Medium-High: This is a significant enhancement that improves observability and aligns with AWS Lambda best practices. It's also required for Lambda Managed Instances support (which mandates JSON format).
AWS Lambda now supports advanced logging controls that enable functions to emit logs in JSON structured format and control log level granularity. The Swift AWS Lambda Runtime should support these capabilities to provide developers with enhanced logging, filtering, and observability features.
Currently, the Swift runtime emits logs in plaintext (unstructured) format. Adding support for JSON structured logging would enable:
Current Behavior
Expected Behavior
The Swift runtime should support the logging configuration provided by Lambda through environment variables:
Text(default) andJSONformats viaAWS_LAMBDA_LOG_FORMATAWS_LAMBDA_LOG_LEVELNote: Custom runtimes are NOT responsible for emitting system logs (START, END, REPORT). The Lambda service handles these automatically.
JSON Log Format Structure
When JSON format is enabled, application logs should follow this structure:
{ "timestamp": "2024-01-16T10:30:45.586Z", "level": "INFO", "message": "Processing request", "requestId": "79b4f56e-95b1-4643-9700-2807f4e68189" }Additional fields can be included based on the logging context:
logger: The name/source of the loggerImplementation Considerations
1. Configuration via Environment Variables
Lambda provides logging configuration through environment variables that custom runtimes should read:
AWS_LAMBDA_LOG_FORMAT:TextorJSON(default:Text)AWS_LAMBDA_LOG_LEVEL: Application log level -TRACE,DEBUG,INFO,WARN,ERROR,FATAL(default:INFO)LOG_LEVEL: Legacy environment variable for log level (backward compatibility)Log Level Precedence:
The runtime should support both
AWS_LAMBDA_LOG_LEVEL(new) andLOG_LEVEL(existing) with the following precedence:Text format (default):
LOG_LEVELandAWS_LAMBDA_LOG_LEVELare set: UseLOG_LEVEL(backward compatibility)AWS_LAMBDA_LOG_LEVELis set: Use itLOG_LEVELis set: Use it (existing behavior)JSON format:
LOG_LEVELandAWS_LAMBDA_LOG_LEVELare set: UseAWS_LAMBDA_LOG_LEVELand emit a warningAWS_LAMBDA_LOG_LEVELis set: Use itLOG_LEVELis set: Use it but emit a warning recommendingAWS_LAMBDA_LOG_LEVELImportant: When
AWS_LAMBDA_LOG_FORMAT=Text(or not set), the runtime should continue working exactly as it does today with no changes. The implementation considerations below only apply whenAWS_LAMBDA_LOG_FORMAT=JSON.From the AWS documentation:
2. Integration with swift-log
The Swift runtime uses the
swift-loglibrary for logging. WhenAWS_LAMBDA_LOG_FORMAT=JSON, we need to:LogHandlerthat supports JSON outputAWS_LAMBDA_LOG_LEVELWhen
AWS_LAMBDA_LOG_FORMAT=Text(default), continue using the existing logging implementation.Logger Initialization Strategy:
The logger initialization should follow a two-phase approach:
Runtime Initialization (once per runtime instance):
AWS_LAMBDA_LOG_FORMATandAWS_LAMBDA_LOG_LEVELenvironment variablesLoggingConfigurationobjectPer-Request Logger Creation (once per invocation):
Loggerinstance with request-specific metadata (requestID, traceID)LogHandlerbased on configuration (JSON or Text)LambdaContextWhy create a new Logger per invocation?
Loggeris a value type (struct) - copying is cheapLogHandleris immutable - cannot be swapped after Logger creationThis approach ensures:
LambdaContextto all components and user handlerscontext.loggeras they do today3. Structured Logging API
Provide a developer-friendly API for structured logging:
4. Log Level Filtering
When
AWS_LAMBDA_LOG_LEVELis set, implement efficient log level filtering:This allows operators to control log verbosity without code changes, regardless of whether the format is Text or JSON.
5. Supported Runtimes and Logging Methods
According to AWS documentation, for managed runtimes to support JSON application logs, they must use specific logging methods. For custom runtimes like Swift:
AWS_LAMBDA_LOG_FORMATand format logs accordinglyAWS_LAMBDA_LOG_LEVELfor filteringtimestamp,level,message,requestIdProposed Changes
Files to Create/Modify
Sources/AWSLambdaRuntime/Logging/JSONLogHandler.swift(new)LogHandlerimplementation for JSON formatSources/AWSLambdaRuntime/Logging/LoggingConfiguration.swift(new)AWS_LAMBDA_LOG_FORMAT,AWS_LAMBDA_LOG_LEVEL, andLOG_LEVELfrom environmentSources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift(modify)LoggingConfigurationonce during runtime initializationSources/AWSLambdaRuntime/Lambda.swift(modify)runLoop, create per-request logger usingLoggingConfiguration.makeLogger()LambdaContextSources/AWSLambdaRuntime/Docs.docc/(modify)Actual Implementation
Implementation Status: ✅ Complete and tested on both macOS and Linux (Amazon Linux 2)
The implementation has been completed in the
feature/structured-json-loggingbranch with the following files:1.
Sources/AWSLambdaRuntime/Logging/LoggingConfiguration.swiftComplete implementation with log level precedence rules. See the actual file for full details.
Key features:
AWS_LAMBDA_LOG_FORMATandAWS_LAMBDA_LOG_LEVELenvironment variablesLOG_LEVELfor backward compatibilitymakeLogger()factory method for per-request logger creation2.
Sources/AWSLambdaRuntime/Logging/JSONLogHandler.swiftComplete
LogHandlerimplementation for JSON format. See the actual file for full details.Key features:
JSONEncoderwith.iso8601date encoding strategyImportant Implementation Details:
Date Formatting: Uses
JSONEncoder.dateEncodingStrategy = .iso8601which is available and works correctly on both macOS and Linux (Amazon Linux 2). This produces RFC 3339 compliant timestamps like2024-01-16T10:30:45.586Z.Cross-Platform Compatibility: Uses conditional imports to support both FoundationEssentials (Linux) and Foundation (macOS):
Per-Request Logger Creation: Each invocation gets a new
Loggerinstance with request-specific metadata. This is efficient because:Loggeris a struct (value type) - copying is cheapLogHandleris immutable - cannot be swapped after creationCompilation Verified:
Integration Points
In
LambdaRuntime.swift:In
Lambda.swiftrun loop:SAM/CloudFormation Configuration Example
Testing Requirements
Completed Tests
Remaining Tests
JSONLogHandlerLoggingConfigurationparsingBenefits
Migration Considerations
LoggingConfig)LOG_LEVELenvironment variable continues to work for backward compatibilityAWS_LAMBDA_LOG_LEVELoverLOG_LEVELReferences
Related Issues
Labels
enhancementloggingobservabilitycloudwatchPriority
Medium-High: This is a significant enhancement that improves observability and aligns with AWS Lambda best practices. It's also required for Lambda Managed Instances support (which mandates JSON format).