English | δΈζ
A powerful yet simple logging library for Swift 6, providing comprehensive cross-platform logging with enhanced backends, configurable output levels, and seamless Apple ecosystem integration.
- Log Levels: Supports
.debug
,.info
,.warning
, and.error
levels with intelligent filtering - Cross-Platform: Full support for Apple platforms, Linux, Android (experimental), and other Unix-like systems
- Thread Safety: Utilizes
DispatchQueue
for thread-safe asynchronous logging - Environment Configurable: Flexible logging control via environment variables
- Configurable Verbosity: Silent, minimal, standard, and detailed output levels
- Smart Output Routing: Choose between stdout/stderr for proper log separation
- ANSI Colors: Beautiful colored output with automatic terminal detection
- Cross-Platform Optimization: Optimized buffering and flushing for Linux/Unix systems
- Privacy Support: Automatic privacy annotations for iOS 14.0+
- Enhanced Warning Levels: Optional fault-level mapping for critical warnings
- Input Validation: Robust error handling and configuration validation
- Intelligent Fallback: Automatic fallback to console logging on older platforms
- Custom Backends: Easily create custom log backends by conforming to
LoggerBackend
- Protocol-Oriented Design: Clean abstractions for testing and mocking
- MockLogBackend: Powerful testing utilities with thread-safe log capture and inspection
- Comprehensive Documentation: Built-in Claude Code development guidance
- Swift 6.0+
- Apple Platforms: iOS 14.0+, macOS 11.0+, watchOS 7.0+, tvOS 14.0+, visionOS 1.0+
- Other Platforms: Linux, Android (experimental via Swift Android SDK), other Unix-like systems with Swift support
Add the package to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/fatbobman/SimpleLogger.git", from: "0.7.0")
]
Then import the module in your code:
import SimpleLogger
import SimpleLogger
// Default logger - OSLog on Apple platforms, Console elsewhere
let logger: LoggerManagerProtocol = .default(subsystem: "com.yourapp", category: "main")
// Log at different levels
logger.debug("Debugging information")
logger.info("App started successfully")
logger.warning("Potential issue detected")
logger.error("Critical error occurred")
// Standard OSLog integration
let logger: LoggerManagerProtocol = .default(subsystem: "com.yourapp", category: "networking")
// Enhanced OSLog with fault-level warnings
let criticalLogger = LoggerManager(backend: OSLogBackend(
subsystem: "com.yourapp.security",
category: "auth",
enhancedWarnings: true // Maps warnings to .fault for better visibility
))
// Production-ready console logger
let serverLogger = LoggerManager.console(
subsystem: "MyServer",
category: "API",
verbosity: .standard,
useStderr: true, // Send logs to stderr
enableColors: false // Disable colors for log files
)
// Development console logger
let devLogger = LoggerManager.console(
verbosity: .detailed, // Full metadata
enableColors: true // Colored output
)
// Android-specific configuration
// Note: ANSI colors are automatically disabled on Android
let androidLogger = LoggerManager.console(
subsystem: "AndroidApp",
category: "Main",
verbosity: .standard,
useStderr: true // Recommended for Android logging
)
// Silent mode - no output
let silentLogger = LoggerManager.console(verbosity: .silent)
// Minimal - message only
let minimalLogger = LoggerManager.console(verbosity: .minimal)
// Output: "Hello, World!"
// Standard - timestamp + level + message
let standardLogger = LoggerManager.console(verbosity: .standard)
// Output: "2024-06-22 10:30:15 [INFO] Hello, World!"
// Detailed - full metadata (default)
let detailedLogger = LoggerManager.console(verbosity: .detailed)
// Output: "2024-06-22 10:30:15 [INFO] MyApp[Category] Hello, World! in main() at main.swift:10"
// Automatic color detection
let colorLogger = LoggerManager.console(enableColors: true)
// Colors by log level:
// π DEBUG: Gray
// βΉοΈ INFO: Cyan
// β οΈ WARNING: Yellow
// β ERROR: Red
// Production - disable colors for log files
let prodLogger = LoggerManager.console(enableColors: false)
// Note: Colors are not supported on Android platform
# Disable all logging
DisableLogger=true ./myapp
# Custom environment keys
DISABLE_NETWORK_LOGS=true ./myapp
// Custom environment key
let logger = LoggerManager(backend: ConsoleLogBackend(
subsystem: "NetworkService",
environmentKey: "DISABLE_NETWORK_LOGS"
))
// Multiple loggers with different controls
let apiLogger = LoggerManager(backend: OSLogBackend(
subsystem: "com.app.api",
category: "requests",
environmentKey: "DISABLE_API_LOGS"
))
Create powerful custom backends by conforming to LoggerBackend
:
struct FileLoggerBackend: LoggerBackend {
let subsystem: String
let category: String
private let fileURL: URL
init(subsystem: String, category: String, logFile: URL) {
self.subsystem = subsystem
self.category = category
self.fileURL = logFile
}
func log(level: LogLevel, message: String, metadata: [String: String]?) {
let timestamp = ISO8601DateFormatter().string(from: Date())
let logLine = "[\(timestamp)] [\(level.rawValue.uppercased())] \(message)\n"
try? logLine.data(using: .utf8)?.write(to: fileURL, options: .atomic)
}
}
// Usage
let fileLogger = LoggerManager(backend: FileLoggerBackend(
subsystem: "com.app.file",
category: "storage",
logFile: URL(fileURLWithPath: "/var/log/myapp.log")
))
import SimpleLogger
class AppDelegate {
// Main app logger with enhanced warnings for critical issues
private let appLogger = LoggerManager(backend: OSLogBackend(
subsystem: "com.mycompany.myapp",
category: "lifecycle",
enhancedWarnings: true
))
// Network layer with separate category
private let networkLogger = LoggerManager.default(
subsystem: "com.mycompany.myapp",
category: "network"
)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
appLogger.info("App launched successfully")
return true
}
}
import SimpleLogger
class AndroidApp {
// Console logger configured for Android
private let logger = LoggerManager.console(
subsystem: "com.example.androidapp",
category: "main",
verbosity: .standard,
useStderr: true, // Recommended for Android
enableColors: false // Colors automatically disabled on Android
)
func onCreate() {
logger.info("Android app started")
// The logger works seamlessly with Android's logging system
logger.debug("Debug information")
logger.warning("Warning message")
logger.error("Error occurred")
}
}
import SimpleLogger
class WebServer {
// Access logs to stdout for log aggregation
private let accessLogger = LoggerManager.console(
subsystem: "WebServer",
category: "Access",
verbosity: .minimal,
useStderr: false, // stdout for access logs
enableColors: false // no colors in production
)
// Error logs to stderr with colors for development
private let errorLogger = LoggerManager.console(
subsystem: "WebServer",
category: "Error",
verbosity: .detailed,
useStderr: true, // stderr for errors
enableColors: true
)
func handleRequest(_ request: Request) {
accessLogger.info("\\(request.method) \\(request.path) - \\(request.clientIP)")
do {
try processRequest(request)
} catch {
errorLogger.error("Request failed: \\(error)")
}
}
}
SimpleLogger provides a powerful MockLogBackend
for comprehensive testing scenarios:
import Testing
import SimpleLogger
@Test func userServiceLogsCorrectly() async throws {
// Create a mock logger to capture log calls
let mockLogger = MockLogBackend()
let userService = UserService(logger: mockLogger)
// Perform the operation
await userService.createUser(name: "John")
// Verify logging behavior
#expect(mockLogger.hasInfoLogs)
#expect(mockLogger.logCount(for: .info) == 1)
#expect(mockLogger.hasLog(level: .info, containing: "User created"))
// Check specific log details
let lastLog = mockLogger.getLastLog()
#expect(lastLog?.level == .info)
#expect(lastLog?.message.contains("John"))
}
@Test func asyncLoggingTest() async throws {
let mockLogger = MockLogBackend()
let service = AsyncService(logger: mockLogger)
// Start async operation
Task {
try await Task.sleep(nanoseconds: 50_000_000) // 50ms delay
service.processData()
}
// Wait for the async log to appear
let found = await mockLogger.waitForLog(
level: .info,
containing: "Processing complete",
timeout: 0.2
)
#expect(found)
}
@Test func logSequenceVerification() async throws {
let mockLogger = MockLogBackend()
let workflow = DataWorkflow(logger: mockLogger)
await workflow.execute()
// Verify the exact sequence of log levels
let expectedSequence: [LogLevel] = [.info, .debug, .info, .warning]
#expect(mockLogger.verifyLogSequence(expectedSequence))
// Verify last few logs only
#expect(mockLogger.verifyLastLogs([.info, .warning]))
}
@Test func patternMatchingTest() async throws {
let mockLogger = MockLogBackend()
let apiClient = APIClient(logger: mockLogger)
await apiClient.makeRequest(url: "https://api.example.com/users/123")
// Complex pattern matching
let hasAPICall = mockLogger.hasLogMatching(level: .info) { message in
message.contains("API request") && message.contains("users/123")
}
#expect(hasAPICall)
// Check for specific error patterns
if mockLogger.hasErrorLogs {
let hasTimeoutError = mockLogger.hasLogMatching(level: .error) { message in
message.lowercased().contains("timeout")
}
// Handle timeout-specific test logic
}
}
- Thread-Safe: Uses Swift 6
Synchronization.Mutex
for concurrent access - Comprehensive Inspection: Check logs by level, content, patterns, and sequences
- Async Support:
waitForLog()
method for testing asynchronous operations - Cross-Platform: Available on all platforms supporting Swift 6
- Debug-Only: Only compiled in DEBUG builds to avoid production overhead
// Quick level checks
mockLogger.hasDebugLogs // Bool
mockLogger.hasInfoLogs // Bool
mockLogger.hasWarningLogs // Bool
mockLogger.hasErrorLogs // Bool
// Counting and filtering
mockLogger.totalLogCount // Int
mockLogger.logCount(for: .info) // Int
mockLogger.getLogMessages(for: .error) // [String]
mockLogger.allLogMessages // [String]
// Content searching
mockLogger.hasLog(level: .info, containing: "substring")
mockLogger.hasLogMatching(level: .error) { $0.contains("timeout") }
// Sequence verification
mockLogger.verifyLogSequence([.debug, .info, .warning])
mockLogger.verifyLastLogs([.info, .error])
// Async testing
await mockLogger.waitForLog(level: .info, containing: "complete", timeout: 1.0)
// Maintenance
mockLogger.clearLogs() // Clear all captured logs
mockLogger.getLastLog() // (level, message)?
For simpler testing scenarios, you can also create a custom logger:
struct TestLogger: LoggerManagerProtocol {
let expectation: @Sendable (String, LogLevel) -> Void
func log(_ message: String, level: LogLevel, file: String, function: String, line: Int) {
expectation(message, level)
}
}
@Test func simpleLoggingTest() async throws {
var capturedLog: (String, LogLevel)?
let testLogger = TestLogger { message, level in
capturedLog = (message, level)
}
let service = SimpleService(logger: testLogger)
service.doSomething()
#expect(capturedLog?.0.contains("Action completed"))
#expect(capturedLog?.1 == .info)
}
// Use reverse DNS for subsystems
let logger = LoggerManager.default(
subsystem: "com.yourcompany.yourapp", // Unique identifier
category: "authentication" // Functional area
)
// Examples of good category names:
// - "network", "database", "ui", "auth", "payment"
// - "lifecycle", "performance", "security"
// β
Good - Conditional expensive operations
if logger.isLoggingEnabled(for: .debug) { // Hypothetical API
let expensiveDebugInfo = generateDetailedReport()
logger.debug("Debug report: \\(expensiveDebugInfo)")
}
// β
Good - Simple messages are fine
logger.info("User logged in: \\(userID)")
// β Avoid - Expensive string interpolation for debug messages in production
logger.debug("Complex calculation result: \\(performExpensiveCalculation())")
// β
Good - Don't log sensitive information
logger.info("User authentication successful for user: \\(userID)")
// β Bad - Logging sensitive data
logger.info("User logged in with password: \\(password)") // Never do this!
// β
Good - Use appropriate log levels
logger.error("Authentication failed") // Error level for failures
logger.warning("Unusual login pattern detected") // Warning for suspicious activity
logger.info("User session started") // Info for normal operations
logger.debug("JWT token validation steps") // Debug for development only
This project is licensed under the MIT License. See the LICENSE file for details.