Skip to content

Commit 553b5e3

Browse files
sebstoSebastien Stormacq
andauthored
Add support for JSON Structured Logging (#638)
This PR adds support for Structured Logging, as per [the design document](https://github.com/awslabs/swift-aws-lambda-runtime/blob/feature/structured-json-logging/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0002-logging.md) --------- Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
1 parent 1df89fd commit 553b5e3

File tree

16 files changed

+1848
-39
lines changed

16 files changed

+1848
-39
lines changed

Examples/JSONLogging/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
samconfig.toml

Examples/JSONLogging/Package.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version:6.2
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-aws-lambda-runtime-example",
7+
platforms: [.macOS(.v15)],
8+
products: [
9+
.executable(name: "JSONLogging", targets: ["JSONLogging"])
10+
],
11+
dependencies: [
12+
// For local development (default)
13+
// When using the below line, use LAMBDA_USE_LOCAL_DEPS=../.. for swift package archive command, e.g.
14+
// `LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker`
15+
.package(name: "swift-aws-lambda-runtime", path: "../..")
16+
17+
// For standalone usage, comment the line above and uncomment below:
18+
// .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"),
19+
],
20+
targets: [
21+
.executableTarget(
22+
name: "JSONLogging",
23+
dependencies: [
24+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
25+
],
26+
path: "Sources"
27+
)
28+
]
29+
)

Examples/JSONLogging/README.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# JSON Logging Example
2+
3+
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.
4+
5+
## Features
6+
7+
- Structured JSON log output
8+
- Automatic inclusion of request ID and trace ID
9+
- Support for all log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
10+
- Custom metadata in logs
11+
- Compatible with CloudWatch Logs Insights queries
12+
13+
## Code
14+
15+
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:
16+
17+
```json
18+
{
19+
"timestamp": "2024-10-27T19:17:45.586Z",
20+
"level": "INFO",
21+
"message": "Processing request for Alice",
22+
"requestId": "79b4f56e-95b1-4643-9700-2807f4e68189",
23+
"traceId": "Root=1-67890abc-def12345678901234567890a"
24+
}
25+
```
26+
27+
## Configuration
28+
29+
### Environment Variables
30+
31+
- `AWS_LAMBDA_LOG_FORMAT`: Set to `JSON` for structured logging (default: `Text`)
32+
- `AWS_LAMBDA_LOG_LEVEL`: Control which logs are sent to CloudWatch
33+
- Valid values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`
34+
- Default: `INFO` when JSON format is enabled
35+
36+
### SAM Template Configuration
37+
38+
Add the `LoggingConfig` property to your Lambda function:
39+
40+
```yaml
41+
Resources:
42+
JSONLoggingFunction:
43+
Type: AWS::Serverless::Function
44+
Properties:
45+
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip
46+
Handler: swift.bootstrap
47+
Runtime: provided.al2023
48+
Architectures:
49+
- arm64
50+
LoggingConfig:
51+
LogFormat: JSON
52+
ApplicationLogLevel: INFO # TRACE | DEBUG | INFO | WARN | ERROR | FATAL
53+
SystemLogLevel: INFO # DEBUG | INFO | WARN
54+
```
55+
56+
## Test Locally
57+
58+
Start the local server with TEXT logging:
59+
60+
```bash
61+
swift run
62+
```
63+
64+
Send test requests:
65+
66+
```bash
67+
# Basic request
68+
curl -d '{"name":"Alice"}' http://127.0.0.1:7000/invoke
69+
70+
# Request with custom level
71+
curl -d '{"name":"Bob","level":"debug"}' http://127.0.0.1:7000/invoke
72+
73+
# Trigger error logging
74+
curl -d '{"name":"error"}' http://127.0.0.1:7000/invoke
75+
```
76+
77+
To test with JSON logging locally, set the environment variable:
78+
79+
```bash
80+
AWS_LAMBDA_LOG_FORMAT=JSON swift run
81+
```
82+
83+
## Build & Package
84+
85+
```bash
86+
swift build
87+
LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker
88+
```
89+
90+
The deployment package will be at:
91+
`.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip`
92+
93+
## Deploy with SAM
94+
95+
Create a `template.yaml` file:
96+
97+
```yaml
98+
AWSTemplateFormatVersion: '2010-09-09'
99+
Transform: AWS::Serverless-2016-10-31
100+
Description: JSON Logging Example
101+
102+
Resources:
103+
JSONLoggingFunction:
104+
Type: AWS::Serverless::Function
105+
Properties:
106+
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip
107+
Timeout: 60
108+
Handler: swift.bootstrap
109+
Runtime: provided.al2023
110+
Architectures:
111+
- arm64
112+
LoggingConfig:
113+
LogFormat: JSON
114+
ApplicationLogLevel: DEBUG
115+
SystemLogLevel: INFO
116+
117+
Outputs:
118+
FunctionName:
119+
Description: Lambda Function Name
120+
Value: !Ref JSONLoggingFunction
121+
```
122+
123+
Deploy:
124+
125+
```bash
126+
sam deploy --guided
127+
```
128+
129+
## Deploy with AWS CLI
130+
131+
As an alternative to SAM, you can use the AWS CLI:
132+
133+
```bash
134+
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
135+
aws lambda create-function \
136+
--function-name JSONLoggingExample \
137+
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip \
138+
--runtime provided.al2023 \
139+
--handler swift.bootstrap \
140+
--architectures arm64 \
141+
--role arn:aws:iam::${ACCOUNT_ID}:role/lambda_basic_execution \
142+
--logging-config LogFormat=JSON,ApplicationLogLevel=DEBUG,SystemLogLevel=INFO
143+
```
144+
145+
## Invoke
146+
147+
```bash
148+
aws lambda invoke \
149+
--function-name JSONLoggingExample \
150+
--cli-binary-format raw-in-base64-out \
151+
--payload '{"name":"Alice","level":"debug"}' \
152+
response.json && cat response.json && rm response.json
153+
```
154+
155+
## Query Logs with CloudWatch Logs Insights
156+
157+
With JSON formatted logs, you can use powerful queries in [CloudWatch Logs Insights](https://console.aws.amazon.com/cloudwatch/home#logsV2:logs-insights).
158+
159+
### Using the AWS Console
160+
161+
1. Open the [CloudWatch Logs Insights console](https://console.aws.amazon.com/cloudwatch/home#logsV2:logs-insights)
162+
2. In the "Select log group(s)" dropdown, choose the log group for your Lambda function (typically `/aws/lambda/JSONLoggingExample`)
163+
3. Type or paste one of the queries below into the query editor
164+
4. Adjust the time range in the top-right corner to cover the period you're interested in
165+
5. Click "Run query"
166+
167+
```
168+
# Find all ERROR level logs
169+
fields @timestamp, level, message, requestId
170+
| filter level = "ERROR"
171+
| sort @timestamp desc
172+
173+
# Find logs for a specific request
174+
fields @timestamp, level, message
175+
| filter requestId = "79b4f56e-95b1-4643-9700-2807f4e68189"
176+
| sort @timestamp asc
177+
178+
# Count logs by level
179+
stats count() by level
180+
181+
# Find logs with specific metadata
182+
fields @timestamp, message, metadata.errorType
183+
| filter metadata.errorType = "SimulatedError"
184+
```
185+
186+
### Using the AWS CLI
187+
188+
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.
189+
190+
```bash
191+
# 1. Start a query (adjust --start-time and --end-time as needed)
192+
QUERY_ID=$(aws logs start-query \
193+
--log-group-name '/aws/lambda/JSONLoggingExample' \
194+
--start-time $(date -v-1H +%s) \
195+
--end-time $(date +%s) \
196+
--query-string 'fields @timestamp, level, message | filter level = "ERROR" | sort @timestamp desc' \
197+
--query 'queryId' --output text)
198+
199+
# 2. Wait a moment for the query to complete, then get the results
200+
sleep 2
201+
aws logs get-query-results --query-id "$QUERY_ID"
202+
```
203+
204+
A few more examples:
205+
206+
```bash
207+
# Count logs by level over the last 24 hours
208+
QUERY_ID=$(aws logs start-query \
209+
--log-group-name '/aws/lambda/JSONLoggingExample' \
210+
--start-time $(date -v-24H +%s) \
211+
--end-time $(date +%s) \
212+
--query-string 'stats count() by level' \
213+
--query 'queryId' --output text)
214+
sleep 2
215+
aws logs get-query-results --query-id "$QUERY_ID"
216+
217+
# Find logs with a specific error type in the last hour
218+
QUERY_ID=$(aws logs start-query \
219+
--log-group-name '/aws/lambda/JSONLoggingExample' \
220+
--start-time $(date -v-1H +%s) \
221+
--end-time $(date +%s) \
222+
--query-string 'fields @timestamp, message, metadata.errorType | filter metadata.errorType = "SimulatedError"' \
223+
--query 'queryId' --output text)
224+
sleep 2
225+
aws logs get-query-results --query-id "$QUERY_ID"
226+
```
227+
228+
> **Note**: On Linux, replace `date -v-1H +%s` with `date -d '1 hour ago' +%s` (and similarly for other time offsets).
229+
230+
## Log Levels
231+
232+
The runtime maps Swift's `Logger.Level` to AWS Lambda log levels:
233+
234+
| Swift Logger.Level | JSON Output | Description |
235+
|-------------------|-------------|-------------|
236+
| `.trace` | `TRACE` | Most detailed |
237+
| `.debug` | `DEBUG` | Debug information |
238+
| `.info` | `INFO` | Informational |
239+
| `.notice` | `INFO` | Notable events |
240+
| `.warning` | `WARN` | Warning conditions |
241+
| `.error` | `ERROR` | Error conditions |
242+
| `.critical` | `FATAL` | Critical failures |
243+
244+
## Benefits of JSON Logging
245+
246+
1. **Structured Data**: Logs are key-value pairs, not plain text
247+
2. **Easy Filtering**: Query specific fields in CloudWatch Logs Insights
248+
3. **Automatic Context**: Request ID and trace ID included automatically
249+
4. **Metadata Support**: Add custom fields to logs
250+
5. **No Double Encoding**: Already-JSON logs aren't double-encoded
251+
6. **Better Analysis**: Automated log analysis and alerting
252+
253+
## Clean Up
254+
255+
```bash
256+
# SAM deployment
257+
sam delete
258+
259+
# AWS CLI deployment
260+
aws lambda delete-function --function-name JSONLoggingExample
261+
```
262+
263+
## ⚠️ Important Notes
264+
265+
- JSON logging adds metadata, which increases log size
266+
- Default log level is `INFO` when JSON format is enabled
267+
- For Python functions, the default changes from `WARN` to `INFO` with JSON format
268+
- Logs are only formatted as JSON in the Lambda environment, not in local testing (unless you set `AWS_LAMBDA_LOG_FORMAT=JSON`)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright SwiftAWSLambdaRuntime project authors
6+
// Copyright (c) Amazon.com, Inc. or its affiliates.
7+
// Licensed under Apache License v2.0
8+
//
9+
// See LICENSE.txt for license information
10+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
11+
//
12+
// SPDX-License-Identifier: Apache-2.0
13+
//
14+
//===----------------------------------------------------------------------===//
15+
16+
import AWSLambdaRuntime
17+
18+
#if canImport(FoundationEssentials)
19+
import FoundationEssentials
20+
#else
21+
import Foundation
22+
#endif
23+
24+
// This example demonstrates structured JSON logging in AWS Lambda
25+
// When AWS_LAMBDA_LOG_FORMAT=JSON, logs are automatically formatted as JSON
26+
27+
struct Request: Decodable {
28+
let name: String
29+
let level: String?
30+
}
31+
32+
struct Response: Encodable {
33+
let message: String
34+
let timestamp: String
35+
}
36+
37+
let runtime = LambdaRuntime {
38+
(event: Request, context: LambdaContext) in
39+
40+
// These log statements will be formatted as JSON when AWS_LAMBDA_LOG_FORMAT=JSON
41+
context.logger.trace("Processing request with trace level")
42+
context.logger.debug("Request details", metadata: ["name": .string(event.name)])
43+
context.logger.info("Processing request for \(event.name)")
44+
45+
if let level = event.level {
46+
context.logger.notice("Custom log level requested: \(level)")
47+
}
48+
49+
context.logger.warning("This is a warning message")
50+
51+
// Simulate different scenarios
52+
if event.name.lowercased() == "error" {
53+
context.logger.error(
54+
"Error scenario triggered",
55+
metadata: [
56+
"errorType": .string("SimulatedError"),
57+
"errorCode": .string("TEST_ERROR"),
58+
]
59+
)
60+
}
61+
62+
return Response(
63+
message: "Hello \(event.name)! Logs are in JSON format.",
64+
timestamp: Date().ISO8601Format()
65+
)
66+
}
67+
68+
try await runtime.run()

Examples/JSONLogging/template.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: JSON Logging Example
4+
5+
Resources:
6+
JSONLoggingFunction:
7+
Type: AWS::Serverless::Function
8+
Properties:
9+
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/JSONLogging/JSONLogging.zip
10+
Timeout: 60
11+
Handler: swift.bootstrap
12+
Runtime: provided.al2023
13+
MemorySize: 128
14+
Architectures:
15+
- arm64
16+
LoggingConfig:
17+
LogFormat: JSON
18+
ApplicationLogLevel: DEBUG
19+
SystemLogLevel: INFO
20+
21+
Outputs:
22+
FunctionName:
23+
Description: Lambda Function Name
24+
Value: !Ref JSONLoggingFunction

0 commit comments

Comments
 (0)