A well-placed log message can save hours of debugging
In the world of software development, efficient logging is crucial for maintaining and debugging applications effectively. It helps developers understand the flow of code execution, troubleshoot issues, and monitor the application’s behavior.
However, managing logs effectively can be challenging, especially in complex projects. That’s where CocoaLumberjack comes in, with a fast & simple, yet powerful & flexible logging framework for macOS, iOS, tvOS, and watchOS.
In this article, we will explore how to leverage CocoaLumberjack in a Swift project to efficiently handle logs, enhance debugging capabilities, and optimize performance.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
CocoaLumberjack is widely recognized and highly regarded in the iOS and macOS development communities for its flexibility, performance, and rich set of features.
It offers a robust solution for managing logs in applications of all sizes and complexities. Whether we’re building a simple utility app or a large-scale enterprise application, it can help us to streamline our logging process and gain valuable insights into our code.
It offers several key benefits that make it a popular choice among developers:
So, let’s dive into the world of efficient logging with CocoaLumberjack and discover how it can elevate our development process!
CocoaLumberjack can be installed in our project using either CocoaPods or Swift Package Manager (SPM) or with Carthage. For further information on installation, you can check out the documentation of its Repo.
After this, we will be ready to start using CocoaLumberjack in our project!
In this, we will explore the fundamental aspects of configuring logging with understanding log levels, creating and adding loggers, and setting the log level for loggers.
CocoaLumberjack provides several predefined log levels that play a crucial role in categorizing the importance and severity of log messages. It is essential for effectively managing and filtering log output.
By these levels, we can control the verbosity of the log output and focus on the relevant information during debugging and troubleshooting.
Loggers are responsible for handling log messages and directing them to the desired output destinations.
CocoaLumberjack provides various built-in loggers, such as console loggers, file loggers, and more. Let’s explore the process of creating and adding a logger, specifically a console logger.
let consoleLogger = DDOSLogger.sharedInstance
The DDOSLogger
is a built-in console logger that directs log messages to the console output, which means it can store all console-written logs.
After that, we need to add it to the CocoaLumberjack logging system. This allows the logger to receive and process log messages. we can do this using the DDLog
class as follows:
DDLog.add(consoleLogger)
let fileLogger = DDFileLogger() // Create a file logger instance
DDLog.add(fileLogger) // Add the file logger to the logging system
The DDFileLogger
is a built-in file logger that writes log messages to a file on the device's file system.
CocoaLumberjack allows us to add multiple loggers to the logging system, which enables us to send log messages to different output destinations simultaneously.
For example, we can add a file logger to write log messages to a log file in addition to the console logger, from which we can have the flexibility to direct log messages to different outputs based on our needs.
After creating and adding a logger, we can configure the log level for that particular logger.
Let’s consider an example of creating and adding a console logger with a specific log level:
let consoleLogger = DDOSLogger.sharedInstance
let fileLogger = DDFileLogger() // Create a file logger instance
DDLog.add(consoleLogger, with: .debug)
DDLog.add(fileLogger, with: .error)
By setting different log levels for the different loggers, we can control the log output and direct specific log messages to different output destinations parallelly.
Log formatters in CocoaLumberjack allow us to customize the appearance and structure of log messages by formatting them according to our specific requirements to make them more readable and meaningful.
Example:
To create a custom log formatter in CocoaLumberjack, we need to implement the DDLogFormatter
protocol. This protocol defines methods that enable us to customize the log message format, timestamp, log level, and any other additional information we want to include.
Here's an example of a custom log formatter:
class MyLogFormatter: NSObject, DDLogFormatter {
let dateFormatter: DateFormatter
override init() {
dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
super.init()
}
func format(message logMessage: DDLogMessage) -> String? {
let timestamp = dateFormatter.string(from: logMessage.timestamp)
let logLevel = logMessage.level
let logText = logMessage.message
return "\(timestamp) [\(logLevel)] - \(logText)"
}
}
Once we define our custom log formatter, we can apply it to our loggers.
let consoleLogger = DDOSLogger.sharedInstance
let customFormatter = MyLogFormatter()
consoleLogger.logFormatter = customFormatter
By creating and applying a custom log formatter, we have the flexibility to control the appearance and structure of our log output to make it more readable.
To add contextual information to logs in CocoaLumberjack, we can utilize the concept of logging contexts.
A logging context represents a specific context or category of log messages. It can be any value that helps us to identify and differentiate log events.
For example, we can use a user ID, a session ID, or any other relevant identifier as a context value.
Here’s an example of adding contextual information to logs using logging contexts:
let consoleLogger = DDOSLogger.sharedInstance
let fileLogger = DDFileLogger() // Create a file logger instance
// Adding loggers with different contexts
DDLog.add(consoleLogger, with: .debug, context: 123) // Console logger with context value 123
DDLog.add(fileLogger, with: .error, context: 456) // File logger with context value 456
This can help analyze logs from different parts of our application or when we want to filter or search for specific log events based on their contexts.
Logs are the breadcrumbs left behind in the software forest. Follow them to find the root cause of bugs and understand user behavior.
Now you might be wondering how exactly this logging can be used in our view or ViewModel functions or in debugging.
Let’s add a new file DDFileLoggerProvider.swift
that will only work for file logs.
First, have to configure the logger for the console and files to differentiate the debug and production logging as we have discussed before.
public func addDDLoggers() {
#if DEBUG
DDLog.add(DDOSLogger.sharedInstance) // console logger
#else
let fileLogger: DDFileLogger = DDFileLogger() // File Logger
fileLogger.rollingFrequency = 0
fileLogger.maximumFileSize = 1 * 1024 * 1024
fileLogger.logFileManager.maximumNumberOfLogFiles = 2
DDLog.add(fileLogger)
#endif
}
This method is setting up the logger for the debug and release app along with providing rolling frequency, maximum file size, and the number of files.
Call this method in AppDelegate’s didFinishLaunchingWithOptions
method before calling or setting anything.
Now, consider if we are working with a controller and we want to check its life cycle with logs, then how will we add logs?
import CocoaLumberjack
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Log a simple message
DDLogDebug("View loaded")
// Log a formatted message with additional information
let count = 10
let user = "ABC User"
DDLogInfo("Count: \(count), User: \(user)")
// Log an error message
let error = NSError(domain: "com.example.app", code: 404, userInfo: [NSLocalizedDescriptionKey: "Resource not found"])
DDLogError("Error: \(error.localizedDescription)")
}
}
func makeAPIRequest() {
let endpoint = "https://api.example.com/data"
AF.request(endpoint).responseDecodable(of: YourResponseModel.self) { response in
switch response.result {
case .success(let data):
DDLogDebug("API Response: \(data)")
case .failure(let error):
DDLogError("API Request Failed: \(error.localizedDescription)")
}
}
}
If we don’t want to add the import statement import CocoaLumberjack
in each file where we add logs, then we can create a middle way.
public class DDFileLoggerProvider {
public init() { }
public func provideLogger() -> DDFileLogger {
return DDFileLogger()
}
}
public func LogD(_ message: DDLogMessageFormat) {
return DDLogVerbose(message)
}
public func LogE(_ message: DDLogMessageFormat) {
return DDLogError(message)
}
public func LogW(_ message: DDLogMessageFormat) {
return DDLogWarn(message)
}
public func LogI(_ message: DDLogMessageFormat) {
return DDLogInfo(message)
}
We only need to call its init when the app loads to set up the logger after that we can call the logging methods very easily.
Let’s refine our function’s debug logs.
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Log a simple message
LogD("View loaded")
// Log a formatted message with additional information
let count = 10
let user = "ABC User"
LogI("Count: \(count), User: \(user)")
// Log an error message
let error = NSError(domain: "com.example.app", code: 404, userInfo: [NSLocalizedDescriptionKey: "Resource not found"])
LogE("Error: \(error.localizedDescription)")
}
}
Integrating CocoaLumberjack with networking libraries (e.g., Alamofire):
When working with network requests, it’s essential to log all the relevant information for debugging and troubleshooting purposes like Alamofire to capture and log network request and response details.
To integrate CocoaLumberjack with Alamofire, we can leverage Alamofire’s EventMonitor
protocol and its API. It allows us to intercept various events during a network request's lifecycle.
Here's an example of how we can integrate CocoaLumberjack with Alamofire using an event monitor:
First, make sure we have both CocoaLumberjack and Alamofire installed in our project using CocoaPods or Swift Package Manager.
import Alamofire
import CocoaLumberjack
class NetworkLogger: EventMonitor {
func requestDidResume(_ request: Request) {
let message = "⚡️ Alamofire Request Started: \(request)"
LogI(message)
}
func request(_ request: DataRequest, didParseResponse response: DataResponse<Data?, AFError>) {
if let data = response.data,
let requestURL = request.request?.url {
let responseString = String(data: data, encoding: .utf8) ?? ""
let message = "⚡️ AlamofireResponse Received from \(requestURL): \(responseString)"
LogD(message)
}
}
func requestDidFinish(_ request: Request) {
LogI("⚡️ Alamofire Response: \(request.response?.statusCode ?? -1)"
+ " -> \(request.response?.url?.absoluteString ?? "No response")"
)
}
}
Note: You might have noticed that I’ve added logs with the same prefix “⚡️ Alamofire”, to easily identify network-related logs.
Crashlytics is a crash reporting and analytics tool provided by Firebase. It helps us to track, analyze, and diagnose crashes that occur in our application.
I’m assuming you have Firebase Crashlytics integration in your project, otherwise, you can follow its guideline for setup which is used to leverage Crashlytics’ logging capabilities.
By default, Crashlytics automatically captures crash reports and collects log data. Crashlytics saves the logs generated by CocoaLumberjack and crash information when a crash occurs.
If we want to log other level logs specially Error level logs to Crashlytics then we can do it with CocoaLumberjack.
Here’s an example of how we can log messages to Crashlytics using CocoaLumberjack automatically with DDLog, follow these steps:
First, we have to create a custom class of DDAbstractLogger
for Crashlytics logging which only logs Error-level logs.
class DDCrashlyticsLogger: DDAbstractLogger {
let firebaseCrashlytics = Crashlytics.crashlytics()
override func log(message logMessage: DDLogMessage) {
if logMessage.level == .error {
firebaseCrashlytics.log(logMessage.message)
}
}
}
Now, we need to add the logger class to the CocoaLumberjack logging system with DDLog
.
DDLog.add(DDCrashlyticsLogger()) // Crashlytics logger
Note: We only need to add this logging for the production build, not for the debug build.
By following these steps, the project logs generated by CocoaLumberjack will be automatically captured and sent to Crashlytics. You can view and analyze these logs in the Crashlytics dashboard, alongside crash reports and other relevant information.
Remember to consult the official documentation provided by Firebase and Crashlytics for the most up-to-date and accurate integration instructions.
Keep Logging… Keep Coding !!! 🤖🍻
Logging is not just for errors; it’s a window into the soul of your code.
CocoaLumberjack is a robust logging framework that empowers developers to effectively manage and analyze logs in their Swift projects. Its versatility, performance, and extensibility make it an ideal choice for logging needs.
By implementing the techniques discussed in this article, you can take full advantage of CocoaLumberjack’s capabilities and enhance your app development workflow.
Remember to always tailor your logging approach to your specific project requirements and best practices. Logging is a powerful tool when used effectively, providing valuable insights and aiding in the development and maintenance of robust and reliable apps.
Hope you have a solid understanding of how to leverage CocoaLumberjack to maximize the benefits of logging in your Swift projects.
Whether you need...