Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Tests/KeystoneTests/Tests/Utility/LogFileStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct LogFileStoreTests {
#expect(LogFileStore(fileLogger: makeFileLogger()).logFiles().isEmpty)
}

/// Crash reports and Zendesk attach the file the app is writing to, which is the newest one.
/// Events sent now (Zendesk, non-fatal errors) attach the file the app is writing to, which is the newest one.
@Test func eventLoggingAttachesTheNewestFile() {
let newest = AppLogFile(url: URL(fileURLWithPath: "/tmp/logs/today.log"), creationDate: Date())
let older = AppLogFile(
Expand Down
60 changes: 60 additions & 0 deletions Tests/KeystoneTests/Tests/Utility/LogFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import Testing

@testable import WordPress

struct EventLoggingDataProviderTests {
private let now = Date(timeIntervalSince1970: 1_800_000_000)

private func logFile(_ name: String, createdSecondsAgo seconds: TimeInterval?) -> AppLogFile {
AppLogFile(
url: URL(fileURLWithPath: "/logs/\(name).log"),
creationDate: seconds.map { now.addingTimeInterval(-$0) }
)
}

@Test func picksTheFileThatWasCurrentAtTheEventDate() {
// The app crashed an hour ago, then a new log file was started on relaunch.
let files = [logFile("relaunch", createdSecondsAgo: 60), logFile("crashed", createdSecondsAgo: 86_400)]

let selected = EventLoggingDataProvider.logFile(in: files, at: now.addingTimeInterval(-3_600))

#expect(selected?.url.lastPathComponent == "crashed.log")
}

@Test func picksTheNewestFileForTheCurrentSession() {
let files = [logFile("current", createdSecondsAgo: 60), logFile("older", createdSecondsAgo: 86_400)]

let selected = EventLoggingDataProvider.logFile(in: files, at: now)

#expect(selected?.url.lastPathComponent == "current.log")
}

@Test func fallsBackToTheNewestFileWhenNoneQualifies() {
let files = [logFile("newest", createdSecondsAgo: nil), logFile("older", createdSecondsAgo: 60)]

let selected = EventLoggingDataProvider.logFile(in: files, at: now.addingTimeInterval(-3_600))

#expect(selected?.url.lastPathComponent == "newest.log")
}

@Test func returnsNilWithoutLogFiles() {
#expect(EventLoggingDataProvider.logFile(in: [], at: now) == nil)
}
}

struct WpLogDataProviderTests {
@Test func skipsFilesThatNoLongerExist() throws {
let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: directory) }

let existing = directory.appendingPathComponent("existing.log")
try Data("log".utf8).write(to: existing)
let missing = directory.appendingPathComponent("missing.log")

let logs = WpLogDataProvider.applicationLogs(atPaths: [missing.path, existing.path])

#expect(logs.map(\.path.lastPathComponent) == ["existing.log"])
}
}
18 changes: 13 additions & 5 deletions WordPress/Classes/Utility/Logging/EventLoggingDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import AutomatticEncryptedLogs

struct EventLoggingDataProvider: EventLoggingDataSource {

typealias LogFilesCallback = (() -> [URL])
typealias LogFilesCallback = (() -> [AppLogFile])

/// A block that returns all existing log files
/// A block that returns all existing log files, newest first
private let fetchLogFiles: LogFilesCallback?

let loggingAuthenticationToken: String
Expand All @@ -27,14 +27,22 @@ struct EventLoggingDataProvider: EventLoggingDataSource {
self.fetchLogFiles = block
}

/// The current session log will almost always be the correct one, because they're split by day
/// Returns the log file that was being written to at `date`.
///
/// A crash is sent on the next launch, which may have started a new log file, so the newest file isn't always the
/// right one. The file that was current at `date` is the newest one created at or before it. If no file qualifies
/// (for example, because creation dates are unavailable), the newest file is returned.
func logFilePath(forErrorLevel: EventLoggingErrorType, at date: Date) -> URL? {
return fetchLogFiles?().first
Self.logFile(in: fetchLogFiles?() ?? [], at: date)?.url
}

static func logFile(in newestFirst: [AppLogFile], at date: Date) -> AppLogFile? {
newestFirst.first { $0.creationDate.map { $0 <= date } ?? false } ?? newestFirst.first
}

static func from(_ store: LogFileStore) -> EventLoggingDataSource {
EventLoggingDataProvider {
store.logFiles().map(\.url)
store.logFiles()
}
}
}
5 changes: 0 additions & 5 deletions WordPress/Classes/Utility/Logging/WPLogger.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,4 @@
*/
- (void)deleteAllLogs;

