From ff35688088b7833780e7f3887aca57a8ed3c05e4 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:29:13 +0000 Subject: [PATCH] Handover log file management to Rust. (#2495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handover log file management to Rust. Fix all the tests, disabling the logging ones for now. * Merge logging tests into a single case 😬 Not ideal but at least they're now running. * Move TracingConfiguration into its own file. * Split up the tests into smaller functions instead of writing code like a caveman. --- ElementX.xcodeproj/project.pbxproj | 14 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../BugReportFlowCoordinator.swift | 2 +- ElementX/Sources/Other/Logging/MXLog.swift | 34 +- ElementX/Sources/Other/Logging/MXLogger.swift | 335 ----------------- .../Sources/Other/Logging/RustTracing.swift | 179 +++------- .../Other/Logging/TracingConfiguration.swift | 151 ++++++++ .../BugReportScreenViewModel.swift | 2 +- .../LogViewerScreenCoordinator.swift | 7 +- .../LogViewerScreenViewModel.swift | 2 +- .../View/DeveloperOptionsScreen.swift | 2 +- .../Services/BugReport/BugReportService.swift | 59 +-- .../BugReport/BugReportServiceProtocol.swift | 1 - UnitTests/Sources/BugReportServiceTests.swift | 2 - .../Sources/BugReportViewModelTests.swift | 16 +- UnitTests/Sources/LoggingTests.swift | 336 +++++++----------- .../Sources/TracingConfigurationTests.swift | 2 +- changelog.d/pr-2495.change | 1 + project.yml | 2 +- 19 files changed, 379 insertions(+), 772 deletions(-) delete mode 100644 ElementX/Sources/Other/Logging/MXLogger.swift create mode 100644 ElementX/Sources/Other/Logging/TracingConfiguration.swift create mode 100644 changelog.d/pr-2495.change diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index aea354c17..73edf8f00 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ 24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; }; 24B7CD41342C143117ADA768 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B1CC9AA154F4D5435BF60A /* Comparable.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; + 24DF253C18D3E2C56DD0E597 /* TracingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */; }; 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */; }; 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */; }; 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; @@ -556,7 +557,6 @@ 8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */; }; 8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; }; 8C42B5B1642D189C362A5EDF /* SecureBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91831D7042EADD0CC2B5EC36 /* SecureBackupScreenUITests.swift */; }; - 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; @@ -728,7 +728,6 @@ B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; }; B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; - B66757D0254843162595B25D /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; }; B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; @@ -895,6 +894,7 @@ E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; }; E3291AD16D7A5CB14781819C /* UserNotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */; }; E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B788615712FED326F73D3F83 /* GlobalSearchScreenViewModelProtocol.swift */; }; + E37044401D9951D6C02C0855 /* TracingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */; }; E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */; }; E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; E3E1E255DC8CB34BD8573E0D /* UserIndicatorControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */; }; @@ -1659,7 +1659,6 @@ A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; - A34A814CBD56230BC74FFCF4 /* MXLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = ""; }; A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; @@ -1953,6 +1952,7 @@ ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenUITests.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; + ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; @@ -2175,8 +2175,8 @@ isa = PBXGroup; children = ( 111B698739E3410E2CDB7144 /* MXLog.swift */, - A34A814CBD56230BC74FFCF4 /* MXLogger.swift */, 542D4F49FABA056DEEEB3400 /* RustTracing.swift */, + ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */, ); path = Logging; sourceTree = ""; @@ -5281,7 +5281,6 @@ A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */, FB0A9D06FC9122E37992D962 /* LayoutDirection.swift in Sources */, AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */, - 8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */, 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */, 8B76191B9DDD1AC90A6E3A35 /* MediaFileHandleProxy.swift in Sources */, 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */, @@ -5312,6 +5311,7 @@ 6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */, 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */, E0FB26262689F04D66A949D7 /* TestablePreview.swift in Sources */, + 24DF253C18D3E2C56DD0E597 /* TracingConfiguration.swift in Sources */, DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */, 281BED345D59A9A6A99E9D98 /* UNNotificationContent.swift in Sources */, 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */, @@ -5716,7 +5716,6 @@ A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */, 4362C770C7E05ADC750E5070 /* LongPressWithFeedback.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */, - B66757D0254843162595B25D /* MXLogger.swift in Sources */, C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */, C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */, E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */, @@ -6059,6 +6058,7 @@ 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */, FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */, 2B1E080B32167AE9EFC763A2 /* TimelineTableViewController.swift in Sources */, + E37044401D9951D6C02C0855 /* TracingConfiguration.swift in Sources */, 6D6E651ACACE27E9C5690818 /* TypingIndicatorView.swift in Sources */, 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */, A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */, @@ -6891,7 +6891,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.1.42; + version = 1.1.43; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8a306a493..9ab274f46 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -130,8 +130,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "62a45668e8e8a70079b8fae0b8031852533900e9", - "version" : "1.1.42" + "revision" : "87f6686023df7a7053bfb18162a17ee803ab1bc7", + "version" : "1.1.43" } }, { diff --git a/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift index 1b70c6afe..298a37617 100644 --- a/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/BugReportFlowCoordinator.swift @@ -98,7 +98,7 @@ class BugReportFlowCoordinator: FlowCoordinatorProtocol { } private func presentLogViewerScreen() { - let coordinator = LogViewerScreenCoordinator(parameters: .init()) + let coordinator = LogViewerScreenCoordinator() coordinator.actions.sink { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/Other/Logging/MXLog.swift b/ElementX/Sources/Other/Logging/MXLog.swift index 6961b1ccf..34903690f 100644 --- a/ElementX/Sources/Other/Logging/MXLog.swift +++ b/ElementX/Sources/Other/Logging/MXLog.swift @@ -24,15 +24,6 @@ import MatrixRustSDK enum MXLog { private enum Constants { static let target = "elementx" - - // Avoid redirecting NSLogs to files if we are attached to a debugger. - static let redirectToFiles = isatty(STDERR_FILENO) == 0 - - /// the maximum number of log files to use before rolling. `10` by default. - static let maxLogFileCount: UInt = 10 - - /// the maximum total space to use for log files in bytes. `100MB` by default. - static let logFilesSizeLimit: UInt = 100 * 1024 * 1024 // 100MB } // Rust side crashes if invoking setupTracing multiple times @@ -43,39 +34,20 @@ enum MXLog { static func configure(target: String? = nil, logLevel: TracingConfiguration.LogLevel, - otlpConfiguration: OTLPConfiguration? = nil, - redirectToFiles: Bool = Constants.redirectToFiles, - maxLogFileCount: UInt = Constants.maxLogFileCount, - logFileSizeLimit: UInt = Constants.logFilesSizeLimit) { - guard didConfigureOnce == false else { - if let target { - MXLogger.setSubLogName(target) - } - - // SubLogName needs to be set before calling configure in order to be applied - MXLogger.configure(redirectToFiles: redirectToFiles, - maxLogFileCount: maxLogFileCount, - logFileSizeLimit: logFileSizeLimit) - return - } + otlpConfiguration: OTLPConfiguration? = nil) { + guard !didConfigureOnce else { return } - RustTracing.setup(configuration: .init(logLevel: logLevel), otlpConfiguration: otlpConfiguration) + RustTracing.setup(configuration: .init(logLevel: logLevel, target: target), otlpConfiguration: otlpConfiguration) if let target { self.target = target - MXLogger.setSubLogName(target) } else { self.target = Constants.target } rootSpan = Span(file: #file, line: #line, level: .info, target: self.target, name: "root") - rootSpan.enter() - MXLogger.configure(redirectToFiles: redirectToFiles, - maxLogFileCount: maxLogFileCount, - logFileSizeLimit: logFileSizeLimit) - didConfigureOnce = true } diff --git a/ElementX/Sources/Other/Logging/MXLogger.swift b/ElementX/Sources/Other/Logging/MXLogger.swift deleted file mode 100644 index a1906d952..000000000 --- a/ElementX/Sources/Other/Logging/MXLogger.swift +++ /dev/null @@ -1,335 +0,0 @@ -// -// Copyright 2022 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import UIKit - -/// The `MXLogger` tool redirects NSLog output into a fixed pool of files. -/// Another log file is used every time `MXLogger redirectNSLog(toFiles: true)` -/// is called. The pool contains 3 files. -/// -/// `MXLogger` can track and log uncaught exceptions or crashes. -enum MXLogger { - /// stderr so it can be restored. - static var stderrSave: Int32 = 0 - - private enum Constants { - /// The filename used for the crash log. - static let crashLogFileName = "crash.log" - } - - /// Redirect NSLog output to MXLogger files. - /// - /// It is advised to condition this redirection in `#if (!isatty(STDERR_FILENO))` block to enable - /// it only when the device is not attached to the debugger. - /// - /// - Parameters: - /// - redirectToFiles: `true` to enable the redirection. - /// - maxLogFileCount: number of files to keep (default is 10). - /// - logFileSizeLimit: size limit of log files in bytes. 0 means no limitation, the default value for other methods - static func configure(redirectToFiles: Bool, - maxLogFileCount: UInt, - logFileSizeLimit: UInt) { - if redirectToFiles { - var tempLog = "" - - // Do a circular buffer based on X files - for index in (0...(maxLogFileCount - 2)).reversed() { - rotateLog(at: index, tempLog: &tempLog) - } - - // Save stderr so it can be restored. - stderrSave = dup(STDERR_FILENO) - - let nsLogURL = logURL(for: "console\(subLogName).log") - freopen((nsLogURL as NSURL).fileSystemRepresentation, "w+", stderr) - - MXLog.info("redirectNSLogToFiles: true") - if !tempLog.isEmpty { - // We can now log into files - MXLog.info(tempLog) - } - - removeExtraFiles(from: maxLogFileCount) - - if logFileSizeLimit > 0 { - removeFiles(after: logFileSizeLimit) - } - } else if stderrSave > 0 { - // Flush before restoring stderr - fflush(stderr) - - // Now restore stderr, so new output goes to console. - dup2(stderrSave, STDERR_FILENO) - close(stderrSave) - } - } - - private static func rotateLog(at index: UInt, tempLog: inout String) { - let fileManager = FileManager.default - - let currentURL: URL - let newURL: URL - - if index == 0 { - currentURL = logURL(for: String("console\(subLogName).log")) - newURL = logURL(for: String("console\(subLogName).1.log")) - } else { - currentURL = logURL(for: String("console\(subLogName).\(index).log")) - newURL = logURL(for: String("console\(subLogName).\(index + 1).log")) - } - - guard fileManager.fileExists(atPath: currentURL.path()) else { return } - - if fileManager.fileExists(atPath: newURL.path()) { - // Temp log - tempLog.append("removeItemAt: \(newURL)\n") - - do { - try fileManager.removeItem(at: newURL) - } catch { - tempLog.append("removeItemAt: \(newURL). Error: \(error)\n") - } - } - - // Temp log - tempLog.append("moveItemAt: \(currentURL) to: \(newURL)\n") - - do { - try fileManager.moveItem(at: currentURL, to: newURL) - } catch { - tempLog.append("moveItemAt: \(currentURL) to: \(newURL). Error: \(error)\n") - } - } - - private static func logURL(for fileName: String) -> URL { - MXLogger.logsFolderURL.appending(path: fileName) - } - - /// Delete all log files. - static func deleteLogFiles() { - let fileManager = FileManager.default - for logFileURL in logFiles { - try? fileManager.removeItem(at: logFileURL) - } - } - - /// The list of all log file URLs, sorted chronologically. - static var logFiles: [URL] { - var logFiles = [(url: URL, modificationDate: Date)]() - - let fileManager = FileManager.default - let enumerator = fileManager.enumerator(at: logsFolderURL, includingPropertiesForKeys: [.contentModificationDateKey]) - - // Find all *.log files and their modification dates. - while let logURL = enumerator?.nextObject() as? URL { - guard let resourceValues = try? logURL.resourceValues(forKeys: [.contentModificationDateKey]), - let modificationDate = resourceValues.contentModificationDate - else { continue } - - if logURL.pathExtension == "log" { - logFiles.append((logURL, modificationDate)) - } - } - - let sortedFiles = logFiles.sorted { $0.modificationDate > $1.modificationDate }.map(\.url) - - MXLog.info("logFiles: \(sortedFiles.map(\.lastPathComponent))") - - return sortedFiles - } - - // MARK: - Exceptions and crashes - - /// Exceptions uncaught by try catch block are handled here - static func handleUncaughtException(_ exception: NSException) { - MXLogger.logCrashes(false) - - // Extract running app information - let app = InfoPlistReader.main.bundleExecutable - let appId = InfoPlistReader.main.bundleIdentifier - let appVersion = "\(InfoPlistReader.main.bundleShortVersionString) (r\(InfoPlistReader.main.bundleVersion))" - - // Build the crash log - let model = UIDevice.current.model - let version = UIDevice.current.systemVersion - - let backtrace = exception.callStackSymbols - let description = String(format: "%.0f - %@\n%@\nApplication: %@ (%@)\nApplication version: %@\nBuild: %@\n%@ %@\n\nMain thread: %@\n%@\n", - Date.now.timeIntervalSince1970, - NSDate(), - exception.description, - app, appId, - appVersion, - buildVersion ?? "Unknown", - model, version, - Thread.isMainThread ? "true" : "false", - backtrace) - - // Write to the crash log file - MXLogger.deleteCrashLog() - let crashLog = crashLogURL - try? description.write(to: crashLog, atomically: false, encoding: .utf8) - - MXLog.error("handleUncaughtException", context: ["description": description]) - } - - // Signals emitted by the app are handled here - private static func handleSignal(_ signalValue: Int32) { - // Throw a custom Objective-C exception - // The Objective-C runtime will then be able to build a readable call stack in handleUncaughtException - withVaList([signalValue]) { NSException.raise(.init("Signal detected"), format: "Signal detected: %d", arguments: $0) } - } - - /// Make `MXLogger` catch and log unmanaged exceptions or application crashes. - /// - /// When such error happens, `MXLogger` stores the application stack trace into a file - /// just before the application leaves. The path of this file is provided by `MXLogger.crashLog`. - /// - /// - Parameter enabled: `true` to enable the catch. - static func logCrashes(_ enabled: Bool) { - if enabled { - // Handle not managed exceptions by ourselves - NSSetUncaughtExceptionHandler { exception in - MXLogger.handleUncaughtException(exception) - } - - // Register signal event (seg fault & cie) - signal(SIGABRT) { MXLogger.handleSignal($0) } - signal(SIGILL) { MXLogger.handleSignal($0) } - signal(SIGSEGV) { MXLogger.handleSignal($0) } - signal(SIGFPE) { MXLogger.handleSignal($0) } - signal(SIGBUS) { MXLogger.handleSignal($0) } - signal(SIGABRT) { MXLogger.handleSignal($0) } - } else { - // Disable crash handling - NSSetUncaughtExceptionHandler(nil) - signal(SIGABRT, SIG_DFL) - signal(SIGILL, SIG_DFL) - signal(SIGSEGV, SIG_DFL) - signal(SIGFPE, SIG_DFL) - signal(SIGBUS, SIG_DFL) - } - } - - /// Set the app build version. - /// It will be reported in crash report. - static var buildVersion: String? - - /// Set a sub name for namespacing log files. - /// - /// A sub name must be set when running from an app extension because extensions can - /// run in parallel to the app. - /// It must be called before `redirectNSLog(toFiles)`. - /// - /// - Parameter name: the subname for log files. Files will be named as `console-[subLogName].log` - /// Default is nil. - static func setSubLogName(_ name: String) { - if name.isEmpty { - subLogName = "" - } else { - subLogName = "-\(name)" - } - } - - private static var subLogName = "" - - /// The URL used for a crash log file. - static var crashLogURL: URL { - MXLogger.logsFolderURL.appending(path: Constants.crashLogFileName) - } - - /// The URL of the file containing the last application crash if one exists or `nil` if there is none. - /// - /// Only one crash log is stored at a time. The best moment for the app to handle it is the - /// at its next startup. - static var crashLog: URL? { - let crashLogURL = MXLogger.crashLogURL - let fileManager = FileManager.default - guard fileManager.fileExists(atPath: crashLogURL.path()) else { return nil } - - return crashLogURL - } - - /// Delete the crash log file. - static func deleteCrashLog() { - let crashLog = MXLogger.crashLogURL - let fileManager = FileManager.default - - if fileManager.fileExists(atPath: crashLog.path()) { - try? fileManager.removeItem(at: crashLog) - } - } - - // MARK: - Private - - /// The folder where logs are stored - private static var logsFolderURL: URL { - .appGroupContainerDirectory - } - - /// If `self.redirectNSLog(toFiles:numberOfFiles:)` is called with a lower numberOfFiles we need to do some cleanup. - private static func removeExtraFiles(from count: UInt) { - let fileManager = FileManager.default - - for index in count... { - let fileName = "console\(subLogName).\(index).log" - let logFile = logURL(for: fileName) - - if fileManager.fileExists(atPath: logFile.path()) { - try? fileManager.removeItem(at: logFile) - MXLog.info("removeExtraFilesFromCount: \(count). removeItemAt: \(logFile)\n") - } else { - break - } - } - } - - /// If `redirectNSLog(toFiles:sizeLimit:)` is called with a size limit, we may need to do some cleanup. - private static func removeFiles(after sizeLimit: UInt) { - var logSize: UInt = 0 - var indexExceedingSizeLimit: Int? - let fileManager = FileManager.default - - // Start from console.1.log. Do not consider console.log. It should be almost empty - for index in 1... { - let fileName = "console\(subLogName).\(index).log" - let logFile = logURL(for: fileName) - - if fileManager.fileExists(atPath: logFile.path()) { - if let attributes = try? fileManager.attributesOfItem(atPath: logFile.path()), let fileSize = attributes[.size] as? UInt { - logSize += fileSize - } - - if logSize >= sizeLimit { - indexExceedingSizeLimit = index - break - } - } else { - break - } - } - - let logSizeString = logSize.formatted(.byteCount(style: .binary)) - let sizeLimitString = sizeLimit.formatted(.byteCount(style: .binary)) - - if let indexExceedingSizeLimit { - MXLog.info("removeFilesAfterSizeLimit: Remove files from index \(indexExceedingSizeLimit) because logs are too large (\(logSizeString) for a limit of \(sizeLimitString)\n") - removeExtraFiles(from: UInt(indexExceedingSizeLimit)) - } else { - MXLog.info("removeFilesAfterSizeLimit: No need: \(logSizeString) for a limit of \(sizeLimitString)\n") - } - } -} diff --git a/ElementX/Sources/Other/Logging/RustTracing.swift b/ElementX/Sources/Other/Logging/RustTracing.swift index c83e6efc9..f8ace330b 100644 --- a/ElementX/Sources/Other/Logging/RustTracing.swift +++ b/ElementX/Sources/Other/Logging/RustTracing.swift @@ -14,7 +14,7 @@ // limitations under the License. // -import Collections +import Foundation import MatrixRustSDK struct OTLPConfiguration { @@ -23,134 +23,21 @@ struct OTLPConfiguration { let password: String } -// This exposes the full Rust side tracing subscriber filter for more flexibility. -// We can filter by level, crate and even file. See more details here: -// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples -struct TracingConfiguration { - enum LogLevel: Codable, Hashable { - case error, warn, info, debug, trace - case custom(String) - - var title: String { - switch self { - case .error: - return "Error" - case .warn: - return "Warning" - case .info: - return "Info" - case .debug: - return "Debug" - case .trace: - return "Trace" - case .custom: - return "Custom" - } - } - - fileprivate var rawValue: String { - switch self { - case .error: - return "error" - case .warn: - return "warn" - case .info: - return "info" - case .debug: - return "debug" - case .trace: - return "trace" - case .custom(let filter): - return filter - } - } - } - - enum Target: String { - case common = "" - - case elementx - - case hyper, matrix_sdk_ffi, matrix_sdk_crypto - - case matrix_sdk_client = "matrix_sdk::client" - case matrix_sdk_crypto_account = "matrix_sdk_crypto::olm::account" - case matrix_sdk_oidc = "matrix_sdk::oidc" - case matrix_sdk_http_client = "matrix_sdk::http_client" - case matrix_sdk_sliding_sync = "matrix_sdk::sliding_sync" - case matrix_sdk_base_sliding_sync = "matrix_sdk_base::sliding_sync" - case matrix_sdk_ui_timeline = "matrix_sdk_ui::timeline" - case matrix_sdk_base_read_receipts = "matrix_sdk_base::read_receipts" - } - - static let targets: OrderedDictionary = [ - .common: .info, - .elementx: .info, - .hyper: .warn, - .matrix_sdk_ffi: .info, - .matrix_sdk_client: .trace, - .matrix_sdk_crypto: .debug, - .matrix_sdk_crypto_account: .trace, - .matrix_sdk_oidc: .trace, - .matrix_sdk_http_client: .debug, - .matrix_sdk_sliding_sync: .info, - .matrix_sdk_base_sliding_sync: .info, - .matrix_sdk_ui_timeline: .info, - .matrix_sdk_base_read_receipts: .trace - ] - - let filter: String - - /// Sets the same log level for all Targets - /// - Parameter logLevel: the desired log level - /// - Returns: a custom tracing configuration - init(logLevel: LogLevel) { - if case let .custom(filter) = logLevel { - self.filter = filter - return - } - - let overrides = Self.targets.keys.reduce(into: [Target: LogLevel]()) { partialResult, target in - // Keep the defaults here - let ignoredTargets: [Target] = [.common, - .hyper, - .matrix_sdk_ffi, - .matrix_sdk_oidc, - .matrix_sdk_client, - .matrix_sdk_crypto, - .matrix_sdk_crypto_account, - .matrix_sdk_http_client, - .matrix_sdk_base_read_receipts] - if ignoredTargets.contains(target) { - return - } - - partialResult[target] = logLevel - } - - var newTargets = Self.targets - for (target, logLevel) in overrides { - newTargets.updateValue(logLevel, forKey: target) - } - - let components = newTargets.map { (target: Target, logLevel: LogLevel) in - guard !target.rawValue.isEmpty else { - return logLevel.rawValue - } - - return "\(target.rawValue)=\(logLevel.rawValue)" - } - - filter = components.joined(separator: ",") - } -} - enum RustTracing { - private(set) static var currentTracingConfiguration: TracingConfiguration? + /// The base filename used for log files. This may be suffixed by the target + /// name and other log management metadata during rotation. + static let filePrefix = "console" + /// The directory that stores all of the log files. + static var logsDirectory: URL { .appGroupContainerDirectory } + private(set) static var currentTracingConfiguration: TracingConfiguration? static func setup(configuration: TracingConfiguration, otlpConfiguration: OTLPConfiguration?) { currentTracingConfiguration = configuration + // Keep a minimum of 1 week of log files. In reality it will be longer + // as the app is unlikely to be running continuously. + let maxFiles: UInt64 = 24 * 7 + if let otlpConfiguration { setupOtlpTracing(config: .init(clientName: "ElementX-iOS", user: otlpConfiguration.username, @@ -158,11 +45,51 @@ enum RustTracing { otlpEndpoint: otlpConfiguration.url, filter: configuration.filter, writeToStdoutOrSystem: true, - writeToFiles: nil)) + writeToFiles: .init(path: logsDirectory.path(), + filePrefix: configuration.fileName, + fileSuffix: configuration.fileExtension, + maxFiles: maxFiles))) } else { setupTracing(config: .init(filter: configuration.filter, writeToStdoutOrSystem: true, - writeToFiles: nil)) + writeToFiles: .init(path: logsDirectory.path(), + filePrefix: configuration.fileName, + fileSuffix: configuration.fileExtension, + maxFiles: maxFiles))) + } + } + + /// A list of all log file URLs, sorted chronologically. This is only public for testing purposes, within + /// the app please use ``copyLogs(to:)`` so that the files are name appropriates for QuickLook. + static var logFiles: [URL] { + var logFiles = [(url: URL, modificationDate: Date)]() + + let fileManager = FileManager.default + let enumerator = fileManager.enumerator(at: logsDirectory, includingPropertiesForKeys: [.contentModificationDateKey]) + + // Find all *.log files and their modification dates. + while let logURL = enumerator?.nextObject() as? URL { + guard let resourceValues = try? logURL.resourceValues(forKeys: [.contentModificationDateKey]), + let modificationDate = resourceValues.contentModificationDate + else { continue } + + if logURL.lastPathComponent.hasPrefix(filePrefix) { + logFiles.append((logURL, modificationDate)) + } + } + + let sortedFiles = logFiles.sorted { $0.modificationDate > $1.modificationDate }.map(\.url) + + MXLog.info("logFiles: \(sortedFiles.map(\.lastPathComponent))") + + return sortedFiles + } + + /// Delete all log files. + static func deleteLogFiles() { + let fileManager = FileManager.default + for logFileURL in logFiles { + try? fileManager.removeItem(at: logFileURL) } } } diff --git a/ElementX/Sources/Other/Logging/TracingConfiguration.swift b/ElementX/Sources/Other/Logging/TracingConfiguration.swift new file mode 100644 index 000000000..938989f8e --- /dev/null +++ b/ElementX/Sources/Other/Logging/TracingConfiguration.swift @@ -0,0 +1,151 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Collections + +// This exposes the full Rust side tracing subscriber filter for more flexibility. +// We can filter by level, crate and even file. See more details here: +// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples +struct TracingConfiguration { + enum LogLevel: Codable, Hashable { + case error, warn, info, debug, trace + case custom(String) + + var title: String { + switch self { + case .error: + return "Error" + case .warn: + return "Warning" + case .info: + return "Info" + case .debug: + return "Debug" + case .trace: + return "Trace" + case .custom: + return "Custom" + } + } + + fileprivate var rawValue: String { + switch self { + case .error: + return "error" + case .warn: + return "warn" + case .info: + return "info" + case .debug: + return "debug" + case .trace: + return "trace" + case .custom(let filter): + return filter + } + } + } + + enum Target: String { + case common = "" + + case elementx + + case hyper, matrix_sdk_ffi, matrix_sdk_crypto + + case matrix_sdk_client = "matrix_sdk::client" + case matrix_sdk_crypto_account = "matrix_sdk_crypto::olm::account" + case matrix_sdk_oidc = "matrix_sdk::oidc" + case matrix_sdk_http_client = "matrix_sdk::http_client" + case matrix_sdk_sliding_sync = "matrix_sdk::sliding_sync" + case matrix_sdk_base_sliding_sync = "matrix_sdk_base::sliding_sync" + case matrix_sdk_ui_timeline = "matrix_sdk_ui::timeline" + case matrix_sdk_base_read_receipts = "matrix_sdk_base::read_receipts" + } + + static let targets: OrderedDictionary = [ + .common: .info, + .elementx: .info, + .hyper: .warn, + .matrix_sdk_ffi: .info, + .matrix_sdk_client: .trace, + .matrix_sdk_crypto: .debug, + .matrix_sdk_crypto_account: .trace, + .matrix_sdk_oidc: .trace, + .matrix_sdk_http_client: .debug, + .matrix_sdk_sliding_sync: .info, + .matrix_sdk_base_sliding_sync: .info, + .matrix_sdk_ui_timeline: .info, + .matrix_sdk_base_read_receipts: .trace + ] + + let filter: String + + /// The filename that logs should be written to. + let fileName: String + /// The file extension to use for log files. + let fileExtension = "log" + + /// Sets the same log level for all Targets + /// - Parameter logLevel: the desired log level + /// - Parameter target: the name of the target being configured + /// - Returns: a custom tracing configuration + init(logLevel: LogLevel, target: String?) { + fileName = if let target { + "\(RustTracing.filePrefix)-\(target)" + } else { + RustTracing.filePrefix + } + + if case let .custom(filter) = logLevel { + self.filter = filter + return + } + + let overrides = Self.targets.keys.reduce(into: [Target: LogLevel]()) { partialResult, target in + // Keep the defaults here + let ignoredTargets: [Target] = [.common, + .hyper, + .matrix_sdk_ffi, + .matrix_sdk_oidc, + .matrix_sdk_client, + .matrix_sdk_crypto, + .matrix_sdk_crypto_account, + .matrix_sdk_http_client, + .matrix_sdk_base_read_receipts] + if ignoredTargets.contains(target) { + return + } + + partialResult[target] = logLevel + } + + var newTargets = Self.targets + for (target, logLevel) in overrides { + newTargets.updateValue(logLevel, forKey: target) + } + + let components = newTargets.map { (target: Target, logLevel: LogLevel) in + guard !target.rawValue.isEmpty else { + return logLevel.rawValue + } + + return "\(target.rawValue)=\(logLevel.rawValue)" + } + + filter = components.joined(separator: ",") + } +} diff --git a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift index f4e11fd53..506b4eeee 100644 --- a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift +++ b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift @@ -23,6 +23,7 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie private let bugReportService: BugReportServiceProtocol private let userID: String? private let deviceID: String? + private let actionsSubject: PassthroughSubject = .init() // periphery:ignore - when set to nil this is automatically cancelled @CancellableTask private var uploadTask: Task? @@ -89,7 +90,6 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie deviceID: deviceID, text: context.reportText, includeLogs: context.sendingLogsEnabled, - includeCrashLog: true, canContact: context.canContact, githubLabels: [], files: files) diff --git a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift index e7dbbf6b4..3ff982983 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift @@ -19,14 +19,11 @@ import Combine import SwiftUI -struct LogViewerScreenCoordinatorParameters { } - enum LogViewerScreenCoordinatorAction { case done } final class LogViewerScreenCoordinator: CoordinatorProtocol { - private let parameters: LogViewerScreenCoordinatorParameters private var viewModel: LogViewerScreenViewModelProtocol private var cancellables = Set() @@ -35,9 +32,7 @@ final class LogViewerScreenCoordinator: CoordinatorProtocol { actionsSubject.eraseToAnyPublisher() } - init(parameters: LogViewerScreenCoordinatorParameters) { - self.parameters = parameters - + init() { viewModel = LogViewerScreenViewModel() } diff --git a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift index 990cc475d..d4333dc43 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift @@ -27,7 +27,7 @@ class LogViewerScreenViewModel: LogViewerScreenViewModelType, LogViewerScreenVie } init() { - super.init(initialViewState: LogViewerScreenViewState(urls: MXLogger.logFiles)) + super.init(initialViewState: LogViewerScreenViewState(urls: RustTracing.logFiles)) } // MARK: - Public diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 03c991721..02f4da8df 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -143,7 +143,7 @@ private struct LogLevelConfigurationView: View { if case .custom(let configuration) = logLevel.wrappedValue { customTracingConfiguration = configuration } else { - customTracingConfiguration = TracingConfiguration(logLevel: .info).filter + customTracingConfiguration = TracingConfiguration(logLevel: .info, target: nil).filter } } diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift index 9a68c4af1..2198675d9 100644 --- a/ElementX/Sources/Services/BugReport/BugReportService.swift +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -44,9 +44,6 @@ class BugReportService: NSObject, BugReportServiceProtocol { self.maxUploadSize = maxUploadSize self.session = session super.init() - - // set build version for logger - MXLogger.buildVersion = InfoPlistReader.main.bundleShortVersionString } // MARK: - BugReportServiceProtocol @@ -100,14 +97,12 @@ class BugReportService: NSObject, BugReportServiceProtocol { self?.lastCrashEventId = event.eventId.sentryIdString } } - MXLogger.logCrashes(true) MXLog.info("Started.") } func stop() { guard isRunning else { return } SentrySDK.close() - MXLogger.logCrashes(false) MXLog.info("Stopped.") } @@ -115,7 +110,8 @@ class BugReportService: NSObject, BugReportServiceProtocol { lastCrashEventId = nil MXLog.info("Reset.") } - + + // swiftlint:disable:next cyclomatic_complexity func submitBugReport(_ bugReport: BugReport, progressListener: CurrentValueSubject) async -> Result { var params = [ @@ -136,11 +132,12 @@ class BugReportService: NSObject, BugReportServiceProtocol { for label in bugReport.githubLabels { params.append(MultipartFormData(key: "label", type: .text(value: label))) } - let logAttachments = await zipFiles(includeLogs: bugReport.includeLogs, - includeCrashLog: bugReport.includeCrashLog) - for url in logAttachments.files { - params.append(MultipartFormData(key: "compressed-log", type: .file(url: url))) + if bugReport.includeLogs { + let logAttachments = await zipFiles(RustTracing.logFiles) + for url in logAttachments.files { + params.append(MultipartFormData(key: "compressed-log", type: .file(url: url))) + } } if let crashEventId = lastCrashEventId { @@ -197,7 +194,6 @@ class BugReportService: NSObject, BugReportServiceProtocol { let uploadResponse = try decoder.decode(SubmitBugReportResponse.self, from: data) if !uploadResponse.reportUrl.isEmpty { - MXLogger.deleteCrashLog() lastCrashEventId = nil } @@ -243,21 +239,12 @@ class BugReportService: NSObject, BugReportServiceProtocol { "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" } - private func zipFiles(includeLogs: Bool, - includeCrashLog: Bool) async -> Logs { - MXLog.info("zipFiles: includeLogs: \(includeLogs), includeCrashLog: \(includeCrashLog)") - - var filesToCompress: [URL] = [] - if includeLogs { - filesToCompress.append(contentsOf: MXLogger.logFiles) - } - if includeCrashLog, let crashLogFile = MXLogger.crashLog { - filesToCompress.append(crashLogFile) - } + private func zipFiles(_ logFiles: [URL]) async -> Logs { + MXLog.info("zipFiles") var compressedLogs = Logs(maxFileSize: maxUploadSize) - for url in filesToCompress { + for url in logFiles { do { try attachFile(at: url, to: &compressedLogs) } catch { @@ -275,23 +262,15 @@ class BugReportService: NSObject, BugReportServiceProtocol { private func attachFile(at url: URL, to zippedFiles: inout Logs) throws { let fileHandle = try FileHandle(forReadingFrom: url) - var chunkIndex = -1 - while let data = try fileHandle.read(upToCount: 10 * 1024 * 1024) { - do { - chunkIndex += 1 - if let zippedData = (data as NSData).gzipped() { - let zippedFilename = url.deletingPathExtension().lastPathComponent + "_\(chunkIndex).log" - let chunkURL = URL.temporaryDirectory.appending(path: zippedFilename) - - // Remove old zipped file if exists - try? FileManager.default.removeItem(at: chunkURL) - - try zippedData.write(to: chunkURL) - zippedFiles.appendFile(at: chunkURL, zippedSize: zippedData.count, originalSize: data.count) - } - } catch { - MXLog.error("Failed attaching log chunk \(chunkIndex) from (\(url.lastPathComponent)") - continue + while let data = try fileHandle.readToEnd() { + if let zippedData = (data as NSData).gzipped() { + let zippedURL = URL.temporaryDirectory.appending(path: url.lastPathComponent) + + // Remove old zipped file if exists + try? FileManager.default.removeItem(at: zippedURL) + + try zippedData.write(to: zippedURL) + zippedFiles.appendFile(at: zippedURL, zippedSize: zippedData.count, originalSize: data.count) } } } diff --git a/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift index 77645dbc1..5a2f4d7d1 100644 --- a/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift +++ b/ElementX/Sources/Services/BugReport/BugReportServiceProtocol.swift @@ -23,7 +23,6 @@ struct BugReport: Equatable { let deviceID: String? let text: String let includeLogs: Bool - let includeCrashLog: Bool let canContact: Bool let githubLabels: [String] let files: [URL] diff --git a/UnitTests/Sources/BugReportServiceTests.swift b/UnitTests/Sources/BugReportServiceTests.swift index d5155ac58..1e3564036 100644 --- a/UnitTests/Sources/BugReportServiceTests.swift +++ b/UnitTests/Sources/BugReportServiceTests.swift @@ -39,7 +39,6 @@ class BugReportServiceTests: XCTestCase { deviceID: nil, text: "i cannot send message", includeLogs: true, - includeCrashLog: true, canContact: false, githubLabels: [], files: []) @@ -70,7 +69,6 @@ class BugReportServiceTests: XCTestCase { deviceID: nil, text: "i cannot send message", includeLogs: true, - includeCrashLog: true, canContact: false, githubLabels: [], files: []) diff --git a/UnitTests/Sources/BugReportViewModelTests.swift b/UnitTests/Sources/BugReportViewModelTests.swift index 683db20ec..82265b974 100644 --- a/UnitTests/Sources/BugReportViewModelTests.swift +++ b/UnitTests/Sources/BugReportViewModelTests.swift @@ -85,7 +85,13 @@ class BugReportViewModelTests: XCTestCase { try await deferred.fulfill() XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) - XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, canContact: false, githubLabels: [], files: [])) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID, "@mock.client.com") + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID, nil) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text, "") + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.includeLogs, true) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact, false) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels, []) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files, []) } func testSendReportWithError() async throws { @@ -112,6 +118,12 @@ class BugReportViewModelTests: XCTestCase { try await deferred.fulfill() XCTAssert(mockService.submitBugReportProgressListenerCallsCount == 1) - XCTAssert(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport == BugReport(userID: "@mock.client.com", deviceID: nil, text: "", includeLogs: true, includeCrashLog: true, canContact: false, githubLabels: [], files: [])) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.userID, "@mock.client.com") + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.deviceID, nil) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.text, "") + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.includeLogs, true) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.canContact, false) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.githubLabels, []) + XCTAssertEqual(mockService.submitBugReportProgressListenerReceivedArguments?.bugReport.files, []) } } diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index c8037797a..ddc48ff69 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -24,202 +24,65 @@ class LoggingTests: XCTestCase { } override func setUpWithError() throws { - // Force MXLogger to flush stderr and clean up after itself - MXLog.configure(logLevel: .info, redirectToFiles: false) - - MXLogger.deleteLogFiles() + RustTracing.deleteLogFiles() } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testFileLogging() throws { - XCTAssertTrue(MXLogger.logFiles.isEmpty) - - let log = UUID().uuidString - - MXLog.configure(logLevel: .info, redirectToFiles: true) + + func testLogging() async throws { + let target = "tests" + XCTAssertTrue(RustTracing.logFiles.isEmpty) - MXLog.info(log) - guard let logFile = MXLogger.logFiles.first else { + MXLog.configure(target: target, logLevel: .info) + + // There is something weird with Rust logging where the file writing handle doesn't + // notice that the file it is writing to was deleted, so we can't run these checks + // as separate tests. So instead we need to make sure we run all the tests that + // write logs in this single test case after configuring the log system. + + try validateFileLogging() + try validateLogLevels() + try validateTargetName(target) + + try validateRoomSummaryContentIsRedacted() + try validateTimelineContentIsRedacted() + try validateRustMessageContentIsRedacted() + } + + func validateFileLogging() throws { + let infoLog = UUID().uuidString + + MXLog.info(infoLog) + + guard let logFile = RustTracing.logFiles.first else { XCTFail(Constants.genericFailure) return } - - let content = try String(contentsOf: logFile) - XCTAssert(content.contains(log)) + + try XCTAssertTrue(String(contentsOf: logFile).contains(infoLog)) } - - func testFileRotationOnLaunch() throws { - // Given a fresh launch with no logs. - XCTAssertTrue(MXLogger.logFiles.isEmpty) - // When launching the app 5 times. - let launchCount = 5 - for index in 0..\(textString)")) + let rustTextMessage = TextMessageContent(body: "", + formatted: FormattedBody(format: .html, body: "\(textString)")) let noticeString = "NoticeString" - let noticeMessage = NoticeMessageContent(body: noticeString, - formatted: FormattedBody(format: .html, body: "\(noticeString)")) + let rustNoticeMessage = NoticeMessageContent(body: noticeString, + formatted: FormattedBody(format: .html, body: "\(noticeString)")) let emoteString = "EmoteString" - let emoteMessage = EmoteMessageContent(body: emoteString, - formatted: FormattedBody(format: .html, body: "\(emoteString)")) + let rustEmoteMessage = EmoteMessageContent(body: emoteString, + formatted: FormattedBody(format: .html, body: "\(emoteString)")) let pointer = Unmanaged.passRetained(NSURL(fileURLWithPath: "/tmp/file")).toOpaque() - let imageMessage = ImageMessageContent(body: "ImageString", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) - let videoMessage = VideoMessageContent(body: "VideoString", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) - let fileMessage = FileMessageContent(body: "FileString", filename: "FileName", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) + let rustImageMessage = ImageMessageContent(body: "ImageString", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) + let rustVideoMessage = VideoMessageContent(body: "VideoString", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) + let rustFileMessage = FileMessageContent(body: "FileString", filename: "FileName", source: MediaSource(unsafeFromRawPointer: pointer), info: nil) // When logging that value - XCTAssert(MXLogger.logFiles.isEmpty) - - MXLog.configure(logLevel: .info, redirectToFiles: true) - - MXLog.info(textMessage) - MXLog.info(noticeMessage) - MXLog.info(emoteMessage) - MXLog.info(imageMessage) - MXLog.info(videoMessage) - MXLog.info(fileMessage) + MXLog.info(rustTextMessage) + MXLog.info(rustNoticeMessage) + MXLog.info(rustEmoteMessage) + MXLog.info(rustImageMessage) + MXLog.info(rustVideoMessage) + MXLog.info(rustFileMessage) // Then the log file should not include the text content - guard let logFile = MXLogger.logFiles.first else { + guard let logFile = RustTracing.logFiles.first else { XCTFail(Constants.genericFailure) return } @@ -399,12 +252,67 @@ class LoggingTests: XCTestCase { XCTAssertFalse(content.contains(emoteString)) XCTAssertTrue(content.contains(String(describing: ImageMessageContent.self))) - XCTAssertFalse(content.contains(imageMessage.body)) + XCTAssertFalse(content.contains(rustImageMessage.body)) XCTAssertTrue(content.contains(String(describing: VideoMessageContent.self))) - XCTAssertFalse(content.contains(videoMessage.body)) + XCTAssertFalse(content.contains(rustVideoMessage.body)) XCTAssertTrue(content.contains(String(describing: FileMessageContent.self))) - XCTAssertFalse(content.contains(fileMessage.body)) + XCTAssertFalse(content.contains(rustFileMessage.body)) + } + + func testLogFileSorting() async throws { + // Given a collection of log files. + XCTAssertTrue(RustTracing.logFiles.isEmpty) + + // When creating new logs. + let logsFileDirectory = RustTracing.logsDirectory + for i in 1...5 { + let filename = "console.\(i).log" + try "console".write(to: logsFileDirectory.appending(path: filename), atomically: true, encoding: .utf8) + } + + for i in 1...5 { + let nseFilename = "console-nse.\(i).log" + try "nse".write(to: logsFileDirectory.appending(path: nseFilename), atomically: true, encoding: .utf8) + } + + // Then the logs should be sorted chronologically (newest first) and not alphabetically. + XCTAssertEqual(RustTracing.logFiles.map(\.lastPathComponent), + ["console-nse.5.log", + "console-nse.4.log", + "console-nse.3.log", + "console-nse.2.log", + "console-nse.1.log", + "console.5.log", + "console.4.log", + "console.3.log", + "console.2.log", + "console.1.log"]) + + // When updating the oldest log file. + let currentLogFile = logsFileDirectory.appending(path: "console.1.log") + let fileHandle = try FileHandle(forWritingTo: currentLogFile) + try fileHandle.seekToEnd() + guard let newLineData = "newline".data(using: .utf8) else { + XCTFail("Couldn't create data to write to disk.") + return + } + + try fileHandle.write(contentsOf: newLineData) + try fileHandle.close() + + // Then that file should now be the first log file. + XCTAssertEqual(RustTracing.logFiles.map(\.lastPathComponent), + ["console.1.log", + "console-nse.5.log", + "console-nse.4.log", + "console-nse.3.log", + "console-nse.2.log", + "console-nse.1.log", + "console.5.log", + "console.4.log", + "console.3.log", + "console.2.log"]) } } diff --git a/UnitTests/Sources/TracingConfigurationTests.swift b/UnitTests/Sources/TracingConfigurationTests.swift index 937b1ecb8..efe7827d3 100644 --- a/UnitTests/Sources/TracingConfigurationTests.swift +++ b/UnitTests/Sources/TracingConfigurationTests.swift @@ -20,7 +20,7 @@ import XCTest class TracingConfigurationTests: XCTestCase { func testConfiguration() { - let configuration = TracingConfiguration(logLevel: .trace) + let configuration = TracingConfiguration(logLevel: .trace, target: nil) let filterComponents = configuration.filter.components(separatedBy: ",") XCTAssertEqual(filterComponents.first, "info") diff --git a/changelog.d/pr-2495.change b/changelog.d/pr-2495.change new file mode 100644 index 000000000..00a7f28ae --- /dev/null +++ b/changelog.d/pr-2495.change @@ -0,0 +1 @@ +Handover log file management to Rust. \ No newline at end of file diff --git a/project.yml b/project.yml index ce548c159..8f7e4c81b 100644 --- a/project.yml +++ b/project.yml @@ -48,7 +48,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.1.42 + exactVersion: 1.1.43 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios