Logging: Add exportability to logs
Logs can be exported into their own files. I'm still debating on writing a continuous stream to a logfile that persists on app crashes. Also make the show error toasts toggle only apply to generic errors and not customized error toasts. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
7202a95bb2
commit
d2d7d7364f
4 changed files with 88 additions and 7 deletions
|
|
@ -993,7 +993,7 @@
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = NO;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
@ -1028,7 +1028,7 @@
|
||||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = NO;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
@ -1072,8 +1072,8 @@
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
|
repositoryURL = "https://github.com/SwiftUIX/SwiftUIX";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
branch = master;
|
||||||
minimumVersion = 0.1.3;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
// A static DateFormatter is better than initializing new ones
|
||||||
extension DateFormatter {
|
extension DateFormatter {
|
||||||
static let historyDateFormatter: DateFormatter = {
|
static let historyDateFormatter: DateFormatter = {
|
||||||
let df = DateFormatter()
|
let df = DateFormatter()
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,13 @@ import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class LoggingManager: ObservableObject {
|
class LoggingManager: ObservableObject {
|
||||||
|
let logFormatter = DateFormatter()
|
||||||
|
|
||||||
struct Log: Hashable {
|
struct Log: Hashable {
|
||||||
let level: LogLevel
|
let level: LogLevel
|
||||||
let message: String
|
let message: String
|
||||||
let timeStamp: Date = .init()
|
let timeStamp: Date = .init()
|
||||||
|
var isExpanded: Bool = false
|
||||||
|
|
||||||
func toMessage() -> String {
|
func toMessage() -> String {
|
||||||
"[\(level.rawValue)]: \(message)"
|
"[\(level.rawValue)]: \(message)"
|
||||||
|
|
@ -30,6 +33,7 @@ class LoggingManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var messageArray: [Log] = []
|
@Published var messageArray: [Log] = []
|
||||||
|
@Published var showLogExportedAlert = false
|
||||||
|
|
||||||
// Toast variables
|
// Toast variables
|
||||||
@Published var toastDescription: String? = nil {
|
@Published var toastDescription: String? = nil {
|
||||||
|
|
@ -57,7 +61,13 @@ class LoggingManager: ObservableObject {
|
||||||
@Published var indeterminateCancelAction: (() -> Void)? = nil
|
@Published var indeterminateCancelAction: (() -> Void)? = nil
|
||||||
@Published var showIndeterminateToast: Bool = false
|
@Published var showIndeterminateToast: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
logFormatter.dateStyle = .short
|
||||||
|
logFormatter.timeStyle = .long
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Logging functions
|
// MARK: - Logging functions
|
||||||
|
// TODO: Maybe append to a constant logfile?
|
||||||
|
|
||||||
public func info(_ message: String,
|
public func info(_ message: String,
|
||||||
description: String? = nil)
|
description: String? = nil)
|
||||||
|
|
@ -105,8 +115,13 @@ class LoggingManager: ObservableObject {
|
||||||
)
|
)
|
||||||
|
|
||||||
// If a task is run in parallel, don't show a toast on error
|
// If a task is run in parallel, don't show a toast on error
|
||||||
if showToast && showErrorToasts {
|
// Only gate generic error toasts behind the settings option
|
||||||
toastDescription = description.map { $0 } ?? "An error was logged"
|
if showToast {
|
||||||
|
if let description {
|
||||||
|
toastDescription = description
|
||||||
|
} else if showErrorToasts {
|
||||||
|
toastDescription = "An error was logged"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageArray.append(log)
|
messageArray.append(log)
|
||||||
|
|
@ -133,4 +148,33 @@ class LoggingManager: ObservableObject {
|
||||||
indeterminateToastDescription = ""
|
indeterminateToastDescription = ""
|
||||||
indeterminateCancelAction = nil
|
indeterminateCancelAction = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func exportLogs() {
|
||||||
|
logFormatter.dateFormat = "yyyy-MM-dd-HHmmss"
|
||||||
|
let logFileName = "ferrite_session_\(logFormatter.string(from: Date())).txt"
|
||||||
|
let logFolderPath = FileManager.default.appDirectory.appendingPathComponent("Logs")
|
||||||
|
let logPath = logFolderPath.appendingPathComponent(logFileName)
|
||||||
|
|
||||||
|
logFormatter.dateStyle = .short
|
||||||
|
logFormatter.timeStyle = .long
|
||||||
|
let joinedMessages = messageArray.map { "\(logFormatter.string(from: $0.timeStamp)): \($0.toMessage())" }.joined(separator: "\n")
|
||||||
|
|
||||||
|
do {
|
||||||
|
if FileManager.default.fileExists(atPath: logPath.path) {
|
||||||
|
try FileManager.default.removeItem(at: logPath)
|
||||||
|
} else if !FileManager.default.fileExists(atPath: logFolderPath.path) {
|
||||||
|
try FileManager.default.createDirectory(atPath: logFolderPath.path, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
try joinedMessages.write(to: logPath, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
self.info("Log \(logFileName) was written to path \(logPath.description)")
|
||||||
|
showLogExportedAlert.toggle()
|
||||||
|
} catch {
|
||||||
|
self.error(
|
||||||
|
"Log export for file \(logFileName): \(error)",
|
||||||
|
description: "Exporting your log file failed. Please check the logs page."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,51 @@ struct SettingsLogView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
List {
|
List {
|
||||||
ForEach(logManager.messageArray, id: \.self) { log in
|
ForEach($logManager.messageArray, id: \.self) { $log in
|
||||||
Text(log.toMessage())
|
Text(log.toMessage())
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(log.isExpanded ? nil : 5)
|
||||||
|
.onTapGesture {
|
||||||
|
log.isExpanded.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
|
.backport.alert(
|
||||||
|
isPresented: $logManager.showLogExportedAlert,
|
||||||
|
title: "Success",
|
||||||
|
message: "Log successfully exported in Ferrite's logs folder"
|
||||||
|
)
|
||||||
.navigationTitle("Logs")
|
.navigationTitle("Logs")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
logManager.exportLogs()
|
||||||
|
} label: {
|
||||||
|
Label("Export", systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15, *) {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
logManager.messageArray = []
|
||||||
|
} label: {
|
||||||
|
Label("Clear session logs", systemImage: "trash")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
logManager.messageArray = []
|
||||||
|
} label: {
|
||||||
|
Label("Clear session logs", systemImage: "trash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue