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

Closed
wants to merge 11 commits into from
Closed
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to explore using OptionSet to model this here. It would be easily used to configure the selection instead of duplicating all the OSLogEntryLog.Levels. Alternatively it could be morphed into a LogLevelSelection (which describes this a bit better); and use an enum with an associated type with an OSLogEntryLog.Level.

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to move this extension to OSLogEntryLog.Level.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

/// 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] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested the views in the application and I could not see any log messages emitted by, e.g., a default logger in the view:

Logger().log(level: .error, "Test Log Message ...")

Not sure if this is just for me but it would be good to test the basic behavior in a UI/unit test within the template application to ensure that the basic query functionality is working as expected.

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()
Comment on lines +56 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of reversing the list we can pass this as an argument to getEntries as an Option: https://developer.apple.com/documentation/oslog/oslogenumerator/options

.compactMap { $0 as? OSLogEntryLog }

return logs
.filter { logEntry in
/// Filter by log type if specified
if let logLevel, logEntry.level != logLevel {
return false
}
Comment on lines +63 to +65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might even be able to optimize this query by moving this into the predicate; given the configuration options documented in man log you should be able to create predicates that cover this:

PREDICATE-BASED FILTERING
     Using predicate-based filters via the --predicate option allows users to focus on messages based on the provided filter criteria.  For detailed information on the use of predicate based filtering,
     please refer to the Predicate Programming Guide: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html

     The filter argument defines one or more pattern clauses following NSPredicate rules.  See log help predicates for the full list of supported keys.  Supported keys include:

     eventType          The type of event: activityCreateEvent, activityTransitionEvent, logEvent, signpostEvent, stateEvent, timesyncEvent, traceEvent and userActionEvent.

     eventMessage       The pattern within the message text, or activity name of a log/trace entry.

     messageType        For logEvent and traceEvent, the type of the message itself: default, info, debug, error or fault.

     process            The name of the process the originated the event.

     processImagePath   The full path of the process that originated the event.

     sender             The name of the library, framework, kernel extension, or mach-o image, that originated the event.

     senderImagePath    The full path of the library, framework, kernel extension, or mach-o image, that originated the event.

     subsystem          The subsystem used to log an event.  Only works with log messages generated with os_log(3) APIs.

     category           The category used to log an event.  Only works with log messages generated with os_log(3) APIs.  When category is used, the subsystem filter should also be provided.

PREDICATE-BASED FILTERING EXAMPLES
     Filter for specific subsystem:
      log show --predicate 'subsystem == "com.example.my_subsystem"'

     Filter for specific subsystem and category:
      log show --predicate '(subsystem == "com.example.my_subsystem") && (category == "desired_category")'

     Filter for specific subsystem and categories:
      log show --predicate '(subsystem == "com.example.my_subsystem") && (category IN { "category1", "category2" })'

     Filter for a specific subsystem and sender(s):
      log show --predicate '(subsystem == "com.example.my_subsystem") && ((senderImagePath ENDSWITH "mybinary") || (senderImagePath ENDSWITH "myframework"))'

PREDICATE-BASED FILTERING EXAMPLES WITH LOG LINE

     log show system_logs.logarchive --predicate 'subsystem == "com.example.subsystem" and category contains "CHECK"'

     Timestamp                       Thread     Type        Activity     PID
     2016-06-13 11:46:37.248693-0700 0x7c393    Default     0x0          10371  timestamp: [com.example.subsystem.CHECKTIME] Time is 06/13/2016 11:46:37

     log show --predicate 'processImagePath endswith "hidd" and senderImagePath contains[cd] "IOKit"' --info

     Timestamp                       Thread     Type        Activity     PID
     2016-06-10 13:54:34.593220-0700 0x250      Info        0x0          113    hidd: (IOKit) [com.apple.iohid.default] Loaded 6 HID plugins

I could imagine that the predicate String creation of the messageType part could be part of the LocLevelSelection OptionSet?
Unfortunately there doesn't seem to be a way to filter based on the date of the log message; the end date filtering would be on us but that should be doable.


/// 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