diff --git a/Sources/Earthquake Network.xcodeproj/project.pbxproj b/Sources/Earthquake Network.xcodeproj/project.pbxproj index e392277..5ec359c 100644 --- a/Sources/Earthquake Network.xcodeproj/project.pbxproj +++ b/Sources/Earthquake Network.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ 65E6AC702C2DB3B60073F8FE /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 65E6AC6F2C2DB3B60073F8FE /* FirebaseMessaging */; }; 65EA58802A60269C0038EE9D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8C10B0BD2281FE7F00125C9F /* Localizable.strings */; }; 65EA58822A60360D0038EE9D /* EQNRealtimePushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65EA58812A60360D0038EE9D /* EQNRealtimePushNotification.swift */; }; + 65F9A60C2D70781A008A12B5 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F9A60B2D70781A008A12B5 /* Log.swift */; }; 65F9B49C2A8CA22800F13260 /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F9B49B2A8CA22800F13260 /* BackgroundTaskManager.swift */; }; 65F9B49E2A8CA2AC00F13260 /* EQNBackgroundPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F9B49D2A8CA2AC00F13260 /* EQNBackgroundPosition.swift */; }; 65F9B4A02A8CC58200F13260 /* UpdateUserLocationTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F9B49F2A8CC58200F13260 /* UpdateUserLocationTask.swift */; }; @@ -377,6 +378,7 @@ 65DBFB7225E2BBF20041CBA6 /* GADTTemplateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GADTTemplateView.m; sourceTree = ""; }; 65DBFB7325E2BBF20041CBA6 /* GADTMediumTemplateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GADTMediumTemplateView.h; sourceTree = ""; }; 65EA58812A60360D0038EE9D /* EQNRealtimePushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNRealtimePushNotification.swift; sourceTree = ""; }; + 65F9A60B2D70781A008A12B5 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 65F9B49B2A8CA22800F13260 /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; 65F9B49D2A8CA2AC00F13260 /* EQNBackgroundPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNBackgroundPosition.swift; sourceTree = ""; }; 65F9B49F2A8CC58200F13260 /* UpdateUserLocationTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateUserLocationTask.swift; sourceTree = ""; }; @@ -993,6 +995,7 @@ DC10563F251E7EC0002579BB /* Extensions */, 8CF66054214C566A009F4314 /* Reachability.h */, 8CF66056214C566A009F4314 /* Reachability.m */, + 65F9A60B2D70781A008A12B5 /* Log.swift */, ); path = Libs; sourceTree = ""; @@ -1631,6 +1634,7 @@ DC7EEE4A252A11C9004B4A2A /* AlertsSmartphoneNetworkTableViewCell.swift in Sources */, DC7EEE4F252A1634004B4A2A /* AlertsPriorityServiceTableViewCell.swift in Sources */, 653604E9262348FA00B2B651 /* EQNBaseMapFilter.swift in Sources */, + 65F9A60C2D70781A008A12B5 /* Log.swift in Sources */, 65583A05261B83BE00ECA9F9 /* UIKit+Extensions.swift in Sources */, 65DBFB7625E2BBF20041CBA6 /* GADTTemplateView.m in Sources */, 8C5EA23D2177B51C002DC156 /* SegnalazioniViewController.m in Sources */, diff --git a/Sources/Earthquake Network/Libs/Log.swift b/Sources/Earthquake Network/Libs/Log.swift new file mode 100644 index 0000000..cad67a7 --- /dev/null +++ b/Sources/Earthquake Network/Libs/Log.swift @@ -0,0 +1,151 @@ +// +// Log.swift +// Earthquake Network +// +// Created by Andrea Busi on 27/02/25. +// Copyright © 2025 Earthquake Network. All rights reserved. +// + +import Foundation +import OSLog + + +/// Use this protocol to have a base TAG in a Swift class +public protocol Loggable { + static var TAG: String { get } +} + +public extension Loggable { + static var TAG: String { + String(describing: Self.self) + } +} + +extension UIViewController: Loggable { } + + +public class Log { + + private static let dumpDateFormatter: DateFormatter = { + // create the default date formatter using ISO8601 date format + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + return formatter + }() + + private static let shared = Log() + + // MARK: - Properties + + private let maxNumberOfLogsInDump: Int + private let logsLifespanMillis: Int + /// Subsystem for OSLog + private let subsystem: String + /// Logging in everything in a single "APP" category + private let appCategory: String = "APP" + + private lazy var logger: os.Logger = { + os.Logger(subsystem: subsystem, category: appCategory) + }() + + // MARK: - Init + + @objc + public init( + subsystem: String = Bundle.main.bundleIdentifier!, + maxNumberOfLogsInDump: Int = 5000, + logsLifespanMillis: Int = 3 * 24 * 3600 * 1000 + ) { + self.subsystem = subsystem + self.maxNumberOfLogsInDump = maxNumberOfLogsInDump + self.logsLifespanMillis = logsLifespanMillis + } + + // MARK: - Internal + + public static func error(tag: String?, _ message: String?, _ functionName: String = #function) { + shared.log(level: .fault, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName) + } + + public static func warning(tag: String?, _ message: String?, _ functionName: String = #function) { + shared.log(level: .error, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName) + } + + public static func info(tag: String?, _ message: String?, _ functionName: String = #function) { + shared.log(level: .info, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName) + } + + public static func debug(tag: String?, _ message: String?, _ functionName: String = #function) { + shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName) + } + + public static func verbose(tag: String?, _ message: String?, _ functionName: String = #function) { + shared.log(level: .debug, tag: tag ?? "nil", message: message ?? "nil", functionName: functionName) + } + + @available(iOS 15.0, *) + public func dumpLog() async -> String { + return (try? await getLogEntries()) ?? "" + } + + // MARK: - Private + + private func log(level: OSLogType, tag: String, message: String, functionName: String) { + let formattedMessage = "[\(tag)] \(functionName): \(message)" + switch level { + case .fault: logger.fault("\(formattedMessage, privacy: .public)") + case .error: logger.error("\(formattedMessage, privacy: .public)") + case .default: logger.notice("\(formattedMessage, privacy: .public)") + case .info: logger.info("\(formattedMessage, privacy: .public)") + default: logger.debug("\(formattedMessage, privacy: .public)") + } + } + + /// Retrieve log entries from a specified time. + /// - Returns: String of log entries, newlines separated + @available(iOS 15.0, *) + private func getLogEntries() async throws -> String { + let logTask = Task.init(priority: .utility) { () -> String in + let logs = try retrieveLogEntries() + let text = logs + .compactMap { "\(Self.dumpDateFormatter.string(from: $0.date)) [\($0.level)] \($0.composedMessage)" } + .joined(separator: "\n") + return text + } + return try await logTask.value + } + + @available(iOS 15.0, *) + private func retrieveLogEntries() throws -> [OSLogEntryLog] { + // Open the log store. + let logStore = try OSLogStore(scope: .currentProcessIdentifier) + + // Fetch log objects from the given time interval + let intervalPosition = logStore.position(date: Date().addingTimeInterval(TimeInterval(-logsLifespanMillis / 1000))) + let allEntries = try logStore.getEntries(at: intervalPosition) + + // Filter the log to be relevant for our specific subsystem + // and remove other elements (signposts, etc). + return allEntries + .compactMap { $0 as? OSLogEntryLog } + .filter { $0.subsystem == subsystem } + .suffix(maxNumberOfLogsInDump) + } +} + +@available(iOS 15.0, *) +extension OSLogEntryLog.Level: @retroactive CustomStringConvertible { + public var description: String { + switch self { + case .fault: return "FAULT" + case .error: return "ERROR" + case .notice: return "WARNING" + case .info: return "INFO" + case .debug: return "DEBUG" + case .undefined: return "UNDEFINED" + @unknown default: + return "UNKNOWN" + } + } +}