Logging, as well as remote crash and error reporting frameworks, have been around for a while now. The use of both these frameworks is quite different depending upon the case.
In this article, we will cover the uses of both of these types of frameworks, including problems in the release builds of our mobile apps and some suggested solutions. I also include a centralized framework which will help us avoid these problems and get the most out of logging and remote error reporting.
First, let’s define what exactly logging and error reporting frameworks do.
Ever used the log statements in Android or the print statements in iOS? They are logging frameworks. They allow us, the developers, to print pretty much anything in the console window of our IDEs.
And the list goes on.
The most common usage of logs is while debugging. Currently, all major IDEs come equipped with built in debuggers. It allows the developers to add breakpoints and navigate through the code. It also allows us to access the variable values while stepping through the code.
Still, a vast number of developers depend on the traditional logging approach! Don’t believe me? See these memes for yourself:
Apart from the loggers available by default in both Java and Swift, there are various log frameworks built on top of them. These frameworks extend the capabilities of loggers and their uses. Common examples are Timber (Android), Willow (iOS), and CocoaLumberjack (iOS).
Now that we have a fair idea about what logging frameworks are, let’s move on to crash and error reporting frameworks.
We use logs while an app is in development. Developers use them to access variable values at each stage, identify crashes, and debug the issue. Log outputs are visible in the IDE’s console.
So what about getting error and crash reports while the app is already in production?
Let’s consider a scenario: You have tested your app thoroughly on your device, then publish the app in its respective store. A few users complain about app crashes or functionalities not working on their device.
What would you do here?
Because there are a vast number of device manufacturers, operating systems, custom ROMs, and device sizes, it’s almost impossible to test an app across all of these permutations and combinations. This leaves room for possible errors in the production environment. But how can you debug such errors when you don’t have access to the physical device?
Thankfully, some tools let us do this. Firebase Crashlytics is a popular tool. Once integrated into an app, it automatically captures the app crash reports and saves them on the console. The developers can then easily access these log reports and debug the error.
It also allows us to capture non-fatal errors and logs from our app. These can be API error responses, catch exceptions, or whatever we wish to log.
If you’ll notice, there’s something common here in both these frameworks. You see, the main purpose of both logging frameworks and crash and error reporting frameworks is debugging errors. The primary difference is that one is used during development and the other in production.
Now that we have an understanding of both of these framework types and their uses, let’s learn about what problems we might face once we start using them in the traditional approach. Once we understand the problem, we’ll be in a better position to devise a solution.
If your mobile apps have gone through vulnerability assessment and penetration testing (VAPT), you might have come across this one vulnerability: “Log messages reveal sensitive information. Disable loggers in production builds.”
This is very common while in development. We log the API responses and catch errors and other variables. What we forget is how to remove these log commands before creating the production build.
If someone plugs their device into the computer and observes the logs printed in the console, they might be able to view everything that we have logged. This can include sensitive parameters, entire API responses, or other private information.
Even if we do remember to remove these log commands, we’ll have to remove or comment out these loggers manually across the entire source code. A hectic and repetitive process!
With the build-type of the app, whether it’s a release build or a debug, we can control which log statements need to be printed in the console and which can be ignored. Using this we can forget worrying about logging sensitive information in the production apps.
Most of our mobile apps are powered by data from remote APIs. If the expected data structure doesn’t match that of the API response coded in the app, the functionality dependent on it may fail.
But, when an app is in production and an API structure change like this happens, our app’s functionality won’t work. How would we know about such scenarios earlier, so that we can release a fix before it affects too many users? Do we keep monitoring the entire functionality of the app daily? Do we wait for someone to report?
No, we can’t do that! What we need is a process in which we can report and get notified of these issues as soon as possible.
Firebase Crashlytics, with its custom error reporting, provides a solution: We need to identify the level of our logs. Some can be just informational, some can be an error, some can be for debugging.
The API errors, for example, would fall in the “error” category. We can devise a logic in which we share the log statements with the correct level as “error” to our Firebase remote error reporting. In this way, we can track the non-fatal yet functionality-breaking issues and address them as quickly as possible.
But, does that mean we would have to write this code everywhere across the app? This takes us to our next problem…
Problems one and two have a few viable solutions: Adding build flags and using Firebase Crashlytics for remote error logging. But implementing them around each log statement wouldn’t be a good solution.
Our log statements are scattered across the entire app. While debugging, we end up releasing a flurry of log statements into our code. I know this because I’m guilty of doing it. We can’t go on adding our custom logic around each of these log statements.
Let’s look at it from a code maintainability perspective, too. What happens when we want to change the logic of our loggers? Do we go on changing it around every log statement across the entire codebase? No way! We code to make the lives of our users easier. Why not make ours too?
Now, the missing piece: We need all of our above solutions to work hand in hand. A single class that will control both the build-type-based and the log-level-based logs, and no repeated if-else logic around every log statement in the codebase. This will avoid code scattering and help in code maintainability and scalability.
Let’s build a framework around the log-levels and build-types, including which statements should be executed where and when.
Log Level | Log Level – Usage | Build Type | Console | Remote Log |
---|---|---|---|---|
Error | A non-fatal error has occurred, and caused the app’s functionality to break, e.g. a wrong JSON format. The app cannot parse this format and hence the functionality of the app stopped working. | Debug | ✔️ | |
Release | ✔️ | |||
Warning | An unexpected error has occurred in the app that shouldn’t have occurred in the first place, e.g. a device-specific exception in a function, or code moving into a catch block that wasn’t expected. | Debug | ✔️ | |
Release | ✔️ | |||
Info | Log messages added to observe the behavior of the app, e.g. screen opened or closed, the API call returned successfully, or DB queries returning success. | Debug | ✔️ | |
Release | ||||
Debug | Log messages that are added to debug a particular error, e.g. variable values or API responses values. | Debug | ✔️ | |
Release |
Now that we have the solution designed, let’s move ahead quickly and check the implementation of the same in both Android and iOS.
We’ll be using existing third-party logging frameworks that will help us create loggers based on the build type during runtime. For remote error reporting, we will be using Firebase Crashlytics. You can learn more about customizing your crash reports with Crashlytics here.
The blueprint for both implementations goes like this:
For creating build-type specific loggers, we will be using one of the best logging libraries in Android: Timber. If you are already using it, great! If not, I highly recommend using this in your projects. We will be creating our log-level-based error reporting framework using the capabilities that Timber provides.
Please note that I am skipping the integration details of Timber and Firebase Crashlytics. It is best described on their official pages, which I have linked in this section.
Let’s dive into creating our framework.
First, let’s implement the build-type logic in the framework initialization. We will be using two different loggers: One for debug mode, and the other for release. The release mode logger will be our custom one:
public class App extends Application { @Override public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { Timber.plant(new Timber.DebugTree()); } else { Timber.plant(new LoggingController()); } } }
Now, let’s implement our custom remote logger for the release mode, which we mentioned above. This will contain the log-level logic:
public class LoggingController extends Timber.Tree { @Override protected void log(int logLevel, String tag, @NonNull String message, Throwable t) { if (logLevel == Log.ERROR || logLevel == Log.WARN) { FirebaseCrashlytics.getInstance().recordException(t); }else{ return; } } }
Let’s check the example usage:
Timber.d("Test debug message"); Timber.i("Test info message"); Timber.w(new RuntimeException(), "Test warning message"); Timber.e(new RuntimeException(),"Test error message");
Instead of using Log.d()
or Log.e()
, we will now have to use the Timber.d()
or Timber.e()
. The rest will be handled by our framework!
In iOS, to implement build-type specific loggers, we will be using Willow. Created by Nike, it’s one of the best Swift implementations of a custom logger.
We will be creating our log-level-based error reporting framework using the capabilities that Willow provides.
Please note that, like with our previous Android implementation, I am skipping the integration details of Willow and Firebase Crashlytics. It is best described on their official pages, which I have linked previously in this article.
Let’s dig straight into creating our framework.
First, let’s implement the build-type logic in the framework configuration. We will be using two different loggers: One for debug mode and the other for release. The release mode logger will be our custom one:
var logger: Logger! public struct LoggingConfiguration { func configure() { #if DEBUG logger = buildDebugLogger() #else logger = buildReleaseLogger() #endif } private func buildReleaseLogger() -> Logger { let consoleWriter = LoggingController.sharedInstance let queue = DispatchQueue(label: "serial.queue", qos: .utility) return Logger(logLevels: [.error,.warn], writers: [consoleWriter],executionMethod: .asynchronous(queue: queue)) } private func buildDebugLogger() -> Logger { let consoleWriter = ConsoleWriter() return Logger(logLevels: [.all], writers: [consoleWriter], executionMethod: .synchronous(lock: NSRecursiveLock())) } }
Now, let’s implement our custom remote logger for the release mode, which we mentioned above. This will have the log-level logic:
open class LoggingController: LogWriter{ static public var sharedInstance = LoggingController() static public var attributeKey = "error" private init(){} public func writeMessage(_ message: String, logLevel: LogLevel) { // Since this is a release logger, we won't be using this... } public func writeMessage(_ message: LogMessage, logLevel: LogLevel) { if logLevel == .error || logLevel == .warn{ if let error = message.attributes[LoggingController.attributeKey] as? Error{ Crashlytics.crashlytics().record(error: error) } } } } extension Error{ func getLogMessage()->LogMessage{ return ErrorLogMessage(name: "Error", error: self) } } struct ErrorLogMessage: LogMessage { var name: String var attributes: [String: Any] init(name:String,error:Error) { self.name = name self.attributes = [LoggingController.attributeKey:error] } }
We’ll have to initialize this framework in AppDelegate
:
class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { FirebaseApp.configure() LoggingConfiguration().configure() return true } }
You can see the example usage here:
// Debug Logs logger.debugMessage("Logging Debug message") // Info Logs logger.infoMessage("Logging Info message") // Error & Warning Logs let logMessage = getSampleErrorObj().getLogMessage() logger.error(logMessage) func getSampleErrorObj()->Error{ let userInfo = [] // You can add any relevant error info here to help debug it return NSError.init(domain: NSCocoaErrorDomain, code: -1001, userInfo: userInfo) }
So instead of using the traditional print()
command, we would now have to use the logger.debugMessage()
or logger.error()
, for example. Everything else is handled by our framework!
We did it! We built our remote error reporting and logging framework. Well, not exactly a framework, but more like a “wrapper” framework that extends upon the capabilities of existing libraries.
Because this is our custom implementation, and the entire logic resides in a single controller, we can extend its capability any time to add more filters and enhance our loggers. This should also keep our code clean and help with maintainability.
I hope you learned something new and useful today. Keep learning and building, and happy logging!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Logging and remote error reporting in mobile apps"
Hi Nabil, thanks for your post.
I have a question about the Android example: in order to get the log message remotely sent via Firebase, shouldn’t you do like this?
Timber.e(new RuntimeException(“Test error message”),”Test error message”);
It seems to me that, if you do not add the message to the Throwable, the text is not sent to Firebase…
Thank you in advance for your reply.
Filippo