Swift package for collecting and exporting Apple unified logging (OSLog) entries with structured metadata and redaction.
Add OSLogExtractor as a package dependency via Xcode or in your Package.swift:
.package(path: "../OSLogExtractor")Then add OSLogExtractor to your target dependencies.
import OSLogExtractor
@available(iOS 15.0, macOS 12.0, *)
func exportRecentLogs() async throws {
let extractor = try LogExtractor()
let filter = LogFilter(
startDate: Date().addingTimeInterval(-3600),
levels: [.info],
subsystem: "app.speakthis"
)
let logsDirectory = FileManager.default.temporaryDirectory
try await extractor.export(matching: filter, to: logsDirectory, format: .json)
// Produces logs-YYYYMMdd-HHmmss.zip containing logs-YYYYMMdd-HHmmss.json with entries and metadata.
}To keep the raw files without compression, pass zip: false:
try await extractor.export(matching: filter, to: logsDirectory, format: .json, zip: false)LogFilter accepts a levels array so you can include multiple log levels in a single run. Provide the levels you care about; leave the array empty to include every level.
// Capture only error and fault entries.
let strictFilter = LogFilter(levels: [.error, .fault])
// Capture all entries regardless of level.
let everythingFilter = LogFilter()import AppKit
import OSLogExtractor
import UniformTypeIdentifiers
@available(macOS 12.0, *)
func exportWithSavePanel() {
let panel = NSOpenPanel()
panel.canCreateDirectories = true
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.prompt = "Choose"
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
panel.begin { response in
guard response == .OK, let directoryURL = panel.url else { return }
Task {
do {
let extractor = try LogExtractor()
let filter = LogFilter(
startDate: Date().addingTimeInterval(-3600),
levels: [.info],
subsystem: "<subsystem>"
)
try await extractor.export(matching: filter, to: directoryURL, format: .json)
} catch {
// handle error
}
}
}
}import OSLogExtractor
import SwiftUI
import UniformTypeIdentifiers
@available(iOS 15.0, *)
final class LogExportController: NSObject, UIDocumentPickerDelegate {
func present(from presenter: UIViewController) {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)
picker.delegate = self
picker.allowsMultipleSelection = false
presenter.present(picker, animated: true)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let directoryURL = urls.first else { return }
Task {
do {
let extractor = try LogExtractor()
let filter = LogFilter(
startDate: Date().addingTimeInterval(-3600),
levels: [.info],
subsystem: "<subsystem>"
)
try await extractor.export(matching: filter, to: directoryURL, format: .text, zip: false)
// Produces logs-YYYYMMdd-HHmmss.log and logs-YYYYMMdd-HHmmss.metadata.json.
} catch {
// handle error
}
}
}
}Each export writes a companion metadata.json file containing:
- app — bundle identifier, display name, marketing version, and build number discovered from the provided bundle.
- device — platform (
iOS/macOS), hardware model identifier (when available), OS version string, locale, region, timezone, battery info (iOS), and low power mode flag (iOS). - process — current process name, PID/parent PID, CPU architecture, system boot time, app launch time, and uptime in seconds.
- resources — human-readable byte strings gathered at export time:
- memory:
physical,footprint, andfreesizes for total RAM, process footprint, and free memory. - disk:
total,free, andappContainerFree(space remaining inside the app's sandbox container).
- memory:
- networking — active reachability (
wifi,cellular, ornone) and whether the path is marked as expensive. - loggingScope — the effective log filter (start/end dates, level, subsystem, category, process, substring match), whether extraction was
restrictToCurrentProcess, and the covered interval in seconds. - statistics — counts by log level, top subsystems/categories, and timestamps for the first and last included entries.
Metadata collection is best effort and omits fields when unavailable; all sizes are formatted using ByteCountFormatter.
Basic redaction removes or masks:
- Bearer tokens (
Bearer ***) - API keys (
ApiKey ***) - Email addresses (
***@***) - Phone numbers (
***-***-****) - URL query parameters
To disable redaction, initialize LogExtractor with redactionMode: .none:
let extractor = try LogExtractor(redactionMode: .none)Run unit tests:
swift test --package-path SpeakThis/OSLogExtractor- Works on macOS 12+ and iOS 15+ (iOS limited to current process logs).
- Accessing system-wide logs may require running outside the App Store sandbox.