mit platform Lang SPM compatible Tests codebeat badge
Simple console logger
- Four levels of severity: π΄ Error, π Warning, π΅ Debug, π£ Info
- Nine tag types: π‘ Network, π Database, π₯ UI, πΎ File, π Security, π Payment, βοΈ System, π§° Utility, π Other
- Output to console, file, or a custom endpoint like Google Analytics or Firebase Crashlytics
- Efficiently debug complex apps by filtering logs to avoid console clutter.
- Easily toggle logging for specific components like UI or database interactions.
- Send errors and warnings to external services like Google Analytics or Firebase Crashlytics for better monitoring.
Logger.debug("Network.connect - connection established successfully", tag: .net) // Output: [π΅ Debug] [2023εΉ΄12ζ24ζ₯ 22:00:45] β π‘ Network.connect: connection established successfully Logger.warning("Network.connect \(error.localizedDescription)", tag: .net) // Output: [π Warning] [2023εΉ΄12ζ24ζ₯ 22:00:45] β π‘ Network.connect: Wi-Fi is not turned on Logger.error("Network.processData \(error.localizedDescription)", tag: .net) // Output: [π΄ Error] [2023εΉ΄12ζ24ζ₯ 22:00:45] β π‘ Network.processData: Decoding was unsuccessful. Nothing was saved
// Configure the logger output format Logger.config = .plain // Options: .plain (no date), .full (includes date and verbose level) // Set the output transport method Logger.type = .console // Options: .console, .file(filePath: String), .custom(onLog: LogType.OnLog) // Define the logging mode for levels and tags Logger.mode = .everything // Options: .everything (all logs), .nothing (disable logging), .essential (warnings and errors only) // Convenient one-liner setup Logger.setup(config: .full, mode: .essential, type: .console)
// Define a custom logging function let onLog: LogType.OnLog = { msg, level, tag in // Only send warnings and errors to the custom endpoint if [.error, .warning].contains(level) { sendToAnalytics(msg, level: level, tag: tag) } } // Set the logger to use the custom output Logger.type = .custom(onLog) Logger.warn("User session expired", tag: .security) // This will be sent to the custom endpoint Logger.error("Failed to save data", tag: .db) // This will be sent to the custom endpoint Logger.info("User opened settings", tag: .ui) // This will not be sent
Note
Since iOS14+ Target apples own Logger class, write: os.Logger
If mesages in console.app only shows messages as private. Read the logger article on eon.codes on how to change that.
import os // Need to import os.Logger let logger = os.Logger(subsystem: "co.acme.ExampleApp", category: "ExampleApp") let onLog: LogType.OnLog = { msg, level, _ in logger.log("\(msg, privacy: .public)") // Reveals the redacted text from the message } Logger.type = .custom(onLog) // Add the custom output closure to the logger Logger.info("Something happened") // Prints to Console.app (filter by category or subsystem)
The Trace class can be combined with Logger to include function names, class names, and line numbers in your logs.
class Test { func myFunction() { Trace.trace("This msg") } } Test().myFunction() // Prints "This msg is called from function: myFunction in class: Test on line: 13"
Logger.warn("\(Trace.trace() - error occured", tag: .net) - error occured") // Called inside NetManager.connect // Prints: [οΈπ Warning] [23-12-24 22:00:45] β π‘ NetManager.connect - error occured
- Print only works when debugging an app. When the app is built for running, Swift.print doesn't work anymore. Use file logging in release if needed.
- Use the Telemetry for GA hook.
Add the following line to your Package.swift file:
.package(url: "https://github.com/sentryco/Logger", branch: "main")
Then add Logger as a dependency for your targets:
.target( name: "MyTarget", dependencies: [ .product(name: "Logger", package: "Logger"), ] ),
- Consider including the
Trace.trace()call in log call so it can be toggled on and off - Add the tag-type emoji to output just before the message
- Research how to log fatal crashes, if possible. Exception handling needs to be explored
- Conduct more research on logging best practices
- Add terminal color to formatting text: https://github.com/sushichop/Puppy/blob/main/Sources/Puppy/LogColor.swift
- Add native OS support: https://www.avanderlee.com/debugging/oslog-unified-logging/
- Test Firebase Crashlytics in a demo project
- Add support for oslog in the framework. We currently support it in the ad-hoc callback. Add this to unit test as well as instructions on Console.app usage and limitations.
- Consider adding another log type called "important"
- Add usage gif exploring system console, google-analytics, xcode consol
- Add problem / solution to readme
- Add a note about apples OS.Logger. And its limitations.
- Add protocol oriented design:
protocol LoggerProtocol { func log(message: String, level: LogLevel, tag: LogTag) } // Implementations class ConsoleLogger: LoggerProtocol { func log(message: String, level: LogLevel, tag: LogTag) { Swift.print(message) } } class FileLogger: LoggerProtocol { // File logging implementation... }
- Add console color codes:
extension String { enum ConsoleColor: String { case red = "\u{001B}[0;31m" case orange = "\u{001B}[0;33m" case blue = "\u{001B}[0;34m" case purple = "\u{001B}[0;35m" case reset = "\u{001B}[0;0m" } func colored(_ color: ConsoleColor) -> String { return "\(color.rawValue)\(self)\(ConsoleColor.reset.rawValue)" } } // Apply Colors in Logging: extension Logger { fileprivate static func formatMessage(_ msg: String, level: LogLevel, tag: LogTag) -> String { // Existing formatting code... var text = "[\(levelText)]" if config.showDate { let date = config.dateFormatter.string(from: Date()) text += " [\(date)]" } text += " β \(tag.rawValue)\(msg)" // Apply color based on log level switch level { case .error: text = text.colored(.red) case .warning: text = text.colored(.orange) case .debug: text = text.colored(.blue) case .info: text = text.colored(.purple) } return text } }
- Add Native OS Logging Support (os.log)
import os public enum LogType { // Existing cases... case osLog(OSLog = .default) } extension LogType { internal func log(msg: String, level: LogLevel, tag: LogTag) { switch self { // Existing cases... case let .osLog(logger): if #available(iOS 14.0, macOS 11.0, *) { logger.log("\(msg, privacy: .public)") } else { os_log("%{public}@", log: logger, type: .default, msg) } } } } // Configure logger to use osLog Logger.type = .osLog() // Log messages Logger.info("Application started", tag: .system)
- Implement Exception Handling for Fatal Crashes
// Set Up Exception Handler: func setUpExceptionHandler() { NSSetUncaughtExceptionHandler { exception in Logger.error("Uncaught exception: \(exception)", tag: .system) } } // Set Up Signal Handler: import Darwin func setUpSignalHandler() { signal(SIGABRT) { _ in Logger.error("Received SIGABRT signal", tag: .system) } signal(SIGILL) { _ in Logger.error("Received SIGILL signal", tag: .system) } // Add handlers for other signals as needed } // Call Handlers at App Launch: // In AppDelegate or main entry point func applicationDidFinishLaunching(_ application: UIApplication) { setUpExceptionHandler() setUpSignalHandler() // Other initialization code... } // Extend LogType: public enum LogType { // Existing cases... case crashlytics } // Implement Crashlytics Logging: extension LogType { internal func log(msg: String, level: LogLevel, tag: LogTag) { switch self { // Existing cases... case .crashlytics: Crashlytics.crashlytics().log(msg) if level == .error { let error = NSError(domain: Bundle.main.bundleIdentifier ?? "Logger", code: 0, userInfo: [NSLocalizedDescriptionKey: msg]) Crashlytics.crashlytics().record(error: error) } } } } // Usage Example: Logger.type = .crashlytics Logger.error("Critical failure", tag: .system)
Introduce a New Log Level "Important"
Description: Add a new log level for messages that are more significant than info but not quite warning.
Implementation:
- Add New Case in
LogLevel:
public enum LogLevel: String, CaseIterable { // Existing cases... case important = "π’" }
- Add Title for the New Level:
extension LogLevel { var title: String { switch self { // Existing cases... case .important: return "Important" } } }
- Add Method in
Logger+Command:
extension Logger { public static func important(_ msg: String, tag: LogTag = .other) { log(msg, level: .important, tag: tag) } }
- Usage Example:
Logger.important("User achieved a significant milestone", tag: .achievement)
Adopt Protocol-Oriented Design
Description: Refactor the logger to use protocols, allowing for greater flexibility, easier testing, and adherence to SOLID principles.
Implementation:
- Define
LoggerProtocol:
public protocol LoggerProtocol { func log(_ msg: String, level: LogLevel, tag: LogTag) }
- Create Concrete Implementations:
public class ConsoleLogger: LoggerProtocol { public func log(_ msg: String, level: LogLevel, tag: LogTag) { print(msg) } } public class FileLogger: LoggerProtocol { private let filePath: String public init(filePath: String) { self.filePath = filePath } public func log(_ msg: String, level: LogLevel, tag: LogTag) { // Implement file writing logic here } }
- Modify
Loggerto UseLoggerProtocol:
public final class Logger { public static var logger: LoggerProtocol = ConsoleLogger() // Modify log methods to use `logger.log(...)` }
- Usage Example:
Logger.logger = FileLogger(filePath: "/path/to/log.txt")
Logger.info("This will be logged to a file")
Add Support for Swift Concurrency (async/await)
Description: Modernize the logger to be compatible with Swift's async/await concurrency model.
Implementation:
extension Logger { public static func logAsync(_ msg: String, level: LogLevel, tag: LogTag) async { await withCheckedContinuation { continuation in DispatchQueue.global(qos: .utility).async { log(msg, level: level, tag: tag) continuation.resume() } } } } Task { await Logger.logAsync("Asynchronous log message", level: .info, tag: .system) }
Description: Ensure that logging is safe in multi-threaded environments, particularly when writing to shared resources like files.
Implementation:
- Use Serial Dispatch Queues:
extension LogType { private static let fileWriteQueue = DispatchQueue(label: "com.logger.fileWriteQueue") static func writeToFile(string: String, filePath: String) { fileWriteQueue.async { // File writing code... } } }