/**
* @brief Deletes all the old archived logs from the device
*/
- (void)deleteArchivedLogs;

@end
12 changes: 0 additions & 12 deletions WordPress/Classes/Utility/Logging/WPLogger.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,6 @@ - (void)deleteAllLogs
WPLogWarning(@"All log files erased.");
}

- (void)deleteArchivedLogs
{
NSArray *logFiles = self.fileLogger.logFileManager.sortedLogFileInfos;
for (DDLogFileInfo *logFileInfo in logFiles) {
if (logFileInfo.isArchived) {
[[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil];
}
}

WPLogWarning(@"All archived log files erased.");
}

#pragma mark - Public static methods

+ (void)configureLoggerLevelWithExtraDebug {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,18 @@ struct SupportActivityLogView: View {
private class ActivityLogViewModel: ObservableObject {
@Published private(set) var logFiles: [AppLogFile] = []

private let store: LogFileStore
private let dateFormatter: DateFormatter

init(store: LogFileStore = .shared) {
self.store = store

dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.doesRelativeDateFormatting = true
dateFormatter.timeStyle = .short

logFiles = store.logFiles()
reloadLogFiles()
}

func getFormattedDate(for logFile: AppLogFile) -> String {
Expand All @@ -63,15 +66,19 @@ private class ActivityLogViewModel: ObservableObject {
}

func buttonDeleteLogsTapped() {
WPLogger.shared().deleteArchivedLogs()
logFiles = []
WPLogger.shared().deleteAllLogs()
reloadLogFiles()
}

private func reloadLogFiles() {
logFiles = store.logFiles()
}
}

private enum Strings {
static let title = NSLocalizedString("support.activityLogs.navigationTitle", value: "Activity Logs", comment: "Title shown in the navigation bar of the Activity Logs screen.")
static let currentLog = NSLocalizedString("support.activityLogs.currentLogTilte", value: "Current", comment: "Label for the current activity log file in the list.")
static let sectionFooter = NSLocalizedString("support.activityLogs.sectionFooter", value: "Up to seven days worth of logs are saved.", comment: "Footer text explaining the log retention policy in the Activity Logs screen.")
static let deleteTitle = NSLocalizedString("support.activityLogs.deleteLogsAlert.title", value: "Delete Logs", comment: "Title shown in the alert when attempting to clear old activity logs.")
static let deleteMessage = NSLocalizedString("support.activityLogs.deleteLogsAlert.message", value: "Are you sure you want to delete all activity logs?", comment: "Message shown in the alert when attempting to clear old activity logs.")
static let deleteTitle = NSLocalizedString("support.activityLogs.deleteLogsAlert.title", value: "Delete Logs", comment: "Title shown in the alert when attempting to delete all activity logs.")
static let deleteMessage = NSLocalizedString("support.activityLogs.deleteLogsAlert.message", value: "Are you sure you want to delete all activity logs?", comment: "Message shown in the alert when attempting to delete all activity logs.")
}
15 changes: 13 additions & 2 deletions WordPress/Classes/ViewRelated/NewSupport/SupportDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,19 @@ actor WpLogDataProvider: ApplicationLogDataProvider {
}

func fetchApplicationLogs() async throws -> [Support.ApplicationLog] {
try LogFileStore.shared.logFiles()
.compactMap { try ApplicationLog(filePath: $0.url.path) }
Self.applicationLogs(atPaths: LogFileStore.shared.logFiles().map(\.url.path))
}

/// A file can be removed between listing the folder and reading its attributes (for example, by CocoaLumberjack's
/// automatic cleanup), so files that can't be read are skipped rather than failing the whole list.
static func applicationLogs(atPaths paths: [String]) -> [Support.ApplicationLog] {
paths.compactMap { path in
do {
return try ApplicationLog(filePath: path)
} catch {
return nil
}
}
}

func deleteApplicationLogs(in logs: [Support.ApplicationLog]) async throws {
Expand Down