diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 00000000..c386bf54 --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Wesley de Groot, email+OSS@WesleydeGroot.nl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..ab86050e --- /dev/null +++ b/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "OSLogViewer", + platforms: [ + .macOS(.v12), + .iOS(.v16), + .watchOS(.v9), + .tvOS(.v16) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "OSLogViewer", + targets: ["OSLogViewer"]), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "OSLogViewer"), + .testTarget( + name: "OSLogViewerTests", + dependencies: ["OSLogViewer"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 00000000..bd8ab5a1 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# OSLogViewer + +This SwiftUI view/framework is meant for viewing your apps OS_Log history. + +## Requirements + +- Swift 5.9+ (Xcode 15+) +- iOS 13+, macOS 10.15+ + +## Installation + +Install using Swift Package Manager + +```swift +dependencies: [ + .package(url: "https://github.com/0xWDG/OSLogViewer.git", .branch("main")), +], +targets: [ + .target(name: "MyTarget", dependencies: [ + .product(name: "OSLogViewer", package: "OSLogViewer"), + ]), +] +``` + +And import it: + +```swift +import OSLogViewer +``` + +## Usage + +### Quick usage + +```swift +import OSLogViewer + +// Default configuration +// uses your app's bundle identifier as subsystem +// and shows all logs from the last hour. + +OSLogViewer() +``` + +### Custom usage + +custom subsystem + +```swift +import OSLogViewer + +OSLogViewer( + subsystem: "nl.wesleydegroot.exampleapp", +) +``` + +custom time + +```swift +import OSLogViewer + +OSLogViewer( + since: Date().addingTimeInterval(-7200) // 2 hours +) +``` + +custom subsystem and time + +```swift +import OSLogViewer + +OSLogViewer( + subsystem: "nl.wesleydegroot.exampleapp", + since: Date().addingTimeInterval(-7200) // 2 hours +) +``` + +## Contact + +We can get in touch via [Twitter/X](https://twitter.com/0xWDG), [Discord](https://discordapp.com/users/918438083861573692), [Mastodon](https://iosdev.space/@0xWDG), [Threads](https://threads.net/@0xwdg), [Bluesky](https://bsky.app/profile/0xwdg.bsky.social). + +Alternatively you can visit my [Website](https://wesleydegroot.nl). diff --git a/Sources/OSLogViewer/OSLogViewer.swift b/Sources/OSLogViewer/OSLogViewer.swift new file mode 100644 index 00000000..1d5b84d3 --- /dev/null +++ b/Sources/OSLogViewer/OSLogViewer.swift @@ -0,0 +1,160 @@ +// +// OSLogViewer.swift +// OSLogViewer +// +// Created by Wesley de Groot on 01/06/2024. +// https://wesleydegroot.nl +// +// https://github.com/0xWDG/OSLogViewer +// MIT LICENCE + +import SwiftUI +import OSLog + +/// OSLogViewer +public struct OSLogViewer: View { + /// Subsystem to read + public var subsystem: String + + /// From which date preriod + public var since: Date + + /// OSLogViewer + /// - Parameters: + /// - subsystem: which subsystem should be read + /// - since: from which time (standard 1hr) + public init( + subsystem: String = Bundle.main.bundleIdentifier ?? "", + since: Date = Date().addingTimeInterval(-3600) + ) { + self.subsystem = subsystem + self.since = since + } + + @State + private var logMessages: [OSLogEntryLog] = [] + + @State + private var finishedCollecting: Bool = false + + public var body: some View { + VStack { + List { + ForEach(logMessages, id: \.self) { entry in + VStack { + Text(entry.composedMessage) + detailsBuilder(for: entry) + .font(.footnote) + } + .listRowBackground(getBackgroundColor(level: entry.level)) + } + } + .navigationTitle("OSLog viewer") + } + .overlay { + if logMessages.isEmpty { + if !finishedCollecting { + if #available(iOS 17.0, *) { + ContentUnavailableView("Collecting logs...", systemImage: "hourglass") + } else { + VStack { + Image(systemName: "hourglass") + Text("Collecting logs...") + } + } + } else { + if #available(iOS 17.0, *) { + ContentUnavailableView( + "No results found", + systemImage: "magnifyingglass", + description: Text("for subsystem \"\(subsystem)\".") + ) + } else { + VStack { + Image(systemName: "magnifyingglass") + Text("No results found for subsystem \"\(subsystem)\".") + } + } + } + } + } + .refreshable { + await getLog() + } + .onAppear { + Task { + await getLog() + } + } + } + + @ViewBuilder + func detailsBuilder(for entry: OSLogEntryLog) -> Text { + // No accebility labels are used, + // If added it will _always_ file to check in compile time. + getLogLevelIcon(level: entry.level) + + Text(" ") + + Text(entry.date, style: .time) + + Text(" ") + + Text("\(Image(systemName: "building.columns")) \(entry.sender) ") + + Text("\(Image(systemName: "gearshape.2")) \(entry.subsystem) ") + + Text("\(Image(systemName: "square.stack.3d.up")) \(entry.category)") + } + + func getLogLevelIcon(level: OSLogEntryLog.Level) -> Text { + switch level { + case .undefined, .notice: + Text(Image(systemName: "bell.square.fill")) + .accessibilityLabel("Notice") + case .debug: + Text(Image(systemName: "stethoscope")) + .accessibilityLabel("Debug") + case .info: + Text(Image(systemName: "info.square")) + .accessibilityLabel("Information") + case .error: + Text(Image(systemName: "exclamationmark.2")) + .accessibilityLabel("Error") + case .fault: + Text(Image(systemName: "exclamationmark.3")) + .accessibilityLabel("Fault") + default: + Text(Image(systemName: "bell.square.fill")) + .accessibilityLabel("Default") + } + } + + func getBackgroundColor(level: OSLogEntryLog.Level) -> Color { + switch level { + case .undefined, .debug, .info, .notice: + Color.white + case .error: + Color.yellow + case .fault: + Color.red + default: + Color.clear + } + } + + public func getLog() async { + finishedCollecting = false + + do { + let logStore = try OSLogStore(scope: .currentProcessIdentifier) + let sinceDate = logStore.position(date: since) + let predicate = NSPredicate(format: "subsystem BEGINSWITH %@", subsystem) + let allEntries = try logStore.getEntries(at: sinceDate, matching: predicate) + + logMessages = allEntries.compactMap { $0 as? OSLogEntryLog } + } catch { + os_log(.fault, "Something went wrong %@", error as NSError) + } + + finishedCollecting = true + } +} + +#Preview { + OSLogViewer() +} diff --git a/Tests/OSLogViewerTests/OSLogViewerTests.swift b/Tests/OSLogViewerTests/OSLogViewerTests.swift new file mode 100644 index 00000000..835a30d2 --- /dev/null +++ b/Tests/OSLogViewerTests/OSLogViewerTests.swift @@ -0,0 +1,12 @@ +import XCTest +@testable import OSLogViewer + +final class OSLogViewerTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +}