Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creates a log viewer #89

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions TemplateApplication.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@
5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */; };
56E708352BB06B7100B08F0A /* SpeziLicense in Frameworks */ = {isa = PBXBuildFile; productRef = 56E708342BB06B7100B08F0A /* SpeziLicense */; };
56E7083B2BB06F6F00B08F0A /* SwiftPackageList in Frameworks */ = {isa = PBXBuildFile; productRef = 56E7083A2BB06F6F00B08F0A /* SwiftPackageList */; };
63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315522CE14A8E00310EF5 /* LogViewerTests.swift */; };
63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63A315552CE14C0900310EF5 /* LogManagerError.swift */; };
63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */; };
63E8517A2CE0FFDE005554E7 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E851792CE0FFDC005554E7 /* LogLevel.swift */; };
63E8517C2CE0FFF9005554E7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517B2CE0FFF6005554E7 /* LogManager.swift */; };
63E8517E2CE10007005554E7 /* LogViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517D2CE10005005554E7 /* LogViewer.swift */; };
63E851802CE10016005554E7 /* LogsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8517F2CE10014005554E7 /* LogsListView.swift */; };
653A2551283387FE005D4D48 /* TemplateApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A2550283387FE005D4D48 /* TemplateApplication.swift */; };
653A255528338800005D4D48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 653A255428338800005D4D48 /* Assets.xcassets */; };
653A256228338800005D4D48 /* TemplateApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A256128338800005D4D48 /* TemplateApplicationTests.swift */; };
Expand Down Expand Up @@ -118,6 +125,13 @@
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = "<group>"; };
2FF53D8C2A8729D600042B76 /* TemplateApplicationStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TemplateApplicationStandard.swift; sourceTree = "<group>"; };
5680DD3D2AB8CD84004E6D4A /* ContributionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributionsTest.swift; sourceTree = "<group>"; };
63A315522CE14A8E00310EF5 /* LogViewerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerTests.swift; sourceTree = "<group>"; };
63A315552CE14C0900310EF5 /* LogManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManagerError.swift; sourceTree = "<group>"; };
63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLogEntryLog+FormattedLogOutput.swift"; sourceTree = "<group>"; };
63E851792CE0FFDC005554E7 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
63E8517B2CE0FFF6005554E7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = "<group>"; };
63E8517D2CE10005005554E7 /* LogViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewer.swift; sourceTree = "<group>"; };
63E8517F2CE10014005554E7 /* LogsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsListView.swift; sourceTree = "<group>"; };
653A254D283387FE005D4D48 /* TemplateApplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TemplateApplication.app; sourceTree = BUILT_PRODUCTS_DIR; };
653A2550283387FE005D4D48 /* TemplateApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateApplication.swift; sourceTree = "<group>"; };
653A255428338800005D4D48 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -251,6 +265,19 @@
path = SharedContext;
sourceTree = "<group>";
};
63E851762CE0FF85005554E7 /* Logging */ = {
isa = PBXGroup;
children = (
63A315552CE14C0900310EF5 /* LogManagerError.swift */,
63E8517F2CE10014005554E7 /* LogsListView.swift */,
63E8517D2CE10005005554E7 /* LogViewer.swift */,
63E8517B2CE0FFF6005554E7 /* LogManager.swift */,
63E851792CE0FFDC005554E7 /* LogLevel.swift */,
63E851772CE0FFC5005554E7 /* OSLogEntryLog+FormattedLogOutput.swift */,
);
path = Logging;
sourceTree = "<group>";
};
653A2544283387FE005D4D48 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -287,6 +314,7 @@
2FE5DC2829EDD398004B9AB4 /* Onboarding */,
2FE5DC2D29EDD792004B9AB4 /* Resources */,
2FE5DC3B29EDD7D0004B9AB4 /* Schedule */,
63E851762CE0FF85005554E7 /* Logging */,
2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */,
2FC9759D2978E30800BA99FE /* Supporting Files */,
);
Expand All @@ -304,6 +332,7 @@
653A256A28338800005D4D48 /* TemplateApplicationUITests */ = {
isa = PBXGroup;
children = (
63A315522CE14A8E00310EF5 /* LogViewerTests.swift */,
2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */,
653A256B28338800005D4D48 /* SchedulerTests.swift */,
2F4E23862989DB360013F3D9 /* ContactsTests.swift */,
Expand Down Expand Up @@ -528,15 +557,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
63E8517C2CE0FFF9005554E7 /* LogManager.swift in Sources */,
2FE5DC4129EDD7EE004B9AB4 /* StorageKeys.swift in Sources */,
2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */,
2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */,
2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */,
2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */,
2FC975A82978F11A00BA99FE /* HomeView.swift in Sources */,
63E851782CE0FFCB005554E7 /* OSLogEntryLog+FormattedLogOutput.swift in Sources */,
2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */,
63A315562CE14C1000310EF5 /* LogManagerError.swift in Sources */,
63E851802CE10016005554E7 /* LogsListView.swift in Sources */,
A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */,
A9A3DCC82C75CBBD00FC9B69 /* FirebaseConfiguration.swift in Sources */,
63E8517A2CE0FFDE005554E7 /* LogLevel.swift in Sources */,
2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */,
2F1AC9DF2B4E840E00C24973 /* TemplateApplication.docc in Sources */,
2FF53D8D2A8729D600042B76 /* TemplateApplicationStandard.swift in Sources */,
Expand All @@ -551,6 +585,7 @@
653A2551283387FE005D4D48 /* TemplateApplication.swift in Sources */,
2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */,
2F65B44E2A3B8B0600A36932 /* NotificationPermissions.swift in Sources */,
63E8517E2CE10007005554E7 /* LogViewer.swift in Sources */,
2FE5DC2629EDD38A004B9AB4 /* Contacts.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -568,6 +603,7 @@
buildActionMask = 2147483647;
files = (
5680DD3E2AB8CD84004E6D4A /* ContributionsTest.swift in Sources */,
63A315532CE14A9300310EF5 /* LogViewerTests.swift in Sources */,
2F4E23872989DB360013F3D9 /* ContactsTests.swift in Sources */,
2F4E237E2989A2FE0013F3D9 /* OnboardingTests.swift in Sources */,
653A256C28338800005D4D48 /* SchedulerTests.swift in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions TemplateApplication/Account/AccountSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ struct AccountSheet: View {
} label: {
Text("License Information")
}

NavigationLink {
LogViewer()
} label: {
Text("View Logs")
}
}
} else {
AccountSetup { _ in
Expand Down
76 changes: 76 additions & 0 deletions TemplateApplication/Logging/LogLevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// This source file is part of the Stanford Spezi Template Application open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

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 {

Check warning on line 61 in TemplateApplication/Logging/LogLevel.swift

View workflow job for this annotation

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

switch must be exhaustive; this is an error in the Swift 6 language mode

Check warning on line 61 in TemplateApplication/Logging/LogLevel.swift

View workflow job for this annotation

GitHub Actions / Build and Test / Test using xcodebuild or run fastlane

switch must be exhaustive; this is an error in the Swift 6 language mode
case .info:
self = .info
case .debug:
self = .debug
case .error:
self = .error
case .fault:
self = .fault
case .notice:
self = .notice
@unknown default:
self = .undefined
}
}
}
75 changes: 75 additions & 0 deletions TemplateApplication/Logging/LogManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// This source file is part of the Stanford Spezi Template Application open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

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 up the `OSLogStore` with
/// a scope limited to the current process identifier.
///
/// - Throws: An error if the `OSLogStore` cannot be initialized.
init() throws {
do {
self.store = try OSLogStore(scope: .currentProcessIdentifier)
} catch {
throw error
}
}

/// 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 TemplateApplication/Logging/LogManagerError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// This source file is part of the Stanford Spezi Template Application open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

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
Loading