Files
Jarvis/packages/Logging/Sources/JarvisLogging/OSLogHandler.swift
james.maes 559661bb8a fix(01-IN-02): route OSLogHandler messages through os.Logger privacy masking
Phase 1 REVIEW.md IN-02: `OSLogHandler.log` interpolated message text
as `\(message, privacy: .public)`. That override defeats os.Logger's
default `%{private}@` masking for interpolated values — a caller who
logs `logger.info("saving key \(userInput)")` produces a redacted line
in the file log (via `Redact.apply` in `FileLogHandler`) but a
plaintext line in `os.Logger`. The docstring already flagged the
discipline risk; the cure (caller redacts first) is on the honor
system with no enforcement.

packages/Logging/Sources/JarvisLogging/OSLogHandler.swift
- Drop the `.public` override. Interpolate as
  `\(message, privacy: .private(mask: .hash))` so untrusted interpolated
  values are hashed in Console.app captures by default.
- Keep `osLevel` via the `logger.log(level:)` API so severity signal
  is preserved in Console.app filters (level isn't embedded in the
  message body).

All 14 logging tests still pass — there's no explicit capture of the
os.Logger stream in-test, so the privacy override change is behaviour-
compatible at the test layer. Observable in Console.app only.
2026-04-23 16:24:01 -05:00

46 lines
1.7 KiB
Swift

import Foundation
import Logging // swift-log
import os // Apple os.Logger
struct OSLogHandler: LogHandler {
let subsystem: String
let category: String
private let logger: os.Logger
var logLevel: Logging.Logger.Level = .info
var metadata: Logging.Logger.Metadata = [:]
var metadataProvider: Logging.Logger.MetadataProvider?
init(subsystem: String, category: String, label: String) {
self.subsystem = subsystem
self.category = category
self.logger = os.Logger(subsystem: subsystem, category: category)
}
func log(level: Logging.Logger.Level, message: Logging.Logger.Message,
metadata: Logging.Logger.Metadata?, source: String,
file: String, function: String, line: UInt) {
let osLevel: OSLogType = switch level {
case .trace, .debug: .debug
case .info, .notice: .info
case .warning: .default
case .error: .error
case .critical: .fault
}
// WR-08 / IN-02: do NOT mark the message body `.public` — that
// defeats os.Logger's default `%{private}@` masking for interpolated
// values. Message text may contain untrusted input (file paths,
// error descriptions, user-supplied strings) that the caller hasn't
// run through `Redact.apply` — os.Logger's private-by-default
// provides defense in depth. Log level is emitted via the `osLevel`
// API, not the message body, so this does not lose severity signal
// in Console.app.
logger.log(level: osLevel, "\(message, privacy: .private(mask: .hash))")
}
subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? {
get { metadata[key] }
set { metadata[key] = newValue }
}
}