In the previous episode, I showed you the setup I use to log messages with CocoaLumberjack. There's a lot more CocoaLumberjack has to offer. In this episode, you learn how to forward log messages to a remote server. I show you how to integrate CocoaLumberjack and PaperTrail, a popular solution for managing logs. We also take a look at an alternative approach, how to send log messages to Fabric. Logs can be very helpful in combination with crash reporting.

Integrating CocoaLumberjack and PaperTrail

I use PaperTrail for the Cocoacasts website and I'm quite happy with its capabilities. Most developers use it to forward log messages from web applications, but you can also use it for mobile development. Debugging issues in production that are difficult or impossible to reproduce can be frustrating. Logs can be very helpful in such situations. But that means you need access to those logs and that isn't straightforward for a production build. The solution is simpler than you might think, forwarding logs to a remote server. I don't recommend building a custom solution. There are several powerful options for managing logs. The solution I use in this episode is PaperTrail.

On Apple's platforms, PaperTrail depends on CocoaLumberjack. Let me show you how that works. Remember from the previous episode that the application forwards log messages to the terminal or Xcode's console through the DDTTYLogger class. Forwarding log messages to PaperTrail is very similar. We make use of a custom logger that we add to CocoaLumberjack.

Open the project's Podfile and add the PaperTrailLumberjack pod. I install the Swift subspec of the PaperTrailLumberjack pod since we're working on a Swift project.

target 'Cocoacasts' do
  platform :ios, '12.0'
  use_frameworks!
  inhibit_all_warnings!

  # Debugging
  pod 'CocoaLumberjack/Swift', '~> 3.5.2'
  pod 'PaperTrailLumberjack/Swift', '~> 0.1.9'

  # Wrappers
  pod 'KeychainAccess', '~> 3.2'
  pod 'ReachabilitySwift', '~> 4.3.0'

  # Development
  pod 'Reveal-SDK', configurations: ['Debug/Staging', 'Debug/Production']

  target 'CocoacastsTests' do
    inherit! :search_paths
  end

  target 'CocoacastsUITests' do
    inherit! :search_paths
  end
end

Open a terminal and run the bundle exec pod install command to install the dependencies of the project.

bundle exec pod install

Open Logger.swift, add an import statement for PaperTrailLumberjack, and navigate to the setup() method. To forward log messages to PaperTrail, we need to add an instance of the RMPaperTrailLogger class to CocoaLumberjack. We don't need to create an instance. We can access the singleton object through the sharedInstance() class method. We use optional binding to access the singleton object since sharedInstance() returns an optional.

// MARK: - Class Methods

class func setup() {
    // Configure TTY Logger
    DDTTYLogger.sharedInstance.logFormatter = LogFormatter()

    // Add TTY Logger
    DDLog.add(DDTTYLogger.sharedInstance, with: defaultLogLevel)

    // PaperTrail
    if let paperTrailLogger = RMPaperTrailLogger.sharedInstance() {

    }
}

We add the logger to CocoaLumberjack by passing it to the add(_:with:) method of the DDLog class.

// MARK: - Class Methods

class func setup() {
    // Configure TTY Logger
    DDTTYLogger.sharedInstance.logFormatter = LogFormatter()

    // Add TTY Logger
    DDLog.add(DDTTYLogger.sharedInstance, with: defaultLogLevel)

    // PaperTrail
    if let paperTrailLogger = RMPaperTrailLogger.sharedInstance() {
        // Add PaperTrail Logger
        DDLog.add(paperTrailLogger, with: defaultLogLevel)
    }
}

It isn't useful to send log messages to PaperTrail for debug builds so you may want to disable the PaperTrail logger after verifying your setup. It doesn't suffice to add the PaperTrail logger to CocoaLumberjack. We need to configure the PaperTrail logger because it needs to know which log destination it needs to send the log messages to. This is straightforward.

You create and configure a log destination on the PaperTrail website. PaperTrail shows you the host of the log destination as well as the port number. That's the only information we need to configure the PaperTrail logger. There are other options, which I won't cover in this episode. You can find detailed information on the PaperTrail website.

Configuring PaperTrail

Configuring PaperTrail

After setting the host and the port properties of the PaperTrail logger, we are ready to go.

// MARK: - Class Methods

class func setup() {
    // Configure TTY Logger
    DDTTYLogger.sharedInstance.logFormatter = LogFormatter()

    // Add TTY Logger
    DDLog.add(DDTTYLogger.sharedInstance, with: defaultLogLevel)

    // PaperTrail
    if let paperTrailLogger = RMPaperTrailLogger.sharedInstance() {
        // Configure PaperTrail Logger
        paperTrailLogger.port = <PORT>
        paperTrailLogger.host = <HOST>

        // Add PaperTrail Logger
        DDLog.add(paperTrailLogger, with: defaultLogLevel)
    }
}

Open the PaperTrail dashboard and run the application in the simulator or on a device. Log messages show up on PaperTrail after a few seconds.

Configuring PaperTrail

Integrating CocoaLumberjack and Fabric

