diff --git a/Package.swift b/Package.swift index c120dbb..40a159e 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,7 @@ let package = Package( .default(enabledTraits: ["ConfigReader"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-log.git", from: "1.11.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.12.0"), .package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.1.1"), .package(url: "https://github.com/apple/swift-configuration.git", from: "1.0.0", traits: ["CommandLineArguments"]), ], diff --git a/Sources/ConsoleLogger/ConsoleLogger.swift b/Sources/ConsoleLogger/ConsoleLogger.swift index c9b764f..7031da6 100644 --- a/Sources/ConsoleLogger/ConsoleLogger.swift +++ b/Sources/ConsoleLogger/ConsoleLogger.swift @@ -146,6 +146,7 @@ public struct ConsoleLogger: LogHandler, Sendable { var record = LogRecord( level: event.level, message: event.message, + error: event.error, metadata: event.metadata, source: event.source, file: event.file, diff --git a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md index 5b242dd..156f6bf 100644 --- a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md +++ b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md @@ -16,7 +16,7 @@ You then pass the resulting fragment to the ``ConsoleLogger``, which is a `Swift `ConsoleLogger` includes a couple of default ``LoggerFragment``s that can be used out of the box: -- ``LoggerFragment/default``, which outputs the label of the logger, the level of the logged message, the logged message itself, the metadata and the file location of the logged message, including the line. +- ``LoggerFragment/default``, which outputs the label of the logger, the level of the logged message, the logged message itself, the error and metadata, and the file location of the logged message, including the line. - ``LoggerFragment/timestampDefault(timestampSource:)``, which adds a timestamp at the front of the default fragment. ### Creating a LoggerFragment diff --git a/Sources/ConsoleLogger/Docs.docc/index.md b/Sources/ConsoleLogger/Docs.docc/index.md index 6632a75..0f56fcc 100644 --- a/Sources/ConsoleLogger/Docs.docc/index.md +++ b/Sources/ConsoleLogger/Docs.docc/index.md @@ -46,4 +46,4 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - ``LogRecord`` - ``ConsoleLoggerPrinter`` -- ``DefaultConsoleLoggerPrinter`` \ No newline at end of file +- ``DefaultConsoleLoggerPrinter`` diff --git a/Sources/ConsoleLogger/LogRecord.swift b/Sources/ConsoleLogger/LogRecord.swift index ec845af..a32c90d 100644 --- a/Sources/ConsoleLogger/LogRecord.swift +++ b/Sources/ConsoleLogger/LogRecord.swift @@ -5,6 +5,7 @@ public struct LogRecord { public init( level: Logger.Level, message: Logger.Message, + error: (any Error)?, metadata: Logger.Metadata? = nil, source: String, file: String, @@ -17,6 +18,7 @@ public struct LogRecord { ) { self.level = level self.message = message + self.error = error self.metadata = metadata self.source = source self.file = file @@ -32,6 +34,8 @@ public struct LogRecord { public var level: Logger.Level /// The logged message public var message: Logger.Message + /// The error associated with the logged message + public var error: (any Error)? /// The metadata explicitly associated with the logged message public var metadata: Logger.Metadata? /// The source of the log message, usually the module name diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index 52787ff..7ece8d2 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -259,18 +259,22 @@ public struct MessageFragment: LoggerFragment { } } -/// Writes the combined metadata to the output, and requests a separator for the next fragment only if the metadata was not empty. +/// Writes the combined metadata (including the logged error) to the output, and requests a separator for the next fragment only if the metadata was not empty. /// /// This fragment is considered to not have content if the metadata is empty. public struct MetadataFragment: LoggerFragment { public init() {} public func hasContent(record: inout LogRecord) -> Bool { - !record.allMetadata().isEmpty + !record.allMetadata().isEmpty || record.error != nil } public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - let allMetadata = record.allMetadata() + var allMetadata = record.allMetadata() + if let error = record.error { + allMetadata["error.message"] = "\(error)" + allMetadata["error.type"] = "\(String(reflecting: type(of: error)))" + } guard !allMetadata.isEmpty else { return } diff --git a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift index b1d53c2..4141409 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -72,6 +72,29 @@ struct LoggerFragmentBuilderTests { #expect(printer.testOutputQueue.first == "ConsoleLogger [ codes.vapor.console ] [ INFO ] Test message") } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) + @Test("Error Metadata Fragment") + func errorMetadataFragment() throws { + let printer = TestingConsoleLoggerPrinter() + + @LoggerFragmentBuilder<0> + var fragment: some LoggerFragment { + MessageFragment() + MetadataFragment() + } + + let logger = Logger(label: "codes.vapor.console") { label in + ConsoleLogger(fragment: fragment, printer: printer, label: label) + } + + logger.info("Test message", error: TestError()) + + #expect( + printer.testOutputQueue.first + == "Test message[error.message: Boom!, error.type: ConsoleLoggerTests.LoggerFragmentBuilderTests.TestError]" + ) + } + @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @Test("Conditional Fragment", arguments: [true, false]) func conditionalFragment(includeTimestamp: Bool) throws { @@ -209,4 +232,10 @@ struct LoggerFragmentBuilderTests { // so the next `.separated(" ")` still inserts a space. #expect(printer.testOutputQueue.first == "Hello[ INFO ] Test message") } + + struct TestError: Error, CustomStringConvertible { + var description: String { + "Boom!" + } + } } diff --git a/Tests/ConsoleLoggerTests/LoggingTests.swift b/Tests/ConsoleLoggerTests/LoggingTests.swift index cb06765..a3b2245 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -69,6 +69,21 @@ struct ConsoleLoggerTests { expect(printer: printer, logs: .critical, message: "critical") } + @Test("Error") + func error() { + let printer = TestingConsoleLoggerPrinter() + let logger = Logger(label: "codes.vapor.console") { label in + ConsoleLogger(printer: printer, label: label, level: .info) + } + + logger.warning("warning", error: TestError(), metadata: ["error.message": "Overridden", "error.type": "Overridden"]) + expect( + printer: printer, + logs: .warning, + message: "warning [error.message: Boom!, error.type: ConsoleLoggerTests.ConsoleLoggerTests.TestError]" + ) + } + @Test("Metadata") func metadata() { let printer = TestingConsoleLoggerPrinter() @@ -216,6 +231,12 @@ struct ConsoleLoggerTests { expect(printer: printer, logs: level, message: expectedMessage) } #endif + + struct TestError: Error, CustomStringConvertible { + var description: String { + "Boom!" + } + } } private func expect(