LogManager.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. //
  2. // LogManager.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-01-10.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import Foundation
  9. class LogManager {
  10. static let shared = LogManager()
  11. private let fileManager = FileManager.default
  12. private let logDirectory: URL
  13. private let dateFormatter: DateFormatter
  14. private let consoleQueue = DispatchQueue(label: "com.loopfollow.log.console", qos: .background)
  15. private let rateLimitQueue = DispatchQueue(label: "com.loopfollow.log.ratelimit")
  16. private var lastLoggedTimestamps: [String: Date] = [:]
  17. enum Category: String, CaseIterable {
  18. case bluetooth = "Bluetooth"
  19. case nightscout = "Nightscout"
  20. case apns = "APNS"
  21. case general = "General"
  22. case contact = "Contact"
  23. case taskScheduler = "Task Scheduler"
  24. case dexcom = "Dexcom"
  25. case alarm = "Alarm"
  26. case calendar = "Calendar"
  27. case deviceStatus = "Device Status"
  28. }
  29. init() {
  30. let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
  31. logDirectory = documentsDirectory.appendingPathComponent("Logs")
  32. if !fileManager.fileExists(atPath: logDirectory.path) {
  33. try? fileManager.createDirectory(at: logDirectory, withIntermediateDirectories: true, attributes: nil)
  34. }
  35. dateFormatter = DateFormatter()
  36. dateFormatter.dateFormat = "yyyy-MM-dd"
  37. }
  38. /// Logs a message with an optional rate limit.
  39. ///
  40. /// - Parameters:
  41. /// - category: The log category.
  42. /// - message: The message to log.
  43. /// - isDebug: Indicates if this is a debug log.
  44. /// - limitIdentifier: Optional key to rate-limit similar log messages.
  45. /// - limitInterval: Time interval (in seconds) to wait before logging the same type again.
  46. func log(category: Category, message: String, isDebug: Bool = false, limitIdentifier: String? = nil, limitInterval: TimeInterval = 300) {
  47. let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
  48. let logMessage = "[\(timestamp)] [\(category.rawValue)] \(message)"
  49. consoleQueue.async {
  50. print(logMessage)
  51. }
  52. if let key = limitIdentifier, !Storage.shared.debugLogLevel.value {
  53. let shouldLog: Bool = rateLimitQueue.sync {
  54. if let lastLogged = lastLoggedTimestamps[key] {
  55. let interval = Date().timeIntervalSince(lastLogged)
  56. if interval < limitInterval {
  57. return false
  58. }
  59. }
  60. lastLoggedTimestamps[key] = Date()
  61. return true
  62. }
  63. if !shouldLog {
  64. return
  65. }
  66. }
  67. if !isDebug || Storage.shared.debugLogLevel.value {
  68. let logFileURL = self.currentLogFileURL
  69. self.append(logMessage + "\n", to: logFileURL)
  70. }
  71. }
  72. func cleanupOldLogs() {
  73. let today = dateFormatter.string(from: Date())
  74. let yesterday = dateFormatter.string(from: Calendar.current.date(byAdding: .day, value: -1, to: Date())!)
  75. do {
  76. let logFiles = try fileManager.contentsOfDirectory(at: logDirectory, includingPropertiesForKeys: nil)
  77. for logFile in logFiles {
  78. let filename = logFile.lastPathComponent
  79. if !filename.contains(today) && !filename.contains(yesterday) {
  80. try fileManager.removeItem(at: logFile)
  81. }
  82. }
  83. } catch {
  84. print("Failed to clean up old logs: \(error)")
  85. }
  86. }
  87. func logFileURL(for date: Date) -> URL {
  88. let dateString = dateFormatter.string(from: date)
  89. return logDirectory.appendingPathComponent("LoopFollow \(dateString).log")
  90. }
  91. func logFilesForTodayAndYesterday() -> [URL] {
  92. let today = logFileURL(for: Date())
  93. let yesterday = logFileURL(for: Calendar.current.date(byAdding: .day, value: -1, to: Date())!)
  94. return [today, yesterday].filter { fileManager.fileExists(atPath: $0.path) }
  95. }
  96. var currentLogFileURL: URL {
  97. return logFileURL(for: Date())
  98. }
  99. private func append(_ message: String, to fileURL: URL) {
  100. if !fileManager.fileExists(atPath: fileURL.path) {
  101. fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
  102. }
  103. if let fileHandle = try? FileHandle(forWritingTo: fileURL) {
  104. defer { fileHandle.closeFile() }
  105. fileHandle.seekToEndOfFile()
  106. if let data = message.data(using: .utf8) {
  107. fileHandle.write(data)
  108. }
  109. } else {
  110. print("Failed to open log file at \(fileURL.path)")
  111. }
  112. }
  113. }