I won't cover Fabric and Crashlytics integration in this episode. You can find detailed instructions on the Fabric website. I have already added the Crashlytics pod to the project's Podfile. Fabric is a dependency of Crashlytics, which means that Fabric is installed alongside Crashlytics when you install the project's dependencies.

target 'Cocoacasts' do
  platform :ios, '12.0'
  use_frameworks!
  inhibit_all_warnings!

  # Debugging
  pod 'CocoaLumberjack/Swift', '~> 3.5.2'
  pod 'PaperTrailLumberjack/Swift', '~> 0.1.9'

  # Wrappers
  pod 'KeychainAccess', '~> 3.2'
  pod 'ReachabilitySwift', '~> 4.3.0'

  # Crash Reporting
  pod 'Crashlytics', '~> 3.12.0'

  # Development
  pod 'Reveal-SDK', configurations: ['Debug/Staging', 'Debug/Production']

  target 'CocoacastsTests' do
    inherit! :search_paths
  end

  target 'CocoacastsUITests' do
    inherit! :search_paths
  end
end

There isn't a CocoaLumberjack logger available for Crashlytics, which means that we need to build one to integrate with Crashlytics. This isn't complex, though. Add a Swift file to the Logging group and name it CrashlyticsLogger.swift.

We start by adding import statements for Crashlytics and CocoaLumberjack. We define a final class, CrashlyticsLogger, that inherits from DDAbstractLogger. As the name implies, the DDAbstractLogger class is the base class for loggers.

import Crashlytics
import CocoaLumberjack

final class CrashlyticsLogger: DDAbstractLogger {

}

I'd like to use the log formatter we created in the previous episode. We define a private, constant property, _logFormatter, of type DDLogFormatter. We create and assign an instance of the LogFormatter class to this property. Remember that the LogFormatter class conforms to the DDLogFormatter protocol.

We override the logFormatter property of the DDAbstractLogger class. The getter returns the LogFormatter instance that is referenced by the _logFormatter property. We leave the setter empty. This may seem odd, but this is due to an issue in the CocoaLumberjack library. I won't go into the technical details in this episode.

import Crashlytics
import CocoaLumberjack

final class CrashlyticsLogger: DDAbstractLogger {

    // MARK: - Properties

    private let _logFormatter: DDLogFormatter = LogFormatter()

    override var logFormatter: DDLogFormatter? {
        get { return _logFormatter }
        set { }
    }

}

The only step that's left is overriding the log(message:) method. This method accepts one argument, the DDLogMessage instance. We covered the DDLogMessage class in the previous episode. We pass the log message to the log formatter to format it as defined in the LogFormatter class. To send it to Crashlytics, we pass the log message to the CLSLogv(_:_:) function, which is defined in the Crashlytics framework. The first argument is the formatted log message. The second argument may be a bit confusing. You only need to understand that we pass an empty array of arguments to the CLSLogv(_:_:) function.

import Crashlytics
import CocoaLumberjack

final class CrashlyticsLogger: DDAbstractLogger {

    // MARK: - Initialization

    override init() {
        super.init()

        // Set Log Formatter
        logFormatter = LogFormatter()
    }

    // MARK: - Overrides

    override func log(message logMessage: DDLogMessage) {
        guard let formattedLogMessage = logFormatter?.format(message: logMessage) else {
            return
        }

        CLSLogv(formattedLogMessage, getVaList([]))
    }

}

That's it. Open Logger.swift and navigate to the setup() method. We instantiate an instance of the CrashylitcsLogger class and pass it to the add(_:with:) method of the DDLog class.

// MARK: - Class Methods

class func setup() {
    // Configure TTY Logger
    DDTTYLogger.sharedInstance.logFormatter = LogFormatter()

    // Add TTY Logger
    DDLog.add(DDTTYLogger.sharedInstance, with: defaultLogLevel)

    // PaperTrail
    if let paperTrailLogger = RMPaperTrailLogger.sharedInstance() {
        // Configure PaperTrail Logger
        paperTrailLogger.port = <PORT>
        paperTrailLogger.host = <HOST>

        // Add PaperTrail Logger
        DDLog.add(paperTrailLogger, with: defaultLogLevel)
    }

    // Initialize Crasylytics Logger
    let crashlyticsLogger = CrashlyticsLogger()

    // Add Crashlytics Logger
    DDLog.add(crashlyticsLogger, with: defaultLogLevel)
}

The logs that are sent to Crashlytics are linked to the crash report on Crashlytics. This is useful to understand what happened moments before the problem took place. You can test the implementation by adding a few log statements, launching the application, and triggering a crash. This is what that looks like on Fabric.

Forwarding Log Messages to Fabric

We're currently sending every log message to Fabric. In production, the application only sends log messages with log level error to Fabric.

What's Next?

Having access to logs in production can save time and frustration. Debugging issues is much easier if you have more context, especially if the issue you're debugging is difficult or impossible to reproduce. Forwarding log messages to a remote server is a solution that isn't hard to set up if you take advantage of CocoaLumberjack.