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:
Doug
2024-02-23 18:29:13 +00:00
committed by GitHub
parent c573b7caa1
commit ff35688088
19 changed files with 379 additions and 772 deletions
+7 -7
View File
@@ -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 }
+3 -31
View File
@@ -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")
}
}
}
+53 -126
View File
@@ -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
@@ -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, [])
}
}
+122 -214
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
Handover log file management to Rust.
+1 -1
View File
@@ -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