iOS — How to Setup Logging Correctly with CocoaLumberjack

Explore the Endless Possibilities of Effectively Collecting Logs, Including Writing to Files and Crashlytics
Jun 7 2023 · 9 min read

Background

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!

Introduction

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:

  1. Multiple Log Levels: CocoaLumberjack supports various log levels, including verbose, debug, info, warning, and error. These levels allow us to categorize logs based on their importance and severity, making it easier to filter and focus on specific types of logs during debugging or troubleshooting.
  2. Dynamic Log Levels: With CocoaLumberjack, we can dynamically change log levels at runtime, providing flexibility in adjusting the level of detail in our logs without modifying the code.
  3. Scalability and Performance: CocoaLumberjack is designed to handle high-volume logging efficiently, utilizing an optimized and asynchronous logging architecture to minimize performance impact.

So, let’s dive into the world of efficient logging with CocoaLumberjack and discover how it can elevate our development process!

Getting Started with CocoaLumberjack

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!

Configuring Logging Basics

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.

Understanding Log Levels and Their Significance

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.

  1. Verbose: The least critical log level, used for detailed and diagnostic information that may be helpful during development but is typically not required(disabled) in production.
  2. Debug: Used for messages that assist in debugging and understanding the flow of the code which may contain more detailed information than verbose logs and are usually disabled in production.
  3. Info: Used for general information about the application’s execution flow. This can help to track the overall behavior of the application and provide high-level insights.
  4. Warning: Indicates potential issues or conditions that may lead to problems but do not disrupt the application’s functionality.
  5. Error: The most critical log level, reserved for logging errors or exceptional conditions that require immediate attention.

By these levels, we can control the verbosity of the log output and focus on the relevant information during debugging and troubleshooting.

Creating and Adding Loggers

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.

  • To create a console logger, we can use the following code snippet:
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)
  • To add a file logger, we can use the following code snippet:
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.

Setting the Log Level for Loggers

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.

Customizing Log Output

Introduction to the log formatters and their purpose

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.

Adding Contextual Information to Logs

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.

A Simple Use Case

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.

Write to file with max size

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)")
        }
    }
}

Simplify logging methods

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)")
    }
}

Advanced Logging Techniques

A. Logging Network Requests

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.

B. Crash and Error Reporting to Crashlytics

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 !!! 🤖🍻

To Conclude

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.


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development

canopas-logo
We build products that customers can't help but love!
Get in touch

Whether you need...

  • *
    High-performing mobile apps
  • *
    Bulletproof cloud solutions
  • *
    Custom solutions for your business.
Bring us your toughest challenge and we'll show you the path to a sleek solution.
Talk To Our Experts
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.