import Foundation // MARK: Public API func log(_ message: String, file: String = #file, line: Int = #line) { Log.log(info: message, file: file, line: line) } func log(error message: String, file: String = #file, line: Int = #line) { Log.log(error: message, file: file, line: line) } func log(warning message: String, file: String = #file, line: Int = #line) { Log.log(warning: message, file: file, line: line) } func log(info message: String, file: String = #file, line: Int = #line) { Log.log(info: message, file: file, line: line) } func log(debug message: String, file: String = #file, line: Int = #line) { Log.log(debug: message, file: file, line: line) } struct Log { enum Level: Int, Comparable { case debug = 1 case info = 2 case warning = 3 case error = 4 case none = 5 var tag: String { switch self { case .debug: return "DEBUG" case .info: return "INFO" case .warning: return "WARN" case .error: return "ERROR" case .none: return "" } } var description: String { switch self { case .debug: return "Debug" case .info: return "Infos" case .warning: return "Warnungen" case .error: return "Fehler" case .none: return "Kein Logging" } } static func < (lhs: Level, rhs: Level) -> Bool { lhs.rawValue < rhs.rawValue } } private static var logLevels = [String : Level]() private static var fileHandle: FileHandle? private(set) static var path: URL? /// The date formatter for the logging timestamps private static let df: DateFormatter = { let df = DateFormatter() df.dateStyle = .short df.timeStyle = .short df.locale = Locale(identifier: "de") return df }() private init() { // Prevent any initializations } // MARK: Public API static func log(error message: String, file: String = #file, line: Int = #line) { log(message, level: .error, file: file, line: line) } static func log(warning message: String, file: String = #file, line: Int = #line) { log(message, level: .warning, file: file, line: line) } static func log(info message: String, file: String = #file, line: Int = #line) { log(message, level: .info, file: file, line: line) } static func log(debug message: String, file: String = #file, line: Int = #line) { log(message, level: .debug, file: file, line: line) } static func log(raw message: String) { } /** The global log level. Used when no specific log level is set for a file via `LogLevel.logLevel()` */ static var globalLevel: Level = .debug /** Remove the custom log level for a file, reverting it to the `globalLevel` Best called without arguments within the file to modify: `Log.clearLogLevel()` */ static func defaultLogLevel(file: String = #file) { logLevels[name(from: file)] = nil } /** Set a custom log level for a file, ignoring the global default. Example: `Log.set(logLevel = .debug)` */ static func set(logLevel: Level, file: String = #file) { logLevels[name(from: file)] = logLevel } /** Set the file to log to. */ static func set(logFile: URL) { fileHandle?.closeFile() if !FileManager.default.fileExists(atPath: logFile.path) { do { try "New log started.\n".data(using: .utf8)!.write(to: logFile) } catch { print("Failed to start logging to \(logFile.path): \(error)") } } do { let f = try FileHandle(forWritingTo: logFile) path = logFile fileHandle = f print("Log file set to \(logFile.path)") } catch { print("No file handle to write log: \(error)") fileHandle = nil path = nil } } /** Close the log file. Subsequent logging event will no longer be written to the log file. Set a new log file by calling `Log.set(logFile:)` */ static func close() { fileHandle?.closeFile() fileHandle = nil } /** Clear all data from the log file. The log file will remain active, to close it permanently call `Log.close()` */ static func clear() { guard let f = fileHandle, let p = path else { return } f.closeFile() try? FileManager.default.removeItem(at: p) fileHandle = try? FileHandle(forWritingTo: p) } /** Get the full text from the log file. */ static func fullText() -> String? { guard let u = path else { return nil } return try? String(contentsOf: u) } /** Get the full data from the log file. */ static func data() -> Data? { guard let f = fileHandle, let p = path else { print("No log file set to get data") return nil } f.closeFile() defer { do { fileHandle = try FileHandle(forWritingTo: p) } catch { fileHandle = nil path = nil print("Failed to create log file handle: \(error)") } } do { return try Data(contentsOf: p) } catch { print("Failed to get log data: \(error)") return nil } } // MARK: Private helper /// Get the pure file name for a file path from `#file` fileprivate static func name(from file: String) -> String { file.components(separatedBy: "/").last!.replacingOccurrences(of: ".swift", with: "") } /// Get the pure file name tag if the specified log level is high enough fileprivate static func tagIf(logLevel: Level, isSufficientFor file: String) -> String? { let tag = name(from: file) let requiredLevel = logLevels[tag] ?? globalLevel if logLevel >= requiredLevel { return tag } return nil } fileprivate static func log(_ message: String, level: Level, file: String, line: Int) { guard let tag = tagIf(logLevel: level, isSufficientFor: file) else { return } let date = df.string(from: Date()) let msg = "[\(date)][\(level.tag)][\(tag):\(line)] \(message)" log(msg) } private static func log(_ msg: String) { print(msg) if let f = fileHandle, let data = (msg + "\n").data(using: .utf8) { f.write(data) try? f.synchronize() } } }