On the Logging in Mobile Applications

Here I describe an approach from iOS point of view. An approach for Android platform is no different. I can recommend to use Timber framework as an equivalent for CocoaLumberjack.

Logs? Phew! Just write something like:

print("Expected `foo` to be 'bar', got: \(foo)")  

Later, when you debug an issue, the message in the console could give you a hint for the solution. Simple enough. But it’s good only for a “Hello World” application.

Why?


1st of all, logs should be available when you need them desperately. Having a large number of users is a guarantee that every possible crash will happen including those under really strange circumstances. Unable to reproduce the issue, with only a stack trace in your hands you would clearly benefit from an additional source of information — application logs. Is it possible to attach logs to a crash report? Yes. For example, Crashlytics Custom Logs can be used for that.

Let’s assume that in the 3rd party SDK, which you are using to collect crash reports there is a func cloudLog(_ msg: String) method (like CLSLogv method in Crashlytics). Say this method follows the Unix philosophy and the only thing it does is sending a provided message to the 3rd party no matter what the current build configuration is. But we would want to use this handy method only in production builds in order to avoid the noise from the 3rd party when debugging.

So it makes sense to introduce your own method like this:

func myLog(_ msg: String) {  
#if DEBUG
    print(msg)
#else
    cloudLog(msg)
#endif
}

Hmm, didn’t we just start writing our own logging framework?

Our new myLog(_:) method works for most cases. But what if the application you are working on handles sensitive information? Banking, dating or health apps are good examples. If you are logging payloads of your network requests and responses (that would be handy, so probably you are) via previously introduced myLog method you might be sending sensitive information to the 3rd party. For some organisations that’s a huge no-no.

So what can we do about that? Something like this should work:

func myLog(_ msg: String, isSensitive: Bool = true) {  
#if DEBUG
    print(msg)
#else
    if !isSensitive {
        cloudLog(msg)
    }
#endif
}

Hmm, myLog keeps growing…


2ndly, sometimes a desire to log is in itself an indication of something unexpected happening and usually an additional action should be taken in such a case.

To be more specific imagine your application is talking to your beloved server and suddenly a terrible thing happens — incompatible JSON is served. Shouldn’t we do the following?

myLog("Error, look how terrible that JSON is: \(json)")`  

Not bad. But the problem won’t be noticed unless your application crashes right after. A crash is better than nothing in such a case, but the user might benefit from an offline data or remaining parts of the working functionality. A much better option would be to silently report the problem, receive the report, fix the API, meanwhile showing “Something went wrong. Sorry! We are already working on it.” message to the user. Is there a ready solution to report such a problem? Yes. For example, Crashlytics Caught Exceptions can be used for that. I will reference those as non-fatals — issues that are not crashing the application but should be reported.

Let’s assume that there is a func reportNonFatalError(_ error: Error) method to report problems silently (like recordError in Crashlytics).

We don’t want to report the issue in debug builds (to avoid the noise), but we still want to log to the console a message describing the problem. As for reporting, reusing this log message looks like a good idea. So the straightforward way to do that is like this:

let message = "Error, look how terrible that JSON is: \(json)"  
#if DEBUG
myLog(message)  
#else
reportNonFatalError(makeError(withMessage: message))  
#endif

The above code looks too cumbersome. Much better is to rewrite our myLog method like this:

enum LogBehavior {  
    case writeToLog(isSensitive: Bool)
    case reportAsNonFatal
}
struct WrappedStringError: Error {  
    var value: String
    var localizedDescription: String {
        return value
    }
}
func myLog(_ msg: String, behavior: LogBehavior = .writeToLog(isSensitive: true)) {  
#if DEBUG
    print(msg)
#else
    switch behavior {
    case let .writeToLog(isSensitive):
        if !isSensitive {
            cloudLog(msg) 
        }
    case .reportAsNonFatal:
        reportNonFatalError(WrappedStringError(value: msg))
    }
#endif
}

Now myLog‘s signature seems too complicated and its implementation is like a trash bin full of 3rd party specific logic. Is there a better way? Yes!


We better use some logging framework. Something like CocoaLumberjack. The idea behind it is really simple, but powerful. On the launch of the application you plug in some loggers (you can create your own or select from the predefined ones). When you log the message, it is broadcasted to every logger you plugged in. It’s up to a particular logger how to process the message. Given the lack of restriction on the number of loggers you can plug in you can untangle logging spaghetti using single responsibility principle.

But let’s give some examples.

The most simple but useful logger is a console logger which just prints a message to the console.

You might want to use file logger which persists logs on disk and rotates them. File logger is quite useful for feedback functionality — ask the user if he want to attach logs when composing email to customer support. A problem description with recent log is much more helpful than just the description. Of course you should not store any sensitive data in such logs.

Passing log messages to cloud log or reporting non-fatals is a good enough job for a separate logger as well.

Okay. Seems pretty straightforward. But how can we pass the information to a logger if the message is sensitive or if it should be reported as a non-fatal?

To answer that question let’s look at how we actually log. We do it the same way as we did before when using standard print, but now we have to flag the message as error, warning, info, debug or verbose one. Too many of them, isn’t it a complication? A bit it is, but a handy one. Of course those types impose specific semantics on messages and could be used to filter or color them appropriately in the console. But they can be also used to make a decision on what particular logger should do with a particular message. For example, the non-fatals logger should ignore all the messages except for of error type.

No more words, just look at the following logging policy scheme:

Build Console Non-fatal Cloud Log When To Use
Error Debug

Something bad has happened that should never happen. App is not crashing but a part of the functionality is not working. The situation should be reported.

E.g., a wrong JSON format, parsing errors when a source format is unsupported, some system service is not available but we expect it to be.

Release
Warning Debug

Something bad has happened that should never happen. App is not crashing but a part of the functionality is not working. The situation should be reported.

E.g., a wrong JSON format, parsing errors when a source format is unsupported, some system service is not available but we expect it to be.

Release
Info Debug

Something expected happened. Messages of that level are useful to understand the reason for a crash or an unclear bug.

E.g., a view controller is presented or dismissed, user interactions, some service finished an initialization, a network request succeeded, a DB is saved successfully.

Release
Debug Debug Messages of that level are useful to log sensitive information such as a network response content if it’s really needed. Such log messages should not litter the console.
Release
Verbose Debug Messages that are littering the console, should be manually turned on.
Release

Important details:

  • log sensitive information only on debug and lower levels (that’s only verbose);

  • report only error messages as non-fatals;

That’s roughly the scheme we are using in TransferWise for our iOS application. Of course some amount of work is needed to setup such an approach for an existing application, but it will make the maintenance simpler. For applications that are suffocating from the tech debt it’s one of the first steps to do. But even if your application is pretty healthy, efforts made to keep a finger on the pulse are a thousand times worth it!

DDLogInfo(“Happy logging!")