Skip to content

Commit

Permalink
Creates a log viewer for debugging (#49)
Browse files Browse the repository at this point in the history
* Create log viewer

* Add flag for debug options

* Fix swiftlint errors

* Share logs as a text file

* Improve filtering

* Make the task cancellable

* Display logs in a list view

* Update UI

* Adjust styling of error messages in list

* Surface errors in UI

* Update log levels

* Update log entry style

* Add colors to level labels

* Update log manager

* Additional improvements to log manager and view
  • Loading branch information
vishnuravi authored Nov 12, 2024
1 parent e4fd6db commit 555e4dd
Show file tree
Hide file tree
Showing 10 changed files with 523 additions and 7 deletions.
44 changes: 38 additions & 6 deletions LifeSpace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,18 @@
6347EB742BBBF442008E0C4A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6347EB732BBBF442008E0C4A /* Constants.swift */; };
63497B702BBF6ECE001F8419 /* LocationDataPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */; };
63497B732BBF855E001F8419 /* OptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63497B722BBF855E001F8419 /* OptionsPanel.swift */; };
634E38422CDE6B4000B16E20 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E38412CDE6B3B00B16E20 /* LogManager.swift */; };
634E38442CDE6E2B00B16E20 /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E38432CDE6E2900B16E20 /* LogViewer.swift */; };
634E38482CDE7A7400B16E20 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E38472CDE7A7100B16E20 /* LogLevel.swift */; };
634E38542CE055CC00B16E20 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E38532CE055C300B16E20 /* OSLogEntryLog+FormattedLogOutput.swift */; };
634E38562CE0EA4600B16E20 /* LogsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E38552CE0EA4600B16E20 /* LogsListView.swift */; };
634FFF672C169F40005E8217 /* LifeSpaceConsent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634FFF662C169F40005E8217 /* LifeSpaceConsent.swift */; };
634FFF6D2C16B81A005E8217 /* HIPAAAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634FFF6C2C16B81A005E8217 /* HIPAAAuthorization.swift */; };
635198792CD53FF40087B1F3 /* FirebaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635198782CD53FF10087B1F3 /* FirebaseConfiguration.swift */; };
639B69DB2C2BCD6A00C0FF4A /* ConsentPDFViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639B69DA2C2BCD6A00C0FF4A /* ConsentPDFViewer.swift */; };
63A28D312C0580310025A1E0 /* RefreshIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A28D302C0580310025A1E0 /* RefreshIcon.swift */; };
63A28D332C062E2E0025A1E0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 63A28D322C062E2E0025A1E0 /* GoogleService-Info.plist */; };
63A3155D2CE2936400310EF5 /* LogManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A3155C2CE2936100310EF5 /* LogManagerError.swift */; };
63A8DB292C1FE81200939757 /* AppIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 63A8DB282C1FE81200939757 /* AppIcon.png */; };
63AB851F2C3DACA40011AEB6 /* HealthKitDataPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63AB851E2C3DACA40011AEB6 /* HealthKitDataPoint.swift */; };
63BBF8162BB8993B006890CE /* StudyIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BBF8152BB8993B006890CE /* StudyIDView.swift */; };
Expand Down Expand Up @@ -137,12 +143,18 @@
6347EB732BBBF442008E0C4A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
63497B6F2BBF6ECE001F8419 /* LocationDataPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationDataPoint.swift; sourceTree = "<group>"; };
63497B722BBF855E001F8419 /* OptionsPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsPanel.swift; sourceTree = "<group>"; };
634E38412CDE6B3B00B16E20 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
634E38432CDE6E2900B16E20 /* LogViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = "<group>"; };
634E38472CDE7A7100B16E20 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
634E38532CE055C300B16E20 /* OSLogEntryLog+FormattedLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLogEntryLog+FormattedLogOutput.swift"; sourceTree = "<group>"; };
634E38552CE0EA4600B16E20 /* LogsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsListView.swift; sourceTree = "<group>"; };
634FFF662C169F40005E8217 /* LifeSpaceConsent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifeSpaceConsent.swift; sourceTree = "<group>"; };
634FFF6C2C16B81A005E8217 /* HIPAAAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HIPAAAuthorization.swift; sourceTree = "<group>"; };
635198782CD53FF10087B1F3 /* FirebaseConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseConfiguration.swift; sourceTree = "<group>"; };
639B69DA2C2BCD6A00C0FF4A /* ConsentPDFViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentPDFViewer.swift; sourceTree = "<group>"; };
63A28D302C0580310025A1E0 /* RefreshIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIcon.swift; sourceTree = "<group>"; };
63A28D322C062E2E0025A1E0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
63A3155C2CE2936100310EF5 /* LogManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManagerError.swift; sourceTree = "<group>"; };
63A8DB282C1FE81200939757 /* AppIcon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AppIcon.png; sourceTree = "<group>"; };
63AB851E2C3DACA40011AEB6 /* HealthKitDataPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitDataPoint.swift; sourceTree = "<group>"; };
63BBF8152BB8993B006890CE /* StudyIDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyIDView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -302,6 +314,19 @@
path = Map;
sourceTree = "<group>";
};
634E38462CDE790800B16E20 /* Logging */ = {
isa = PBXGroup;
children = (
63A3155C2CE2936100310EF5 /* LogManagerError.swift */,
634E38532CE055C300B16E20 /* OSLogEntryLog+FormattedLogOutput.swift */,
634E38472CDE7A7100B16E20 /* LogLevel.swift */,
634E38412CDE6B3B00B16E20 /* LogManager.swift */,
634E38432CDE6E2900B16E20 /* LogViewer.swift */,
634E38552CE0EA4600B16E20 /* LogsListView.swift */,
);
path = Logging;
sourceTree = "<group>";
};
635198772CD53FE30087B1F3 /* Firestore */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -361,6 +386,7 @@
653A254F283387FE005D4D48 /* LifeSpace */ = {
isa = PBXGroup;
children = (
634E38462CDE790800B16E20 /* Logging */,
635198772CD53FE30087B1F3 /* Firestore */,
A9720E412ABB68B300872D23 /* Account */,
637AA5CF2BBDA686007BD7A3 /* Location */,
Expand Down Expand Up @@ -634,6 +660,8 @@
2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */,
2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */,
634FFF672C169F40005E8217 /* LifeSpaceConsent.swift in Sources */,
634E38482CDE7A7400B16E20 /* LogLevel.swift in Sources */,
634E38562CE0EA4600B16E20 /* LogsListView.swift in Sources */,
2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */,
2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */,
630D3B572C616E9D006066E5 /* WithdrawView.swift in Sources */,
Expand All @@ -647,10 +675,12 @@
63A28D312C0580310025A1E0 /* RefreshIcon.swift in Sources */,
A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */,
2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */,
63A3155D2CE2936400310EF5 /* LogManagerError.swift in Sources */,
2F1AC9DF2B4E840E00C24973 /* LifeSpace.docc in Sources */,
2FF53D8D2A8729D600042B76 /* LifeSpaceStandard.swift in Sources */,
63497B702BBF6ECE001F8419 /* LocationDataPoint.swift in Sources */,
A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */,
634E38542CE055CC00B16E20 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */,
2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */,
2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */,
63AB851F2C3DACA40011AEB6 /* HealthKitDataPoint.swift in Sources */,
Expand All @@ -664,8 +694,10 @@
2F4E23832989D51F0013F3D9 /* LifeSpaceTestingSetup.swift in Sources */,
63F4C39B2BBCCCF80033D985 /* LocationModule.swift in Sources */,
6347EB742BBBF442008E0C4A /* Constants.swift in Sources */,
634E38442CDE6E2B00B16E20 /* LogViewer.swift in Sources */,
63F4C39D2BBCCD200033D985 /* LocationUtils.swift in Sources */,
2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */,
634E38422CDE6B4000B16E20 /* LogManager.swift in Sources */,
634FFF6D2C16B81A005E8217 /* HIPAAAuthorization.swift in Sources */,
2FE5DC5129EDD7FA004B9AB4 /* LifeSpaceTaskContext.swift in Sources */,
63EA5F7B2BC04F8400A48590 /* DailySurveyTask.swift in Sources */,
Expand Down Expand Up @@ -788,7 +820,7 @@
CODE_SIGN_ENTITLEMENTS = "LifeSpace/Supporting Files/LifeSpace.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
Expand All @@ -813,7 +845,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.lifespace;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -992,7 +1024,7 @@
CODE_SIGN_ENTITLEMENTS = "LifeSpace/Supporting Files/LifeSpace.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
Expand All @@ -1017,7 +1049,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.lifespace;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -1040,7 +1072,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = "";
Expand All @@ -1066,7 +1098,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0;
MARKETING_VERSION = 2.1.0;
PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.lifespace;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
13 changes: 13 additions & 0 deletions LifeSpace/Account/AccountSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ struct AccountSheet: View {
locationTrackingToggle
withdrawButton
}
if FeatureFlags.showDebugOptions {
Section(header: Text("DEBUG_SECTION")) {
logExportButton
}
}
}
}

