diff --git a/Sources/Logging/LogHandler.swift b/Sources/Logging/LogHandler.swift index 709ceb44..fddd9650 100644 --- a/Sources/Logging/LogHandler.swift +++ b/Sources/Logging/LogHandler.swift @@ -136,6 +136,20 @@ public protocol LogHandler: _SwiftLogSendableLogHandler { /// - file: The file the log message was emitted from. /// - function: The function the log line was emitted from. /// - line: The line the log message was emitted from. + func log( + level: Logger.Level, + message: Logger.Message, + error: Error?, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) + + /// SwiftLog 1.6 compatibility method. Please do _not_ implement, implement + /// `log(level:message:error:metadata:source:file:function:line:)` instead. + @available(*, deprecated, renamed: "log(level:message:error:metadata:source:file:function:line:)") func log( level: Logger.Level, message: Logger.Message, @@ -147,8 +161,8 @@ public protocol LogHandler: _SwiftLogSendableLogHandler { ) /// SwiftLog 1.0 compatibility method. Please do _not_ implement, implement - /// `log(level:message:metadata:source:file:function:line:)` instead. - @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") + /// `log(level:message:error:metadata:source:file:function:line:)` instead. + @available(*, deprecated, renamed: "log(level:message:error:metadata:source:file:function:line:)") func log( level: Logging.Logger.Level, message: Logging.Logger.Message, @@ -196,6 +210,7 @@ extension LogHandler { level: .warning, message: "Attempted to set metadataProvider on \(Self.self) that did not implement support for them. Please contact the log handler maintainer to implement metadata provider support.", + error: nil, metadata: nil, source: "Logging", file: #file, @@ -210,6 +225,20 @@ extension LogHandler { extension LogHandler { @available(*, deprecated, message: "You should implement this method instead of using the default implementation") + public func log( + level: Logger.Level, + message: Logger.Message, + error: Error?, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + self.log(level: level, message: message, metadata: metadata, source: source, file: file, function: function, line: line) + } + + @available(*, deprecated, renamed: "log(level:message:error:metadata:source:file:function:line:)") public func log( level: Logger.Level, message: Logger.Message, @@ -222,7 +251,7 @@ extension LogHandler { self.log(level: level, message: message, metadata: metadata, file: file, function: function, line: line) } - @available(*, deprecated, renamed: "log(level:message:metadata:source:file:function:line:)") + @available(*, deprecated, renamed: "log(level:message:error:metadata:source:file:function:line:)") public func log( level: Logging.Logger.Level, message: Logging.Logger.Message, @@ -234,6 +263,7 @@ extension LogHandler { self.log( level: level, message: message, + error: nil, metadata: metadata, source: Logger.currentModule(filePath: file), file: file, diff --git a/Sources/Logging/Logging.swift b/Sources/Logging/Logging.swift index 1622edb3..09e815b3 100644 --- a/Sources/Logging/Logging.swift +++ b/Sources/Logging/Logging.swift @@ -104,6 +104,7 @@ extension Logger { /// - parameters: /// - level: The log level to log `message` at. For the available log levels, see `Logger.Level`. /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -117,6 +118,7 @@ extension Logger { public func log( level: Logger.Level, _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -127,6 +129,7 @@ extension Logger { self.handler.log( level: level, message: message(), + error: error, metadata: metadata(), source: source() ?? Logger.currentModule(fileID: (file)), file: file, @@ -135,6 +138,36 @@ extension Logger { ) } } + + /// Log a message passing the log level as a parameter. + /// + /// If the `logLevel` passed to this method is more severe than the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - level: The log level to log `message` at. For the available log levels, see `Logger.Level`. + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func log( + level: Logger.Level, + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.log(level: level, message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing the log level as a parameter. /// @@ -214,6 +247,7 @@ extension Logger { @inlinable public func trace( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -223,6 +257,7 @@ extension Logger { self.log( level: .trace, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -230,6 +265,34 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/trace`` log level. + /// + /// If `.trace` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func trace( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.trace(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/trace`` log level. /// @@ -263,6 +326,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -275,6 +339,7 @@ extension Logger { @inlinable public func debug( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -284,6 +349,7 @@ extension Logger { self.log( level: .debug, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -291,6 +357,34 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/debug`` log level. + /// + /// If `.debug` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func debug( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.debug(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/debug`` log level. /// @@ -324,6 +418,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -336,6 +431,7 @@ extension Logger { @inlinable public func info( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -345,6 +441,7 @@ extension Logger { self.log( level: .info, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -352,6 +449,34 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/info`` log level. + /// + /// If `.info` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func info( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.info(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/info`` log level. /// @@ -385,6 +510,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -397,6 +523,7 @@ extension Logger { @inlinable public func notice( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -406,6 +533,7 @@ extension Logger { self.log( level: .notice, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -413,6 +541,34 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/notice`` log level. + /// + /// If `.notice` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func notice( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.notice(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/notice`` log level. /// @@ -446,6 +602,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -458,6 +615,7 @@ extension Logger { @inlinable public func warning( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -467,6 +625,7 @@ extension Logger { self.log( level: .warning, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -474,6 +633,34 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/warning`` log level. + /// + /// If `.warning` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func warning( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.warning(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/warning`` log level. /// @@ -499,7 +686,7 @@ extension Logger { ) { self.warning(message(), metadata: metadata(), source: nil, file: file, function: function, line: line) } - + /// Log a message passing with the ``Logger/Level/error`` log level. /// /// If `.error` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, @@ -507,6 +694,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -519,6 +707,7 @@ extension Logger { @inlinable public func error( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -528,6 +717,7 @@ extension Logger { self.log( level: .error, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -536,6 +726,34 @@ extension Logger { ) } + /// Log a message passing with the ``Logger/Level/error`` log level. + /// + /// If `.error` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, + /// otherwise nothing will happen. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func error( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.error(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } + /// Log a message passing with the ``Logger/Level/error`` log level. /// /// If `.error` is at least as severe as the `Logger`'s ``logLevel``, it will be logged, @@ -558,7 +776,7 @@ extension Logger { function: String = #function, line: UInt = #line ) { - self.error(message(), metadata: metadata(), source: nil, file: file, function: function, line: line) + self.error(message(), error: nil, metadata: metadata(), source: nil, file: file, function: function, line: line) } /// Log a message passing with the ``Logger/Level/critical`` log level. @@ -567,6 +785,7 @@ extension Logger { /// /// - parameters: /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - error: An `Error` related to the event. /// - metadata: One-off metadata to attach to this log message. /// - source: The source this log messages originates from. Defaults /// to the module emitting the log message. @@ -579,6 +798,7 @@ extension Logger { @inlinable public func critical( _ message: @autoclosure () -> Logger.Message, + error: Error? = nil, metadata: @autoclosure () -> Logger.Metadata? = nil, source: @autoclosure () -> String? = nil, file: String = #fileID, @@ -588,6 +808,7 @@ extension Logger { self.log( level: .critical, message(), + error: error, metadata: metadata(), source: source(), file: file, @@ -595,6 +816,33 @@ extension Logger { line: line ) } + + /// Log a message passing with the ``Logger/Level/critical`` log level. + /// + /// `.critical` messages will always be logged. + /// + /// - parameters: + /// - message: The message to be logged. `message` can be used with any string interpolation literal. + /// - metadata: One-off metadata to attach to this log message. + /// - source: The source this log messages originates from. Defaults + /// to the module emitting the log message. + /// - file: The file this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#fileID`). + /// - function: The function this log message originates from (there's usually no need to pass it explicitly as + /// it defaults to `#function`). + /// - line: The line this log message originates from (there's usually no need to pass it explicitly as it + /// defaults to `#line`). + @inlinable + public func critical( + _ message: @autoclosure () -> Logger.Message, + metadata: @autoclosure () -> Logger.Metadata? = nil, + source: @autoclosure () -> String? = nil, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + self.critical(message(), error: nil, metadata: metadata(), source: source(), file: file, function: function, line: line) + } /// Log a message passing with the ``Logger/Level/critical`` log level. /// @@ -1133,6 +1381,7 @@ public struct MultiplexLogHandler: LogHandler { public func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata: Logger.Metadata?, source: String, file: String, @@ -1143,6 +1392,7 @@ public struct MultiplexLogHandler: LogHandler { handler.log( level: level, message: message, + error: error, metadata: metadata, source: source, file: file, @@ -1371,6 +1621,7 @@ public struct StreamLogHandler: LogHandler { public func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata explicitMetadata: Logger.Metadata?, source: String, file: String, @@ -1392,7 +1643,7 @@ public struct StreamLogHandler: LogHandler { var stream = self.stream stream.write( - "\(self.timestamp()) \(level)\(self.label.isEmpty ? "" : " ")\(self.label):\(prettyMetadata.map { " \($0)" } ?? "") [\(source)] \(message)\n" + "\(self.timestamp()) \(level)\(self.label.isEmpty ? "" : " ")\(self.label):\(prettyMetadata.map { " \($0)" } ?? "") [\(source)] \(message)\(error != nil ? " - \(error!)" : "")\n" ) } @@ -1478,6 +1729,17 @@ public struct SwiftLogNoOpLogHandler: LogHandler { function: String, line: UInt ) {} + + public func log( + level: Logger.Level, + message: Logger.Message, + error: Error?, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) {} @inlinable public subscript(metadataKey _: String) -> Logger.Metadata.Value? { get { diff --git a/Tests/LoggingTests/CompatibilityTest.swift b/Tests/LoggingTests/CompatibilityTest.swift index ca932db7..cc49f558 100644 --- a/Tests/LoggingTests/CompatibilityTest.swift +++ b/Tests/LoggingTests/CompatibilityTest.swift @@ -18,10 +18,10 @@ import Testing struct CompatibilityTest { @available(*, deprecated, message: "Testing deprecated functionality") - @Test func allLogLevelsWorkWithOldSchoolLogHandlerWorks() { - let testLogging = OldSchoolTestLogging() + @Test func allLogLevelsWorkWithPreSourceLogHandler() { + let testLogging = DeprecatedTestLogging() - var logger = Logger(label: "\(#function)", factory: { testLogging.make(label: $0) }) + var logger = Logger(label: "\(#function)", factory: { testLogging.makePreSource(label: $0) }) logger.logLevel = .trace logger.trace("yes: trace") @@ -29,10 +29,10 @@ struct CompatibilityTest { logger.info("yes: info") logger.notice("yes: notice") logger.warning("yes: warning") - logger.error("yes: error") + logger.error("yes: error", error: TestError.error) logger.critical("yes: critical", source: "any also with some new argument that isn't propagated") - // Please note that the source is _not_ propagated (because the backend doesn't support it). + // Please note that the source and error are _not_ propagated (because the backend doesn't support it). testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "no source") testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "no source") testLogging.history.assertExist(level: .info, message: "yes: info", source: "no source") @@ -41,15 +41,51 @@ struct CompatibilityTest { testLogging.history.assertExist(level: .error, message: "yes: error", source: "no source") testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "no source") } + + @available(*, deprecated, message: "Testing deprecated functionality") + @Test func allLogLevelsWorkWithPreErrorLogHandler() { + let testLogging = DeprecatedTestLogging() + + var logger = Logger(label: "\(#function)", factory: { testLogging.makePreError(label: $0) }) + logger.logLevel = .trace + + logger.trace("yes: trace") + logger.debug("yes: debug") + logger.info("yes: info") + logger.notice("yes: notice") + logger.warning("yes: warning") + logger.error("yes: error", error: TestError.error) + logger.critical("yes: critical") + + // Please note that the error is _not_ propagated (because the backend doesn't support it). + testLogging.history.assertExist(level: .trace, message: "yes: trace") + testLogging.history.assertExist(level: .debug, message: "yes: debug") + testLogging.history.assertExist(level: .info, message: "yes: info") + testLogging.history.assertExist(level: .notice, message: "yes: notice") + testLogging.history.assertExist(level: .warning, message: "yes: warning") + testLogging.history.assertExist(level: .error, message: "yes: error") + testLogging.history.assertExist(level: .critical, message: "yes: critical") + } } -private struct OldSchoolTestLogging { +private struct DeprecatedTestLogging { private let _config = Config() // shared among loggers private let recorder = Recorder() // shared among loggers - + @available(*, deprecated, message: "Testing deprecated functionality") - func make(label: String) -> any LogHandler { - OldSchoolLogHandler( + func makePreSource(label: String) -> any LogHandler { + PreSourceLogHandler( + label: label, + config: self.config, + recorder: self.recorder, + metadata: [:], + logLevel: .info + ) + } + + @available(*, deprecated, message: "Testing deprecated functionality") + func makePreError(label: String) -> any LogHandler { + PreErrorLogHandler( label: label, config: self.config, recorder: self.recorder, @@ -63,7 +99,42 @@ private struct OldSchoolTestLogging { } @available(*, deprecated, message: "Testing deprecated functionality") -private struct OldSchoolLogHandler: LogHandler { +private struct PreSourceLogHandler: LogHandler { + var label: String + let config: Config + let recorder: Recorder + + func make(label: String) -> some LogHandler { + TestLogHandler(label: label, config: self.config, recorder: self.recorder) + } + + func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + file: String, + function: String, + line: UInt + ) { + self.recorder.record(level: level, metadata: metadata, message: message, error: nil, source: "no source") + } + + subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { + self.metadata[metadataKey] + } + set { + self.metadata[metadataKey] = newValue + } + } + + var metadata: Logger.Metadata + + var logLevel: Logger.Level +} + +@available(*, deprecated, message: "Testing deprecated functionality") +private struct PreErrorLogHandler: LogHandler { var label: String let config: Config let recorder: Recorder @@ -76,11 +147,12 @@ private struct OldSchoolLogHandler: LogHandler { level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, + source: String, file: String, function: String, line: UInt ) { - self.recorder.record(level: level, metadata: metadata, message: message, source: "no source") + self.recorder.record(level: level, metadata: metadata, message: message, error: nil, source: source) } subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { diff --git a/Tests/LoggingTests/LoggingTest.swift b/Tests/LoggingTests/LoggingTest.swift index d3907748..e9050f52 100644 --- a/Tests/LoggingTests/LoggingTest.swift +++ b/Tests/LoggingTests/LoggingTest.swift @@ -385,10 +385,6 @@ struct LoggingTest { ) } - enum TestError: Error { - case boom - } - @Test func dictionaryMetadata() { let testLogging = TestLogging() @@ -546,6 +542,7 @@ struct LoggingTest { func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata: Logger.Metadata?, source: String, file: String, @@ -630,6 +627,42 @@ struct LoggingTest { testLogging.history.assertExist(level: .error, message: "yes: error") testLogging.history.assertExist(level: .critical, message: "yes: critical") } + + @Test func allLogLevelByFunctionRefWithError() { + let testLogging = TestLogging() + + var logger = Logger( + label: "\(#function)", + factory: { + testLogging.make(label: $0) + } + ) + logger.logLevel = .trace + + let trace = logger.trace(_:error:metadata:source:file:function:line:) + let debug = logger.debug(_:error:metadata:source:file:function:line:) + let info = logger.info(_:error:metadata:source:file:function:line:) + let notice = logger.notice(_:error:metadata:source:file:function:line:) + let warning = logger.warning(_:error:metadata:source:file:function:line:) + let error = logger.error(_:error:metadata:source:file:function:line:) + let critical = logger.critical(_:error:metadata:source:file:function:line:) + + trace("yes: trace", TestError.error, [:], "foo", #file, #function, #line) + debug("yes: debug", TestError.error, [:], "foo", #file, #function, #line) + info("yes: info", TestError.error, [:], "foo", #file, #function, #line) + notice("yes: notice", TestError.error, [:], "foo", #file, #function, #line) + warning("yes: warning", TestError.error, [:], "foo", #file, #function, #line) + error("yes: error", TestError.error, [:], "foo", #file, #function, #line) + critical("yes: critical", TestError.error, [:], "foo", #file, #function, #line) + + testLogging.history.assertExist(level: .trace, message: "yes: trace", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .debug, message: "yes: debug", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .info, message: "yes: info", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .notice, message: "yes: notice", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .warning, message: "yes: warning", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .error, message: "yes: error", error: TestError.error, source: "foo") + testLogging.history.assertExist(level: .critical, message: "yes: critical", error: TestError.error, source: "foo") + } @Test func allLogLevelByFunctionRefWithSource() { let testLogging = TestLogging() @@ -872,13 +905,14 @@ struct LoggingTest { func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt ) { - self.recorder.record(level: level, metadata: metadata, message: message, source: source) + self.recorder.record(level: level, metadata: metadata, message: message, error: error, source: source) } subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { @@ -1039,6 +1073,31 @@ struct LoggingTest { #expect(messageSucceeded) #expect(interceptStream.strings.count == 1) } + + @Test func streamLogHandlerOutputFormatWithError() { + let interceptStream = InterceptStream() + let label = "testLabel" + let source = "testSource" + let log = Logger( + label: label, + factory: { + StreamLogHandler(label: $0, stream: interceptStream) + } + ) + + let testString = "my message is better than yours" + let error = TestError.error + log.error("\(testString)", error: error, source: source) + + let pattern = + "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\+|-)\\d{4}\\s\(Logger.Level.error)\\s\(label):\\s\\[\(source)\\]\\s\(testString)\\s-\\s\(error)$" + + let output = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines) + let messageSucceeded = output?.range(of: pattern, options: .regularExpression) != nil + + #expect(messageSucceeded, "Expected '\(output)' to match regex") + #expect(interceptStream.strings.count == 1) + } @Test func streamLogHandlerOutputFormatWithMetaData() { let interceptStream = InterceptStream() diff --git a/Tests/LoggingTests/MetadataProviderTest.swift b/Tests/LoggingTests/MetadataProviderTest.swift index 7ce7e3e7..8f4fb7b9 100644 --- a/Tests/LoggingTests/MetadataProviderTest.swift +++ b/Tests/LoggingTests/MetadataProviderTest.swift @@ -96,6 +96,7 @@ public struct LogHandlerThatDidNotImplementMetadataProviders: LogHandler { public func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata: Logger.Metadata?, source: String, file: String, @@ -105,6 +106,7 @@ public struct LogHandlerThatDidNotImplementMetadataProviders: LogHandler { self.testLogging.make(label: "fake").log( level: level, message: message, + error: error, metadata: metadata, source: source, file: file, @@ -138,6 +140,7 @@ public struct LogHandlerThatDidImplementMetadataProviders: LogHandler { public func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata: Logger.Metadata?, source: String, file: String, @@ -147,6 +150,7 @@ public struct LogHandlerThatDidImplementMetadataProviders: LogHandler { self.testLogging.make(label: "fake").log( level: level, message: message, + error: error, metadata: metadata, source: source, file: file, diff --git a/Tests/LoggingTests/TestLogger.swift b/Tests/LoggingTests/TestLogger.swift index 7d6258ae..d030977a 100644 --- a/Tests/LoggingTests/TestLogger.swift +++ b/Tests/LoggingTests/TestLogger.swift @@ -77,6 +77,7 @@ internal struct TestLogHandler: LogHandler { func log( level: Logger.Level, message: Logger.Message, + error: Error?, metadata explicitMetadata: Logger.Metadata?, source: String, file: String, @@ -98,13 +99,14 @@ internal struct TestLogHandler: LogHandler { self.logger.log( level: level, message, + error: error, metadata: metadata, source: source, file: file, function: function, line: line ) - self.recorder.record(level: level, metadata: metadata, message: message, source: source) + self.recorder.record(level: level, metadata: metadata, message: message, error: error, source: source) } private var _logLevel: Logger.Level? @@ -175,10 +177,10 @@ internal class Recorder: History { private let lock = NSLock() private var _entries = [LogEntry]() - func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, source: String) { + func record(level: Logger.Level, metadata: Logger.Metadata?, message: Logger.Message, error: Error?, source: String) { self.lock.withLock { self._entries.append( - LogEntry(level: level, metadata: metadata, message: message.description, source: source) + LogEntry(level: level, metadata: metadata, message: message.description, error: error, source: source) ) } } @@ -224,6 +226,7 @@ internal struct LogEntry { let level: Logger.Level let metadata: Logger.Metadata? let message: String + let error: Error? let source: String } @@ -231,6 +234,7 @@ extension History { func assertExist( level: Logger.Level, message: String, + error: Error? = nil, metadata: Logger.Metadata? = nil, source: String? = nil, file: String = #filePath, @@ -239,7 +243,7 @@ extension History { column: Int = #column ) { let source = source ?? Logger.currentModule(fileID: "\(fileID)") - let entry = self.find(level: level, message: message, metadata: metadata, source: source) + let entry = self.find(level: level, message: message, error: error, metadata: metadata, source: source) #expect( entry != nil, "entry not found: \(level), \(source), \(String(describing: metadata)), \(message)", @@ -250,6 +254,7 @@ extension History { func assertNotExist( level: Logger.Level, message: String, + error: Error? = nil, metadata: Logger.Metadata? = nil, source: String? = nil, file: String = #filePath, @@ -258,15 +263,15 @@ extension History { column: Int = #column ) { let source = source ?? Logger.currentModule(fileID: "\(fileID)") - let entry = self.find(level: level, message: message, metadata: metadata, source: source) + let entry = self.find(level: level, message: message, error: error, metadata: metadata, source: source) #expect( entry == nil, - "entry was found: \(level), \(source), \(String(describing: metadata)), \(message)", + "entry was found: \(level), \(source), \(String(describing: metadata)), \(message), \(error)", sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) ) } - func find(level: Logger.Level, message: String, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? { + func find(level: Logger.Level, message: String, error: Error?, metadata: Logger.Metadata? = nil, source: String) -> LogEntry? { self.entries.first { entry in if entry.level != level { return false @@ -274,6 +279,9 @@ extension History { if entry.message != message { return false } + if "\(entry.error, default: "")" != "\(error, default: "")" { + return false + } if let lhs = entry.metadata, let rhs = metadata { if lhs.count != rhs.count { return false @@ -302,6 +310,10 @@ extension History { } } +public enum TestError: Error, Equatable { + case error +} + /// MDC stands for Mapped Diagnostic Context public class MDC { private let lock = NSLock()