Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

OSLogStore on macOS: Query Historical Logs With Explicit Scope

Use OSLogStore in Swift to query bounded historical macOS logs, respect local-store entitlements, preserve redaction, and export only necessary evidence.

The log command is the right tool for many incident-response sessions, but an application sometimes needs a small, controlled view of its own past diagnostics: show recent transfer failures in a support screen, attach a bounded excerpt to a user-approved report, or validate that an expected event followed a workflow. Apple’s OSLogStore API exposes historical unified-log entries as a sequence that can be positioned, filtered, and inspected from Swift or Objective-C.

Historical access is not a promise of permanent retention, unrestricted system-wide visibility, or raw access to every interpolated value. A useful implementation makes the store scope explicit, limits the time window and result count, distinguishes log records from other entry types, and treats every rendered message as potentially sensitive. Use OSLogStore as a diagnostic read path, not as an application database or an audit ledger.

Choose the store before writing a query

OSLogStore can represent an archive opened from a URL or a store available on the Mac. Its scoped initializer also supports currentProcessIdentifier and system scopes. Prefer the narrowest scope that answers the support question. For an app’s own recent output, currentProcessIdentifier avoids starting with a machine-wide query. If an engineer has already collected a .logarchive, open that artifact and analyze it separately from the live store; an archive is a diagnostic input, not proof that the local store still contains the same interval.

System-wide local access has a stronger boundary: Apple’s OSLogStore.local() documentation says the caller must run as an administrator and possess the com.apple.logging.local-store entitlement. Do not assume an ordinary sandboxed application can obtain that entitlement or silently read all users’ and system processes’ logs. If a product does not have the required authorization, report the access error and provide a user-directed collection path instead of attempting to read private log files from disk.

The store represents a fixed range of entries. Its available range is governed by what the logging system retained and by the selected backing store. It is not an append-only archive with an application-controlled retention guarantee. Capture an incident promptly when the time window matters, and make collection explicit if the record needs to survive rotation or a reboot.

Position by time, then filter by stable identity

position(date:) creates a position corresponding to a date. Apple’s documentation notes that if multiple entries have the same time, the method returns the earliest occurrence. Use a precise start time captured with the incident rather than scanning from the beginning of the store. A position is a cursor into a particular store’s entry sequence; do not persist it as a durable cross-boot checkpoint or assume it is a globally meaningful identifier.

For application-owned diagnostics, give loggers stable subsystem and category strings. Those fields identify the component and function area more reliably than a process name or a phrase inside the message. In code, filter on the structured properties of entries rather than searching localized, redacted message text. This also reduces the temptation to place account names, URLs, file paths, or tokens into a predicate or an exported report.

The following example reads a bounded number of ordinary log entries from the current process, beginning at a supplied date. It intentionally does not enumerate the system store or print all messages to a user-visible screen:

import Foundation
import OSLog

func recentTransferDiagnostics(since start: Date, limit: Int) throws -> [String] {
    guard limit > 0 else { return [] }

    let store = try OSLogStore(scope: .currentProcessIdentifier)
    let position = store.position(date: start)
    let entries = try store.getEntries(with: [], at: position, matching: nil)
    var lines: [String] = []
    lines.reserveCapacity(min(limit, 500))

    for entry in entries {
        guard let logEntry = entry as? OSLogEntryLog,
              let payload = entry as? OSLogEntryWithPayload,
              payload.subsystem == "com.example.product",
              payload.category == "transfer" else {
            continue
        }

        lines.append("\(logEntry.date): \(logEntry.composedMessage)")
        if lines.count == limit {
            break
        }
    }
    return lines
}

The sequence is consumed incrementally, and the loop stops at the requested number of matching rows. A production caller should also cap the requested limit and time interval at its own policy boundary, because callers can pass an accidental multi-year date or an unreasonable count. A sequence limit controls returned rows; it does not mean the store has a fixed retention interval or that all matching records were collected.

OSLogEntry has variants such as log, signpost, activity, and boundary entries. The cast above deliberately selects OSLogEntryLog; a performance tool that needs signposts should handle OSLogEntrySignpost separately and interpret its begin/end or event semantics rather than flattening it into a plain text line. Check the entry type before accessing the matching union of information. OSLogEntryWithPayload exposes structured subsystem and category values, while each entry’s timestamp belongs to that entry and should be retained when exporting a report.

Keep privacy and retention assumptions visible

Unified logging applies privacy rules when messages are recorded and presented. Swift interpolation can mark a value private, and Apple’s logging guidance recommends keeping sensitive values private. But an application cannot repair a logging design that deliberately marked personal information public. Treat composedMessage as sensitive output, review which categories are included, and avoid automatic upload. Redact identifiers again at the export boundary if the support task does not require them.

The logging system also distinguishes severity and persistence. Debug messages are not persisted to disk by default, and info messages are normally persisted only when collected with the log tool; more severe levels are retained subject to storage limits and rotation. The precise records visible to a query therefore depend on prior collection, current storage, logging configuration, and scope. A missing row is not evidence that the code path never ran. If a failure must be observable, log a stable error event at a suitable level and separately record the operation’s durable result in application-owned storage.

Keep exported evidence minimal: a short date window, expected subsystem/category, a maximum number of entries, app version, OS build, and the user’s explicit support action. Do not include full system logs in a crash report by default. Store temporary exports with restrictive access, transfer them through the product’s approved secure channel, and delete them according to a retention policy. The logging store is optimized for diagnostics, not for legal or compliance retention.

Validate the reader without relying on live system incidents

Create a test fixture that emits a known message under the application’s own subsystem and category, then query the current-process scope from a controlled test run. Use a unique, non-sensitive marker and verify that the query returns the expected entry only when it falls within the selected date range. Test zero and maximum limits, an unavailable store, permission failures for any broader scope, unknown entry subclasses, empty results, and export redaction. Avoid assertions against exact localized formatting or undocumented message text.

When a team needs a system-wide archive, test the workflow on an authorized administrative Mac with the required entitlement and capture artifact. Test the reader against a copied .logarchive fixture as well as the live store; these are different backing stores and failure modes. A simulator or development machine can validate type handling and UI flows but cannot prove production log persistence, retention, or entitlement approval on every supported macOS release.

The dependable contract is narrow: choose a documented store scope, start from a relevant position, filter structured entries, cap the result, and disclose that the logging system may not retain every event. For long-lived support evidence, capture and govern a separate archive. For application state, use a database or transaction log. Keeping these responsibilities separate makes OSLogStore useful without turning incidental diagnostics into a false source of truth.

Related:

Sources:

Comments