diff --git a/Logger/LogStringConvertible.swift b/Logger/LogStringConvertible.swift index 87bab24..99745c6 100644 --- a/Logger/LogStringConvertible.swift +++ b/Logger/LogStringConvertible.swift @@ -24,11 +24,15 @@ import Foundation +/// A type with a customized textual representation suitable for logging purposes. public protocol LogStringConvertible { + + /// A textual representation of this instance, suitable for logging. var logDescription: String { get } } extension Array: LogStringConvertible where Element: LogStringConvertible { + public var logDescription: String { let descriptions = map { $0.logDescription } let joinedDescriptions = descriptions.joined(separator: ", ") diff --git a/Logger/Logger.swift b/Logger/Logger.swift index 5e229bd..b952d4e 100644 --- a/Logger/Logger.swift +++ b/Logger/Logger.swift @@ -24,23 +24,75 @@ import Foundation +/// A type able to build custom log message and send it to a logging system. public protocol Logger { + + /// Builds log a message from passed parameters and sends it to the logging system. + /// + /// Do not call this method directly. + /// + /// - Parameters: + /// - time: Part of the log message provided by `description(for date: Date)` method. + /// - level: The log level. + /// - location: Part of the log message provided by `description(for file: String, line: Int, function: String)` method. + /// - object: Part of the log message provided by `description(for object: Any)` method. func log(time: String, level: LogLevel, location: String, object: String) + /// Transforms `date` into textual representation which will be a part of the log message. + /// + /// Customization point. Default implementation returns date in format `"yyyy-MM-dd HH:mm:ss.SSS"`. + /// + /// - Parameter date: The date log method was called. func description(for date: Date) -> String + + /// Transforms passed parameters into location textual representation which will be a part of the log message. + /// + /// Customization point. Default implementation returns location in format `": "`, + /// + /// - Parameters: + /// - file: The path to the file log method was called from. + /// - line: The line number log method was called at. + /// - function: The name of the declaration log method was called within. func description(for file: String, line: Int, function: String) -> String + + /// Transforms `object` into textual representation which will be a part of the log message. + /// + /// Customization point. Default implementation returns `logDescription` for objects implementing `LogStringConvertible`, + /// or `String(describing:object)` otherwise. + /// + /// - Parameter object: The object to be logged. func description(for object: Any) -> String } +/// Log level controls the conditions under which a message should be logged. public enum LogLevel: String { + + /// Use this level to capture information about things that might result a failure. case `default` = "Default" + + /// Use this level to capture information that may be helpful, but isn’t essential, for troubleshooting errors. case info = "Info" + + /// Use this level to capture information that may be useful during development or while troubleshooting a specific problem. case debug = "Debug" + + /// Use this log level to capture process-level information to report errors in the process. case error = "Error" + + /// Use this level to capture system-level or multi-process information to report system errors. case fault = "Fault" } extension Logger { + + /// Sends object to the logging system, optionally specifying a custom log level. + /// + /// - Parameters: + /// - object: The object to be logged. + /// - level: The log level. If unspecified, the `default` log level is used. + /// - file: **Do not provide a custom value.** The path to the file log method is called from. + /// - line: **Do not provide a custom value.** The line number log method is called at. + /// - function: **Do not provide a custom value.** The name of the declaration log method is called within. public func log(_ object: Any, level: LogLevel = .default, file: String = #file, line: Int = #line, function: String = #function) { let now = Date() let time = description(for: now) @@ -49,14 +101,17 @@ extension Logger { log(time: time, level: level, location: location, object: objectDescription) } + /// Returns date in format `"yyyy-MM-dd HH:mm:ss.SSS"`. public func description(for date: Date) -> String { return dateFormatter.string(from: date) } + /// Returns location in format `": "`. public func description(for file: String, line: Int, function: String) -> String { return filename(fromFilePath: file) + ":\(line) \(function)" } + /// Returns `logDescription` for objects implementing `LogStringConvertible`, or `String(describing:object)` otherwise. public func description(for object: Any) -> String { if let logStringConvertible = object as? LogStringConvertible { return logStringConvertible.logDescription diff --git a/Logger/Loggers/AgregateLogger.swift b/Logger/Loggers/AgregateLogger.swift index 5bedb00..d2e1a78 100644 --- a/Logger/Loggers/AgregateLogger.swift +++ b/Logger/Loggers/AgregateLogger.swift @@ -24,9 +24,14 @@ import Foundation +/// Logger that forwards messages to all the loggers it is intialized with. public final class AgregateLogger: Logger { + private let loggers: [Logger] + /// Initializes new AgregateLogger instance. + /// + /// - Parameter loggers: Array of loggers all messages will be forwarded to. public init(loggers: [Logger]) { self.loggers = loggers } diff --git a/Logger/Loggers/ConsoleLogger.swift b/Logger/Loggers/ConsoleLogger.swift index 5d1745e..d4eed49 100644 --- a/Logger/Loggers/ConsoleLogger.swift +++ b/Logger/Loggers/ConsoleLogger.swift @@ -24,6 +24,7 @@ import Foundation +/// Logger that writes messages into the standard output. public final class ConsoleLogger: Logger { public init() { } diff --git a/Logger/Loggers/DiskLogger/DiskLogger.swift b/Logger/Loggers/DiskLogger/DiskLogger.swift index 49e6441..b51b9f4 100644 --- a/Logger/Loggers/DiskLogger/DiskLogger.swift +++ b/Logger/Loggers/DiskLogger/DiskLogger.swift @@ -24,7 +24,9 @@ import Foundation +/// Logger that writes messages into the file at specified URL with log rotation support. public final class DiskLogger: Logger { + private let fileURL: URL private let fileSizeLimit: UInt64 private let rotations: Int @@ -32,6 +34,12 @@ public final class DiskLogger: Logger { private var buffer = Data() private var fileWriter: FileWriter! + /// Initializes new DiskLogger instance. + /// + /// - Parameters: + /// - fileURL: URL of the log file. + /// - fileSizeLimit: Maximum size log file can reach in bytes. Attempt to exceeding that limit triggers log files rotation. + /// - rotations: Number of times log files are rotated before being removed. public init(fileURL: URL, fileSizeLimit: UInt64, rotations: Int) { self.fileURL = fileURL self.fileSizeLimit = fileSizeLimit diff --git a/Logger/Loggers/DiskLogger/FileWriter.swift b/Logger/Loggers/DiskLogger/FileWriter.swift index 0a624f1..1083812 100644 --- a/Logger/Loggers/DiskLogger/FileWriter.swift +++ b/Logger/Loggers/DiskLogger/FileWriter.swift @@ -24,20 +24,33 @@ import Foundation +/// Allows writing to a file while respecting allowed size limit. final class FileWriter { + /// Write failed as allowed size limit would be exceeded for the file. struct FileSizeLimitReached: Error {} private let handle: FileHandle private let sizeLimit: UInt64 private var currentSize: UInt64 + /// Initializes new FileWriter instance. + /// + /// - Parameters: + /// - fileURL: URL of the file. + /// - fileSizeLimit: Maximum size the file can reach in bytes. + /// - Throws: An error that may occur while the file is being opened for writing. init(fileURL: URL, fileSizeLimit: UInt64) throws { handle = try FileHandle(forWritingTo: fileURL) self.sizeLimit = fileSizeLimit currentSize = handle.seekToEndOfFile() } + /// Synchronously writes `data` at the end of the file. + /// + /// - Parameter data: The data to be written. + /// - Throws: Throws an error if no free space is left on the file system, or if any other writing error occurs. + /// Throws `FileSizeLimitReached` if allowed size limit would be exceeded for the file. func write(_ data: Data) throws { let dataSize = UInt64(data.count) guard currentSize + dataSize <= sizeLimit else { @@ -47,6 +60,7 @@ final class FileWriter { currentSize += dataSize } + /// Writes all in-memory data to permanent storage and closes the file. func synchronizeAndCloseFile() { handle.synchronizeFile() handle.closeFile() diff --git a/Logger/Loggers/DiskLogger/Logrotate.swift b/Logger/Loggers/DiskLogger/Logrotate.swift index 4d177e5..c377d2e 100644 --- a/Logger/Loggers/DiskLogger/Logrotate.swift +++ b/Logger/Loggers/DiskLogger/Logrotate.swift @@ -24,16 +24,31 @@ import Foundation +/// Allows log files rotation. final class Logrotate { + private let fileURL: URL private let rotations: Int + /// Initializes new Logrotate instance. + /// + /// - Parameters: + /// - fileURL: URL of the log file. + /// - rotations: Number of times log files are rotated before being removed. init(fileURL: URL, rotations: Int) { precondition(rotations > 0) self.fileURL = fileURL self.rotations = rotations } + /// Rotates log files `rotations` number of times. + /// + /// First deletes file at `.`. + /// Next moves files located at: + /// + /// `, .1, .2 ... .` + /// + /// to `.1, .2 ... .` func rotate() throws { let range = 1...rotations let pathExtensions = range.map { "\($0)" } diff --git a/Logger/Loggers/NullLogger.swift b/Logger/Loggers/NullLogger.swift index 7c3160d..0787a99 100644 --- a/Logger/Loggers/NullLogger.swift +++ b/Logger/Loggers/NullLogger.swift @@ -24,6 +24,7 @@ import Foundation +/// Logger that ignores all messages with the intention to minimize observer effect. public class NullLogger: Logger { public init() { } @@ -32,14 +33,17 @@ public class NullLogger: Logger { // noop } + /// Returns empty string to minimize observer effect. public func description(for date: Date) -> String { return "" } + /// Returns empty string to minimize observer effect. public func description(for file: String, line: Int, function: String) -> String { return "" } + /// Returns empty string to minimize observer effect. public func description(for object: Any) -> String { return "" }