Created
October 15, 2025 19:51
-
-
Save mazz/81123679e0652dbd7dd51478e8ef3a74 to your computer and use it in GitHub Desktop.
OSLogWrapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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