mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Handover log file management to Rust. (#2495)
* 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.
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; };
|
||||
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
A34A814CBD56230BC74FFCF4 /* MXLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLogger.swift; sourceTree = "<group>"; };
|
||||
A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = "<group>"; };
|
||||
A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
|
||||
@@ -1953,6 +1952,7 @@
|
||||
ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenUITests.swift; sourceTree = "<group>"; };
|
||||
ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = "<group>"; };
|
||||
ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||
@@ -2175,8 +2175,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
111B698739E3410E2CDB7144 /* MXLog.swift */,
|
||||
A34A814CBD56230BC74FFCF4 /* MXLogger.swift */,
|
||||
542D4F49FABA056DEEEB3400 /* RustTracing.swift */,
|
||||
ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */,
|
||||
);
|
||||
path = Logging;
|
||||
sourceTree = "<group>";
|
||||
@@ -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" */ = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Target, LogLevel> = [
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Target, LogLevel> = [
|
||||
.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: ",")
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let userID: String?
|
||||
private let deviceID: String?
|
||||
|
||||
private let actionsSubject: PassthroughSubject<BugReportScreenViewModelAction, Never> = .init()
|
||||
// periphery:ignore - when set to nil this is automatically cancelled
|
||||
@CancellableTask private var uploadTask: Task<Void, Never>?
|
||||
@@ -89,7 +90,6 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie
|
||||
deviceID: deviceID,
|
||||
text: context.reportText,
|
||||
includeLogs: context.sendingLogsEnabled,
|
||||
includeCrashLog: true,
|
||||
canContact: context.canContact,
|
||||
githubLabels: [],
|
||||
files: files)
|
||||
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
@@ -35,9 +32,7 @@ final class LogViewerScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: LogViewerScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
init() {
|
||||
viewModel = LogViewerScreenViewModel()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Double, Never>) async -> Result<SubmitBugReportResponse, BugReportServiceError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: [])
|
||||
|
||||
@@ -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, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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..<launchCount {
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true)
|
||||
MXLog.info("Launch \(index + 1)")
|
||||
}
|
||||
func validateLogLevels() throws {
|
||||
let verboseLog = UUID().uuidString
|
||||
|
||||
// Then 5 log files should be created each with the correct contents.
|
||||
let logFiles = MXLogger.logFiles
|
||||
|
||||
XCTAssertEqual(logFiles.count, launchCount, "The number of log files should match the number of launches.")
|
||||
try verifyContents(of: logFiles, after: launchCount)
|
||||
}
|
||||
|
||||
func testMaxLogFileCount() throws {
|
||||
// Given a fresh launch with no logs.
|
||||
XCTAssertTrue(MXLogger.logFiles.isEmpty)
|
||||
|
||||
// When launching the app 10 times, with a maxLogCount of 5.
|
||||
let launchCount = 10
|
||||
let logFileCount = 5
|
||||
for index in 0..<launchCount {
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true, maxLogFileCount: UInt(logFileCount))
|
||||
MXLog.info("Launch \(index + 1)")
|
||||
}
|
||||
|
||||
// Then only 5 log files should be stored on disk, with the contents of launches 6 to 10.
|
||||
let logFiles = MXLogger.logFiles
|
||||
|
||||
XCTAssertEqual(logFiles.count, logFileCount, "The number of log files should match the number of launches.")
|
||||
try verifyContents(of: logFiles, after: launchCount)
|
||||
}
|
||||
|
||||
func testLogFileSizeLimit() throws {
|
||||
// Given a fresh launch with no logs.
|
||||
XCTAssertTrue(MXLogger.logFiles.isEmpty)
|
||||
|
||||
// When launching the app 10 times, with a max total log size of 25KB and logging ~5KB data each time.
|
||||
let launchCount = 10
|
||||
let logFileSizeLimit: UInt = 25 * 1024
|
||||
for index in 0..<launchCount {
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true, logFileSizeLimit: logFileSizeLimit)
|
||||
MXLog.info("Launch \(index + 1)")
|
||||
|
||||
// Add ~5KB of logs
|
||||
for _ in 0..<5 {
|
||||
let string = [String](repeating: "a", count: 1024).joined()
|
||||
MXLog.info(string)
|
||||
}
|
||||
}
|
||||
|
||||
// Then only the most recent log files should be stored on disk.
|
||||
let logFiles = MXLogger.logFiles
|
||||
|
||||
XCTAssertGreaterThan(logFiles.count, 0, "There should be at least one log file created.")
|
||||
XCTAssertLessThan(logFiles.count, launchCount, "Some of the log files should have been removed trimmed.")
|
||||
try verifyContents(of: logFiles, after: launchCount)
|
||||
}
|
||||
|
||||
/// Verifies that the log files all contain the correct `Launch #` based on the index
|
||||
/// in the file name and the number of launches of the app.
|
||||
func verifyContents(of logFiles: [URL], after launchCount: Int) throws {
|
||||
for logFile in logFiles {
|
||||
let regex = /\d+/
|
||||
let fileIndex = logFile.lastPathComponent.firstMatch(of: regex)?.0 ?? "0"
|
||||
|
||||
guard let index = Int(fileIndex) else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
|
||||
let content = try String(contentsOf: logFile)
|
||||
XCTAssertTrue(content.contains("Launch \(launchCount - index)"), "The log files should be for the most recent launches in reverse chronological order.")
|
||||
}
|
||||
}
|
||||
|
||||
func testLogLevels() throws {
|
||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||
|
||||
let log = UUID().uuidString
|
||||
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true)
|
||||
|
||||
MXLog.verbose(log)
|
||||
guard let logFile = MXLogger.logFiles.first else {
|
||||
MXLog.verbose(verboseLog)
|
||||
guard let logFile = RustTracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
|
||||
let content = try String(contentsOf: logFile)
|
||||
XCTAssertFalse(content.contains(log))
|
||||
}
|
||||
|
||||
func testSubLogName() {
|
||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||
|
||||
let target = "nse"
|
||||
|
||||
MXLog.configure(target: target, logLevel: .info, redirectToFiles: true)
|
||||
|
||||
try XCTAssertFalse(String(contentsOf: logFile).contains(verboseLog))
|
||||
}
|
||||
|
||||
func validateTargetName(_ target: String) throws {
|
||||
MXLog.info(UUID().uuidString)
|
||||
guard let logFile = MXLogger.logFiles.first else {
|
||||
guard let logFile = RustTracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
XCTAssertTrue(logFile.lastPathComponent.contains(target))
|
||||
}
|
||||
|
||||
func testLogFileSorting() async throws {
|
||||
// Given a collection of log files.
|
||||
XCTAssertTrue(MXLogger.logFiles.isEmpty)
|
||||
|
||||
// When creating new logs.
|
||||
let logsFileDirectory = URL.appGroupContainerDirectory
|
||||
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(MXLogger.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(MXLogger.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"])
|
||||
}
|
||||
|
||||
func testRoomSummaryContentIsRedacted() throws {
|
||||
func validateRoomSummaryContentIsRedacted() throws {
|
||||
// Given a room summary that contains sensitive information
|
||||
let roomName = "Private Conversation"
|
||||
let lastMessage = "Secret information"
|
||||
@@ -240,14 +103,10 @@ class LoggingTests: XCTestCase {
|
||||
isFavourite: false)
|
||||
|
||||
// When logging that value
|
||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true)
|
||||
|
||||
MXLog.info(roomSummary)
|
||||
|
||||
// Then the log file should not include the sensitive information
|
||||
guard let logFile = MXLogger.logFiles.first else {
|
||||
guard let logFile = RustTracing.logFiles.first else {
|
||||
XCTFail(Constants.genericFailure)
|
||||
return
|
||||
}
|
||||
@@ -257,8 +116,8 @@ class LoggingTests: XCTestCase {
|
||||
XCTAssertFalse(content.contains(roomName))
|
||||
XCTAssertFalse(content.contains(lastMessage))
|
||||
}
|
||||
|
||||
func testTimelineContentIsRedacted() throws {
|
||||
|
||||
func validateTimelineContentIsRedacted() throws {
|
||||
// Given timeline items that contain text
|
||||
let textAttributedString = "TextAttributed"
|
||||
let textMessage = TextRoomTimelineItem(id: .random,
|
||||
@@ -313,9 +172,7 @@ class LoggingTests: XCTestCase {
|
||||
content: .init(body: "FileString", source: nil, thumbnailSource: nil, contentType: nil))
|
||||
|
||||
// When logging that value
|
||||
XCTAssert(MXLogger.logFiles.isEmpty)
|
||||
|
||||
MXLog.configure(logLevel: .info, redirectToFiles: true)
|
||||
MXLog.configure(logLevel: .info)
|
||||
|
||||
MXLog.info(textMessage)
|
||||
MXLog.info(noticeMessage)
|
||||
@@ -325,11 +182,11 @@ class LoggingTests: XCTestCase {
|
||||
MXLog.info(fileMessage)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
let content = try String(contentsOf: logFile)
|
||||
XCTAssertTrue(content.contains(textMessage.id.timelineID))
|
||||
XCTAssertFalse(content.contains(textMessage.body))
|
||||
@@ -352,38 +209,34 @@ class LoggingTests: XCTestCase {
|
||||
XCTAssertTrue(content.contains(fileMessage.id.timelineID))
|
||||
XCTAssertFalse(content.contains(fileMessage.body))
|
||||
}
|
||||
|
||||
func testRustMessageContentIsRedacted() throws {
|
||||
|
||||
func validateRustMessageContentIsRedacted() throws {
|
||||
// Given message content that contain text
|
||||
let textString = "TextString"
|
||||
let textMessage = TextMessageContent(body: "",
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(textString)</b>"))
|
||||
let rustTextMessage = TextMessageContent(body: "",
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(textString)</b>"))
|
||||
let noticeString = "NoticeString"
|
||||
let noticeMessage = NoticeMessageContent(body: noticeString,
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(noticeString)</b>"))
|
||||
let rustNoticeMessage = NoticeMessageContent(body: noticeString,
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(noticeString)</b>"))
|
||||
let emoteString = "EmoteString"
|
||||
let emoteMessage = EmoteMessageContent(body: emoteString,
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(emoteString)</b>"))
|
||||
let rustEmoteMessage = EmoteMessageContent(body: emoteString,
|
||||
formatted: FormattedBody(format: .html, body: "<b>\(emoteString)</b>"))
|
||||
|
||||
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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Handover log file management to Rust.
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user