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/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+ diff --git a/Sources/Global.swift b/Sources/Global.swift index ee0669a..bab410b 100644 --- a/Sources/Global.swift +++ b/Sources/Global.swift @@ -1,31 +1,125 @@ import Foundation +// MARK: - Primary OSLog-based API + +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, + 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, + 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, + 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, + 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, + 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) + +@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, 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, 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, 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, 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, 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 5e3f898..d60f572 100644 --- a/Sources/PrettyLogger.swift +++ b/Sources/PrettyLogger.swift @@ -1,5 +1,6 @@ -import Foundation import Combine +import Foundation +import OSLog public class PrettyLogger { public static let shared = PrettyLogger() @@ -8,71 +9,298 @@ 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 + + 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 + } + + logWithPrivacy( + logger: createLogger(for: category), + level: .fault, + prettyLoggerLevel: .fatal, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) + } + + internal func logError( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { + if level < .error { + return + } + + logWithPrivacy( + logger: createLogger(for: category), + level: .error, + prettyLoggerLevel: .error, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) + } + + internal func logWarning( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { + if level < .warn { + return + } + + logWithPrivacy( + logger: createLogger(for: category), + level: .error, + prettyLoggerLevel: .warn, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) + } + + internal func logInfo( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { + if level < .info { + return + } + + logWithPrivacy( + logger: createLogger(for: category), + level: .info, + prettyLoggerLevel: .info, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) + } + + internal func logDebug( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { + if level < .debug { + return + } + + logWithPrivacy( + logger: createLogger(for: category), + level: .debug, + prettyLoggerLevel: .debug, + message: message, + privacy: privacy, + file: file, + line: line, + column: column, + function: function + ) + } + + internal func logTrace( + _ message: String, category: String? = nil, privacy: PrettyLoggerPrivacy = .auto, + file: String, line: Int, column: Int, function: String + ) { + if level < .trace { + return + } + + 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, + line: Int, column: Int, function: String + ) -> String? { if level < .fatal { return nil } - return log(.fatal, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + return log( + .fatal, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - internal func logError(_ 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 } - return log(.error, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + return log( + .error, items: items, separator: separator, terminator: terminator, file: file, + line: line, column: column, function: function) } - internal func logWarning(_ 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 } - return log(.warn, 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) } - internal func logInfo(_ 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 } - return log(.info, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + return log( + .info, 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? { + 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 } - return log(.debug, items: items, separator: separator, terminator: terminator, file: file, line: line, column: column, function: function) + 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, + line: Int, column: Int, function: String + ) -> 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) } - private func log(_ logLevel: PrettyLoggerLevel, items: [Any], separator: String?, terminator: String?, file: String, line: Int, column: Int, function: String, date: Date = Date()) -> String? { + // 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, prettyLoggerLevel: PrettyLoggerLevel, message: String, + privacy: PrettyLoggerPrivacy, file: String, line: Int, column: Int, function: String + ) { + switch privacy { + case .auto: + logger.log(level: level, "\(message)") + case .public: + logger.log(level: level, "\(message, privacy: .public)") + 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( + _ 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 } @@ -86,7 +314,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))" 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 + } + } +} diff --git a/Tests/FormatOutputTests.swift b/Tests/FormatOutputTests.swift index e4868c6..9ba3985 100644 --- a/Tests/FormatOutputTests.swift +++ b/Tests/FormatOutputTests.swift @@ -1,8 +1,29 @@ import XCTest + @testable import PrettyLogger -class FormatOutputTests: XCTestCase { - func testOutputWithTwoParameters() { +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 = " ❎ " let output = logInfo("2", "3") diff --git a/Tests/LevelConfigurationTests.swift b/Tests/LevelConfigurationTests.swift index 5d2c9e7..6e5e4e0 100644 --- a/Tests/LevelConfigurationTests.swift +++ b/Tests/LevelConfigurationTests.swift @@ -1,8 +1,29 @@ import XCTest + @testable import PrettyLogger -class LevelConfigurationTests: XCTestCase { - func testLogOnAllLevels() { +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")) XCTAssertNotNil(logError("error")) @@ -12,7 +33,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNotNil(logTrace("trace")) } - func testLogOnDisableLogger() { + func testLegacyLogOnDisableLogger() { PrettyLogger.shared.level = .disable XCTAssertNil(logFatal("fatal")) XCTAssertNil(logError("error")) @@ -22,7 +43,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnFatalLevel() { + func testLegacyLogOnFatalLevel() { PrettyLogger.shared.level = .fatal XCTAssertNotNil(logFatal("fatal")) XCTAssertNil(logError("error")) @@ -32,7 +53,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnErrorLevel() { + func testLegacyLogOnErrorLevel() { PrettyLogger.shared.level = .error XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -42,7 +63,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnWarnLevel() { + func testLegacyLogOnWarnLevel() { PrettyLogger.shared.level = .warn XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -52,7 +73,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnInfoLevel() { + func testLegacyLogOnInfoLevel() { PrettyLogger.shared.level = .info XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -62,7 +83,7 @@ class LevelConfigurationTests: XCTestCase { XCTAssertNil(logTrace("trace")) } - func testLogOnDebugLevel() { + func testLegacyLogOnDebugLevel() { PrettyLogger.shared.level = .debug XCTAssertNotNil(logFatal("fatal")) XCTAssertNotNil(logError("error")) @@ -72,7 +93,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..457345a --- /dev/null +++ b/Tests/OSLogLevelConfigurationTests.swift @@ -0,0 +1,300 @@ +import Combine +import XCTest + +@testable import PrettyLogger + +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() + + // 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() + } + + 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..c88cc35 100644 --- a/Tests/OutputTests.swift +++ b/Tests/OutputTests.swift @@ -1,11 +1,32 @@ -import XCTest import Combine +import XCTest + @testable import PrettyLogger -class OutputTests: XCTestCase { +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 + } - func testOuputText() { + 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 PrettyLogger.shared.separator = " ❎ " @@ -20,9 +41,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..d14f2f2 --- /dev/null +++ b/Tests/QuickOutputTest.swift @@ -0,0 +1,174 @@ +import Combine +import XCTest + +@testable import PrettyLogger + +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() + } + + 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") + XCTAssertEqual(receivedOutputs[0].file, "QuickOutputTest.swift") + + // Check second output (with category) + XCTAssertEqual(receivedOutputs[1].level, .error) + XCTAssertEqual(receivedOutputs[1].message, "Test error message") + 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") + XCTAssertEqual(receivedOutputs[2].file, "QuickOutputTest.swift") + } + + 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) + + XCTAssertNotNil(receivedOutputs[0].file) // OSLog + XCTAssertNotNil(receivedOutputs[1].file) // Legacy + XCTAssertNotNil(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