mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-05-03 07:22:27 +00:00
553b5e3716
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>
148 lines
5.4 KiB
Swift
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
|
|
}
|
|
}
|