From fd1219580de3ca3f42bdc93182f0c692db085eec Mon Sep 17 00:00:00 2001 From: Beingpax Date: Mon, 12 Jan 2026 16:45:45 +0545 Subject: [PATCH] Enable log export across multiple sessions for better diagnostic support --- VoiceInk/Services/LogExporter.swift | 131 +++++++++++++++++----------- 1 file changed, 80 insertions(+), 51 deletions(-) diff --git a/VoiceInk/Services/LogExporter.swift b/VoiceInk/Services/LogExporter.swift index 6ca4bb1..65c132b 100644 --- a/VoiceInk/Services/LogExporter.swift +++ b/VoiceInk/Services/LogExporter.swift @@ -1,111 +1,140 @@ import Foundation import OSLog -/// Utility class for exporting app logs since launch for diagnostic purposes final class LogExporter { static let shared = LogExporter() private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "LogExporter") private let subsystem = "com.prakashjoshipax.voiceink" + private let maxSessionsToKeep = 3 + private let sessionsKey = "logExporter.sessionStartDates.v1" - /// Timestamp when the app was launched - let launchDate: Date + private(set) var sessionStartDates: [Date] = [] private init() { - self.launchDate = Date() - logger.notice("🎙️ LogExporter initialized, launch timestamp recorded") + var loadedDates: [Date] = [] + if let data = UserDefaults.standard.data(forKey: sessionsKey), + let dates = try? JSONDecoder().decode([Date].self, from: data) { + loadedDates = dates + } + + sessionStartDates = [Date()] + loadedDates + sessionStartDates = Array(sessionStartDates.prefix(maxSessionsToKeep)) + saveSessions() + + logger.notice("🎙️ LogExporter initialized, \(self.sessionStartDates.count) session(s) tracked") + } + + private func saveSessions() { + if let data = try? JSONEncoder().encode(sessionStartDates) { + UserDefaults.standard.set(data, forKey: sessionsKey) + } } - /// Exports logs since app launch to a file and returns the file URL func exportLogs() async throws -> URL { - logger.notice("🎙️ Starting log export since \(self.launchDate)") - - let logs = try await fetchLogsSinceLaunch() + logger.notice("🎙️ Starting log export") + let logs = try await fetchLogs() let fileURL = try saveLogsToFile(logs) logger.notice("🎙️ Log export completed: \(fileURL.path)") - return fileURL } - /// Fetches logs from OSLogStore since app launch - private func fetchLogsSinceLaunch() async throws -> [String] { - let store = try OSLogStore(scope: .currentProcessIdentifier) - - // Get logs since launch - let position = store.position(date: launchDate) - - // Create predicate to filter by our subsystem + private func fetchLogs() async throws -> [String] { + let store = try OSLogStore(scope: .system) let predicate = NSPredicate(format: "subsystem == %@", subsystem) - let entries = try store.getEntries(at: position, matching: predicate) - var logLines: [String] = [] - - // Add header let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" logLines.append("=== VoiceInk Diagnostic Logs ===") logLines.append("Export Date: \(dateFormatter.string(from: Date()))") - logLines.append("App Launch: \(dateFormatter.string(from: launchDate))") logLines.append("Subsystem: \(subsystem)") + logLines.append("Total Sessions: \(sessionStartDates.count)") logLines.append("================================") logLines.append("") - for entry in entries { - guard let logEntry = entry as? OSLogEntryLog else { continue } + // Build session ranges with labels + let totalSessions = sessionStartDates.count + var sessionRanges: [(label: String, start: Date, end: Date?)] = [] - let timestamp = dateFormatter.string(from: logEntry.date) - let level = logLevelString(logEntry.level) - let category = logEntry.category - let message = logEntry.composedMessage + for i in 0..= endDate { break } + + let timestamp = dateFormatter.string(from: logEntry.date) + let level = logLevelString(logEntry.level) + let category = logEntry.category + let message = logEntry.composedMessage + + logLines.append("[\(timestamp)] [\(level)] [\(category)] \(message)") + sessionLogCount += 1 + } + + if sessionLogCount == 0 { + logLines.append("No logs found for this session.") + } + + logLines.append("") } return logLines } - /// Converts OSLogEntryLog.Level to a readable string private func logLevelString(_ level: OSLogEntryLog.Level) -> String { switch level { - case .undefined: - return "UNDEFINED" - case .debug: - return "DEBUG" - case .info: - return "INFO" - case .notice: - return "NOTICE" - case .error: - return "ERROR" - case .fault: - return "FAULT" - @unknown default: - return "UNKNOWN" + case .undefined: return "UNDEFINED" + case .debug: return "DEBUG" + case .info: return "INFO" + case .notice: return "NOTICE" + case .error: return "ERROR" + case .fault: return "FAULT" + @unknown default: return "UNKNOWN" } } - /// Saves logs to a file in the Downloads folder private func saveLogsToFile(_ logs: [String]) throws -> URL { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let timestamp = dateFormatter.string(from: Date()) - let fileName = "VoiceInk_Logs_\(timestamp).log" - // Get Downloads folder guard let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else { throw NSError(domain: "LogExporter", code: 1, userInfo: [NSLocalizedDescriptionKey: "Downloads directory unavailable"]) } - let fileURL = downloadsURL.appendingPathComponent(fileName) + let fileURL = downloadsURL.appendingPathComponent(fileName) let content = logs.joined(separator: "\n") try content.write(to: fileURL, atomically: true, encoding: .utf8)