diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj index ae72cb94..3fc9f5af 100644 --- a/openHAB.xcodeproj/project.pbxproj +++ b/openHAB.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ DA0776F0234788010086C685 /* UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0776EF234788010086C685 /* UserData.swift */; }; DA0F37D023D4ACC7007EAB48 /* SliderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */; }; DA15BFBD23C6726400BD8ADA /* ObservableOpenHABDataObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15BFBC23C6726400BD8ADA /* ObservableOpenHABDataObject.swift */; }; + DA162BEC2CD3B53E0040DAE5 /* LogsViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */; }; DA19E25B22FD801D002F8F2F /* OpenHABGeneralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */; }; DA21EAE22339621C001AB415 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA21EAE12339621C001AB415 /* Throttler.swift */; }; DA242C622C83588600AFB10D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA242C612C83588600AFB10D /* SettingsView.swift */; }; @@ -346,6 +347,7 @@ DA0776EF234788010086C685 /* UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserData.swift; sourceTree = ""; }; DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderRow.swift; sourceTree = ""; }; DA15BFBC23C6726400BD8ADA /* ObservableOpenHABDataObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableOpenHABDataObject.swift; sourceTree = ""; }; + DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsViewer.swift; sourceTree = ""; }; DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABGeneralTests.swift; sourceTree = ""; }; DA1C2E4B230DC28F00FACFB0 /* Appfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Appfile; sourceTree = ""; }; DA1C2E4C230DC28F00FACFB0 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; @@ -775,6 +777,7 @@ isa = PBXGroup; children = ( DA0775262346705F0086C685 /* ContentView.swift */, + DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */, DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */, DAF457A323DB7A820018B495 /* Rows */, DAF457A723DBA2C40018B495 /* Utils */, @@ -1460,6 +1463,7 @@ DA32D1B42C8C98C40018D974 /* IconWithAction.swift in Sources */, DA07764A234683BC0086C685 /* SwitchRow.swift in Sources */, DA2E0AA423DC96E9009B0A99 /* ImageWithAction.swift in Sources */, + DA162BEC2CD3B53E0040DAE5 /* LogsViewer.swift in Sources */, DAF4578723D798A50018B495 /* TextLabelView.swift in Sources */, DA0749DE23E0B5950057FA83 /* ColorPickerRow.swift in Sources */, DA7224D223828D3400712D20 /* PreviewConstants.swift in Sources */, diff --git a/openHABWatch/OpenHABWatch.swift b/openHABWatch/OpenHABWatch.swift index 3a207800..0aaf26ed 100644 --- a/openHABWatch/OpenHABWatch.swift +++ b/openHABWatch/OpenHABWatch.swift @@ -32,6 +32,10 @@ struct OpenHABWatch: App { .tabItem { Label("Preferences", systemSymbol: .circleFill) } + LogsViewer() + .tabItem { + Label("Debug", systemSymbol: .circleFill) + } } .tabViewStyle(.page) .environmentObject(settings) diff --git a/openHABWatch/Views/LogsViewer.swift b/openHABWatch/Views/LogsViewer.swift new file mode 100644 index 00000000..d4ac8cbc --- /dev/null +++ b/openHABWatch/Views/LogsViewer.swift @@ -0,0 +1,111 @@ +// +// LogView.swift +// openHABWatch +// +// Created by Tim Müller-Seydlitz on 31.10.24. +// Copyright © 2024 openHAB e.V. All rights reserved. +// + +import Foundation +import OSLog +import SwiftUI + +// Thanks to https://useyourloaf.com/blog/fetching-oslog-messages-in-swift/ + +extension OSLogEntryLog.Level { + fileprivate var description: String { + switch self { + case .undefined: "undefined" + case .debug: "debug" + case .info: "info" + case .notice: "notice" + case .error: "error" + case .fault: "fault" + @unknown default: "default" + } + } +} + +extension Logger { + static public func fetch(since date: Date, + predicateFormat: String) async throws -> [String] { + let store = try OSLogStore(scope: .currentProcessIdentifier) + let position = store.position(date: date) + let predicate = NSPredicate(format: predicateFormat) + let entries = try store.getEntries( + at: position, + matching: predicate + ) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + + var logs: [String] = [] + for entry in entries { + try Task.checkCancellation() + if let log = entry as? OSLogEntryLog { + var attributedMessage = AttributedString(dateFormatter.string(from: entry.date)) + attributedMessage.font = .headline + + logs.append(""" + \(dateFormatter.string(from: entry.date)): \ + \(log.category):\(log.level.description): \ + \(entry.composedMessage)\n + """) + } else { + logs.append("\(entry.date): \(entry.composedMessage)\n") + } + } + + if logs.isEmpty { logs = ["Nothing found"] } + return logs + } +} + +struct LogsViewer: View { + @State private var text = "Loading..." + + static private let template = NSPredicate(format: + "(subsystem BEGINSWITH $PREFIX)") + + let myFont = Font + .system(size: 10) + .monospaced() + + private func fetchLogs() async -> String { + let calendar = Calendar.current + guard let dayAgo = calendar.date(byAdding: .day, + value: -1, to: Date.now) else { + return "Invalid calendar" + } + + do { + let predicate = Self.template.withSubstitutionVariables( + [ + "PREFIX": "org.openhab" + ]) + + let logs = try await Logger.fetch(since: dayAgo, + predicateFormat: predicate.predicateFormat) + return logs.joined() + } catch { + return error.localizedDescription + } + } + + var body: some View { + + ScrollView { + Text(text) + .font(myFont) + .padding() + } + .task { + text = await fetchLogs() + } + } +} + +#Preview { + LogsViewer() +}