-
Notifications
You must be signed in to change notification settings - Fork 118
Add support for JSON Structured Logging #638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sebsto
wants to merge
30
commits into
main
Choose a base branch
from
feature/structured-json-logging
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
d6c178c
initial implementation of the new loghandler
0b9298e
add proposal doc
sebsto d3611e5
make the log handler public
9f693c3
change version
b3fe872
Merge branch 'main' into feature/structured-json-logging
be0430d
Update design to factor in Lambda Managed Instances
4b086e6
initial implementation
f1661fb
swift-format
3643ffc
fix the example
653b468
use fwrite() and fflush() instead of print()
2395804
add ref to swift log's StreamLogHandler
1c1ea5d
Fix the buffering issue on Lambda
e153f4a
create a runtime level logger for logging before invocations
7bbd5f3
fix YAML lint
b8cefe0
fix docc errors and warnings
b221ff7
LambdaManagedRuntime.init() uses self.loggingConfiguration.makeRuntim…
51ea9d0
fix docc errors
b6c6955
deprcate package-level API to make API checker happy
3e9655b
tiny change on the example README
sebsto 26fab1a
consistent use of traceId
sebsto 27f9ba1
remove duplicate setting of the log level
sebsto d76c53a
remove logging before logger is initialized
sebsto d847a5b
create a JSONEncoder for each log() for thread-safety
sebsto 4036379
consistently uses amazon linux 2023
sebsto 4a0218a
add unit tests for the LogHandler and LoggingConfiguration classes
sebsto 2ecdca4
fix typo
sebsto b869fc3
fix silent error ignoring
sebsto 613c502
Merge branch 'main' into feature/structured-json-logging
sebsto c03c82b
Merge branch 'main' into feature/structured-json-logging
859174c
Merge branch 'main' into feature/structured-json-logging
sebsto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| samconfig.toml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // swift-tools-version:6.2 | ||
|
|
||
| import PackageDescription | ||
|
|
||
| let package = Package( | ||
| name: "swift-aws-lambda-runtime-example", | ||
| platforms: [.macOS(.v15)], | ||
| products: [ | ||
| .executable(name: "JSONLogging", targets: ["JSONLogging"]) | ||
| ], | ||
| dependencies: [ | ||
| // For local development (default) | ||
| // When using the below line, use LAMBDA_USE_LOCAL_DEPS=../.. for swift package archive command, e.g. | ||
| // `LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker` | ||
| .package(name: "swift-aws-lambda-runtime", path: "../..") | ||
|
|
||
| // For standalone usage, comment the line above and uncomment below: | ||
| // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), | ||
| ], | ||
| targets: [ | ||
| .executableTarget( | ||
| name: "JSONLogging", | ||
| dependencies: [ | ||
| .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") | ||
| ], | ||
| path: "Sources" | ||
| ) | ||
| ] | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,268 @@ | ||
| # JSON Logging Example | ||
|
|
||
| This example demonstrates how to use structured JSON logging with AWS Lambda functions written in Swift. When configured with JSON log format, your logs are automatically structured as JSON objects, making them easier to search, filter, and analyze in CloudWatch Logs. | ||
|
|
||
| ## Features | ||
|
|
||
| - Structured JSON log output | ||
| - Automatic inclusion of request ID and trace ID | ||
| - Support for all log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) | ||
| - Custom metadata in logs | ||
| - Compatible with CloudWatch Logs Insights queries | ||
|
|
||
| ## Code | ||
|
|
||
| The Lambda function demonstrates various logging levels and metadata usage. When `AWS_LAMBDA_LOG_FORMAT` is set to `JSON`, all logs are automatically formatted as JSON objects with the following structure: | ||
|
|
||
| ```json | ||
| { | ||
| "timestamp": "2024-10-27T19:17:45.586Z", | ||
| "level": "INFO", | ||
| "message": "Processing request for Alice", | ||
| "requestId": "79b4f56e-95b1-4643-9700-2807f4e68189", | ||
| "traceId": "Root=1-67890abc-def12345678901234567890a" | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Environment Variables | ||
|
|
||
| - `AWS_LAMBDA_LOG_FORMAT`: Set to `JSON` for structured logging (default: `Text`) | ||
| - `AWS_LAMBDA_LOG_LEVEL`: Control which logs are sent to CloudWatch | ||
| - Valid values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL` | ||
| - Default: `INFO` when JSON format is enabled | ||
|
|
||
| ### SAM Template Configuration | ||
|
|
||
| Add the `LoggingConfig` property to your Lambda function: | ||
|
|
||
| ```yaml | ||
| Resources: | ||
| JSONLoggingFunction: | ||
| Type: AWS::Serverless::Function | ||
| Properties: | ||
| CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip | ||
| Handler: swift.bootstrap | ||
| Runtime: provided.al2023 | ||
| Architectures: | ||
| - arm64 | ||
| LoggingConfig: | ||
| LogFormat: JSON | ||
| ApplicationLogLevel: INFO # TRACE | DEBUG | INFO | WARN | ERROR | FATAL | ||
| SystemLogLevel: INFO # DEBUG | INFO | WARN | ||
| ``` | ||
|
|
||
| ## Test Locally | ||
|
|
||
| Start the local server with TEXT logging: | ||
|
|
||
| ```bash | ||
| swift run | ||
| ``` | ||
|
|
||
| Send test requests: | ||
|
|
||
| ```bash | ||
| # Basic request | ||
| curl -d '{"name":"Alice"}' http://127.0.0.1:7000/invoke | ||
|
|
||
| # Request with custom level | ||
| curl -d '{"name":"Bob","level":"debug"}' http://127.0.0.1:7000/invoke | ||
|
|
||
| # Trigger error logging | ||
| curl -d '{"name":"error"}' http://127.0.0.1:7000/invoke | ||
| ``` | ||
|
|
||
| To test with JSON logging locally, set the environment variable: | ||
|
|
||
| ```bash | ||
| AWS_LAMBDA_LOG_FORMAT=JSON swift run | ||
| ``` | ||
|
|
||
| ## Build & Package | ||
|
|
||
| ```bash | ||
| swift build | ||
| LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker | ||
| ``` | ||
|
|
||
| The deployment package will be at: | ||
| `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip` | ||
|
|
||
| ## Deploy with SAM | ||
|
|
||
| Create a `template.yaml` file: | ||
|
|
||
| ```yaml | ||
| AWSTemplateFormatVersion: '2010-09-09' | ||
| Transform: AWS::Serverless-2016-10-31 | ||
| Description: JSON Logging Example | ||
|
|
||
| Resources: | ||
| JSONLoggingFunction: | ||
| Type: AWS::Serverless::Function | ||
| Properties: | ||
| CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip | ||
| Timeout: 60 | ||
| Handler: swift.bootstrap | ||
| Runtime: provided.al2023 | ||
| Architectures: | ||
| - arm64 | ||
| LoggingConfig: | ||
| LogFormat: JSON | ||
| ApplicationLogLevel: DEBUG | ||
| SystemLogLevel: INFO | ||
|
|
||
| Outputs: | ||
| FunctionName: | ||
| Description: Lambda Function Name | ||
| Value: !Ref JSONLoggingFunction | ||
| ``` | ||
|
|
||
| Deploy: | ||
|
|
||
| ```bash | ||
| sam deploy --guided | ||
| ``` | ||
|
|
||
| ## Deploy with AWS CLI | ||
|
|
||
| As an alternative to SAM, you can use the AWS CLI: | ||
|
|
||
| ```bash | ||
| ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text) | ||
| aws lambda create-function \ | ||
| --function-name JSONLoggingExample \ | ||
| --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip \ | ||
| --runtime provided.al2023 \ | ||
| --handler swift.bootstrap \ | ||
| --architectures arm64 \ | ||
| --role arn:aws:iam::${ACCOUNT_ID}:role/lambda_basic_execution \ | ||
| --logging-config LogFormat=JSON,ApplicationLogLevel=DEBUG,SystemLogLevel=INFO | ||
| ``` | ||
|
|
||
| ## Invoke | ||
|
|
||
| ```bash | ||
| aws lambda invoke \ | ||
| --function-name JSONLoggingExample \ | ||
| --cli-binary-format raw-in-base64-out \ | ||
| --payload '{"name":"Alice","level":"debug"}' \ | ||
| response.json && cat response.json && rm response.json | ||
| ``` | ||
|
|
||
| ## Query Logs with CloudWatch Logs Insights | ||
|
|
||
| With JSON formatted logs, you can use powerful queries in [CloudWatch Logs Insights](https://console.aws.amazon.com/cloudwatch/home#logsV2:logs-insights). | ||
|
|
||
| ### Using the AWS Console | ||
|
|
||
| 1. Open the [CloudWatch Logs Insights console](https://console.aws.amazon.com/cloudwatch/home#logsV2:logs-insights) | ||
| 2. In the "Select log group(s)" dropdown, choose the log group for your Lambda function (typically `/aws/lambda/JSONLoggingExample`) | ||
| 3. Type or paste one of the queries below into the query editor | ||
| 4. Adjust the time range in the top-right corner to cover the period you're interested in | ||
| 5. Click "Run query" | ||
|
|
||
| ``` | ||
| # Find all ERROR level logs | ||
| fields @timestamp, level, message, requestId | ||
| | filter level = "ERROR" | ||
| | sort @timestamp desc | ||
|
|
||
| # Find logs for a specific request | ||
| fields @timestamp, level, message | ||
| | filter requestId = "79b4f56e-95b1-4643-9700-2807f4e68189" | ||
| | sort @timestamp asc | ||
|
|
||
| # Count logs by level | ||
| stats count() by level | ||
|
|
||
| # Find logs with specific metadata | ||
| fields @timestamp, message, metadata.errorType | ||
| | filter metadata.errorType = "SimulatedError" | ||
| ``` | ||
|
|
||
| ### Using the AWS CLI | ||
|
|
||
| You can also run Logs Insights queries from the command line. Each query is a two-step process: start the query, then fetch the results. | ||
|
|
||
| ```bash | ||
| # 1. Start a query (adjust --start-time and --end-time as needed) | ||
| QUERY_ID=$(aws logs start-query \ | ||
| --log-group-name '/aws/lambda/JSONLoggingExample' \ | ||
| --start-time $(date -v-1H +%s) \ | ||
| --end-time $(date +%s) \ | ||
| --query-string 'fields @timestamp, level, message | filter level = "ERROR" | sort @timestamp desc' \ | ||
| --query 'queryId' --output text) | ||
|
|
||
| # 2. Wait a moment for the query to complete, then get the results | ||
| sleep 2 | ||
| aws logs get-query-results --query-id "$QUERY_ID" | ||
| ``` | ||
|
|
||
| A few more examples: | ||
|
|
||
| ```bash | ||
| # Count logs by level over the last 24 hours | ||
| QUERY_ID=$(aws logs start-query \ | ||
| --log-group-name '/aws/lambda/JSONLoggingExample' \ | ||
| --start-time $(date -v-24H +%s) \ | ||
| --end-time $(date +%s) \ | ||
| --query-string 'stats count() by level' \ | ||
| --query 'queryId' --output text) | ||
| sleep 2 | ||
| aws logs get-query-results --query-id "$QUERY_ID" | ||
|
|
||
| # Find logs with a specific error type in the last hour | ||
| QUERY_ID=$(aws logs start-query \ | ||
| --log-group-name '/aws/lambda/JSONLoggingExample' \ | ||
| --start-time $(date -v-1H +%s) \ | ||
| --end-time $(date +%s) \ | ||
| --query-string 'fields @timestamp, message, metadata.errorType | filter metadata.errorType = "SimulatedError"' \ | ||
| --query 'queryId' --output text) | ||
| sleep 2 | ||
| aws logs get-query-results --query-id "$QUERY_ID" | ||
| ``` | ||
|
|
||
| > **Note**: On Linux, replace `date -v-1H +%s` with `date -d '1 hour ago' +%s` (and similarly for other time offsets). | ||
|
|
||
| ## Log Levels | ||
|
|
||
| The runtime maps Swift's `Logger.Level` to AWS Lambda log levels: | ||
|
|
||
| | Swift Logger.Level | JSON Output | Description | | ||
| |-------------------|-------------|-------------| | ||
| | `.trace` | `TRACE` | Most detailed | | ||
| | `.debug` | `DEBUG` | Debug information | | ||
| | `.info` | `INFO` | Informational | | ||
| | `.notice` | `INFO` | Notable events | | ||
| | `.warning` | `WARN` | Warning conditions | | ||
| | `.error` | `ERROR` | Error conditions | | ||
| | `.critical` | `FATAL` | Critical failures | | ||
|
|
||
| ## Benefits of JSON Logging | ||
|
|
||
| 1. **Structured Data**: Logs are key-value pairs, not plain text | ||
| 2. **Easy Filtering**: Query specific fields in CloudWatch Logs Insights | ||
| 3. **Automatic Context**: Request ID and trace ID included automatically | ||
| 4. **Metadata Support**: Add custom fields to logs | ||
| 5. **No Double Encoding**: Already-JSON logs aren't double-encoded | ||
| 6. **Better Analysis**: Automated log analysis and alerting | ||
|
|
||
| ## Clean Up | ||
|
|
||
| ```bash | ||
| # SAM deployment | ||
| sam delete | ||
|
|
||
| # AWS CLI deployment | ||
| aws lambda delete-function --function-name JSONLoggingExample | ||
| ``` | ||
|
|
||
| ## ⚠️ Important Notes | ||
|
|
||
| - JSON logging adds metadata, which increases log size | ||
| - Default log level is `INFO` when JSON format is enabled | ||
| - For Python functions, the default changes from `WARN` to `INFO` with JSON format | ||
| - Logs are only formatted as JSON in the Lambda environment, not in local testing (unless you set `AWS_LAMBDA_LOG_FORMAT=JSON`) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the SwiftAWSLambdaRuntime open source project | ||
| // | ||
| // Copyright SwiftAWSLambdaRuntime project authors | ||
| // Copyright (c) Amazon.com, Inc. or its affiliates. | ||
| // Licensed under Apache License v2.0 | ||
| // | ||
| // See LICENSE.txt for license information | ||
| // See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import AWSLambdaRuntime | ||
|
|
||
| #if canImport(FoundationEssentials) | ||
| import FoundationEssentials | ||
| #else | ||
| import Foundation | ||
| #endif | ||
|
|
||
| // This example demonstrates structured JSON logging in AWS Lambda | ||
| // When AWS_LAMBDA_LOG_FORMAT=JSON, logs are automatically formatted as JSON | ||
|
|
||
| struct Request: Decodable { | ||
| let name: String | ||
| let level: String? | ||
| } | ||
|
|
||
| struct Response: Encodable { | ||
| let message: String | ||
| let timestamp: String | ||
| } | ||
|
|
||
| let runtime = LambdaRuntime { | ||
| (event: Request, context: LambdaContext) in | ||
|
|
||
| // These log statements will be formatted as JSON when AWS_LAMBDA_LOG_FORMAT=JSON | ||
| context.logger.trace("Processing request with trace level") | ||
| context.logger.debug("Request details", metadata: ["name": .string(event.name)]) | ||
| context.logger.info("Processing request for \(event.name)") | ||
|
|
||
| if let level = event.level { | ||
| context.logger.notice("Custom log level requested: \(level)") | ||
| } | ||
|
|
||
| context.logger.warning("This is a warning message") | ||
|
|
||
| // Simulate different scenarios | ||
| if event.name.lowercased() == "error" { | ||
| context.logger.error( | ||
| "Error scenario triggered", | ||
| metadata: [ | ||
| "errorType": .string("SimulatedError"), | ||
| "errorCode": .string("TEST_ERROR"), | ||
| ] | ||
| ) | ||
| } | ||
|
|
||
| return Response( | ||
| message: "Hello \(event.name)! Logs are in JSON format.", | ||
| timestamp: Date().ISO8601Format() | ||
| ) | ||
| } | ||
|
|
||
| try await runtime.run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| AWSTemplateFormatVersion: '2010-09-09' | ||
| Transform: AWS::Serverless-2016-10-31 | ||
| Description: JSON Logging Example | ||
|
|
||
| Resources: | ||
| JSONLoggingFunction: | ||
| Type: AWS::Serverless::Function | ||
| Properties: | ||
| CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip | ||
| Timeout: 60 | ||
| Handler: swift.bootstrap | ||
| Runtime: provided.al2023 | ||
| MemorySize: 128 | ||
| Architectures: | ||
| - arm64 | ||
| LoggingConfig: | ||
| LogFormat: JSON | ||
| ApplicationLogLevel: DEBUG | ||
| SystemLogLevel: INFO | ||
|
|
||
| Outputs: | ||
| FunctionName: | ||
| Description: Lambda Function Name | ||
| Value: !Ref JSONLoggingFunction |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.