Skip to content

Instantly share code, notes, and snippets.

@mazz
Created October 15, 2025 19:51
Show Gist options
  • Select an option

  • Save mazz/81123679e0652dbd7dd51478e8ef3a74 to your computer and use it in GitHub Desktop.

Select an option

Save mazz/81123679e0652dbd7dd51478e8ef3a74 to your computer and use it in GitHub Desktop.
OSLogWrapper
import os.log
import Foundation
/// Logs a message with a string format using C-style formatters, optimized for performance, converting objects to their string descriptions.
///
/// This function supports logging any type, including non-`CVarArg` types (e.g., `Keychain`), by converting them to strings using `String(describing:)`. Use `String` formats with C-style formatters (e.g., `%@`, `%{private}@`) for compatibility with `os_log`. Logs are prefixed with the log level (e.g., "🟨 DEBUG"), filename (basename, truncated in the middle if longer than 20 characters), and line number, padded to a fixed width (35 characters) for vertical alignment of message content. Debug logs (`.debug`) are not included in production builds unless explicitly enabled.
///
/// **Available Logging Levels (`OSLogType`)**:
/// - `.default`: General-purpose logging for normal operational messages.
/// - `.info`: Informational messages for tracking app flow or state.
/// - `.debug`: Detailed messages for debugging, not included in release builds unless enabled.
/// - `.error`: Errors or unexpected conditions that may require attention.
/// - `.fault`: Critical issues that indicate a system or app failure.
///
/// **Logging Private Data**:
/// Include `%{private}@` in the format string for each sensitive argument (e.g., purchase status). Private data is redacted in `Console.app` (shown as `<private>`) unless unmasked. Ensure the format string matches the number of arguments provided.
///
/// Usage:
/// ```swift
///
/// swift-like formatter:
///
/// log(.debug, "gFilestoreUrl: \(gFilestoreUrl)")
///
/// C-like formatter:
///
/// log(.debug, "gFilestoreUrl: %@", gFilestoreUrl)
///
/// let keychain = Keychain(service: "ca.sidha.asdf", accessGroup: "asdf.ca.sidha.asdf")
/// let hasPurchased = keychain["hasPurchased"] ?? "not set"
/// log(.debug, "keychain: %@, status: %@", keychain, hasPurchased) // Logs: 🟨 DEBUG MyViewController.swift:123 keychain: <KeychainAccess.Keychain: 0x...>, status: true
/// log(.error, "Failed to access keychain: %@", error) // Logs: 🟧 ERROR MyViewController.swift:123 Failed to access keychain: <NSError: 0x...>
/// log(.debug, "Sensitive: keychain=%{private}@, purchased=%{private}@", keychain, hasPurchased) // Logs: 🟨 DEBUG MyViewController.swift:123 Sensitive: keychain=<private>, purchased=<private>
/// ```
func log(_ type: OSLogType, _ format: @autoclosure () -> String, _ args: Any..., file: String = #file, line: Int = #line) {
// Map log type to string representation with emoji prefix
let levelPrefix: String
switch type {
case .fault: levelPrefix = "πŸŸ₯ FAULT"
case .error: levelPrefix = "🟧 ERROR"
case .debug: levelPrefix = "🟨 DEBUG"
case .info: levelPrefix = "🟦 INFO"
case .default: levelPrefix = "⬜️ DEFAULT"
default: levelPrefix = "⬛️ UNKNOWN"
}
// Extract filename basename
let fileName = (file as NSString).lastPathComponent
// Truncate filename if longer than 20 characters
let maxFileNameLength = 20
let truncatedFileName: String
if fileName.count > maxFileNameLength {
let halfLength = (maxFileNameLength - 3) / 2
let start = fileName.prefix(halfLength)
let end = fileName.suffix(halfLength)
truncatedFileName = "\(start)...\(end)"
} else {
truncatedFileName = fileName
}
// Create prefix string
let prefix = "\(levelPrefix) \(truncatedFileName):\(line)"
// Pad prefix to fixed width (35 characters)
let paddedPrefix = prefix.padding(toLength: 35, withPad: " ", startingAt: 0)
// Evaluate the format string
let formatString = format()
// Map arguments to strings, handling URLs safely
let stringArgs: [String] = args.map { arg in
if let url = arg as? URL {
return url.path // Use path directly for URLs
} else {
return String(describing: arg)
}
}
// Check for %{private}@ or %{public}@ in format string
let isPrivate = formatString.contains("%{private}@")
let osLogFormat: StaticString = isPrivate ? "%{private}@" : "%{public}@"
// Create final message
let finalMessage: String
if stringArgs.isEmpty {
finalMessage = "\(paddedPrefix) \(formatString)"
} else {
// Replace %s with %@ in format string for os_log compatibility
let modifiedFormat = formatString.replacingOccurrences(of: "%s", with: "%@")
let message = String(format: modifiedFormat, arguments: stringArgs)
finalMessage = "\(paddedPrefix) \(message)"
}
// Log using os_log
os_log(type, log: .default, osLogFormat, finalMessage)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment