Files
swift-aws-lambda-runtime/Sources/AWSLambdaRuntime/Logging/LoggingConfiguration.swift
T
Sébastien Stormacq 553b5e3716 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>
2026-02-25 08:36:49 +01:00

148 lines
5.4 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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 Logging
@available(LambdaSwift 2.0, *)
public struct LoggingConfiguration: Sendable {
public enum LogFormat: String, Sendable {
case text = "Text"
case json = "JSON"
}
public let format: LogFormat
public let applicationLogLevel: Logger.Level?
/// Stores the raw environment variable value when it couldn't be parsed as a valid log level.
/// Callers should use `logConfigurationWarnings(logger:)` after obtaining a configured logger.
private let unrecognizedLogLevel: String?
private let baseLogger: Logger
/// Note: No log messages are emitted during initialization because the logging
/// configuration is not yet fully constructed. The provided `logger` still uses its
/// original format and log level, so any messages emitted here would bypass the
/// configured format (e.g. appearing as plain text when JSON mode is selected).
/// Callers should use `makeRuntimeLogger()` after initialization to obtain a
/// properly configured logger for any diagnostic messages.
public init(logger: Logger) {
// Read AWS_LAMBDA_LOG_FORMAT (default: Text)
self.format =
LogFormat(
rawValue: Lambda.env("AWS_LAMBDA_LOG_FORMAT") ?? "Text"
) ?? .text
// Store the base logger for cloning
self.baseLogger = logger
// Determine log level with proper precedence
// When both AWS_LAMBDA_LOG_LEVEL and LOG_LEVEL are set:
// - JSON format: AWS_LAMBDA_LOG_LEVEL takes precedence
// - Text format: LOG_LEVEL takes precedence (backward compatibility)
let awsLambdaLogLevel = Lambda.env("AWS_LAMBDA_LOG_LEVEL")
let logLevel = Lambda.env("LOG_LEVEL")
// Determine which raw env var value to parse based on format and precedence
let rawLevel: String?
switch (self.format, awsLambdaLogLevel, logLevel) {
case (.json, .some(let awsLevel), _):
rawLevel = awsLevel
case (.json, .none, .some(let legacyLevel)):
rawLevel = legacyLevel
case (.text, _, .some(let legacyLevel)):
rawLevel = legacyLevel
case (.text, .some(let awsLevel), .none):
rawLevel = awsLevel
case (_, .none, .none):
rawLevel = nil
}
self.applicationLogLevel = rawLevel.flatMap { Self.parseLogLevel($0) }
self.unrecognizedLogLevel = rawLevel != nil && self.applicationLogLevel == nil ? rawLevel : nil
}
private static func parseLogLevel(_ level: String) -> Logger.Level? {
switch level.uppercased() {
case "TRACE": return .trace
case "DEBUG": return .debug
case "INFO": return .info
case "NOTICE": return .notice
case "WARN", "WARNING": return .warning
case "ERROR": return .error
case "FATAL", "CRITICAL": return .critical
default: return nil
}
}
/// Create a logger for a specific invocation
public func makeLogger(
label: String,
requestID: String,
traceID: String
) -> Logger {
switch self.format {
case .text:
// Clone the base logger and add request metadata
var logger = self.baseLogger
logger[metadataKey: "aws-request-id"] = .string(requestID)
logger[metadataKey: "aws-trace-id"] = .string(traceID)
if let level = self.applicationLogLevel {
logger.logLevel = level
}
return logger
case .json:
// Use JSON log handler
var logger = Logger(label: label) { label in
JSONLogHandler(
label: label,
requestID: requestID,
traceID: traceID
)
}
if let level = self.applicationLogLevel {
logger.logLevel = level
}
return logger
}
}
/// Create a logger for runtime-level messages (before any invocation).
/// In text mode, this returns the base logger provided by the user.
/// In JSON mode, this creates a JSON logger using the base logger's label.
public func makeRuntimeLogger() -> Logger {
var logger: Logger
switch self.format {
case .text:
logger = self.baseLogger
case .json:
logger = Logger(label: self.baseLogger.label) { label in
JSONLogHandler(
label: label,
requestID: "N/A",
traceID: "N/A"
)
}
}
if let level = self.applicationLogLevel {
logger.logLevel = level
}
if let unrecognized = self.unrecognizedLogLevel {
logger.warning(
"Unrecognized log level '\(unrecognized)'. Using default log level. Valid values: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR, FATAL."
)
}
return logger
}
}