From c65c9ac32b9eff8e4634fab33408d6ca0addebed Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 9 May 2026 10:44:06 +0200 Subject: [PATCH 1/5] Add logGroupName and logStreamName to LambdaContext Read AWS_LAMBDA_LOG_GROUP_NAME and AWS_LAMBDA_LOG_STREAM_NAME environment variables once at runtime initialization and pass them as stored properties through LambdaContext, matching the pattern used by Python, Node.js, and other AWS Lambda runtimes. Closes #662 Co-Authored-By: Claude Opus 4.6 --- .../Docs.docc/Proposals/0001-v2-api.md | 6 ++ Sources/AWSLambdaRuntime/Lambda.swift | 7 ++- Sources/AWSLambdaRuntime/LambdaContext.swift | 36 +++++++++-- .../LambdaContextTests.swift | 62 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md index ee66087e0..9d2053b4e 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -247,6 +247,12 @@ public struct LambdaContext: Sendable { /// /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. public var logger: Logger { get } + + /// The name of the Amazon CloudWatch Logs group for the function. + public var logGroupName: String { get } + + /// The name of the Amazon CloudWatch Logs stream for the current invocation of the function. + public var logStreamName: String { get } } ``` diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 10e3522c4..37f6817f3 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -85,6 +85,9 @@ public enum Lambda { ) async throws where Handler: StreamingLambdaHandler { var handler = handler + let logGroupName = Lambda.env("AWS_LAMBDA_LOG_GROUP_NAME") ?? "" + let logStreamName = Lambda.env("AWS_LAMBDA_LOG_STREAM_NAME") ?? "" + do { while !Task.isCancelled { @@ -128,7 +131,9 @@ public enum Lambda { deadline: LambdaClock.Instant( millisecondsSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch ), - logger: requestLogger + logger: requestLogger, + logGroupName: logGroupName, + logStreamName: logStreamName ) ) requestLogger.trace("Handler finished processing invocation") diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index d14e16c6b..5a6450d57 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -99,6 +99,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let cognitoIdentity: String? let clientContext: ClientContext? let logger: Logger + let logGroupName: String + let logStreamName: String init( requestID: String, @@ -108,7 +110,9 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { deadline: LambdaClock.Instant, cognitoIdentity: String?, clientContext: ClientContext?, - logger: Logger + logger: Logger, + logGroupName: String, + logStreamName: String ) { self.requestID = requestID self.traceID = traceID @@ -118,6 +122,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { self.cognitoIdentity = cognitoIdentity self.clientContext = clientContext self.logger = logger + self.logGroupName = logGroupName + self.logStreamName = logStreamName } } @@ -169,7 +175,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { *, deprecated, message: - "This method will be removed in a future major version update. Use init(requestID:traceID:tenantID:invokedFunctionARN:deadline:cognitoIdentity:clientContext:logger) instead." + "This method will be removed in a future major version update. Use init(requestID:traceID:tenantID:invokedFunctionARN:deadline:cognitoIdentity:clientContext:logger:logGroupName:logStreamName) instead." ) public init( requestID: String, @@ -199,7 +205,9 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { deadline: LambdaClock.Instant, cognitoIdentity: String? = nil, clientContext: ClientContext? = nil, - logger: Logger + logger: Logger, + logGroupName: String = "", + logStreamName: String = "" ) { self.storage = _Storage( requestID: requestID, @@ -209,10 +217,22 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { deadline: deadline, cognitoIdentity: cognitoIdentity, clientContext: clientContext, - logger: logger + logger: logger, + logGroupName: logGroupName, + logStreamName: logStreamName ) } + /// The name of the Amazon CloudWatch Logs group for the function. + public var logGroupName: String { + self.storage.logGroupName + } + + /// The name of the Amazon CloudWatch Logs stream for the current invocation of the function. + public var logStreamName: String { + self.storage.logStreamName + } + public func getRemainingTime() -> Duration { let deadline = self.deadline return LambdaClock().now.duration(to: deadline) @@ -230,7 +250,9 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { tenantID: String?, invokedFunctionARN: String, timeout: Duration, - logger: Logger + logger: Logger, + logGroupName: String = "", + logStreamName: String = "" ) -> LambdaContext { LambdaContext( requestID: requestID, @@ -238,7 +260,9 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { tenantID: tenantID, invokedFunctionARN: invokedFunctionARN, deadline: LambdaClock().now.advanced(by: timeout), - logger: logger + logger: logger, + logGroupName: logGroupName, + logStreamName: logStreamName ) } } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index bac85e5e9..00921e694 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -136,4 +136,66 @@ struct LambdaContextTests { #expect(remainingTime <= Duration.seconds(31), "Remaining time should be approximately 30 seconds") #expect(remainingTime >= Duration.seconds(-29), "Remaining time should be approximately -30 seconds") } + + @Test("logGroupName returns the value passed at initialization") + @available(LambdaSwift 2.0, *) + func logGroupNameReturnsInitializedValue() { + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + tenantID: nil, + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test"), + logGroupName: "/aws/lambda/my-function" + ) + + #expect(context.logGroupName == "/aws/lambda/my-function") + } + + @Test("logStreamName returns the value passed at initialization") + @available(LambdaSwift 2.0, *) + func logStreamNameReturnsInitializedValue() { + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + tenantID: nil, + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test"), + logStreamName: "2024/01/01/[$LATEST]abcdef1234567890" + ) + + #expect(context.logStreamName == "2024/01/01/[$LATEST]abcdef1234567890") + } + + @Test("logGroupName defaults to empty string") + @available(LambdaSwift 2.0, *) + func logGroupNameDefaultsToEmptyString() { + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + tenantID: nil, + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test") + ) + + #expect(context.logGroupName == "") + } + + @Test("logStreamName defaults to empty string") + @available(LambdaSwift 2.0, *) + func logStreamNameDefaultsToEmptyString() { + let context = LambdaContext.__forTestsOnly( + requestID: "test-request", + traceID: "test-trace", + tenantID: nil, + invokedFunctionARN: "test-arn", + timeout: .seconds(30), + logger: Logger(label: "test") + ) + + #expect(context.logStreamName == "") + } } From a766031a6adf9e01fd1c99dee23a98e9f77a7d3b Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 9 May 2026 10:46:12 +0200 Subject: [PATCH 2/5] Revert v2 API proposal doc change The proposal document reflects the original design and should not be modified retroactively. Co-Authored-By: Claude Opus 4.6 --- Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md index 9d2053b4e..ee66087e0 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -247,12 +247,6 @@ public struct LambdaContext: Sendable { /// /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. public var logger: Logger { get } - - /// The name of the Amazon CloudWatch Logs group for the function. - public var logGroupName: String { get } - - /// The name of the Amazon CloudWatch Logs stream for the current invocation of the function. - public var logStreamName: String { get } } ``` From 95d66e4247d39c49fdf80de485017a8b414ee539 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 9 May 2026 14:22:48 +0200 Subject: [PATCH 3/5] Change logGroupName and logStreamName to optional types Use String? with nil default instead of empty string, consistent with other optional LambdaContext properties like cognitoIdentity and clientContext. Co-Authored-By: Claude Opus 4.6 --- Sources/AWSLambdaRuntime/Lambda.swift | 4 ++-- Sources/AWSLambdaRuntime/LambdaContext.swift | 20 +++++++++---------- .../LambdaContextTests.swift | 12 +++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 37f6817f3..1d7686535 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -85,8 +85,8 @@ public enum Lambda { ) async throws where Handler: StreamingLambdaHandler { var handler = handler - let logGroupName = Lambda.env("AWS_LAMBDA_LOG_GROUP_NAME") ?? "" - let logStreamName = Lambda.env("AWS_LAMBDA_LOG_STREAM_NAME") ?? "" + let logGroupName = Lambda.env("AWS_LAMBDA_LOG_GROUP_NAME") + let logStreamName = Lambda.env("AWS_LAMBDA_LOG_STREAM_NAME") do { while !Task.isCancelled { diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index 5a6450d57..a62959eef 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -99,8 +99,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let cognitoIdentity: String? let clientContext: ClientContext? let logger: Logger - let logGroupName: String - let logStreamName: String + let logGroupName: String? + let logStreamName: String? init( requestID: String, @@ -111,8 +111,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { cognitoIdentity: String?, clientContext: ClientContext?, logger: Logger, - logGroupName: String, - logStreamName: String + logGroupName: String?, + logStreamName: String? ) { self.requestID = requestID self.traceID = traceID @@ -206,8 +206,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { cognitoIdentity: String? = nil, clientContext: ClientContext? = nil, logger: Logger, - logGroupName: String = "", - logStreamName: String = "" + logGroupName: String? = nil, + logStreamName: String? = nil ) { self.storage = _Storage( requestID: requestID, @@ -224,12 +224,12 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { } /// The name of the Amazon CloudWatch Logs group for the function. - public var logGroupName: String { + public var logGroupName: String? { self.storage.logGroupName } /// The name of the Amazon CloudWatch Logs stream for the current invocation of the function. - public var logStreamName: String { + public var logStreamName: String? { self.storage.logStreamName } @@ -251,8 +251,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { invokedFunctionARN: String, timeout: Duration, logger: Logger, - logGroupName: String = "", - logStreamName: String = "" + logGroupName: String? = nil, + logStreamName: String? = nil ) -> LambdaContext { LambdaContext( requestID: requestID, diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index 00921e694..9c72b667a 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -169,9 +169,9 @@ struct LambdaContextTests { #expect(context.logStreamName == "2024/01/01/[$LATEST]abcdef1234567890") } - @Test("logGroupName defaults to empty string") + @Test("logGroupName defaults to nil") @available(LambdaSwift 2.0, *) - func logGroupNameDefaultsToEmptyString() { + func logGroupNameDefaultsToNil() { let context = LambdaContext.__forTestsOnly( requestID: "test-request", traceID: "test-trace", @@ -181,12 +181,12 @@ struct LambdaContextTests { logger: Logger(label: "test") ) - #expect(context.logGroupName == "") + #expect(context.logGroupName == nil) } - @Test("logStreamName defaults to empty string") + @Test("logStreamName defaults to nil") @available(LambdaSwift 2.0, *) - func logStreamNameDefaultsToEmptyString() { + func logStreamNameDefaultsToNil() { let context = LambdaContext.__forTestsOnly( requestID: "test-request", traceID: "test-trace", @@ -196,6 +196,6 @@ struct LambdaContextTests { logger: Logger(label: "test") ) - #expect(context.logStreamName == "") + #expect(context.logStreamName == nil) } } From 420751f7e569df52f77fa48661f92c4dee1b56e8 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 11 May 2026 20:10:41 +0200 Subject: [PATCH 4/5] use an empty string when log group or stream name is not defined --- Sources/AWSLambdaRuntime/LambdaContext.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift index a62959eef..e6086909f 100644 --- a/Sources/AWSLambdaRuntime/LambdaContext.swift +++ b/Sources/AWSLambdaRuntime/LambdaContext.swift @@ -99,8 +99,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let cognitoIdentity: String? let clientContext: ClientContext? let logger: Logger - let logGroupName: String? - let logStreamName: String? + let logGroupName: String + let logStreamName: String init( requestID: String, @@ -111,8 +111,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { cognitoIdentity: String?, clientContext: ClientContext?, logger: Logger, - logGroupName: String?, - logStreamName: String? + logGroupName: String, + logStreamName: String ) { self.requestID = requestID self.traceID = traceID @@ -218,18 +218,18 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { cognitoIdentity: cognitoIdentity, clientContext: clientContext, logger: logger, - logGroupName: logGroupName, - logStreamName: logStreamName + logGroupName: logGroupName ?? "", + logStreamName: logStreamName ?? "" ) } /// The name of the Amazon CloudWatch Logs group for the function. - public var logGroupName: String? { + public var logGroupName: String { self.storage.logGroupName } /// The name of the Amazon CloudWatch Logs stream for the current invocation of the function. - public var logStreamName: String? { + public var logStreamName: String { self.storage.logStreamName } From ecef936f64e02baaaef50374b3a7f5f930b38f19 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 11 May 2026 20:19:04 +0200 Subject: [PATCH 5/5] fix test for empty values --- Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift index 9c72b667a..8341ba150 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaContextTests.swift @@ -181,7 +181,7 @@ struct LambdaContextTests { logger: Logger(label: "test") ) - #expect(context.logGroupName == nil) + #expect(context.logGroupName == "") } @Test("logStreamName defaults to nil") @@ -196,6 +196,6 @@ struct LambdaContextTests { logger: Logger(label: "test") ) - #expect(context.logStreamName == nil) + #expect(context.logStreamName == "") } }