From 853c8eb28a37569b07f9e0473b41944b33801016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Ruiz=20Lafuente?= Date: Fri, 21 Jun 2024 16:39:57 +0200 Subject: [PATCH 1/5] Add OSLog wrappers and deprecation warnings # Conflicts: # PrettyLogger.xcodeproj/project.pbxproj # Sources/PrettyLoggerPrivacy.swift --- Sources/Global.swift | 38 ++++++++++ Sources/PrettyLogger.swift | 119 ++++++++++++++++++++++++++++++ Sources/PrettyLoggerLevel.swift | 1 + Sources/PrettyLoggerPrivacy.swift | 21 ++++++ 4 files changed, 179 insertions(+) create mode 100644 Sources/PrettyLoggerPrivacy.swift diff --git a/Sources/Global.swift b/Sources/Global.swift index ee0669a..af0e5d2 100644 --- a/Sources/Global.swift +++ b/Sources/Global.swift @@ -1,30 +1,68 @@ import Foundation +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func fatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.fatal(message, category: category, privacy: privacy) +} + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func error(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.error(message, category: category, privacy: privacy) +} + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func warning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.warning(message, category: category, privacy: privacy) +} + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func info(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.info(message, category: category, privacy: privacy) +} + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func debug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.debug(message, category: category, privacy: privacy) +} + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public func trace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.trace(message, category: category, privacy: privacy) +} + +// MARK: - Deprecations + +@available(*, deprecated, renamed: "fatal", message: "Use `fatal` to use the unified OS logger") @discardableResult public func logFatal(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logFatal(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } +@available(*, deprecated, renamed: "error", message: "Use `error` to use the unified OS logger") @discardableResult public func logError(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logError(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } +@available(*, deprecated, renamed: "warning", message: "Use `warning` to use the unified OS logger") @discardableResult public func logWarning(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logWarning(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } +@available(*, deprecated, renamed: "info", message: "Use `info` to use the unified OS logger") @discardableResult public func logInfo(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logInfo(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } +@available(*, deprecated, renamed: "debug", message: "Use `debug` to use the unified OS logger") @discardableResult public func logDebug(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logDebug(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } +@available(*, deprecated, renamed: "trace", message: "Use `trace` to use the unified OS logger") @discardableResult public func logTrace(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { return PrettyLogger.shared.logTrace(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) diff --git a/Sources/PrettyLogger.swift b/Sources/PrettyLogger.swift index 5e3f898..c957f36 100644 --- a/Sources/PrettyLogger.swift +++ b/Sources/PrettyLogger.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import OSLog public class PrettyLogger { public static let shared = PrettyLogger() @@ -22,6 +23,24 @@ public class PrettyLogger { return log(.fatal, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func fatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.fault("\(message)") + case .public: + logger.fault("\(message, privacy: .public)") + case .private: + logger.fault("\(message, privacy: .private)") + } + } + internal func logError(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .error { return nil @@ -29,6 +48,24 @@ public class PrettyLogger { return log(.error, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func error(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.error("\(message)") + case .public: + logger.error("\(message, privacy: .public)") + case .private: + logger.error("\(message, privacy: .private)") + } + } + internal func logWarning(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .warn { return nil @@ -36,6 +73,24 @@ public class PrettyLogger { return log(.warn, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func warning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.warning("\(message)") + case .public: + logger.warning("\(message, privacy: .public)") + case .private: + logger.warning("\(message, privacy: .private)") + } + } + internal func logInfo(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .info { return nil @@ -43,6 +98,24 @@ public class PrettyLogger { return log(.info, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func info(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.info("\(message)") + case .public: + logger.info("\(message, privacy: .public)") + case .private: + logger.info("\(message, privacy: .private)") + } + } + internal func logDebug(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .debug { return nil @@ -50,6 +123,24 @@ public class PrettyLogger { return log(.debug, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func debug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.debug("\(message)") + case .public: + logger.debug("\(message, privacy: .public)") + case .private: + logger.debug("\(message, privacy: .private)") + } + } + internal func logTrace(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .trace { return nil @@ -57,6 +148,24 @@ public class PrettyLogger { return log(.trace, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) } + @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) + func trace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + let logger = if let category { + Logger.custom(category) + } else { + Logger.default + } + + switch privacy { + case .auto: + logger.trace("\(message)") + case .public: + logger.trace("\(message, privacy: .public)") + case .private: + logger.trace("\(message, privacy: .private)") + } + } + private func log(_ logLevel: PrettyLoggerLevel, items: [Any], separator: String?, terminator: String?, file: String, line: Int, column: Int, function: String, date: Date = Date()) -> String? { let separator = separator ?? self.separator let terminator = terminator ?? self.terminator @@ -94,3 +203,13 @@ public class PrettyLogger { return "\(stringDate) ◉ \(level) \(message) \(stringLocation)" } } + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +extension Logger { + private static let subsytem = Bundle.main.bundleIdentifier! + static let `default` = Logger(subsystem: subsytem, category: "default") + + static func custom(_ category: String) -> Self { + .init(subsystem: subsytem, category: category) + } +} diff --git a/Sources/PrettyLoggerLevel.swift b/Sources/PrettyLoggerLevel.swift index 00176c0..0384932 100644 --- a/Sources/PrettyLoggerLevel.swift +++ b/Sources/PrettyLoggerLevel.swift @@ -1,4 +1,5 @@ import Foundation +import OSLog public enum PrettyLoggerLevel: Int, Comparable, CaseIterable { case disable = 0 diff --git a/Sources/PrettyLoggerPrivacy.swift b/Sources/PrettyLoggerPrivacy.swift new file mode 100644 index 0000000..6195f2c --- /dev/null +++ b/Sources/PrettyLoggerPrivacy.swift @@ -0,0 +1,21 @@ +import OSLog + +@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) +public enum PrettyLoggerPrivacy { + case auto + case `public` + case `private` + + var osLogPrivacy: OSLogPrivacy { + switch self { + case .auto: + return .auto + + case .public: + return .public + + case .private: + return .private + } + } +} From d762e3f1a2d3813613aa7ed4819f5057b5be1e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Ruiz=20Lafuente?= Date: Wed, 23 Jul 2025 13:42:18 +0200 Subject: [PATCH 2/5] Improve OSLog support with backward compatibility --- MIGRATION_GUIDE.md | 248 ++++++++++++++++++ Sources/Global.swift | 100 +++++--- Sources/PrettyLogger.swift | 251 +++++++++--------- Sources/PrettyLoggerOutput.swift | 35 ++- Tests/FormatOutputTests.swift | 5 +- Tests/LevelConfigurationTests.swift | 19 +- Tests/OSLogBasicTests.swift | 119 +++++++++ Tests/OSLogLevelConfigurationTests.swift | 288 +++++++++++++++++++++ Tests/OutputTests.swift | 11 +- Tests/QuickOutputTest.swift | 163 ++++++++++++ USAGE_EXAMPLES.md | 310 +++++++++++++++++++++++ 11 files changed, 1371 insertions(+), 178 deletions(-) create mode 100644 MIGRATION_GUIDE.md create mode 100644 Tests/OSLogBasicTests.swift create mode 100644 Tests/OSLogLevelConfigurationTests.swift create mode 100644 Tests/QuickOutputTest.swift create mode 100644 USAGE_EXAMPLES.md diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..d198c35 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,248 @@ +# PrettyLogger Migration Guide + +This guide shows how easy it is to migrate from the old print-based API to the new OSLog-based API while keeping the same function names. + +## Overview + +PrettyLogger now uses Apple's unified logging system (OSLog) instead of `print` statements, providing better performance, privacy controls, and integration with system tools. The best part? **You keep using the same function names!** + +## Quick Migration + +### What Changes +- **Function signatures**: New parameters for `category` and `privacy` +- **Parameter format**: String interpolation instead of variadic parameters +- **Logging backend**: OSLog instead of print statements + +### What Stays the Same +- **Function names**: `logInfo`, `logError`, `logWarning`, etc. +- **Import statement**: `import PrettyLogger` +- **Basic usage patterns** + +## Step-by-Step Migration + +### 1. Simple Messages (No Changes Needed!) + +```swift +// Before and After - EXACTLY THE SAME! +logInfo("User logged in successfully") +logError("Network connection failed") +logWarning("Low memory warning") +logDebug("Processing user data") +``` + +### 2. Multiple Parameters → String Interpolation + +```swift +// Before (deprecated) +logInfo("User:", username, "logged in at:", timestamp) +logError("Error code:", errorCode, "message:", error.localizedDescription) + +// After (recommended) +logInfo("User: \(username) logged in at: \(timestamp)") +logError("Error code: \(errorCode) message: \(error.localizedDescription)") +``` + +### 3. Add Categories for Better Organization (Optional) + +```swift +// Before +logInfo("API request completed") +logError("Database connection failed") +logDebug("Cache hit for key: user_123") + +// After (enhanced with categories) +logInfo("API request completed", category: "Network") +logError("Database connection failed", category: "Database") +logDebug("Cache hit for key: user_123", category: "Cache") +``` + +### 4. Add Privacy Controls for Sensitive Data (Optional) + +```swift +// Before +logDebug("Auth token: abc123xyz") +logInfo("User email: user@example.com") + +// After (with privacy) +logDebug("Auth token: \(token)", category: "Auth", privacy: .private) +logInfo("User email: \(email)", category: "Auth", privacy: .private) +``` + +## Real-World Example + +### Before (Old API) +```swift +class AuthManager { + func login(email: String, password: String) -> Bool { + logInfo("Starting login process") + logDebug("Email:", email, "Password length:", password.count) + + if email.isEmpty { + logError("Login failed:", "Email is empty") + return false + } + + // Simulate API call + logTrace("Making API call to:", "/auth/login") + + // Success + logInfo("Login successful for user:", email) + return true + } +} +``` + +### After (New API) +```swift +class AuthManager { + func login(email: String, password: String) -> Bool { + logInfo("Starting login process", category: "Auth") + logDebug("Email: \(email) Password length: \(password.count)", + category: "Auth", privacy: .private) + + if email.isEmpty { + logError("Login failed: Email is empty", category: "Auth") + return false + } + + // Simulate API call + logTrace("Making API call to: /auth/login", category: "Network") + + // Success + logInfo("Login successful for user: \(email)", + category: "Auth", privacy: .private) + return true + } +} +``` + +## Migration Strategies + +### Strategy 1: Minimal Changes (Easiest) +1. Replace variadic parameters with string interpolation +2. Remove any `separator` and `terminator` parameters +3. Done! Your logs now use OSLog + +### Strategy 2: Gradual Enhancement (Recommended) +1. Start with minimal changes (Strategy 1) +2. Gradually add categories to group related logs +3. Add privacy controls for sensitive data +4. Test with Console.app and Instruments + +### Strategy 3: Complete Modernization +1. Apply minimal changes +2. Define category constants +3. Add comprehensive privacy controls +4. Update log messages for better structure +5. Add performance monitoring + +## Category Best Practices + +### Define Constants +```swift +extension String { + static let auth = "Authentication" + static let network = "Network" + static let database = "Database" + static let ui = "UserInterface" + static let cache = "Cache" +} + +// Usage +logError("Connection timeout", category: .network) +logInfo("User authenticated", category: .auth) +``` + +### Hierarchical Categories +```swift +// Use dot notation for subcategories +logDebug("Cache miss", category: "Cache.User") +logInfo("Database query", category: "Database.Read") +logError("Network timeout", category: "Network.API") +``` + +## Privacy Guidelines + +| Data Type | Recommended Privacy | Example | +|-----------|-------------------|---------| +| User IDs | `.auto` | `logInfo("User \(userID) logged in", privacy: .auto)` | +| Email addresses | `.private` | `logDebug("Email: \(email)", privacy: .private)` | +| Passwords/Tokens | `.private` | `logTrace("Token refreshed", privacy: .private)` | +| App version | `.public` | `logInfo("App version: \(version)", privacy: .public)` | +| Error messages | `.auto` | `logError("Network error: \(error)", privacy: .auto)` | + +## Testing Your Migration + +### 1. Compile and Run +```bash +# Make sure everything compiles +swift build + +# Run your tests +swift test +``` + +### 2. Check Console.app +1. Open Console.app on macOS +2. Filter by your app's bundle identifier +3. Verify logs appear with proper categories +4. Test privacy settings + +### 3. Use Command Line Tools +```bash +# Show logs from your app +log show --predicate 'subsystem == "com.yourapp.bundleid"' --last 1h + +# Filter by category +log show --predicate 'category == "Network"' --last 30m +``` + +## Common Issues and Solutions + +### Issue: Too Many Parameters +```swift +// Problem: Old habit of many parameters +logInfo("User:", user.id, "action:", action, "result:", result, "time:", time) + +// Solution: Use string interpolation +logInfo("User: \(user.id) action: \(action) result: \(result) time: \(time)") +``` + +### Issue: Missing Privacy Controls +```swift +// Problem: Sensitive data exposed +logDebug("Processing payment for card: \(cardNumber)") + +// Solution: Mark as private +logDebug("Processing payment for card: \(cardNumber)", + category: "Payment", privacy: .private) +``` + +### Issue: No Categories +```swift +// Problem: All logs mixed together +logError("Database connection failed") +logError("Network request timeout") + +// Solution: Use categories +logError("Database connection failed", category: "Database") +logError("Network request timeout", category: "Network") +``` + +## Benefits After Migration + +✅ **Better Performance**: OSLog is faster and more efficient than print +✅ **Privacy Controls**: Automatic handling of sensitive data +✅ **System Integration**: Works with Console.app, Instruments, and log command +✅ **Structured Logging**: Categories help organize and filter logs +✅ **Production Ready**: Proper log levels for release builds +✅ **Same Function Names**: No need to learn new API + +## Need Help? + +- Check `USAGE_EXAMPLES.md` for comprehensive examples +- Use Console.app to verify your logs are working +- Test privacy settings in release builds +- Consider gradual migration for large codebases + +The migration is designed to be as smooth as possible while providing modern logging capabilities! \ No newline at end of file diff --git a/Sources/Global.swift b/Sources/Global.swift index af0e5d2..e37ddca 100644 --- a/Sources/Global.swift +++ b/Sources/Global.swift @@ -1,69 +1,95 @@ import Foundation -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func fatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.fatal(message, category: category, privacy: privacy) +// MARK: - Primary OSLog-based API + +public func logFatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logFatal(message, category: category, privacy: privacy) } -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func error(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.error(message, category: category, privacy: privacy) +public func logError(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logError(message, category: category, privacy: privacy) } -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func warning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.warning(message, category: category, privacy: privacy) +public func logWarning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logWarning(message, category: category, privacy: privacy) } -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func info(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.info(message, category: category, privacy: privacy) +public func logInfo(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logInfo(message, category: category, privacy: privacy) } -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func debug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.debug(message, category: category, privacy: privacy) +public func logDebug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logDebug(message, category: category, privacy: privacy) } -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -public func trace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.trace(message, category: category, privacy: privacy) +public func logTrace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + PrettyLogger.shared.logTrace(message, category: category, privacy: privacy) } -// MARK: - Deprecations +// MARK: - Legacy print-based API (deprecated) -@available(*, deprecated, renamed: "fatal", message: "Use `fatal` to use the unified OS logger") +@available(*, deprecated, message: "Use logFatal(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logFatal(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logFatal(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logFatal( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logFatalLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } -@available(*, deprecated, renamed: "error", message: "Use `error` to use the unified OS logger") +@available(*, deprecated, message: "Use logError(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logError(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logError(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logError( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logErrorLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } -@available(*, deprecated, renamed: "warning", message: "Use `warning` to use the unified OS logger") +@available(*, deprecated, message: "Use logWarning(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logWarning(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logWarning(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logWarning( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logWarningLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } -@available(*, deprecated, renamed: "info", message: "Use `info` to use the unified OS logger") +@available(*, deprecated, message: "Use logInfo(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logInfo(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logInfo(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logInfo( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logInfoLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } -@available(*, deprecated, renamed: "debug", message: "Use `debug` to use the unified OS logger") +@available(*, deprecated, message: "Use logDebug(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logDebug(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logDebug(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logDebug( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logDebugLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } -@available(*, deprecated, renamed: "trace", message: "Use `trace` to use the unified OS logger") +@available(*, deprecated, message: "Use logTrace(_ message: String, category: String?, privacy: PrettyLoggerPrivacy) instead") @discardableResult -public func logTrace(_ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - return PrettyLogger.shared.logTrace(items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) +public func logTrace( + _ items: Any..., separator: String? = nil, terminator: String? = nil, file: String = #file, + line: Int = #line, column: Int = #column, function: String = #function +) -> String? { + return PrettyLogger.shared.logTraceLegacy( + items, separator: separator, terminator: terminator, file: file, line: line, column: column, + function: function) } diff --git a/Sources/PrettyLogger.swift b/Sources/PrettyLogger.swift index c957f36..102e5e5 100644 --- a/Sources/PrettyLogger.swift +++ b/Sources/PrettyLogger.swift @@ -1,5 +1,5 @@ -import Foundation import Combine +import Foundation import OSLog public class PrettyLogger { @@ -9,179 +9,195 @@ public class PrettyLogger { public var separator: String = " " public var terminator: String = "\n" public let output = PassthroughSubject() - + private let mFormatter = DateFormatter(dateFormat: "HH:mm:ss.SSS") internal init() { } - internal func logFatal(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + // MARK: - Primary OSLog-based API + + public func logFatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + // Check level filtering if level < .fatal { - return nil + return } - return log(.fatal, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) - } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func fatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default - } + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .fault, message: message, privacy: privacy) - switch privacy { - case .auto: - logger.fault("\(message)") - case .public: - logger.fault("\(message, privacy: .public)") - case .private: - logger.fault("\(message, privacy: .private)") - } + // Send to output stream + let output = PrettyLoggerOutput(level: .fatal, message: message) + self.output.send(output) } - internal func logError(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + public func logError(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { if level < .error { - return nil + return } - return log(.error, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) - } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func error(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default - } + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .error, message: message, privacy: privacy) - switch privacy { - case .auto: - logger.error("\(message)") - case .public: - logger.error("\(message, privacy: .public)") - case .private: - logger.error("\(message, privacy: .private)") - } + // Send to output stream + let output = PrettyLoggerOutput(level: .error, message: message) + self.output.send(output) } - internal func logWarning(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + public func logWarning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { if level < .warn { - return nil + return } - return log(.warn, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .default, message: message, privacy: privacy) + + // Send to output stream + let output = PrettyLoggerOutput(level: .warn, message: message) + self.output.send(output) } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func warning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default + public func logInfo(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + if level < .info { + return } - switch privacy { - case .auto: - logger.warning("\(message)") - case .public: - logger.warning("\(message, privacy: .public)") - case .private: - logger.warning("\(message, privacy: .private)") + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .info, message: message, privacy: privacy) + + // Send to output stream + let output = PrettyLoggerOutput(level: .info, message: message) + self.output.send(output) + } + + public func logDebug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + if level < .debug { + return } + + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .debug, message: message, privacy: privacy) + + // Send to output stream + let output = PrettyLoggerOutput(level: .debug, message: message) + self.output.send(output) } - internal func logInfo(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - if level < .info { - return nil + public func logTrace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + if level < .trace { + return } - return log(.info, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + + let logger = createLogger(for: category) + logWithPrivacy(logger: logger, level: .debug, message: message, privacy: privacy) + + // Send to output stream + let output = PrettyLoggerOutput(level: .trace, message: message) + self.output.send(output) } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func info(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default + // MARK: - Legacy print-based API (internal) + + internal func logFatalLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + if level < .fatal { + return nil } + return log( + .fatal, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) + } - switch privacy { - case .auto: - logger.info("\(message)") - case .public: - logger.info("\(message, privacy: .public)") - case .private: - logger.info("\(message, privacy: .private)") + internal func logErrorLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + if level < .error { + return nil } + return log( + .error, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - internal func logDebug(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { - if level < .debug { + internal func logWarningLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + if level < .warn { return nil } - return log(.debug, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + return log( + .warn, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func debug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default + internal func logInfoLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + if level < .info { + return nil } + return log( + .info, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) + } - switch privacy { - case .auto: - logger.debug("\(message)") - case .public: - logger.debug("\(message, privacy: .public)") - case .private: - logger.debug("\(message, privacy: .private)") + internal func logDebugLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + if level < .debug { + return nil } + return log( + .debug, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - internal func logTrace(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logTraceLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { if level < .trace { return nil } - return log(.trace, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + return log( + .trace, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - @available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) - func trace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - let logger = if let category { - Logger.custom(category) - } else { - Logger.default - } + // MARK: - Private helpers + + private func createLogger(for category: String?) -> Logger { + let subsystem = Bundle.main.bundleIdentifier ?? "com.prettylogger.default" + let categoryName = category ?? "default" + return Logger(subsystem: subsystem, category: categoryName) + } + private func logWithPrivacy( + logger: Logger, level: OSLogType, message: String, privacy: PrettyLoggerPrivacy + ) { switch privacy { case .auto: - logger.trace("\(message)") + logger.log(level: level, "\(message)") case .public: - logger.trace("\(message, privacy: .public)") + logger.log(level: level, "\(message, privacy: .public)") case .private: - logger.trace("\(message, privacy: .private)") + logger.log(level: level, "\(message, privacy: .private)") } } - private func log(_ logLevel: PrettyLoggerLevel, items: [Any], separator: String?, terminator: String?, file: String, line: Int, column: Int, function: String, date: Date = Date()) -> String? { + private func log( + _ logLevel: PrettyLoggerLevel, items: [Any], separator: String?, terminator: String?, + file: String, line: Int, column: Int, function: String, date: Date = Date() + ) -> String? { let separator = separator ?? self.separator let terminator = terminator ?? self.terminator let message = buildMessageForLogLevel(items, separator: separator) - let stringToPrint = stringForCurrentStyle(logLevel: logLevel, message: message, terminator: terminator, file: file, line: line, column: column, function: function, date: date) + let stringToPrint = stringForCurrentStyle( + logLevel: logLevel, message: message, terminator: terminator, file: file, line: line, + column: column, function: function, date: date) print(stringToPrint, terminator: terminator) - - let output = PrettyLoggerOutput(level: logLevel, - message: message, - file: (file as NSString).lastPathComponent, - line: line, - column: column, - formatted: stringToPrint) + + let output = PrettyLoggerOutput( + level: logLevel, + message: message, + file: (file as NSString).lastPathComponent, + line: line, + column: column, + formatted: stringToPrint + ) self.output.send(output) + return stringToPrint } @@ -195,7 +211,10 @@ public class PrettyLogger { return message } - private func stringForCurrentStyle(logLevel: PrettyLoggerLevel, message: String, terminator: String, file: String, line: Int, column: Int, function: String, date: Date) -> String { + private func stringForCurrentStyle( + logLevel: PrettyLoggerLevel, message: String, terminator: String, file: String, line: Int, + column: Int, function: String, date: Date + ) -> String { let level = "\(logLevel.label)" let stringDate = "\(mFormatter.string(from: date))" @@ -203,13 +222,3 @@ public class PrettyLogger { return "\(stringDate) ◉ \(level) \(message) \(stringLocation)" } } - -@available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) -extension Logger { - private static let subsytem = Bundle.main.bundleIdentifier! - static let `default` = Logger(subsystem: subsytem, category: "default") - - static func custom(_ category: String) -> Self { - .init(subsystem: subsytem, category: category) - } -} diff --git a/Sources/PrettyLoggerOutput.swift b/Sources/PrettyLoggerOutput.swift index 6080f97..441412e 100644 --- a/Sources/PrettyLoggerOutput.swift +++ b/Sources/PrettyLoggerOutput.swift @@ -3,8 +3,35 @@ import Foundation public struct PrettyLoggerOutput: Equatable { public let level: PrettyLoggerLevel public let message: String - public let file: String - public let line: Int - public let column: Int - public let formatted: String + public let file: String? + public let line: Int? + public let column: Int? + public let formatted: String? + + // Convenience initializer for OSLog API (no file/line info) + public init(level: PrettyLoggerLevel, message: String) { + self.level = level + self.message = message + self.file = nil + self.line = nil + self.column = nil + self.formatted = nil + } + + // Full initializer for legacy API (with file/line info) + public init( + level: PrettyLoggerLevel, + message: String, + file: String, + line: Int, + column: Int, + formatted: String + ) { + self.level = level + self.message = message + self.file = file + self.line = line + self.column = column + self.formatted = formatted + } } diff --git a/Tests/FormatOutputTests.swift b/Tests/FormatOutputTests.swift index e4868c6..6a2c626 100644 --- a/Tests/FormatOutputTests.swift +++ b/Tests/FormatOutputTests.swift @@ -1,8 +1,9 @@ import XCTest + @testable import PrettyLogger -class FormatOutputTests: XCTestCase { - func testOutputWithTwoParameters() { +class LegacyFormatOutputTests: XCTestCase { + func testLegacyOutputWithTwoParameters() { PrettyLogger.shared.level = .info PrettyLogger.shared.separator = " ❎ " let output = logInfo("2", "3") diff --git a/Tests/LevelConfigurationTests.swift b/Tests/LevelConfigurationTests.swift index 5d2c9e7..5859177 100644 --- a/Tests/LevelConfigurationTests.swift +++ b/Tests/LevelConfigurationTests.swift @@ -1,8 +1,9 @@ import XCTest + @testable import PrettyLogger -class LevelConfigurationTests: XCTestCase { - func testLogOnAllLevels() { +class LegacyLevelConfigurationTests: XCTestCase { + func testLegacyLogOnAllLevels() { PrettyLogger.shared.level = .all XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -12,7 +13,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNotNil(logTrace("trace")) } - func testLogOnDisableLogger() { + func testLegacyLogOnDisableLogger() { PrettyLogger.shared.level = .disable XCTAssertNil(logFatal("fatal")) XCTAssertNil(logError("error")) @@ -22,7 +23,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnFatalLevel() { + func testLegacyLogOnFatalLevel() { PrettyLogger.shared.level = .fatal XCTAssertNotNil(logFatal("fatal")) XCTAssertNil(logError("error")) @@ -32,7 +33,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnErrorLevel() { + func testLegacyLogOnErrorLevel() { PrettyLogger.shared.level = .error XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -42,7 +43,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnWarnLevel() { + func testLegacyLogOnWarnLevel() { PrettyLogger.shared.level = .warn XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -52,7 +53,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnInfoLevel() { + func testLegacyLogOnInfoLevel() { PrettyLogger.shared.level = .info XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -62,7 +63,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnDebugLevel() { + func testLegacyLogOnDebugLevel() { PrettyLogger.shared.level = .debug XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -72,7 +73,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnTraceLevel() { + func testLegacyLogOnTraceLevel() { PrettyLogger.shared.level = .trace XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) diff --git a/Tests/OSLogBasicTests.swift b/Tests/OSLogBasicTests.swift new file mode 100644 index 0000000..8cba76c --- /dev/null +++ b/Tests/OSLogBasicTests.swift @@ -0,0 +1,119 @@ +import XCTest + +@testable import PrettyLogger + +class OSLogBasicTests: XCTestCase { + + override func setUp() { + super.setUp() + // Reset to default state + PrettyLogger.shared.level = .all + } + + func testBasicOSLogFunctions() { + // Test that OSLog functions can be called without crashing + // These functions don't return values, so we test they execute successfully + + XCTAssertNoThrow(logFatal("Fatal message")) + XCTAssertNoThrow(logError("Error message")) + XCTAssertNoThrow(logWarning("Warning message")) + XCTAssertNoThrow(logInfo("Info message")) + XCTAssertNoThrow(logDebug("Debug message")) + XCTAssertNoThrow(logTrace("Trace message")) + } + + func testOSLogWithCategory() { + // Test that category parameter works + XCTAssertNoThrow(logFatal("Fatal with category", category: "Test")) + XCTAssertNoThrow(logError("Error with category", category: "Network")) + XCTAssertNoThrow(logWarning("Warning with category", category: "UI")) + XCTAssertNoThrow(logInfo("Info with category", category: "Database")) + XCTAssertNoThrow(logDebug("Debug with category", category: "Cache")) + XCTAssertNoThrow(logTrace("Trace with category", category: "Auth")) + } + + func testOSLogWithPrivacy() { + // Test that privacy parameter works + XCTAssertNoThrow(logInfo("Public message", privacy: .public)) + XCTAssertNoThrow(logInfo("Private message", privacy: .private)) + XCTAssertNoThrow(logInfo("Auto privacy message", privacy: .auto)) + } + + func testOSLogWithAllParameters() { + // Test that all parameters work together + XCTAssertNoThrow(logFatal("Fatal", category: "Test", privacy: .public)) + XCTAssertNoThrow(logError("Error", category: "Network", privacy: .private)) + XCTAssertNoThrow(logWarning("Warning", category: "UI", privacy: .auto)) + XCTAssertNoThrow(logInfo("Info", category: "Database", privacy: .public)) + XCTAssertNoThrow(logDebug("Debug", category: "Cache", privacy: .private)) + XCTAssertNoThrow(logTrace("Trace", category: "Auth", privacy: .auto)) + } + + func testOSLogWithStringInterpolation() { + // Test that string interpolation works properly + let userID = "user123" + let count = 42 + let isActive = true + + XCTAssertNoThrow(logInfo("User \(userID) has \(count) items")) + XCTAssertNoThrow(logDebug("Status: \(isActive ? "active" : "inactive")")) + XCTAssertNoThrow(logError("Error for user \(userID): count=\(count)", category: "Debug")) + } + + func testOSLogWithEmptyMessage() { + // Test that empty messages work + XCTAssertNoThrow(logInfo("")) + XCTAssertNoThrow(logError("", category: "Test")) + XCTAssertNoThrow(logDebug("", privacy: .private)) + XCTAssertNoThrow(logWarning("", category: "Test", privacy: .auto)) + } + + func testOSLogWithNilCategory() { + // Test that nil category (default) works + XCTAssertNoThrow(logInfo("Message with nil category", category: nil)) + XCTAssertNoThrow(logError("Another message", category: nil, privacy: .auto)) + } + + func testOSLogWithLongMessage() { + // Test that long messages work + let longMessage = String(repeating: "This is a long message. ", count: 50) + + XCTAssertNoThrow(logInfo(longMessage)) + XCTAssertNoThrow(logDebug(longMessage, category: "Performance")) + XCTAssertNoThrow(logError(longMessage, category: "Test", privacy: .private)) + } + + func testOSLogWithSpecialCharacters() { + // Test that special characters work + let messageWithEmojis = "User logged in 🎉 with status ✅" + let messageWithUnicode = "Café naïve résumé" + let messageWithSymbols = "Price: $100.50 @ 50% off" + + XCTAssertNoThrow(logInfo(messageWithEmojis)) + XCTAssertNoThrow(logDebug(messageWithUnicode, category: "i18n")) + XCTAssertNoThrow(logWarning(messageWithSymbols, privacy: .public)) + } + + func testOSLogCategoryConstants() { + // Test using category constants (common pattern) + let networkCategory = "Network" + let authCategory = "Authentication" + let uiCategory = "UserInterface" + + XCTAssertNoThrow(logInfo("API request started", category: networkCategory)) + XCTAssertNoThrow(logError("Login failed", category: authCategory)) + XCTAssertNoThrow(logDebug("Button tapped", category: uiCategory)) + } + + func testOSLogDoesNotReturnValue() { + // Verify that OSLog functions don't return values (unlike legacy API) + // This is a compile-time check, but we can verify the type + + let result1: Void = logInfo("Test message") + let result2: Void = logError("Test error", category: "Test") + let result3: Void = logDebug("Test debug", privacy: .private) + + // If this compiles, the functions correctly return Void + XCTAssertTrue(true) // Just to have an assertion + } +} diff --git a/Tests/OSLogLevelConfigurationTests.swift b/Tests/OSLogLevelConfigurationTests.swift new file mode 100644 index 0000000..db8fbad --- /dev/null +++ b/Tests/OSLogLevelConfigurationTests.swift @@ -0,0 +1,288 @@ +import Combine +import XCTest + +@testable import PrettyLogger + +class OSLogLevelConfigurationTests: XCTestCase { + var cancellables = Set() + + override func setUp() { + super.setUp() + cancellables.removeAll() + + // Reset to default state + PrettyLogger.shared.level = .all + } + + override func tearDown() { + cancellables.removeAll() + super.tearDown() + } + + func testOSLogOnAllLevels() { + PrettyLogger.shared.level = .all + + let expectation = XCTestExpectation(description: "All levels should produce output") + expectation.expectedFulfillmentCount = 6 + + var outputCount = 0 + PrettyLogger.shared.output + .sink { _ in + outputCount += 1 + expectation.fulfill() + } + .store(in: &cancellables) + + // Test all OSLog functions - they don't return values but should trigger output + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(outputCount, 6) + } + + func testOSLogOnDisableLogger() { + PrettyLogger.shared.level = .disable + + let expectation = XCTestExpectation(description: "No output should be produced") + expectation.isInverted = true // We expect this NOT to be fulfilled + + PrettyLogger.shared.output + .sink { _ in + expectation.fulfill() + } + .store(in: &cancellables) + + // None of these should produce output + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 1.0) + } + + func testOSLogOnFatalLevel() { + PrettyLogger.shared.level = .fatal + + let expectation = XCTestExpectation(description: "Only fatal should produce output") + expectation.expectedFulfillmentCount = 1 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal]) + } + + func testOSLogOnErrorLevel() { + PrettyLogger.shared.level = .error + + let expectation = XCTestExpectation(description: "Fatal and error should produce output") + expectation.expectedFulfillmentCount = 2 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal, .error]) + } + + func testOSLogOnWarnLevel() { + PrettyLogger.shared.level = .warn + + let expectation = XCTestExpectation( + description: "Fatal, error, and warning should produce output") + expectation.expectedFulfillmentCount = 3 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal, .error, .warn]) + } + + func testOSLogOnInfoLevel() { + PrettyLogger.shared.level = .info + + let expectation = XCTestExpectation(description: "Fatal through info should produce output") + expectation.expectedFulfillmentCount = 4 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal, .error, .warn, .info]) + } + + func testOSLogOnDebugLevel() { + PrettyLogger.shared.level = .debug + + let expectation = XCTestExpectation( + description: "Fatal through debug should produce output") + expectation.expectedFulfillmentCount = 5 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal, .error, .warn, .info, .debug]) + } + + func testOSLogOnTraceLevel() { + PrettyLogger.shared.level = .trace + + let expectation = XCTestExpectation(description: "All levels should produce output") + expectation.expectedFulfillmentCount = 6 + + var receivedLevels: [PrettyLoggerLevel] = [] + PrettyLogger.shared.output + .sink { output in + receivedLevels.append(output.level) + expectation.fulfill() + } + .store(in: &cancellables) + + logFatal("fatal test") + logError("error test") + logWarning("warning test") + logInfo("info test") + logDebug("debug test") + logTrace("trace test") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedLevels, [.fatal, .error, .warn, .info, .debug, .trace]) + } + + func testOSLogWithCategories() { + PrettyLogger.shared.level = .all + + let expectation = XCTestExpectation(description: "Logs with categories should work") + expectation.expectedFulfillmentCount = 3 + + var receivedMessages: [String] = [] + PrettyLogger.shared.output + .sink { output in + receivedMessages.append(output.message) + expectation.fulfill() + } + .store(in: &cancellables) + + logInfo("Network request", category: "Network") + logError("Auth failed", category: "Authentication") + logDebug("Cache hit", category: "Cache") + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedMessages, ["Network request", "Auth failed", "Cache hit"]) + } + + func testOSLogWithPrivacy() { + PrettyLogger.shared.level = .all + + let expectation = XCTestExpectation(description: "Logs with privacy should work") + expectation.expectedFulfillmentCount = 3 + + var receivedMessages: [String] = [] + PrettyLogger.shared.output + .sink { output in + receivedMessages.append(output.message) + expectation.fulfill() + } + .store(in: &cancellables) + + logInfo("Public info", privacy: .public) + logDebug("Private debug", privacy: .private) + logError("Auto privacy", privacy: .auto) + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedMessages, ["Public info", "Private debug", "Auto privacy"]) + } + + func testOSLogWithCategoryAndPrivacy() { + PrettyLogger.shared.level = .all + + let expectation = XCTestExpectation( + description: "Logs with category and privacy should work") + expectation.expectedFulfillmentCount = 1 + + var receivedOutput: PrettyLoggerOutput? + PrettyLogger.shared.output + .sink { output in + receivedOutput = output + expectation.fulfill() + } + .store(in: &cancellables) + + logDebug("Sensitive data", category: "Auth", privacy: .private) + + wait(for: [expectation], timeout: 2.0) + XCTAssertEqual(receivedOutput?.message, "Sensitive data") + XCTAssertEqual(receivedOutput?.level, .debug) + } +} diff --git a/Tests/OutputTests.swift b/Tests/OutputTests.swift index 041ebed..053bc7a 100644 --- a/Tests/OutputTests.swift +++ b/Tests/OutputTests.swift @@ -1,11 +1,12 @@ -import XCTest import Combine +import XCTest + @testable import PrettyLogger -class OutputTests: XCTestCase { +class LegacyOutputTests: XCTestCase { var cancellables = Set() - func testOuputText() { + func testLegacyOutputText() { PrettyLogger.shared.level = .info PrettyLogger.shared.separator = " ❎ " @@ -20,9 +21,9 @@ class OutputTests: XCTestCase { expirationComplete.fulfill() } .store(in: &cancellables) - + expectedOutput = logInfo("hola holita") - + waitForExpectations(timeout: 10) } } diff --git a/Tests/QuickOutputTest.swift b/Tests/QuickOutputTest.swift new file mode 100644 index 0000000..918a348 --- /dev/null +++ b/Tests/QuickOutputTest.swift @@ -0,0 +1,163 @@ +import Combine +import XCTest + +@testable import PrettyLogger + +class QuickOutputTest: XCTestCase { + var cancellables = Set() + + override func setUp() { + super.setUp() + cancellables.removeAll() + PrettyLogger.shared.level = .all + } + + override func tearDown() { + cancellables.removeAll() + super.tearDown() + } + + func testOSLogOutputStreamIntegration() { + let expectation = XCTestExpectation(description: "OSLog should send to output stream") + expectation.expectedFulfillmentCount = 3 + + var receivedOutputs: [PrettyLoggerOutput] = [] + + PrettyLogger.shared.output + .sink { output in + receivedOutputs.append(output) + expectation.fulfill() + } + .store(in: &cancellables) + + // Test new OSLog API + logInfo("Test message 1") + logError("Test error message", category: "Testing") + logDebug("Test debug message", category: "Debug", privacy: .private) + + wait(for: [expectation], timeout: 3.0) + + // Verify we received the outputs + XCTAssertEqual(receivedOutputs.count, 3) + + // Check first output (basic) + XCTAssertEqual(receivedOutputs[0].level, .info) + XCTAssertEqual(receivedOutputs[0].message, "Test message 1") + XCTAssertNil(receivedOutputs[0].file) // OSLog API doesn't provide file info + XCTAssertNil(receivedOutputs[0].line) // OSLog API doesn't provide line info + XCTAssertNil(receivedOutputs[0].formatted) // OSLog API doesn't provide formatted string + + // Check second output (with category) + XCTAssertEqual(receivedOutputs[1].level, .error) + XCTAssertEqual(receivedOutputs[1].message, "Test error message") + XCTAssertNil(receivedOutputs[1].file) + + // Check third output (with category and privacy) + XCTAssertEqual(receivedOutputs[2].level, .debug) + XCTAssertEqual(receivedOutputs[2].message, "Test debug message") + XCTAssertNil(receivedOutputs[2].file) + } + + func testLegacyOutputStreamStillWorks() { + let expectation = XCTestExpectation( + description: "Legacy API should still send to output stream") + expectation.expectedFulfillmentCount = 2 + + var receivedOutputs: [PrettyLoggerOutput] = [] + + PrettyLogger.shared.output + .sink { output in + receivedOutputs.append(output) + expectation.fulfill() + } + .store(in: &cancellables) + + // Test legacy API (deprecated but should still work) + let _ = logInfo("Legacy message 1", "part 2") + let _ = logError("Legacy error") + + wait(for: [expectation], timeout: 3.0) + + // Verify we received the outputs + XCTAssertEqual(receivedOutputs.count, 2) + + // Check legacy outputs have file/line info + XCTAssertEqual(receivedOutputs[0].level, .info) + XCTAssertEqual(receivedOutputs[0].message, "Legacy message 1 part 2") + XCTAssertNotNil(receivedOutputs[0].file) // Legacy API provides file info + XCTAssertNotNil(receivedOutputs[0].line) // Legacy API provides line info + XCTAssertNotNil(receivedOutputs[0].formatted) // Legacy API provides formatted string + + XCTAssertEqual(receivedOutputs[1].level, .error) + XCTAssertEqual(receivedOutputs[1].message, "Legacy error") + XCTAssertNotNil(receivedOutputs[1].file) + } + + func testMixedAPIUsageInOutputStream() { + let expectation = XCTestExpectation( + description: "Both APIs should work together in output stream") + expectation.expectedFulfillmentCount = 4 + + var receivedOutputs: [PrettyLoggerOutput] = [] + + PrettyLogger.shared.output + .sink { output in + receivedOutputs.append(output) + expectation.fulfill() + } + .store(in: &cancellables) + + // Mix both APIs + logInfo("OSLog message") // New API + let _ = logWarning("Legacy warning") // Legacy API + logError("OSLog error", category: "Test") // New API with category + let _ = logDebug("Legacy debug", "with parts") // Legacy API with parts + + wait(for: [expectation], timeout: 3.0) + + XCTAssertEqual(receivedOutputs.count, 4) + + // OSLog outputs have nil file/line, legacy outputs have them + XCTAssertNil(receivedOutputs[0].file) // OSLog + XCTAssertNotNil(receivedOutputs[1].file) // Legacy + XCTAssertNil(receivedOutputs[2].file) // OSLog + XCTAssertNotNil(receivedOutputs[3].file) // Legacy + } + + func testLevelFilteringWorksForBothAPIs() { + PrettyLogger.shared.level = .error // Only error and fatal should pass + + let expectation = XCTestExpectation( + description: "Level filtering should work for both APIs") + expectation.expectedFulfillmentCount = 4 // 2 from each API + + var receivedOutputs: [PrettyLoggerOutput] = [] + + PrettyLogger.shared.output + .sink { output in + receivedOutputs.append(output) + expectation.fulfill() + } + .store(in: &cancellables) + + // OSLog API - only error and fatal should pass + logFatal("OSLog fatal - should pass") + logError("OSLog error - should pass") + logInfo("OSLog info - should be filtered") + logDebug("OSLog debug - should be filtered") + + // Legacy API - only error and fatal should pass + let _ = logFatal("Legacy fatal - should pass") + let _ = logError("Legacy error - should pass") + let _ = logInfo("Legacy info - should be filtered") + let _ = logDebug("Legacy debug - should be filtered") + + wait(for: [expectation], timeout: 3.0) + + XCTAssertEqual(receivedOutputs.count, 4) + XCTAssertEqual(receivedOutputs[0].level, .fatal) // OSLog fatal + XCTAssertEqual(receivedOutputs[1].level, .error) // OSLog error + XCTAssertEqual(receivedOutputs[2].level, .fatal) // Legacy fatal + XCTAssertEqual(receivedOutputs[3].level, .error) // Legacy error + } +} diff --git a/USAGE_EXAMPLES.md b/USAGE_EXAMPLES.md new file mode 100644 index 0000000..182b3e7 --- /dev/null +++ b/USAGE_EXAMPLES.md @@ -0,0 +1,310 @@ +# PrettyLogger Usage Examples + +This document provides examples of how to use PrettyLogger with the new OSLog-based API. + +## Basic Usage + +### Simple Logging + +```swift +import PrettyLogger + +// Basic logging with different levels +logFatal("Application crashed with critical error") +logError("Failed to load user data") +logWarning("Low memory warning") +logInfo("User logged in successfully") +logDebug("Processing request with ID: 12345") +logTrace("Entering function: processUserData()") +``` + +### Logging with Categories + +Categories help organize logs in different subsystems of your app: + +```swift +// Network-related logs +logError("Connection timeout", category: "Network") +logInfo("API request completed", category: "Network") + +// UI-related logs +logWarning("Button animation took longer than expected", category: "UI") +logDebug("View controller loaded", category: "UI") + +// Database-related logs +logError("Failed to save user preferences", category: "Database") +logInfo("Database migration completed", category: "Database") +``` + +### Privacy Levels + +Control the privacy of sensitive information in logs: + +```swift +let userID = "user123" +let email = "user@example.com" +let password = "secretPassword" + +// Default behavior - sensitive data is automatically handled +logInfo("User \(userID) logged in") + +// Explicitly public - visible in all log viewers +logInfo("App version: 1.2.3", privacy: .public) + +// Explicitly private - hidden in release builds +logDebug("User email: \(email)", privacy: .private) +logWarning("Authentication failed for user", privacy: .private) + +// Auto privacy (recommended) - system decides based on context +logError("Login failed for user \(userID)", privacy: .auto) +``` + +## Advanced Usage + +### Conditional Logging with Levels + +You can control which logs are shown by setting the global log level: + +```swift +// Only show fatal and error logs +PrettyLogger.shared.level = .error + +// Show all logs including trace +PrettyLogger.shared.level = .all + +// Disable all logging +PrettyLogger.shared.level = .disable +``` + +### Real-time Log Monitoring + +Subscribe to log outputs for real-time monitoring: + +```swift +import Combine + +var cancellables = Set() + +PrettyLogger.shared.output + .sink { logOutput in + print("Log received:") + print("Level: \(logOutput.level)") + print("Message: \(logOutput.message)") + print("File: \(logOutput.file)") + print("Line: \(logOutput.line)") + print("Formatted: \(logOutput.formatted)") + } + .store(in: &cancellables) + +// Now any log will be captured by the subscriber +logInfo("This will be captured by the subscriber") +``` + +### Custom Categories for Different Features + +```swift +// Authentication +logInfo("User authentication started", category: "Auth") +logError("Invalid credentials provided", category: "Auth") + +// Payment Processing +logDebug("Processing payment for amount: $\(amount)", category: "Payment") +logWarning("Payment gateway response slow", category: "Payment") + +// Analytics +logTrace("User action tracked: button_tap", category: "Analytics") +logInfo("Analytics batch sent successfully", category: "Analytics") +``` + +## Migration from Legacy API + +### Before (Deprecated - print-based) + +```swift +// Old print-based API (deprecated) +logInfo("User logged in", "additional data") +logError("Something went wrong", error.localizedDescription) +logDebug("Debug info:", debugData, "more info") +``` + +### After (Recommended - OSLog-based) + +```swift +// New OSLog-based API - same function names! +logInfo("User logged in with additional data") +logError("Something went wrong: \(error.localizedDescription)") +logDebug("Debug info: \(debugData) - more info") + +// With categories for better organization +logInfo("User logged in", category: "Authentication") +logError("Network request failed", category: "Network") +logDebug("Cache miss for key: \(key)", category: "Cache") +``` + +### Easy Migration Steps + +1. **Replace variadic parameters with string interpolation:** + ```swift + // Old + logInfo("User:", username, "logged in") + + // New + logInfo("User: \(username) logged in") + ``` + +2. **Add optional categories:** + ```swift + // Before + logError("Database connection failed") + + // After (enhanced) + logError("Database connection failed", category: "Database") + ``` + +3. **Add privacy controls when needed:** + ```swift + // For sensitive data + logDebug("Auth token: \(token)", category: "Auth", privacy: .private) + + // For public information + logInfo("App version: \(version)", privacy: .public) + ``` + +## Best Practices + +### 1. Use Appropriate Log Levels + +```swift +// Fatal: Only for crashes or critical failures +logFatal("Database corruption detected - app cannot continue") + +// Error: For errors that don't crash the app but are significant +logError("Failed to save user data to disk") + +// Warning: For unexpected but recoverable situations +logWarning("Using fallback configuration due to missing config file") + +// Info: For general information about app flow +logInfo("User completed onboarding process") + +// Debug: For detailed debugging information +logDebug("Cache hit rate: \(hitRate)% for session") + +// Trace: For very detailed execution flow +logTrace("Entering method: calculateUserScore()") +``` + +### 2. Use Categories Consistently + +```swift +// Create constants for category names to avoid typos +extension String { + static let networkCategory = "Network" + static let authCategory = "Authentication" + static let cacheCategory = "Cache" + static let uiCategory = "UI" +} + +// Use them consistently +logError("Connection failed", category: .networkCategory) +logInfo("User authenticated", category: .authCategory) +logDebug("Cache cleared", category: .cacheCategory) +``` + +### 3. Handle Privacy Appropriately + +```swift +let userID = getCurrentUserID() +let sensitiveToken = getAuthToken() + +// Good: Sensitive data marked as private +logDebug("Auth token refreshed", category: "Auth", privacy: .private) + +// Good: Non-sensitive data can be public +logInfo("App launched in \(environment) environment", privacy: .public) + +// Good: Let system decide for user IDs +logInfo("Processing request for user \(userID)", privacy: .auto) +``` + +### 4. Structured Logging + +```swift +// Good: Structured and searchable +logInfo("API request completed", category: "Network") +logDebug("Request details - URL: \(url), Status: \(statusCode), Duration: \(duration)ms", category: "Network") + +// Better: Even more structured +logInfo("API_REQUEST_COMPLETED url=\(url) status=\(statusCode) duration=\(duration)", category: "Network") +``` + +## Function Overview + +| Function | OSLog Level | Use Case | +|----------|-------------|----------| +| `logFatal()` | `.fault` | Critical errors that crash the app | +| `logError()` | `.error` | Errors that don't crash but are significant | +| `logWarning()` | `.default` | Unexpected but recoverable situations | +| `logInfo()` | `.info` | General information about app flow | +| `logDebug()` | `.debug` | Detailed debugging information | +| `logTrace()` | `.debug` | Very detailed execution flow | + +## Console and Instruments Integration + +When using the new OSLog-based API, your logs will automatically appear in: + +1. **Xcode Console**: Filter by subsystem and category +2. **Console.app**: System-wide log viewing with advanced filtering +3. **Instruments**: Performance analysis with log correlation +4. **Command Line**: Using `log show` command + +### Viewing Logs in Console.app + +1. Open Console.app on macOS +2. Filter by your app's bundle identifier +3. Use category filters to focus on specific subsystems +4. Search for specific log messages or patterns + +### Using log command line tool + +```bash +# Show logs from your app +log show --predicate 'subsystem == "com.yourapp.bundleid"' + +# Filter by category +log show --predicate 'subsystem == "com.yourapp.bundleid" AND category == "Network"' + +# Show only errors and above +log show --predicate 'subsystem == "com.yourapp.bundleid" AND messageType >= error' +``` + +## Complete Example + +```swift +import PrettyLogger + +class NetworkManager { + func login(username: String, password: String) { + logInfo("Starting user authentication", category: "Auth") + + guard !username.isEmpty else { + logError("Username cannot be empty", category: "Auth") + return + } + + logDebug("Validating credentials for user", category: "Auth", privacy: .private) + + // Simulate network request + logTrace("Making API request to /auth/login", category: "Network") + + // Success case + logInfo("User authenticated successfully", category: "Auth") + logDebug("Session token received", category: "Auth", privacy: .private) + + // Or error case + // logError("Authentication failed: Invalid credentials", category: "Auth") + } +} +``` + +This approach maintains backward compatibility while providing all the benefits of OSLog! \ No newline at end of file From e871fb2e89724362d7cde1ceab02a023097db6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Ruiz=20Lafuente?= Date: Wed, 23 Jul 2025 17:22:01 +0200 Subject: [PATCH 3/5] Improve OSLog context and fix tests --- Sources/Global.swift | 54 ++++-- Sources/PrettyLogger.swift | 201 +++++++++++++++++------ Sources/PrettyLoggerOutput.swift | 35 +--- Tests/FormatOutputTests.swift | 20 +++ Tests/LevelConfigurationTests.swift | 20 +++ Tests/OSLogLevelConfigurationTests.swift | 14 +- Tests/OutputTests.swift | 20 +++ Tests/QuickOutputTest.swift | 27 ++- 8 files changed, 290 insertions(+), 101 deletions(-) diff --git a/Sources/Global.swift b/Sources/Global.swift index e37ddca..bab410b 100644 --- a/Sources/Global.swift +++ b/Sources/Global.swift @@ -2,28 +2,58 @@ import Foundation // MARK: - Primary OSLog-based API -public func logFatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logFatal(message, category: category, privacy: privacy) +public func logFatal( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logFatal( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } -public func logError(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logError(message, category: category, privacy: privacy) +public func logError( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logError( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } -public func logWarning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logWarning(message, category: category, privacy: privacy) +public func logWarning( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logWarning( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } -public func logInfo(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logInfo(message, category: category, privacy: privacy) +public func logInfo( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logInfo( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } -public func logDebug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logDebug(message, category: category, privacy: privacy) +public func logDebug( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logDebug( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } -public func logTrace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { - PrettyLogger.shared.logTrace(message, category: category, privacy: privacy) +public func logTrace( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String = #file, line: Int = #line, column: Int = #column, function: String = #function +) { + PrettyLogger.shared.logTrace( + message, category: category, privacy: privacy, file: file, line: line, column: column, + function: function) } // MARK: - Legacy print-based API (deprecated) diff --git a/Sources/PrettyLogger.swift b/Sources/PrettyLogger.swift index 102e5e5..65e61b9 100644 --- a/Sources/PrettyLogger.swift +++ b/Sources/PrettyLogger.swift @@ -18,88 +18,139 @@ public class PrettyLogger { // MARK: - Primary OSLog-based API - public func logFatal(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logFatal( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { // Check level filtering if level < .fatal { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .fault, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .fatal, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .fault, + prettyLoggerLevel: .fatal, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } - public func logError(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logError( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { if level < .error { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .error, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .error, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .error, + prettyLoggerLevel: .error, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } - public func logWarning(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logWarning( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { if level < .warn { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .default, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .warn, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .default, + prettyLoggerLevel: .warn, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } - public func logInfo(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logInfo( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { if level < .info { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .info, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .info, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .info, + prettyLoggerLevel: .info, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } - public func logDebug(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logDebug( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { if level < .debug { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .debug, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .debug, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .debug, + prettyLoggerLevel: .debug, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } - public func logTrace(_ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto) { + internal func logTrace( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { if level < .trace { return } - let logger = createLogger(for: category) - logWithPrivacy(logger: logger, level: .debug, message: message, privacy: privacy) - - // Send to output stream - let output = PrettyLoggerOutput(level: .trace, message: message) - self.output.send(output) + logWithPrivacy( + logger: createLogger(for: category), + level: .default, + prettyLoggerLevel: .trace, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) } // MARK: - Legacy print-based API (internal) - internal func logFatalLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logFatalLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .fatal { return nil } @@ -108,7 +159,10 @@ public class PrettyLogger { line: line, column: column, function: function) } - internal func logErrorLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logErrorLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .error { return nil } @@ -117,7 +171,10 @@ public class PrettyLogger { line: line, column: column, function: function) } - internal func logWarningLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logWarningLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .warn { return nil } @@ -126,7 +183,10 @@ public class PrettyLogger { line: line, column: column, function: function) } - internal func logInfoLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logInfoLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .info { return nil } @@ -135,7 +195,10 @@ public class PrettyLogger { line: line, column: column, function: function) } - internal func logDebugLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logDebugLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .debug { return nil } @@ -144,7 +207,10 @@ public class PrettyLogger { line: line, column: column, function: function) } - internal func logTraceLegacy(_ items: [Any], separator: String? = nil, terminator: String? = nil, file: String = #file, line: Int = #line, column: Int = #column, function: String = #function) -> String? { + internal func logTraceLegacy( + _ items: [Any], separator: String? = nil, terminator: String? = nil, file: String, + line: Int, column: Int, function: String + ) -> String? { if level < .trace { return nil } @@ -162,7 +228,8 @@ public class PrettyLogger { } private func logWithPrivacy( - logger: Logger, level: OSLogType, message: String, privacy: PrettyLoggerPrivacy + logger: Logger, level: OSLogType, prettyLoggerLevel: PrettyLoggerLevel, message: String, + privacy: PrettyLoggerPrivacy, file: String, line: Int, column: Int, function: String ) { switch privacy { case .auto: @@ -172,6 +239,42 @@ public class PrettyLogger { case .private: logger.log(level: level, "\(message, privacy: .private)") } + + sendOutput( + prettyLoggerLevel, + message: message, + file: file, + line: line, + column: column, + function: function + ) + } + + private func sendOutput( + _ logLevel: PrettyLoggerLevel, message: String, file: String, line: Int, column: Int, + function: String, date: Date = Date() + ) { + let stringToPrint = stringForCurrentStyle( + logLevel: logLevel, + message: message, + terminator: terminator, + file: file, + line: line, + column: column, + function: function, + date: date + ) + + output.send( + PrettyLoggerOutput( + level: logLevel, + message: message, + file: (file as NSString).lastPathComponent, + line: line, + column: column, + formatted: stringToPrint + ) + ) } private func log( diff --git a/Sources/PrettyLoggerOutput.swift b/Sources/PrettyLoggerOutput.swift index 441412e..6080f97 100644 --- a/Sources/PrettyLoggerOutput.swift +++ b/Sources/PrettyLoggerOutput.swift @@ -3,35 +3,8 @@ import Foundation public struct PrettyLoggerOutput: Equatable { public let level: PrettyLoggerLevel public let message: String - public let file: String? - public let line: Int? - public let column: Int? - public let formatted: String? - - // Convenience initializer for OSLog API (no file/line info) - public init(level: PrettyLoggerLevel, message: String) { - self.level = level - self.message = message - self.file = nil - self.line = nil - self.column = nil - self.formatted = nil - } - - // Full initializer for legacy API (with file/line info) - public init( - level: PrettyLoggerLevel, - message: String, - file: String, - line: Int, - column: Int, - formatted: String - ) { - self.level = level - self.message = message - self.file = file - self.line = line - self.column = column - self.formatted = formatted - } + public let file: String + public let line: Int + public let column: Int + public let formatted: String } diff --git a/Tests/FormatOutputTests.swift b/Tests/FormatOutputTests.swift index 6a2c626..9ba3985 100644 --- a/Tests/FormatOutputTests.swift +++ b/Tests/FormatOutputTests.swift @@ -3,6 +3,26 @@ import XCTest @testable import PrettyLogger class LegacyFormatOutputTests: XCTestCase { + private var originalSeparator: String! + private var originalTerminator: String! + private var originalLevel: PrettyLoggerLevel! + + override func setUp() { + super.setUp() + // Save original state + originalSeparator = PrettyLogger.shared.separator + originalTerminator = PrettyLogger.shared.terminator + originalLevel = PrettyLogger.shared.level + } + + override func tearDown() { + // Restore original state + PrettyLogger.shared.separator = originalSeparator + PrettyLogger.shared.terminator = originalTerminator + PrettyLogger.shared.level = originalLevel + super.tearDown() + } + func testLegacyOutputWithTwoParameters() { PrettyLogger.shared.level = .info PrettyLogger.shared.separator = " ❎ " diff --git a/Tests/LevelConfigurationTests.swift b/Tests/LevelConfigurationTests.swift index 5859177..6e5e4e0 100644 --- a/Tests/LevelConfigurationTests.swift +++ b/Tests/LevelConfigurationTests.swift @@ -3,6 +3,26 @@ import XCTest @testable import PrettyLogger class LegacyLevelConfigurationTests: XCTestCase { + private var originalLevel: PrettyLoggerLevel! + private var originalSeparator: String! + private var originalTerminator: String! + + override func setUp() { + super.setUp() + // Save original state + originalLevel = PrettyLogger.shared.level + originalSeparator = PrettyLogger.shared.separator + originalTerminator = PrettyLogger.shared.terminator + } + + override func tearDown() { + // Restore original state + PrettyLogger.shared.level = originalLevel + PrettyLogger.shared.separator = originalSeparator + PrettyLogger.shared.terminator = originalTerminator + super.tearDown() + } + func testLegacyLogOnAllLevels() { PrettyLogger.shared.level = .all XCTAssertNotNil(logFatal("fatal")) diff --git a/Tests/OSLogLevelConfigurationTests.swift b/Tests/OSLogLevelConfigurationTests.swift index db8fbad..457345a 100644 --- a/Tests/OSLogLevelConfigurationTests.swift +++ b/Tests/OSLogLevelConfigurationTests.swift @@ -5,16 +5,28 @@ import XCTest class OSLogLevelConfigurationTests: XCTestCase { var cancellables = Set() + private var originalLevel: PrettyLoggerLevel! + private var originalSeparator: String! + private var originalTerminator: String! override func setUp() { super.setUp() cancellables.removeAll() - // Reset to default state + // Save original state + originalLevel = PrettyLogger.shared.level + originalSeparator = PrettyLogger.shared.separator + originalTerminator = PrettyLogger.shared.terminator + + // Reset to default state for tests PrettyLogger.shared.level = .all } override func tearDown() { + // Restore original state + PrettyLogger.shared.level = originalLevel + PrettyLogger.shared.separator = originalSeparator + PrettyLogger.shared.terminator = originalTerminator cancellables.removeAll() super.tearDown() } diff --git a/Tests/OutputTests.swift b/Tests/OutputTests.swift index 053bc7a..c88cc35 100644 --- a/Tests/OutputTests.swift +++ b/Tests/OutputTests.swift @@ -5,6 +5,26 @@ import XCTest class LegacyOutputTests: XCTestCase { var cancellables = Set() + private var originalSeparator: String! + private var originalTerminator: String! + private var originalLevel: PrettyLoggerLevel! + + override func setUp() { + super.setUp() + // Save original state + originalSeparator = PrettyLogger.shared.separator + originalTerminator = PrettyLogger.shared.terminator + originalLevel = PrettyLogger.shared.level + } + + override func tearDown() { + // Restore original state + PrettyLogger.shared.separator = originalSeparator + PrettyLogger.shared.terminator = originalTerminator + PrettyLogger.shared.level = originalLevel + cancellables.removeAll() + super.tearDown() + } func testLegacyOutputText() { PrettyLogger.shared.level = .info diff --git a/Tests/QuickOutputTest.swift b/Tests/QuickOutputTest.swift index 918a348..d14f2f2 100644 --- a/Tests/QuickOutputTest.swift +++ b/Tests/QuickOutputTest.swift @@ -5,14 +5,28 @@ import XCTest class QuickOutputTest: XCTestCase { var cancellables = Set() + private var originalLevel: PrettyLoggerLevel! + private var originalSeparator: String! + private var originalTerminator: String! override func setUp() { super.setUp() cancellables.removeAll() + + // Save original state + originalLevel = PrettyLogger.shared.level + originalSeparator = PrettyLogger.shared.separator + originalTerminator = PrettyLogger.shared.terminator + + // Reset to default state for tests PrettyLogger.shared.level = .all } override func tearDown() { + // Restore original state + PrettyLogger.shared.level = originalLevel + PrettyLogger.shared.separator = originalSeparator + PrettyLogger.shared.terminator = originalTerminator cancellables.removeAll() super.tearDown() } @@ -43,19 +57,17 @@ class QuickOutputTest: XCTestCase { // Check first output (basic) XCTAssertEqual(receivedOutputs[0].level, .info) XCTAssertEqual(receivedOutputs[0].message, "Test message 1") - XCTAssertNil(receivedOutputs[0].file) // OSLog API doesn't provide file info - XCTAssertNil(receivedOutputs[0].line) // OSLog API doesn't provide line info - XCTAssertNil(receivedOutputs[0].formatted) // OSLog API doesn't provide formatted string + XCTAssertEqual(receivedOutputs[0].file, "QuickOutputTest.swift") // Check second output (with category) XCTAssertEqual(receivedOutputs[1].level, .error) XCTAssertEqual(receivedOutputs[1].message, "Test error message") - XCTAssertNil(receivedOutputs[1].file) + XCTAssertEqual(receivedOutputs[1].file, "QuickOutputTest.swift") // Check third output (with category and privacy) XCTAssertEqual(receivedOutputs[2].level, .debug) XCTAssertEqual(receivedOutputs[2].message, "Test debug message") - XCTAssertNil(receivedOutputs[2].file) + XCTAssertEqual(receivedOutputs[2].file, "QuickOutputTest.swift") } func testLegacyOutputStreamStillWorks() { @@ -117,10 +129,9 @@ class QuickOutputTest: XCTestCase { XCTAssertEqual(receivedOutputs.count, 4) - // OSLog outputs have nil file/line, legacy outputs have them - XCTAssertNil(receivedOutputs[0].file) // OSLog + XCTAssertNotNil(receivedOutputs[0].file) // OSLog XCTAssertNotNil(receivedOutputs[1].file) // Legacy - XCTAssertNil(receivedOutputs[2].file) // OSLog + XCTAssertNotNil(receivedOutputs[2].file) // OSLog XCTAssertNotNil(receivedOutputs[3].file) // Legacy } From 998ef26e55a66479f2d4e766366456799ff4291a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Ruiz=20Lafuente?= Date: Wed, 23 Jul 2025 18:00:28 +0200 Subject: [PATCH 4/5] Minor fix --- Sources/PrettyLogger.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PrettyLogger.swift b/Sources/PrettyLogger.swift index 65e61b9..d60f572 100644 --- a/Sources/PrettyLogger.swift +++ b/Sources/PrettyLogger.swift @@ -71,7 +71,7 @@ public class PrettyLogger { logWithPrivacy( logger: createLogger(for: category), - level: .default, + level: .error, prettyLoggerLevel: .warn, message: message, privacy: privacy, From f015c77037dadbf2a08945c97f6c8a3f4d6a8f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Ruiz=20Lafuente?= Date: Thu, 24 Jul 2025 12:55:02 +0200 Subject: [PATCH 5/5] Update README.md --- README.md | 163 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 127 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index ae9ada5..3fb6afb 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,150 @@ # PrettyLogger -![Platform](https://img.shields.io/badge/platform-iOS-blue.svg?style=flat) +![Platform](https://img.shields.io/badge/platform-iOS-blue.svg?style=flat) ![Platform](https://img.shields.io/badge/platform-tvOS-blue.svg?style=flat) ![Platform](https://img.shields.io/badge/platform-mac-blue.svg?style=flat) ## Introduction -A pretty set of log functions to print message in console using levels (Debug, Info, Trace, Warning & Error) and emojis to improve visibility 💪 +A modern Swift logging library that integrates with Apple's unified logging system (OSLog) while maintaining a simple and familiar API. Provides structured logging with categories, privacy controls, and seamless integration with system debugging tools 💪 -## Platforms -Support for iOS, tvOS and macOS +## Platforms +Support for iOS 14.0+, tvOS 14.0+, watchOS 7.0+, and macOS 11.0+ ## Support For Swift 4 please use v1 -For Swift 5 please use v2+ +For Swift 5 please use v2-v3 + +For Swift 5.5+ with OSLog please use v4+ ## Installation -PrettyLogger is available through [Swift Package Manager](https://swift.org/package-manager/). +PrettyLogger is available through [Swift Package Manager](https://swift.org/package-manager/). 1. Follow Apple's [Adding Package Dependencies to Your App]( https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app ) guide on how to add a Swift Package dependency. 2. Use `https://github.com/hyperdevs-team/PrettyLogger` as the repository URL. -3. Specify the version to be at least `3.0.0`. +3. Specify the version to be at least `4.0.0`. ## Usage -### Print messages -To print a message in the console you simply use any of the global functions: -```swift - logWarning("This a warning!!") - logError("This is showed as error") - logFatal("This is showed as fatal message") - logInfo("This is an info message") - logDebug("This is a debug message") - logTrace("This is a trace info") -``` -The previous example will print: -```ogdl -13:31:59.632 ◉ ⚠️⚠️⚠️ This a warning!! [File.swift:L109] -13:31:59.639 ◉ ❌❌❌ This is showed as error [File.swift:L110] -13:31:59.639 ◉ ☠️☠️☠️ This is showed as fatal message [File.swift:L111] -13:31:59.639 ◉ 🔍 This is an info message [File.swift:L112] -13:31:59.639 ◉ 🐛 This is a debug message [File.swift:L113] -13:31:59.640 ◉ ✏️ This is a trace info [File.swift:L114] -``` -### Level -You can silent all logs (or some, depending on level) by setting the property `level` on the shared instance: -```swift -PrettyLogger.shared.level = .all //To show all messages -PrettyLogger.shared.level = .disable //To silent logger -PrettyLogger.shared.level = .info //To show all message except debug & trace -``` -The available levels, in order, are: disable, fatal, error, warn, info, debug, trace & all -### Global framework -If you want to import all functions in your project without import PrettyLogger in every file you could use this directive in your AppDelegate: + +### Basic Logging +To log messages, simply use any of the global functions: +```swift +logFatal("Critical error occurred") +logError("Something went wrong") +logWarning("This is a warning") +logInfo("User logged in successfully") +logDebug("Processing user data") +logTrace("Entering function") +``` + +### Logging with Categories +Organize your logs with categories for better filtering and debugging: +```swift +logInfo("API request started", category: "Network") +logError("Database connection failed", category: "Database") +logDebug("Cache hit for user data", category: "Cache") +logWarning("Low memory warning", category: "System") +``` + +### Privacy Controls +Control the visibility of sensitive information in your logs: +```swift +let userEmail = "user@example.com" +let authToken = "abc123xyz" + +// Public information (visible in all log viewers) +logInfo("App version 1.2.3", privacy: .public) + +// Private information (hidden in release builds) +logDebug("User email: \(userEmail)", category: "Auth", privacy: .private) +logTrace("Auth token: \(authToken)", category: "Auth", privacy: .private) + +// Auto privacy (system decides based on context) - Default behavior +logInfo("User logged in successfully", privacy: .auto) +``` + +### Complete Examples +```swift +// Basic usage +logInfo("User authentication started") + +// With category +logError("Network timeout occurred", category: "Network") + +// With privacy +logDebug("Processing sensitive data", privacy: .private) + +// With both category and privacy +logWarning("Failed login attempt", category: "Security", privacy: .private) +``` + +> **Note**: Starting with version 4.0.0, PrettyLogger uses Apple's unified logging system (OSLog). This means logs are now visible not only in Xcode's console but also in system tools like Console.app, Instruments, and the command-line `log` tool, providing better integration with Apple's debugging ecosystem. + +### Log Levels +You can control which logs are shown by setting the level on the shared instance: +```swift +PrettyLogger.shared.level = .all // Show all messages +PrettyLogger.shared.level = .disable // Disable all logging +PrettyLogger.shared.level = .info // Show info and above (hide debug & trace) +PrettyLogger.shared.level = .error // Show only error and fatal messages +``` + +The available levels, in order from most restrictive to least restrictive, are: +`disable`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`, `all` + +### Real-time Log Monitoring +You can subscribe to log outputs for real-time monitoring or custom handling: +```swift +import Combine + +var cancellables = Set() + +PrettyLogger.shared.output + .sink { logOutput in + print("Received log: \(logOutput.level) - \(logOutput.message)") + // Send to analytics, crash reporting, etc. + } + .store(in: &cancellables) +``` + +### Viewing Logs in System Tools + +With version 4.0.0, you can view your app's logs in various Apple debugging tools: + +- **Xcode Console**: Shows logs during development and debugging +- **Console.app**: System-wide log viewer with advanced filtering capabilities +- **Instruments**: Correlate logs with performance data +- **Command Line**: Use `log show --predicate 'subsystem == "com.yourapp.bundleid"'` + +### Global Framework Import +If you want to use logging functions throughout your project without importing PrettyLogger in every file, add this to your AppDelegate: ```swift @_exported import PrettyLogger ``` + +## Migration from v3 to v4 + +Version 4.0.0 introduces OSLog integration while maintaining the same familiar function names. The main changes are: + +- **Enhanced API**: New optional `category` and `privacy` parameters +- **Better Performance**: Uses Apple's optimized logging system +- **System Integration**: Logs appear in Console.app, Instruments, and other system tools +- **Privacy Controls**: Built-in support for sensitive data handling + +Your existing code will continue to work with deprecation warnings guiding you to the new API: + +```swift +// v3 style (still works, but deprecated) +logInfo("User:", username, "logged in") + +// v4 style (recommended) +logInfo("User: \(username) logged in", category: "Authentication") +``` + +## Requirements + +- iOS 14.0+ / tvOS 14.0+ / watchOS 7.0+ / macOS 11.0+ +- Swift 5.5+ +- Xcode 13.0+