import Foundation final class Logger { static let shared = Logger() private let queue = DispatchQueue(label: "org.prole.tools.logger", qos: .utility) private var currentDateStr: String = Logger.dateString(Date()) private init() { // Ensure directories exist ProleEnv.bootstrap() // Ensure current symlink points to today's file (best effort) queue.async { [weak self] in self?.ensureSymlink() } } private static func dateString(_ date: Date) -> String { let fmt = DateFormatter() fmt.locale = Locale(identifier: "en_US_POSIX") fmt.dateFormat = "yyyy-MM-dd" return fmt.string(from: date) } private func logDir() -> URL { ProleEnv.logsDir() } private func datedLogURL(for dateStr: String) -> URL { return logDir().appendingPathComponent("prole-tools-app-\(dateStr).log") } private func currentLogURL() -> URL { return logDir().appendingPathComponent("prole-tools-app.log") } private func ensureSymlink() { let fm = FileManager.default let target = datedLogURL(for: currentDateStr) let link = currentLogURL() // If link exists and points to target, nothing to do if let attrs = try? fm.attributesOfItem(atPath: link.path), attrs[.type] as? FileAttributeType == .typeSymbolicLink { if let dest = try? fm.destinationOfSymbolicLink(atPath: link.path), dest.hasSuffix(target.lastPathComponent) { return } } // Recreate symlink to today's file _ = try? fm.removeItem(at: link) try? fm.createSymbolicLink(at: link, withDestinationURL: target) } private func rotateIfNeeded(now: Date = Date()) { let today = Logger.dateString(now) if today != currentDateStr { currentDateStr = today ensureSymlink() } } func info(_ message: String) { write(level: "INFO", message) } func error(_ message: String) { write(level: "ERROR", message) } func write(level: String, _ message: String) { queue.async { self.rotateIfNeeded() let ts = ISO8601DateFormatter().string(from: Date()) let line = "[\(level)] \(ts) \(message)\n" let fileURL = self.datedLogURL(for: self.currentDateStr) if let data = line.data(using: .utf8) { if FileManager.default.fileExists(atPath: fileURL.path) { if let h = try? FileHandle(forWritingTo: fileURL) { defer { try? h.close() } try? h.seekToEnd() try? h.write(contentsOf: data) } } else { try? data.write(to: fileURL, options: .atomic) } } } } }