diff --git a/README.md b/README.md index ce07a5f..8246347 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ ![Swift](https://github.com/aliaslab-1984/BattleAxe/workflows/Swift/badge.svg) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/) -Welcome to BattleAxe, an easy and super extstensible Logger for iOS and macOS. +Welcome to BattleAxe, an easy and super extensible Logger for iOS and macOS. To start using BattleAxe follow this steps: -It might be useful to initialize all the LogProviders whenever the app is launched/created, in order to mantain syncronization. +It might be useful to initialize all the LogProviders whenever the app is launched/created, in order to mantain synchronization. A good place for that could be `application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool` or `scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)`if you use `SceneDelegate`. -Here's a small example to intilaize `LogService`: +Here's a small example to initilaize `LogService`: ``` swift let logDateFormatter = LogDateFormatter(dateFormat: "yyyy-MM-dd HH:mm:ssSSS") LogService.register(provider: ConsoleLogProvider(dateFormatter: logDateFormatter)) @@ -70,3 +70,51 @@ public protocol LogMessage { ``` All of these information could be useful when you write your own `LogProvider` object. + +## Channels + +BattleAxe lets you organize your logs per Channel! This makes log filtering easier. +By default, if you don't specify a channel to log into, BattleAxe is going to log on the default general channel (`BattleAxe 🪓`). +To start using channels all you have to do is: +1. Register all your `LogProvider` instances as it follows: + +```swift + +let provider = ConsoleLogProvider(dateFormatter: BattleAxe.LogDateFormatter.init(dateFormat: "dd-mm-yy"), configuration: .naive) +LogService.register(provider: provider) + +``` + +2. After that, you need to register all the channels that you intend to use on your project. We highly raccomend to define them in an enum, as we show below. This will make sure that you won't make typos and log on unwanted channels. +```swift +// Define a unique enum that holds all the LogChannels. +enum LogChannel: String, CaseIterable { + + case networking = "Networking 📻" + case authentication = "Authentication 🗝" + case general = "General Logger 📝" + +} +``` +3. After defining your channels you only need to register them by using the `LogSevice` static method, or to register each provider with a subset of your channels: +```swift +// .. Into your logger: + +LogChannel.allCases.forEach { + LogService.add($0.rawValue) +} +``` +This approach subscribes *all* your registered providers to the specified channel. +An alternative is to call the `addChannel(_ channel: String)` to a single `LogProvider`, like so: + +``` swift +let provider = ConsoleLogProvider(dateFormatter: BattleAxe.LogDateFormatter.init(dateFormat: "dd-mm-yy"), configuration: .naive) +let remoteProvider = MyProvider(dateFormatter: BattleAxe.LogDateFormatter.init(dateFormat: "dd-mm-yy"), configuration: .naive) + +// .. we want to share our .network Logs with the remoteProvider instance. + +remoteProvider.addChannel(LogChannel.networking) +// from now all the logs that are shared via the `.network` channel, are going to be passed to the remoteProvider instance. + +``` + diff --git a/Sources/BattleAxe/FileWriter/Writers/BaseFileWriter.swift b/Sources/BattleAxe/FileWriter/Writers/BaseFileWriter.swift index c985b89..62060c1 100644 --- a/Sources/BattleAxe/FileWriter/Writers/BaseFileWriter.swift +++ b/Sources/BattleAxe/FileWriter/Writers/BaseFileWriter.swift @@ -47,6 +47,7 @@ open class BaseFileWriter: FileWriter { public func write(_ message: String) { // Does nothing + fatalError("Subclasses need to implement the `write(_ message: String)` method.") } /// Returns the URL where the log collection is stored. diff --git a/Sources/BattleAxe/LogMessage.swift b/Sources/BattleAxe/LogMessage.swift index fae85ba..bd9a743 100644 --- a/Sources/BattleAxe/LogMessage.swift +++ b/Sources/BattleAxe/LogMessage.swift @@ -23,6 +23,7 @@ public protocol LogMessage { var callingStackFrame: String { get } var callingThreadID: UInt64 { get } var timestamp: Date { get } + var channel: String { get } } /** @@ -43,7 +44,7 @@ public struct LoggedMessage: Codable, LogMessage, Hashable, Equatable { public var callingFileLine: Int public var callingStackFrame: String public var callingThreadID: UInt64 - + public var channel: String public var timestamp: Date = Date() } @@ -51,7 +52,7 @@ public struct LoggedMessage: Codable, LogMessage, Hashable, Equatable { /** Helper struct that gathers the current ProcessIdentification and encapsulates the process's name and identifier. */ -fileprivate struct ProcessIdentification { +struct ProcessIdentification { // this ensures we only look up process info once public static let current = ProcessIdentification() diff --git a/Sources/BattleAxe/LogMessageComposer.swift b/Sources/BattleAxe/LogMessageComposer.swift new file mode 100644 index 0000000..7a9c26b --- /dev/null +++ b/Sources/BattleAxe/LogMessageComposer.swift @@ -0,0 +1,46 @@ +// +// File.swift +// +// +// Created by Francesco Bianco on 14/05/21. +// + +import Foundation + +/// Helper struct that handles the creation of a printable message, given the message and the needed ingredients. +struct LogMessageFormatter { + + /// Given a log message creates a correclty formatted string following the ingredients provided. + /// - Parameters: + /// - message: The message that needs to be formatted. + /// - ingredients: The ingredients that need to be added to the formatted message. + /// - dateFormatter: The dateformetter style. + /// - Returns: the formatted string. + static func compose(_ message: LogMessage, + using ingredients: [LoggerConfiguration.LogIngredient], + dateFormatter: DateFormatter) -> String { + var finalMessage: String = "" + + ingredients.forEach { ingredient in + switch ingredient { + case .channel: + finalMessage.append("{\(message.channel)} ") + case .severity: + finalMessage.append("[\(message.severity.prettyDescription)] ") + case .date: + finalMessage.append("\(dateFormatter.getCurrentDateAsString()) ") + case .fileName: + finalMessage.append("\(message.callingFilePath):") + case .functionName: + finalMessage.append("\(message.callingStackFrame):") + case .lineNumber: + finalMessage.append("(\(message.callingFileLine))") + case .payload: + finalMessage.append("\(message.payload)") + } + } + + return finalMessage + } + +} diff --git a/Sources/BattleAxe/LogService.swift b/Sources/BattleAxe/LogService.swift index 2b73047..594d95e 100644 --- a/Sources/BattleAxe/LogService.swift +++ b/Sources/BattleAxe/LogService.swift @@ -17,6 +17,9 @@ public final class LogService { /// Whether the logging is enabled or not. public var enabled: Bool = true + /// The default channel + public static let defaultChannel: String = "BattleAxe 🪓" + public typealias Dump = () -> Any private init(providers: [LogProvider]) { @@ -39,10 +42,27 @@ public final class LogService { providers.remove(at: index) } + /// Removes all the registered providers. public static func empty() { self.providers = [] } + /// Removes the passed channel to all the providers. + /// - Parameter channel: the channel that is going to be removed. + public static func silence(_ channel: String) { + providers.forEach { provider in + provider.removeChannel(channel) + } + } + + /// Adds the passed channel to all the providers. + /// - Parameter channel: the channel that is going to be added. If a provider already uses the passed channel it won't duplicate. + public static func add(_ channel: String) { + providers.forEach { provider in + provider.addChannel(channel) + } + } + /// The currently registered providers public static var currentProviders: [LogProvider] { return providers } @@ -75,21 +95,16 @@ public final class LogService { /// - line: The file's line. public func log(_ severity: LogSeverity, _ object: @autoclosure @escaping Dump, + channel: String? = nil, filename: String = #file, funcName: String = #function, line: Int = #line) { - switch severity { - case .verbose: - self.evaluate(severity: .verbose, object, filename: filename, line: line, funcName: funcName) - case .debug: - self.evaluate(severity: .debug, object, filename: filename, line: line, funcName: funcName) - case .info: - self.evaluate(severity: .info, object, filename: filename, line: line, funcName: funcName) - case .warning: - self.evaluate(severity: .warning, object, filename: filename, line: line, funcName: funcName) - case .error: - self.evaluate(severity: .error, object, filename: filename, line: line, funcName: funcName) - } + self.evaluate(severity: severity, + object, + channel: channel ?? Self.defaultChannel, + filename: filename, + line: line, + funcName: funcName) } @@ -101,6 +116,7 @@ public final class LogService { /// - funcName: The method name from which it is getting called. /// - line: The line from where it's getting called. public func info(_ object: @autoclosure Dump, + channel: String? = nil, filename: String = #file, funcName: String = #function, line: Int = #line) { @@ -111,6 +127,7 @@ public final class LogService { propagate(object(), .info, + channel: channel ?? Self.defaultChannel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -124,9 +141,10 @@ public final class LogService { /// - funcName: The method name from which it is getting called. /// - line: The line from where it's getting called. public func debug(_ object: @autoclosure Dump, - filename: String = #file, - line: Int = #line, - funcName: String = #function) { + channel: String? = nil, + filename: String = #file, + line: Int = #line, + funcName: String = #function) { guard minimumSeverity <= .debug, enabled else { return @@ -134,6 +152,7 @@ public final class LogService { propagate(object(), .debug, + channel: channel ?? Self.defaultChannel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -147,9 +166,10 @@ public final class LogService { /// - funcName: The method name from which it is getting called. /// - line: The line from where it's getting called. public func verbose(_ object: @autoclosure Dump, - filename: String = #file, - line: Int = #line, - funcName: String = #function) { + channel: String? = nil, + filename: String = #file, + line: Int = #line, + funcName: String = #function) { guard minimumSeverity <= .verbose, enabled else { return @@ -157,6 +177,7 @@ public final class LogService { propagate(object(), .verbose, + channel: channel ?? Self.defaultChannel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -170,9 +191,10 @@ public final class LogService { /// - funcName: The method name from which it is getting called. /// - line: The line from where it's getting called. public func warning(_ object: @autoclosure Dump, - filename: String = #file, - line: Int = #line, - funcName: String = #function) { + channel: String? = nil, + filename: String = #file, + line: Int = #line, + funcName: String = #function) { guard minimumSeverity <= .warning, enabled else { return @@ -180,6 +202,7 @@ public final class LogService { propagate(object(), .warning, + channel: channel ?? Self.defaultChannel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -193,9 +216,10 @@ public final class LogService { /// - funcName: The method name from which it is getting called. /// - line: The line from where it's getting called. public func error(_ object: @autoclosure Dump, - filename: String = #file, - line: Int = #line, - funcName: String = #function) { + channel: String? = nil, + filename: String = #file, + line: Int = #line, + funcName: String = #function) { guard minimumSeverity <= .error, enabled else { return @@ -203,6 +227,7 @@ public final class LogService { propagate(object(), .error, + channel: channel ?? Self.defaultChannel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -210,6 +235,7 @@ public final class LogService { private func evaluate(severity: LogSeverity, _ object: @autoclosure Dump, + channel: String, filename: String = #file, line: Int = #line, funcName: String = #function) { @@ -220,6 +246,7 @@ public final class LogService { propagate(object(), severity, + channel: channel, filename: LogService.fileName(filePath: filename), line: line, funcName: funcName) @@ -228,6 +255,7 @@ public final class LogService { /// Propagates the log to all the registered providers. private func propagate(_ object: Any, _ severity: LogSeverity, + channel: String, filename: String = #file, line: Int = #line, funcName: String = #function) { @@ -242,7 +270,7 @@ public final class LogService { oggetto = object } - let entity = LoggedMessage(payload: String(describing: oggetto), severity: severity, callingFilePath: filename, callingFileLine: line, callingStackFrame: funcName, callingThreadID: threadID) + let entity = LoggedMessage(payload: String(describing: oggetto), severity: severity, callingFilePath: filename, callingFileLine: line, callingStackFrame: funcName, callingThreadID: threadID, channel: channel) LogService.providers.forEach { $0.log(entity) diff --git a/Sources/BattleAxe/LogSeverity.swift b/Sources/BattleAxe/LogSeverity.swift index e47395a..fa843c3 100644 --- a/Sources/BattleAxe/LogSeverity.swift +++ b/Sources/BattleAxe/LogSeverity.swift @@ -62,7 +62,7 @@ import os.log @available (iOS 10.0, *) internal extension LogSeverity { - func toOSLogLevel() -> OSLogType { + var OSLogLevel: OSLogType { switch self { case .debug: return .debug diff --git a/Sources/BattleAxe/LoggerConfiguration.swift b/Sources/BattleAxe/LoggerConfiguration.swift index 616ebd6..1a11f05 100644 --- a/Sources/BattleAxe/LoggerConfiguration.swift +++ b/Sources/BattleAxe/LoggerConfiguration.swift @@ -7,12 +7,23 @@ import Foundation +/// Defines what pieces are going to be printed/shared by each LogProvider. public struct LoggerConfiguration: Equatable { - enum LogIngredient: CaseIterable, Equatable { - case functionName - case lineNumber - case fileName + /// Represents each component that is going to be printed/shared by a LogProvider. + enum LogIngredient: Int, CaseIterable, Equatable, Comparable { + + static func < (lhs: LoggerConfiguration.LogIngredient, rhs: LoggerConfiguration.LogIngredient) -> Bool { + return lhs.rawValue < rhs.rawValue + } + + case channel = 0 + case severity = 1 + case date = 2 + case functionName = 3 + case lineNumber = 4 + case fileName = 5 + case payload = 6 } var ingredients: Set @@ -21,7 +32,7 @@ public struct LoggerConfiguration: Equatable { /// filename, the function's name and the line number. public static let standard: LoggerConfiguration = .init(ingredients: .init(LogIngredient.allCases)) /// A smaller log information: it includes only the function name in the log output. - public static let minimal: LoggerConfiguration = .init(ingredients: .init(arrayLiteral: .functionName)) + public static let minimal: LoggerConfiguration = .init(ingredients: .init(arrayLiteral: .channel, .functionName, .payload)) /// An even smaller log information: it doesn't includes any information about the file or the function's name. - public static let onlyMessage: LoggerConfiguration = .init(ingredients: .init()) + public static let naive: LoggerConfiguration = .init(ingredients: .init(arrayLiteral: .channel, .severity, .payload)) } diff --git a/Sources/BattleAxe/Providers/ConsoleLogProvider.swift b/Sources/BattleAxe/Providers/ConsoleLogProvider.swift index 90cc302..b40497a 100644 --- a/Sources/BattleAxe/Providers/ConsoleLogProvider.swift +++ b/Sources/BattleAxe/Providers/ConsoleLogProvider.swift @@ -1,25 +1,45 @@ import Foundation -public struct ConsoleLogProvider: LogProvider { +public final class ConsoleLogProvider: LogProvider { public var logIdentifier: String private var dateFormatter: DateFormatter + public var channels: Set = .init([LogService.defaultChannel]) + private var configuration: LoggerConfiguration + private var ingredients: [LoggerConfiguration.LogIngredient] public init(dateFormatter: DateFormatter, - identifier: String = "ConsoleLog Provider") { + identifier: String = "ConsoleLog Provider", + configuration: LoggerConfiguration = LogService.shared.configuration) { self.dateFormatter = dateFormatter self.logIdentifier = identifier + self.configuration = configuration + ingredients = configuration.ingredients.sorted() } public func log(_ message: LogMessage) { - switch LogService.shared.configuration { - case .standard: - print("[\(message.severity.prettyDescription) \(dateFormatter.getCurrentDateAsString())] \(message.callingFilePath):\(message.callingStackFrame):\(message.callingFileLine) \(message.payload)") - case .minimal: - print("[\(message.severity.prettyDescription) \(dateFormatter.getCurrentDateAsString())] \(message.callingStackFrame) \(message.payload)") - default: - print("[\(message.severity.prettyDescription) \(dateFormatter.getCurrentDateAsString())] \(message.payload)") + + guard !channels.isEmpty else { + self.printLog(message) + return } + + if channels.contains(message.channel) { + self.printLog(message) + } + } + + public func addChannel(_ channel: String) { + channels.insert(channel) + } + + public func removeChannel(_ channel: String) { + channels.remove(channel) + } + + private func printLog(_ message: LogMessage) { + let finalMessage = LogMessageFormatter.compose(message, using: ingredients, dateFormatter: dateFormatter) + print(finalMessage) } } diff --git a/Sources/BattleAxe/Providers/ExternalLogHandler.swift b/Sources/BattleAxe/Providers/ExternalLogHandler.swift index a0217bf..94d6bf8 100644 --- a/Sources/BattleAxe/Providers/ExternalLogHandler.swift +++ b/Sources/BattleAxe/Providers/ExternalLogHandler.swift @@ -23,6 +23,8 @@ public protocol LogListener: AnyObject { /// In this case, the app decides what to do with logs. public class ExternalLogHandler: LogProvider { + public var channels: Set = .init([LogService.defaultChannel]) + public var logIdentifier: String = "External Log Handler Identifier" private weak var listener: LogListener? = nil @@ -32,7 +34,23 @@ public class ExternalLogHandler: LogProvider { } public func log(_ message: LogMessage) { - listener?.log(message.severity, message: message.payload) + + guard !channels.isEmpty else { + listener?.log(message.severity, message: message.payload) + return + } + + if channels.contains(message.channel) { + listener?.log(message.severity, message: message.payload) + } + + } + + public func addChannel(_ channel: String) { + channels.insert(channel) } + public func removeChannel(_ channel: String) { + channels.remove(channel) + } } diff --git a/Sources/BattleAxe/Providers/FileLogProvider.swift b/Sources/BattleAxe/Providers/FileLogProvider.swift index 89bcaa9..784b4a0 100644 --- a/Sources/BattleAxe/Providers/FileLogProvider.swift +++ b/Sources/BattleAxe/Providers/FileLogProvider.swift @@ -1,33 +1,65 @@ import Foundation -public struct FileLogProvider: LogProvider { +public final class FileLogProvider: LogProvider { public var logIdentifier: String + public var channels: Set = .init([LogService.defaultChannel]) private var dateFormatter: DateFormatter private var fileWriter: FileWriter + private var configuration: LoggerConfiguration + private var ingredients: [LoggerConfiguration.LogIngredient] public init(dateFormatter: DateFormatter, fileWriter: FileWriter, - identifier: String = "Default FileLogProvider") { + identifier: String = "Default FileLogProvider", + configuration: LoggerConfiguration = LogService.shared.configuration) { self.dateFormatter = dateFormatter self.fileWriter = fileWriter self.logIdentifier = identifier + self.configuration = configuration + self.ingredients = configuration.ingredients.sorted() } - public func log(_ severity: LogSeverity, message: String, file: String, function: String, line: Int) { - if let _ = fileWriter as? BriefLogFileWriter { - fileWriter.write("[\(severity.prettyDescription) \(file):\(function):\(line)] \(message)") - } else { - fileWriter.write("[\(severity.prettyDescription) \(dateFormatter.getCurrentDateAsString()) \(file):\(function):\(line)] \(message)") - } + public func log(_ severity: LogSeverity, + message: String, + file: String, + function: String, + line: Int, + channel: String?) { + let message = LoggedMessage(payload: message, severity: severity, callingFilePath: file, callingFileLine: line, callingStackFrame: function, callingThreadID: UInt64(ProcessIdentification.current.processID), channel: channel ?? LogService.defaultChannel) + evaluate(message) } public func log(_ message: LogMessage) { - if let _ = fileWriter as? BriefLogFileWriter { - fileWriter.write("[\(message.severity.prettyDescription) \(message.callingFilePath):\(message.callingStackFrame):\(message.callingFileLine)] \(message.payload)") - } else { - fileWriter.write("[\(message.severity.prettyDescription) \(dateFormatter.getCurrentDateAsString()) \(message.callingFilePath):\(message.callingStackFrame):\(message.callingFileLine)] \(message.payload)") + evaluate(message) + } + + public func addChannel(_ channel: String) { + channels.insert(channel) + } + + public func removeChannel(_ channel: String) { + channels.remove(channel) + } + +} + +private extension FileLogProvider { + + func evaluate(_ message: LogMessage) { + guard !channels.isEmpty else { + writeLog(message: message) + return } + + if channels.contains(message.channel) { + writeLog(message: message) + } + } + + func writeLog(message: LogMessage) { + let finalMessage = LogMessageFormatter.compose(message, using: ingredients, dateFormatter: dateFormatter) + fileWriter.write(finalMessage) } } diff --git a/Sources/BattleAxe/Providers/LogProvider.swift b/Sources/BattleAxe/Providers/LogProvider.swift index f56780f..e5c96cb 100644 --- a/Sources/BattleAxe/Providers/LogProvider.swift +++ b/Sources/BattleAxe/Providers/LogProvider.swift @@ -5,6 +5,16 @@ public protocol LogProvider { var logIdentifier: String { get set } + /// Tells if a LogProvider should log only for a specific channel or not. + /// If it's empty it means that it will log for every channel. + var channels: Set { get } + + /// This method gets called by the LogService every time the LogSerivce.shared.log() gets called. (If the logger is enabled and the minimum log level is lower than the logged severity) + /// - Parameters: + /// - message: The message that's being logged. func log(_ message: LogMessage) + func addChannel(_ channel: String) + + func removeChannel(_ channel: String) } diff --git a/Sources/BattleAxe/Providers/OSLogProvider.swift b/Sources/BattleAxe/Providers/OSLogProvider.swift index 27db1bf..9d4c3a8 100644 --- a/Sources/BattleAxe/Providers/OSLogProvider.swift +++ b/Sources/BattleAxe/Providers/OSLogProvider.swift @@ -9,12 +9,13 @@ import Foundation import os.log @available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *) -public struct OSLogProvider: LogProvider { +public final class OSLogProvider: LogProvider { public var logIdentifier: String private var dateFormatter: DateFormatter - private var subsystem: String + public var channels: Set = .init([LogService.defaultChannel]) + public var subsystem: String public init(dateFormatter: DateFormatter, subsystem: String = "", @@ -25,8 +26,27 @@ public struct OSLogProvider: LogProvider { } public func log(_ message: LogMessage) { - let log = OSLog(subsystem: self.subsystem, category: "BattleAxe") - let type = message.severity.toOSLogLevel() + guard !channels.isEmpty else { + writeLog(with: message) + return + } + + if channels.contains(message.channel) { + writeLog(with: message) + } + } + + public func addChannel(_ channel: String) { + channels.insert(channel) + } + + public func removeChannel(_ channel: String) { + channels.remove(channel) + } + + private func writeLog(with message: LogMessage) { + let log = OSLog(subsystem: self.subsystem, category: message.channel) + let type = message.severity.OSLogLevel os_log("%{public}@", log: log, type: type, message.severity.emoji + " " + message.payload) } diff --git a/Tests/BattleAxeTests/FullFileWriterTests.swift b/Tests/BattleAxeTests/FullFileWriterTests.swift index 815831c..864312a 100644 --- a/Tests/BattleAxeTests/FullFileWriterTests.swift +++ b/Tests/BattleAxeTests/FullFileWriterTests.swift @@ -49,6 +49,7 @@ final class FullFileWriterTests: XCTestCase { measure { fileWriter.write("Hello") } + } func testDefaultPerformances() { @@ -119,22 +120,50 @@ final class FullFileWriterTests: XCTestCase { let configuration = FileWriterConfiguration(filename: "myFile", appGroup: nil, queueName: "StandardLogFileWriter", rotationConfiguration: .none, fileManager: mockManager, fileSeeker: BAFileController(fileSystemController: FileManager.default)) let fileWriter = MockFileWriter(configuration) let formatter = LogDateFormatter(dateFormat: "yyyy-MM-dd") - let logprovider = FileLogProvider(dateFormatter: formatter, fileWriter: fileWriter) + let logprovider = FileLogProvider(dateFormatter: formatter, fileWriter: fileWriter, configuration: .naive) let message = "Ciao" let expectedFunc = "function" let expectedFile = "file" let line = 44 + let channel = "Additional Channel" + logprovider.addChannel(channel) - logprovider.log(.debug, message: message, file: expectedFile, function: expectedFunc, line: line) + logprovider.log(.debug, message: message, file: expectedFile, function: expectedFunc, line: line, channel: channel) XCTAssertNotNil(fileWriter.lastPrintedMessage) if let lastMessage = fileWriter.lastPrintedMessage { - XCTAssert(lastMessage == "[\(LogSeverity.debug.prettyDescription) \(self.getCurrentDateString()) \(expectedFile):\(expectedFunc):\(line)] \(message)") + XCTAssert(lastMessage == "{\(channel)} [\(LogSeverity.debug.prettyDescription)] \(message)") } else { XCTFail("The message is nil") } + + logprovider.removeChannel(channel) + logprovider.log(.debug, message: message, file: expectedFile, function: expectedFunc, line: line, channel: nil) + + XCTAssertNotNil(fileWriter.lastPrintedMessage) + + if let lastMessage = fileWriter.lastPrintedMessage { + XCTAssert(lastMessage == "{\(LogService.defaultChannel)} [\(LogSeverity.debug.prettyDescription)] \(message)") + } else { + XCTFail("The message is nil") + } + } + + func testLogComposer() { + let message = "Ciao" + let file = "myFile" + let function = "myFunction" + let fileLine = 1 + let severity = LogSeverity.error + let data = Date() + let formatter = LogDateFormatter(dateFormat: "yyyy-MM-dd") + let loggedMessage = LogMessageFormatter.compose(LoggedMessage(callingThread: "MyThread", processId: 4, payload: message, severity: severity, callingFilePath: file, callingFileLine: fileLine, callingStackFrame: function, callingThreadID: 4, channel: LogService.defaultChannel, timestamp: data), using: LoggerConfiguration.naive.ingredients.sorted(), dateFormatter: formatter) + + let expectedMessage = "{\(LogService.defaultChannel)} [\(LogSeverity.error.prettyDescription)] \(message)" + + XCTAssertEqual(loggedMessage, expectedMessage) } private func getCurrentDateString() -> String { @@ -170,6 +199,7 @@ final class FullFileWriterTests: XCTestCase { ("testOSLogPerformances", testOSLogPerformances), ("testConsoleLogPerformances", testConsoleLogPerformances), ("testMultipleProviderPerformances", testMultipleProviderPerformances), + ("testLogComposer", testLogComposer) // ("testDeletion", testDeletion) ] diff --git a/Tests/BattleAxeTests/LogServiceTests.swift b/Tests/BattleAxeTests/LogServiceTests.swift index 6a391ee..d366b67 100644 --- a/Tests/BattleAxeTests/LogServiceTests.swift +++ b/Tests/BattleAxeTests/LogServiceTests.swift @@ -87,6 +87,56 @@ final class LogServiceTests: XCTestCase { } } + func testLogChannel() { + LogService.empty() + let message1 = "Ciao" + let message2 = "Ciao 2" + let message3 = "Ciao 3" + let channelName = "Channel" + let handler = MockConsoleLogger() + let handler2 = MockConsoleLogger() + handler2.channels = .init([channelName]) + LogService.shared.minimumSeverity = .verbose + LogService.register(provider: handler) + LogService.register(provider: handler2) + LogService.shared.debug(message1) + + XCTAssertNil(handler2.lastMessage) + XCTAssertNotNil(handler.lastMessage) + XCTAssertEqual(handler.lastMessage?.payload, message1) + + LogService.shared.log(.debug, message2, channel: channelName) + + XCTAssertNotNil(handler2.lastMessage) + XCTAssertNotNil(handler.lastMessage) + XCTAssertEqual(handler2.lastMessage?.payload, message2) + XCTAssertEqual(handler.lastMessage?.payload, message1) + + let newChannel = "New Channel 📟" + handler.addChannel(newChannel) + handler2.addChannel(newChannel) + + handler.removeChannel(channelName) + handler2.removeChannel(channelName) + + LogService.shared.log(.debug, message3, channel: newChannel) + + XCTAssertNotNil(handler2.lastMessage) + XCTAssertNotNil(handler.lastMessage) + XCTAssertEqual(handler2.lastMessage?.payload, message3) + XCTAssertEqual(handler.lastMessage?.payload, message3) + + handler.lastMessage = nil + handler2.lastMessage = nil + LogService.shared.log(.debug, "Not going to be printed", channel: channelName) + + XCTAssertNil(handler2.lastMessage) + XCTAssertNil(handler.lastMessage) + + XCTAssert(handler.channels.count == 2) + XCTAssert(handler2.channels.count == 1) + } + func testLogDebug() { // let listener = MockConsoleLogger() // listener.lastMessage = nil @@ -178,11 +228,44 @@ final class LogServiceTests: XCTestCase { XCTAssertNotNil(fileWriter.lastPrintedMessage) } + func testSilenceChannel() { + LogService.empty() + let message1 = "Ciao" + let message2 = "Ciao 2" + let channelName = "Channel" + let handler = MockConsoleLogger() + let handler2 = MockConsoleLogger() + LogService.shared.minimumSeverity = .verbose + LogService.register(provider: handler) + LogService.register(provider: handler2) + LogService.add(channelName) + LogService.shared.debug(message1, channel: channelName) + LogService.currentProviders.forEach { provider in + XCTAssert(provider.channels.contains(channelName)) + } + + // XCTAssertNotNil(handler.lastMessage) + // XCTAssertNotNil(handler2.lastMessage) + + LogService.silence(channelName) + LogService.currentProviders.forEach { provider in + XCTAssertFalse(provider.channels.contains(channelName)) + } + handler.lastMessage = nil + handler2.lastMessage = nil + LogService.shared.debug(message2, channel: channelName) + + XCTAssertNil(handler.lastMessage) + XCTAssertNil(handler2.lastMessage) + } + func testExternalLogProvider() { let message = "Ciao" + let additionalChannel = "New Channel" let externalHandler = ExternalLogHandler() let listener = MockListener() externalHandler.setListener(listener: listener) + externalHandler.addChannel(additionalChannel) LogService.register(provider: externalHandler) LogService.shared.minimumSeverity = .info let cases = LogSeverity.allCases @@ -191,6 +274,9 @@ final class LogServiceTests: XCTestCase { } XCTAssertNotNil(listener.lastMessage) + XCTAssert(externalHandler.channels.count == 2) + externalHandler.removeChannel(additionalChannel) + XCTAssert(externalHandler.channels.count == 1) } static var allTests = [ @@ -204,6 +290,8 @@ final class LogServiceTests: XCTestCase { ("testEmptyProviders", testEmptyProviders), ("testRemoveProvider", testRemoveProvider), ("testAllSeveritiesLogging", testAllSeveritiesLogging), - ("testAllLoggingShortcuts", testAllLoggingShortcuts) + ("testAllLoggingShortcuts", testAllLoggingShortcuts), + ("testLogChannel", testLogChannel), + ("testSilenceChannel", testSilenceChannel) ] } diff --git a/Tests/BattleAxeTests/LogSeverityTest.swift b/Tests/BattleAxeTests/LogSeverityTest.swift index 2f890e1..625e157 100644 --- a/Tests/BattleAxeTests/LogSeverityTest.swift +++ b/Tests/BattleAxeTests/LogSeverityTest.swift @@ -16,15 +16,15 @@ final class LogSeverityTest: XCTestCase { for severity in severities { switch severity { case .info: - XCTAssert(severity.toOSLogLevel() == .info) + XCTAssert(severity.OSLogLevel == .info) case .verbose: - XCTAssert(severity.toOSLogLevel() == .default) + XCTAssert(severity.OSLogLevel == .default) case .debug: - XCTAssert(severity.toOSLogLevel() == .debug) + XCTAssert(severity.OSLogLevel == .debug) case .warning: - XCTAssert(severity.toOSLogLevel() == .fault) + XCTAssert(severity.OSLogLevel == .fault) case .error: - XCTAssert(severity.toOSLogLevel() == .error) + XCTAssert(severity.OSLogLevel == .error) } } } diff --git a/Tests/BattleAxeTests/Mocks/MockConsoleLogger.swift b/Tests/BattleAxeTests/Mocks/MockConsoleLogger.swift index e4e574b..865eb95 100644 --- a/Tests/BattleAxeTests/Mocks/MockConsoleLogger.swift +++ b/Tests/BattleAxeTests/Mocks/MockConsoleLogger.swift @@ -9,12 +9,30 @@ import Foundation import BattleAxe final class MockConsoleLogger: LogProvider { + public var channels: Set = .init([LogService.defaultChannel]) + var logIdentifier: String = "MockConsole Logger" + var lastMessage: LogMessage? = nil - func log(_ message: LogMessage) { - lastMessage = message + + public func log(_ message: LogMessage) { + + guard !channels.isEmpty else { + lastMessage = message + return + } + + if channels.contains(message.channel) { + lastMessage = message + } } - var lastMessage: LogMessage? = nil + func addChannel(_ channel: String) { + channels.insert(channel) + } + + func removeChannel(_ channel: String) { + channels.remove(channel) + } } diff --git a/Tests/BattleAxeTests/Mocks/MockFileLogProvider.swift b/Tests/BattleAxeTests/Mocks/MockFileLogProvider.swift index fde0c55..8db5212 100644 --- a/Tests/BattleAxeTests/Mocks/MockFileLogProvider.swift +++ b/Tests/BattleAxeTests/Mocks/MockFileLogProvider.swift @@ -10,6 +10,8 @@ import BattleAxe final class MockFileLogProvider: LogProvider { + public var channels: Set = .init([LogService.defaultChannel]) + private var dateFormatter: DateFormatter private var fileWriter: FileWriter public var logIdentifier: String = "Mock FileLogProvider" @@ -23,4 +25,12 @@ final class MockFileLogProvider: LogProvider { fileWriter.write(message.payload + " " + message.severity.prettyDescription) } + func addChannel(_ channel: String) { + channels.insert(channel) + } + + func removeChannel(_ channel: String) { + channels.remove(channel) + } + }