Expand Down Expand Up @@ -170,6 +175,14 @@ struct AccountSheet: View {
}
}

private var logExportButton: some View {
NavigationLink(destination: {
LogViewer()
}) {
Text("VIEW_LOGS")
}
}


private func getDocumentURL(for fileName: String) -> URL? {
guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
Expand Down
75 changes: 75 additions & 0 deletions LifeSpace/Logging/LogLevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// LogLevel.swift
// LifeSpace
//
// Created by Vishnu Ravi on 11/8/24.
//

import OSLog
import SwiftUI


enum LogLevel: String, CaseIterable, Identifiable {
case all = "All"
case info = "Info"
case debug = "Debug"
case error = "Error"
case fault = "Fault"
case notice = "Notice"
case undefined = "Undefined"

var id: String { self.rawValue }

var osLogLevel: OSLogEntryLog.Level? {
switch self {
case .all:
return nil
case .info:
return .info
case .debug:
return .debug
case .error:
return .error
case .fault:
return .fault
case .notice:
return .notice
case .undefined:
return .undefined
}
}

var color: Color {
switch self {
case .info:
return .blue
case .debug:
return .green
case .error:
return .red
case .fault:
return .purple
case .notice:
return .orange
case .all, .undefined:
return .gray
}
}

init(from osLogLevel: OSLogEntryLog.Level) {
switch osLogLevel {
case .info:
self = .info
case .debug:
self = .debug
case .error:
self = .error
case .fault:
self = .fault
case .notice:
self = .notice
@unknown default:
self = .undefined
}
}
}
68 changes: 68 additions & 0 deletions LifeSpace/Logging/LogManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// LogStore.swift
// LifeSpace
//
// Created by Vishnu Ravi on 11/8/24.
//

import Foundation
import OSLog
import Spezi
import SwiftUI

/// Manages log entries within the application using `OSLogStore`, allowing querying
/// based on date ranges and log levels.
class LogManager {
/// Reference to the `OSLogStore`, which provides access to system logs.
private let store: OSLogStore?

/// Initializes the `LogManager` and attempts to set the `OSLogStore` with
/// a scope limited to the current process identifier.
init() {
self.store = try? OSLogStore(scope: .currentProcessIdentifier)
}

/// Queries logs within a specified date range and optional log level.
///
/// - Parameters:
/// - startDate: The start date from which logs should be queried.
/// - endDate: An optional end date up to which logs should be queried.
/// - logLevel: An optional log level filter, returning only entries of this level if specified.
/// - Returns: An array of `OSLogEntryLog` entries that match the specified criteria.
/// - Throws: `LogManagerError.invalidLogStore` if `OSLogStore` is unavailable, or
/// `LogManagerError.invalidBundleIdentifier` if the bundle identifier cannot be retrieved.
func query(
startDate: Date,
endDate: Date? = nil,
logLevel: OSLogEntryLog.Level? = nil
) throws -> [OSLogEntryLog] {
guard let store else {
throw LogManagerError.invalidLogStore
}

guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
throw LogManagerError.invalidBundleIdentifier
}

let position = store.position(date: startDate)
let predicate = NSPredicate(format: "subsystem == %@", bundleIdentifier)
let logs = try store.getEntries(at: position, matching: predicate)
.reversed()
.compactMap { $0 as? OSLogEntryLog }

return logs
.filter { logEntry in
/// Filter by log type if specified
if let logLevel, logEntry.level != logLevel {
return false
}

/// Filter by end date if specified
if let endDate, logEntry.date > endDate {
return false
}

return true
}
}
}
25 changes: 25 additions & 0 deletions LifeSpace/Logging/LogManagerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// LogManagerError.swift
// LifeSpace
//
// Created by Vishnu Ravi on 11/11/24.
//


enum LogManagerError: Error {
/// Throw when the log store is invalid
case invalidLogStore
/// Throw when the bundle identifier is invalid
case invalidBundleIdentifier
}

extension LogManagerError: CustomStringConvertible {
public var description: String {
switch self {
case .invalidLogStore:
return "The OSLogStore is invalid."
case .invalidBundleIdentifier:
return "The bundle identifier is invalid."
}
}
}
Loading

0 comments on commit 555e4dd

Please sign in to comment.