Enable log export across multiple sessions for better diagnostic support
This commit is contained in:
parent
05cc14ab6c
commit
fd1219580d
@ -1,111 +1,140 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
/// Utility class for exporting app logs since launch for diagnostic purposes
|
|
||||||
final class LogExporter {
|
final class LogExporter {
|
||||||
static let shared = LogExporter()
|
static let shared = LogExporter()
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "LogExporter")
|
private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "LogExporter")
|
||||||
private let subsystem = "com.prakashjoshipax.voiceink"
|
private let subsystem = "com.prakashjoshipax.voiceink"
|
||||||
|
private let maxSessionsToKeep = 3
|
||||||
|
private let sessionsKey = "logExporter.sessionStartDates.v1"
|
||||||
|
|
||||||
/// Timestamp when the app was launched
|
private(set) var sessionStartDates: [Date] = []
|
||||||
let launchDate: Date
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
self.launchDate = Date()
|
var loadedDates: [Date] = []
|
||||||
logger.notice("🎙️ LogExporter initialized, launch timestamp recorded")
|
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 {
|
func exportLogs() async throws -> URL {
|
||||||
logger.notice("🎙️ Starting log export since \(self.launchDate)")
|
logger.notice("🎙️ Starting log export")
|
||||||
|
|
||||||
let logs = try await fetchLogsSinceLaunch()
|
|
||||||
|
|
||||||
|
let logs = try await fetchLogs()
|
||||||
let fileURL = try saveLogsToFile(logs)
|
let fileURL = try saveLogsToFile(logs)
|
||||||
|
|
||||||
logger.notice("🎙️ Log export completed: \(fileURL.path)")
|
logger.notice("🎙️ Log export completed: \(fileURL.path)")
|
||||||
|
|
||||||
return fileURL
|
return fileURL
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches logs from OSLogStore since app launch
|
private func fetchLogs() async throws -> [String] {
|
||||||
private func fetchLogsSinceLaunch() async throws -> [String] {
|
let store = try OSLogStore(scope: .system)
|
||||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
|
||||||
|
|
||||||
// Get logs since launch
|
|
||||||
let position = store.position(date: launchDate)
|
|
||||||
|
|
||||||
// Create predicate to filter by our subsystem
|
|
||||||
let predicate = NSPredicate(format: "subsystem == %@", subsystem)
|
let predicate = NSPredicate(format: "subsystem == %@", subsystem)
|
||||||
|
|
||||||
let entries = try store.getEntries(at: position, matching: predicate)
|
|
||||||
|
|
||||||
var logLines: [String] = []
|
var logLines: [String] = []
|
||||||
|
|
||||||
// Add header
|
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
|
||||||
|
|
||||||
logLines.append("=== VoiceInk Diagnostic Logs ===")
|
logLines.append("=== VoiceInk Diagnostic Logs ===")
|
||||||
logLines.append("Export Date: \(dateFormatter.string(from: Date()))")
|
logLines.append("Export Date: \(dateFormatter.string(from: Date()))")
|
||||||
logLines.append("App Launch: \(dateFormatter.string(from: launchDate))")
|
|
||||||
logLines.append("Subsystem: \(subsystem)")
|
logLines.append("Subsystem: \(subsystem)")
|
||||||
|
logLines.append("Total Sessions: \(sessionStartDates.count)")
|
||||||
logLines.append("================================")
|
logLines.append("================================")
|
||||||
logLines.append("")
|
logLines.append("")
|
||||||
|
|
||||||
for entry in entries {
|
// Build session ranges with labels
|
||||||
guard let logEntry = entry as? OSLogEntryLog else { continue }
|
let totalSessions = sessionStartDates.count
|
||||||
|
var sessionRanges: [(label: String, start: Date, end: Date?)] = []
|
||||||
|
|
||||||
let timestamp = dateFormatter.string(from: logEntry.date)
|
for i in 0..<totalSessions {
|
||||||
let level = logLevelString(logEntry.level)
|
let start = sessionStartDates[i]
|
||||||
let category = logEntry.category
|
let end: Date? = (i == 0) ? nil : sessionStartDates[i - 1]
|
||||||
let message = logEntry.composedMessage
|
let sessionNumber = totalSessions - i
|
||||||
|
|
||||||
logLines.append("[\(timestamp)] [\(level)] [\(category)] \(message)")
|
let label: String
|
||||||
|
if totalSessions == 1 {
|
||||||
|
label = "Session 1 (Current)"
|
||||||
|
} else if i == 0 {
|
||||||
|
label = "Session \(sessionNumber) (Current)"
|
||||||
|
} else if i == totalSessions - 1 {
|
||||||
|
label = "Session 1 (Oldest)"
|
||||||
|
} else {
|
||||||
|
label = "Session \(sessionNumber)"
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionRanges.append((label, start, end))
|
||||||
}
|
}
|
||||||
|
|
||||||
if logLines.count <= 6 { // Only header lines
|
// Fetch logs for each session (oldest first for chronological order)
|
||||||
logLines.append("No logs found since app launch.")
|
for (label, startDate, endDate) in sessionRanges.reversed() {
|
||||||
|
logLines.append("--- \(label) ---")
|
||||||
|
logLines.append("")
|
||||||
|
|
||||||
|
let position = store.position(date: startDate)
|
||||||
|
let entries = try store.getEntries(at: position, matching: predicate)
|
||||||
|
|
||||||
|
var sessionLogCount = 0
|
||||||
|
for entry in entries {
|
||||||
|
guard let logEntry = entry as? OSLogEntryLog else { continue }
|
||||||
|
|
||||||
|
if let endDate, logEntry.date >= 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
|
return logLines
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts OSLogEntryLog.Level to a readable string
|
|
||||||
private func logLevelString(_ level: OSLogEntryLog.Level) -> String {
|
private func logLevelString(_ level: OSLogEntryLog.Level) -> String {
|
||||||
switch level {
|
switch level {
|
||||||
case .undefined:
|
case .undefined: return "UNDEFINED"
|
||||||
return "UNDEFINED"
|
case .debug: return "DEBUG"
|
||||||
case .debug:
|
case .info: return "INFO"
|
||||||
return "DEBUG"
|
case .notice: return "NOTICE"
|
||||||
case .info:
|
case .error: return "ERROR"
|
||||||
return "INFO"
|
case .fault: return "FAULT"
|
||||||
case .notice:
|
@unknown default: return "UNKNOWN"
|
||||||
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 {
|
private func saveLogsToFile(_ logs: [String]) throws -> URL {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||||
let timestamp = dateFormatter.string(from: Date())
|
let timestamp = dateFormatter.string(from: Date())
|
||||||
|
|
||||||
let fileName = "VoiceInk_Logs_\(timestamp).log"
|
let fileName = "VoiceInk_Logs_\(timestamp).log"
|
||||||
|
|
||||||
// Get Downloads folder
|
|
||||||
guard let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
guard let downloadsURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
||||||
throw NSError(domain: "LogExporter", code: 1, userInfo: [NSLocalizedDescriptionKey: "Downloads directory unavailable"])
|
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")
|
let content = logs.joined(separator: "\n")
|
||||||
try content.write(to: fileURL, atomically: true, encoding: .utf8)
|
try content.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user