// // Logging.swift // oAI // // Dual logging: os.Logger (unified log) + file (~Library/Logs/oAI.log) // import Foundation import os // MARK: - Log Level enum LogLevel: Int, Comparable, CaseIterable, Sendable { case debug = 0 case info = 1 case warning = 2 case error = 3 var label: String { switch self { case .debug: return "DEBUG" case .info: return "INFO" case .warning: return "WARN" case .error: return "ERROR" } } var displayName: String { switch self { case .debug: return "Debug" case .info: return "Info" case .warning: return "Warning" case .error: return "Error" } } static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { lhs.rawValue < rhs.rawValue } } // MARK: - File Logger final class FileLogger: @unchecked Sendable { static let shared = FileLogger() private let fileHandle: FileHandle? private let queue = DispatchQueue(label: "com.oai.filelogger") private let dateFormatter: DateFormatter = { let f = DateFormatter() f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" return f }() /// Current minimum log level (read from UserDefaults for thread safety) var minimumLevel: LogLevel { get { let raw = UserDefaults.standard.integer(forKey: "logLevel") return LogLevel(rawValue: raw) ?? .info } set { UserDefaults.standard.set(newValue.rawValue, forKey: "logLevel") } } private init() { let logsDir = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Logs") let logFile = logsDir.appendingPathComponent("oAI.log") // Ensure file exists if !FileManager.default.fileExists(atPath: logFile.path) { FileManager.default.createFile(atPath: logFile.path, contents: nil) } fileHandle = try? FileHandle(forWritingTo: logFile) fileHandle?.seekToEndOfFile() } func write(_ level: LogLevel, category: String, message: String) { guard level >= minimumLevel else { return } queue.async { [weak self] in guard let self, let fh = self.fileHandle else { return } let timestamp = self.dateFormatter.string(from: Date()) let line = "[\(timestamp)] [\(level.label)] [\(category)] \(message)\n" if let data = line.data(using: .utf8) { fh.write(data) } } } deinit { fileHandle?.closeFile() } } // MARK: - App Logger (wraps os.Logger + file) struct AppLogger { let osLogger: Logger let category: String func debug(_ message: String) { FileLogger.shared.write(.debug, category: category, message: message) osLogger.debug("\(message, privacy: .public)") } func info(_ message: String) { FileLogger.shared.write(.info, category: category, message: message) osLogger.info("\(message, privacy: .public)") } func warning(_ message: String) { FileLogger.shared.write(.warning, category: category, message: message) osLogger.warning("\(message, privacy: .public)") } func error(_ message: String) { FileLogger.shared.write(.error, category: category, message: message) osLogger.error("\(message, privacy: .public)") } } // MARK: - Log Namespace enum Log { private static let subsystem = "com.oai.oAI" static let api = AppLogger(osLogger: Logger(subsystem: subsystem, category: "api"), category: "api") static let db = AppLogger(osLogger: Logger(subsystem: subsystem, category: "database"), category: "database") static let mcp = AppLogger(osLogger: Logger(subsystem: subsystem, category: "mcp"), category: "mcp") static let settings = AppLogger(osLogger: Logger(subsystem: subsystem, category: "settings"), category: "settings") static let search = AppLogger(osLogger: Logger(subsystem: subsystem, category: "search"), category: "search") static let ui = AppLogger(osLogger: Logger(subsystem: subsystem, category: "ui"), category: "ui") static let general = AppLogger(osLogger: Logger(subsystem: subsystem, category: "general"), category: "general") }