From 6a91819b4c2a5a08aa66469214c454d41a3ce7ee Mon Sep 17 00:00:00 2001 From: Samuel Murray Date: Thu, 13 Aug 2026 15:41:04 +0200 Subject: [PATCH 1/3] Include information from LogEvent.error in log output if present --- Package.swift | 2 +- Sources/ConsoleLogger/ConsoleLogger.swift | 1 + .../ConsoleLogger/Docs.docc/GettingStarted.md | 5 ++++- Sources/ConsoleLogger/Docs.docc/index.md | 1 + Sources/ConsoleLogger/LogRecord.swift | 4 ++++ .../DefaultLoggerFragments.swift | 9 ++++++++- .../LoggerFragments/LoggerFragment.swift | 15 ++++++++++++++ .../LoggerFragmentBuilderTests.swift | 20 +++++++++++++++++++ Tests/ConsoleLoggerTests/LoggingTests.swift | 11 ++++++++++ Tests/ConsoleLoggerTests/TestError.swift | 5 +++++ 10 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 Tests/ConsoleLoggerTests/TestError.swift diff --git a/Package.swift b/Package.swift index 8d29cb5..8539cab 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..d8d56e9 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 @@ -27,6 +27,7 @@ As an example, here's how to recreate the ``LoggerFragment/default`` fragment st ```swift let myDefaultLoggerFragment = LabelFragment().maxLevel(.trace) .and(LevelFragment().separated(" ").and(MessageFragment().separated(" "))) + .and(ErrorFragment().separated(" ")) .and(MetadataFragment().separated(" ")) .and(SourceLocationFragment().separated(" ").maxLevel(.debug)) ``` @@ -39,6 +40,7 @@ let myDefaultLoggerFragment = SpacedFragment { LabelFragment().maxLevel(.trace) LevelFragment() MessageFragment() + ErrorFragment() MetadataFragment() SourceLocationFragment().maxLevel(.debug) } @@ -64,6 +66,7 @@ Here's a list of all the fragments available out of the box in `ConsoleLogger`: - ``SeparatorFragment`` - ``SpacedFragment`` - ``MessageFragment`` +- ``ErrorFragment`` - ``MetadataFragment`` - ``SourceLocationFragment`` - ``LoggerSourceFragment`` diff --git a/Sources/ConsoleLogger/Docs.docc/index.md b/Sources/ConsoleLogger/Docs.docc/index.md index 6632a75..acfa2ef 100644 --- a/Sources/ConsoleLogger/Docs.docc/index.md +++ b/Sources/ConsoleLogger/Docs.docc/index.md @@ -33,6 +33,7 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - ``SeparatorFragment`` - ``SpacedFragment`` - ``MessageFragment`` +- ``ErrorFragment`` - ``MetadataFragment`` - ``SourceLocationFragment`` - ``LoggerSourceFragment`` 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/DefaultLoggerFragments.swift b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift index 0481e04..a73af02 100644 --- a/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift +++ b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift @@ -3,7 +3,13 @@ import Logging /// The type of the default ``LoggerFragment``. public typealias DefaultLoggerFragmentType = AndFragment< AndFragment< - AndFragment, AndFragment, SeparatorFragment>>, + AndFragment< + AndFragment< + IfMaxLevelFragment, + AndFragment, SeparatorFragment> + >, + SeparatorFragment + >, SeparatorFragment >, IfMaxLevelFragment> @@ -14,6 +20,7 @@ extension LoggerFragment where Self == DefaultLoggerFragmentType { public static var `default`: DefaultLoggerFragmentType { LabelFragment().maxLevel(.trace) .and(LevelFragment().separated(" ").and(MessageFragment().separated(" "))) + .and(ErrorFragment().separated(" ")) .and(MetadataFragment().separated(" ")) .and(SourceLocationFragment().separated(" ").maxLevel(.debug)) } diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index b8406ae..b2594d1 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -259,6 +259,21 @@ public struct MessageFragment: LoggerFragment { } } +/// Writes the logger error to the output, and requests a separator for the next fragment only if the error was not nil +public struct ErrorFragment: LoggerFragment { + public init() {} + + public func hasContent(record: inout LogRecord) -> Bool { + record.error != nil + } + + public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { + guard let error = record.error else { return } + output += "[ \(String(reflecting: type(of: error))): \(error) ]" + output.needsSeparator = true + } +} + /// Writes the combined metadata 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. diff --git a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift index b1d53c2..520421e 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -72,6 +72,26 @@ 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 Fragment") + func errorFragment() throws { + let printer = TestingConsoleLoggerPrinter() + + @LoggerFragmentBuilder<0> + var fragment: some LoggerFragment { + MessageFragment() + ErrorFragment() + } + + 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[ ConsoleLoggerTests.TestError: Something went wrong! ]") + } + @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 { diff --git a/Tests/ConsoleLoggerTests/LoggingTests.swift b/Tests/ConsoleLoggerTests/LoggingTests.swift index 65f0bd3..cea2ded 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -69,6 +69,17 @@ 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()) + expect(printer: printer, logs: .warning, message: "warning [ ConsoleLoggerTests.TestError: Something went wrong! ]") + } + @Test("Metadata") func metadata() { let printer = TestingConsoleLoggerPrinter() diff --git a/Tests/ConsoleLoggerTests/TestError.swift b/Tests/ConsoleLoggerTests/TestError.swift new file mode 100644 index 0000000..403c0b1 --- /dev/null +++ b/Tests/ConsoleLoggerTests/TestError.swift @@ -0,0 +1,5 @@ +struct TestError: Error, CustomStringConvertible { + var description: String { + "Something went wrong!" + } +} From 0c33be5139f6000d5ec5d28bf9b79fa81cc81d66 Mon Sep 17 00:00:00 2001 From: Samuel Murray Date: Fri, 14 Aug 2026 14:04:36 +0200 Subject: [PATCH 2/3] fixup! Include information from LogEvent.error in log output if present Namespace error to test suite --- Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift | 8 +++++++- Tests/ConsoleLoggerTests/LoggingTests.swift | 8 +++++++- Tests/ConsoleLoggerTests/TestError.swift | 5 ----- 3 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 Tests/ConsoleLoggerTests/TestError.swift diff --git a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift index 520421e..562ffb9 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -89,7 +89,7 @@ struct LoggerFragmentBuilderTests { logger.info("Test message", error: TestError()) - #expect(printer.testOutputQueue.first == "Test message[ ConsoleLoggerTests.TestError: Something went wrong! ]") + #expect(printer.testOutputQueue.first == "Test message[ ConsoleLoggerTests.LoggerFragmentBuilderTests.TestError: Boom! ]") } @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) @@ -229,4 +229,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 cea2ded..082dc7b 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -77,7 +77,7 @@ struct ConsoleLoggerTests { } logger.warning("warning", error: TestError()) - expect(printer: printer, logs: .warning, message: "warning [ ConsoleLoggerTests.TestError: Something went wrong! ]") + expect(printer: printer, logs: .warning, message: "warning [ ConsoleLoggerTests.ConsoleLoggerTests.TestError: Boom! ]") } @Test("Metadata") @@ -217,6 +217,12 @@ struct ConsoleLoggerTests { expect(printer: printer, logs: level, message: expectedMessage) } #endif + + struct TestError: Error, CustomStringConvertible { + var description: String { + "Boom!" + } + } } private func expect( diff --git a/Tests/ConsoleLoggerTests/TestError.swift b/Tests/ConsoleLoggerTests/TestError.swift deleted file mode 100644 index 403c0b1..0000000 --- a/Tests/ConsoleLoggerTests/TestError.swift +++ /dev/null @@ -1,5 +0,0 @@ -struct TestError: Error, CustomStringConvertible { - var description: String { - "Something went wrong!" - } -} From 06f18907ff56f4b9e8e7305f295845302e3414cd Mon Sep 17 00:00:00 2001 From: Samuel Murray Date: Thu, 20 Aug 2026 11:20:14 +0200 Subject: [PATCH 3/3] Serialize error in MetadataFragment instead of new ErrorFragment --- .../ConsoleLogger/Docs.docc/GettingStarted.md | 3 --- Sources/ConsoleLogger/Docs.docc/index.md | 3 +-- .../DefaultLoggerFragments.swift | 9 +------ .../LoggerFragments/LoggerFragment.swift | 25 ++++++------------- .../LoggerFragmentBuilderTests.swift | 11 +++++--- Tests/ConsoleLoggerTests/LoggingTests.swift | 8 ++++-- 6 files changed, 22 insertions(+), 37 deletions(-) diff --git a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md index d8d56e9..156f6bf 100644 --- a/Sources/ConsoleLogger/Docs.docc/GettingStarted.md +++ b/Sources/ConsoleLogger/Docs.docc/GettingStarted.md @@ -27,7 +27,6 @@ As an example, here's how to recreate the ``LoggerFragment/default`` fragment st ```swift let myDefaultLoggerFragment = LabelFragment().maxLevel(.trace) .and(LevelFragment().separated(" ").and(MessageFragment().separated(" "))) - .and(ErrorFragment().separated(" ")) .and(MetadataFragment().separated(" ")) .and(SourceLocationFragment().separated(" ").maxLevel(.debug)) ``` @@ -40,7 +39,6 @@ let myDefaultLoggerFragment = SpacedFragment { LabelFragment().maxLevel(.trace) LevelFragment() MessageFragment() - ErrorFragment() MetadataFragment() SourceLocationFragment().maxLevel(.debug) } @@ -66,7 +64,6 @@ Here's a list of all the fragments available out of the box in `ConsoleLogger`: - ``SeparatorFragment`` - ``SpacedFragment`` - ``MessageFragment`` -- ``ErrorFragment`` - ``MetadataFragment`` - ``SourceLocationFragment`` - ``LoggerSourceFragment`` diff --git a/Sources/ConsoleLogger/Docs.docc/index.md b/Sources/ConsoleLogger/Docs.docc/index.md index acfa2ef..0f56fcc 100644 --- a/Sources/ConsoleLogger/Docs.docc/index.md +++ b/Sources/ConsoleLogger/Docs.docc/index.md @@ -33,7 +33,6 @@ A `SwiftLog` `LogHandler` implementation for customizable logging to a console. - ``SeparatorFragment`` - ``SpacedFragment`` - ``MessageFragment`` -- ``ErrorFragment`` - ``MetadataFragment`` - ``SourceLocationFragment`` - ``LoggerSourceFragment`` @@ -47,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/LoggerFragments/DefaultLoggerFragments.swift b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift index a73af02..0481e04 100644 --- a/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift +++ b/Sources/ConsoleLogger/LoggerFragments/DefaultLoggerFragments.swift @@ -3,13 +3,7 @@ import Logging /// The type of the default ``LoggerFragment``. public typealias DefaultLoggerFragmentType = AndFragment< AndFragment< - AndFragment< - AndFragment< - IfMaxLevelFragment, - AndFragment, SeparatorFragment> - >, - SeparatorFragment - >, + AndFragment, AndFragment, SeparatorFragment>>, SeparatorFragment >, IfMaxLevelFragment> @@ -20,7 +14,6 @@ extension LoggerFragment where Self == DefaultLoggerFragmentType { public static var `default`: DefaultLoggerFragmentType { LabelFragment().maxLevel(.trace) .and(LevelFragment().separated(" ").and(MessageFragment().separated(" "))) - .and(ErrorFragment().separated(" ")) .and(MetadataFragment().separated(" ")) .and(SourceLocationFragment().separated(" ").maxLevel(.debug)) } diff --git a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift index 9df59bc..7ece8d2 100644 --- a/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift +++ b/Sources/ConsoleLogger/LoggerFragments/LoggerFragment.swift @@ -259,33 +259,22 @@ public struct MessageFragment: LoggerFragment { } } -/// Writes the logger error to the output, and requests a separator for the next fragment only if the error was not nil -public struct ErrorFragment: LoggerFragment { - public init() {} - - public func hasContent(record: inout LogRecord) -> Bool { - record.error != nil - } - - public func write(_ record: inout LogRecord, to output: inout FragmentOutput) { - guard let error = record.error else { return } - output += "[ \(String(reflecting: type(of: error))): \(error) ]" - output.needsSeparator = true - } -} - -/// 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 562ffb9..4141409 100644 --- a/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift +++ b/Tests/ConsoleLoggerTests/LoggerFragmentBuilderTests.swift @@ -73,14 +73,14 @@ struct LoggerFragmentBuilderTests { } @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, macCatalyst 26.0, visionOS 26.0, *) - @Test("Error Fragment") - func errorFragment() throws { + @Test("Error Metadata Fragment") + func errorMetadataFragment() throws { let printer = TestingConsoleLoggerPrinter() @LoggerFragmentBuilder<0> var fragment: some LoggerFragment { MessageFragment() - ErrorFragment() + MetadataFragment() } let logger = Logger(label: "codes.vapor.console") { label in @@ -89,7 +89,10 @@ struct LoggerFragmentBuilderTests { logger.info("Test message", error: TestError()) - #expect(printer.testOutputQueue.first == "Test message[ ConsoleLoggerTests.LoggerFragmentBuilderTests.TestError: Boom! ]") + #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, *) diff --git a/Tests/ConsoleLoggerTests/LoggingTests.swift b/Tests/ConsoleLoggerTests/LoggingTests.swift index 7accbf1..a3b2245 100644 --- a/Tests/ConsoleLoggerTests/LoggingTests.swift +++ b/Tests/ConsoleLoggerTests/LoggingTests.swift @@ -76,8 +76,12 @@ struct ConsoleLoggerTests { ConsoleLogger(printer: printer, label: label, level: .info) } - logger.warning("warning", error: TestError()) - expect(printer: printer, logs: .warning, message: "warning [ ConsoleLoggerTests.ConsoleLoggerTests.TestError: Boom! ]") + 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")