-
-
Notifications
You must be signed in to change notification settings - Fork 30
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
Changes from all commits
af4fac2
4c45eb5
2b51581
9f92b23
7e70b18
b5570ca
d2c52b1
0adfca6
dee5610
bf23aae
167120f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest to move this extension to |
||
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
|
||
case .info: | ||
self = .info | ||
case .debug: | ||
self = .debug | ||
case .error: | ||
self = .error | ||
case .fault: | ||
self = .fault | ||
case .notice: | ||
self = .notice | ||
@unknown default: | ||
self = .undefined | ||
} | ||
} | ||
} |
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 | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||
.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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
I could imagine that the predicate String creation of the |
||||||||
|
||||||||
/// Filter by end date if specified | ||||||||
if let endDate, logEntry.date > endDate { | ||||||||
return false | ||||||||
} | ||||||||
|
||||||||
return true | ||||||||
} | ||||||||
} | ||||||||
} |
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." | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.Level
s. Alternatively it could be morphed into aLogLevelSelection
(which describes this a bit better); and use an enum with an associated type with anOSLogEntryLog.Level
.