Overview

Logs are very useful while debugging an issue and finding a root cause for it. I would write tons of garbage logs lines such as “inside save image method”, “user clicked on button send” or something like “image-Id=83488131". I would delete all these stuff as soon as I know what is the issue, and ready to commit changes.

But, having too much verbosity in logs is equally bad and pollutes the log cat. Yeah, Logs are useful daily but if done properly. Here are a few tips and suggestion that I learned the hard way that would help you up the logging game.

Wrap it like a present

How do you use logging statements? Do you use inbuilt Log class and its methods like Log.d(), Log.e(), Log.i()? It is fine and all, But is it flexible? No.

Always create a wrapper class that wraps logging activity and inject it into the classes. This has several benefits:

  • Toggling logging becomes central. You can disable/enable logging at your will.
  • You redirect the logs to some other output. i.e., You can dump logs to secure server in prod builds.
  • Decorate lines as you want them to. You might think, Timber does the same thing. And you are right. It is very nice and functional library to achieve these things.

Are your logs relevant?

Log LevelNeedFrequency
ErrorWhen you reach not recoverable stage. This can also be a known bug.Only few lines per session
WarningA known degraded state like no internet connection.Few lines per minute
InfoUser interactions that mutates the UI or state of the app.A screen page per minute
DebugInformation about states which are more technical. i.e., Network traffic, App state etc.Few screen pages per minute
VerboseDetailed technical information. i.e., recurring information that might include polling.No Limit

Tagging your logs

Almost everyone uses class names as tags for your logging statements. But, Is it the way? NO!

Why? Because it is leaking the information about the classes and the code implementation in logs, Moreover it makes it very difficult to filter the logs if a particular feature is implemented in multiple classes. Furthermore, If you have not faced this issue, but Android TAG length cannot be greater than 23 characters. So, it will be truncated in log cat.

So, What is a solution? I use a pattern like [Prefix]. [Feature]. For an instance, if I would log stuff for Nyx I would use val TAG = "nyx.featureOne". I add a file at the root of the package with all the constants. You can also inject this file with your dependency injection definitions for easy access throughout the codebase.

What should you log?

You can log anything and everything but do you need that madness? No right. What I suggest is log these things at bare minimum and anything else that your architecture needs to.

Lifecycle Events

There are around 60% issues and misbehavior which relates to Android Lifecycle. So, You should always log every lifecycle event of your Activities, Fragments, Custom Views etc. If you have BaseFragment for your app, you can do something like this:

open class BaseAppFragment {

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart() invoked: $this")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "onCreate() invoked: $this")
    }

    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume() invoked: $this")
    }

    override fun onPause() {
        Log.d(TAG, "onPause() invoked: $this")
        super.onPause()
    }

    override fun onStop() {
        Log.d(TAG, "onStop() invoked: $this")
        super.onStop()
    }

    override fun onDestroy() {
        Log.d(TAG, "onDestroy() invoked: $this")
        super.onDestroy()
    }

    override fun onDetach() {
        Log.d(TAG, "onDetach() invoked: $this")
        super.onDetach()
    }

    companion object {
        private const val TAG = "nyx.lifecycle"
    }
}

Such logging activity also gives you the ability to trace all the navigation actions that took place in your application with current fragment names in logs. This way you can save a lot of time discovering the issue and root cause if that is related to the lifecycle.

User actions

Log all the user actions, so you can have breadcrumbs to trace back the issue and reproduce easily. But be careful, I don’t want you to log every click listeners. That is madness. You should log the user actions directly relates to domain layer and should log it with relevant scope.

For an instance,

class User {
    fun registerUser(user: UserModel) {
        // some logic related to registration
        Log.i(TAG, "Registering User with ID: ${user.id} and Name: ${user.fullName}")
        // some other code
    }
}

Application Meta

Using the Application class in your app, you can log metadata about the app. You can log App Version, Variant, Code etc. This way you can identify the issues and bugs related to specific version and flavor. If you have multiple environments like dev, uat, production etc. logging such things will be easier tracing issues. This will also help you identify the environment and installed version without having conversation with other teams.

class Nyx : Application() {

    override fun onCreate() {
        super.onCreate()

        // some stuff which is already there
        Log.i(TAG, "Nyx is starting with version ${BuildConfig.VERSION_NAME} and code ${BuildConfig.VERSION_CODE}")
    }
}

This way you can directly see the log cat and find out if something was happening with multiple builds or a specific builds with ease.

Config Changes

Dynamic Config changes like screen rotation, Theme (Dark/Light theme) change when app is running etc. should be logged as well because it is crucial information about the app if something goes wrong. This can provide you an excellent insight and enough breadcrumbs to trace back the issue/bug.

Avoid Unicode characters

Did you know you can use Unicode characters in your logs? Yeah, You can use emojis in your logs. This may sound amazing, right? But wait. This eye candy is not reliable. Keep your logs simple and lean. Why, you may ask? Imagine copying a log statement with emojis and googling it for Stack Overflow. Or more practical example, If you use a terminal like me for log cat, searching through so many log statements is a pain in the back with emojis. I have tried this for eye candy and hated it from the beginning. So, avoid emoji or other Unicode characters in logs. Keep them boring and simple.

Don’t just use Log cat from Android Studio

Log cat window in android studio is just one click away, I know. But is it enough? If you ask me, no.

Why? There are multiple reasons:

  • You can have only one instance of log cat in android studio
  • Filtering is limited. Although it supports regex but are you regex geek?
  • It is buggy, You would randomly lose all the logs from window.

Try using Terminal. You can pay more attention to the logs if you use a dedicated terminal window. How would it make any difference? Well, it has many advantages.

Since we already talked about separating tags for each feature, Each feature in your app will have a separate tag by now, right? So, You can open multiple windows for each feature and tag each of them individually. I also keep an unfiltered log cat terminal window open, just in case log cat from Android Studio decides to bug out for a random reason and clear all the content. Other benefit of this unfiltered log cat is it will record all the activity and helps you trace/reproduce transient issues.

If you are using the tagging system I suggested, You can use the following commands:

  • Network calls
$ adb logcat | grep --line-buffered "nyx.network"
  • User inputs
$ adb logcat | grep --line-buffered "nyx.userInteractions"
  • Multi module tracking
$ adb logcat | grep --line-buffered "nyx.featureOne|nyx.featureTwo"

Write logs for non-techies

Okay, So why should developers care about non-technical people reading logs, right? No, It is wrong practice. You might want to consider your logs readable by anyone available on the project. Especially the testing and quality assurance team. Because you would be getting a bug or an issue that would take hours to reproduce if the feature is complex, which can easily be detected by logging statement. Occasionally, you might have to leak sensitive information like auth token, user IDs etc. to logs, and it is fine. It is up to the testing or QA team to mask that entries while logging the bug.

Keep your production build clean

Always make sure you write your wrapper class in a way that when you build a production app, logs are disabled. Since logs might expose certain private and sensitive information, which can lead to a security concern. So, it is always a best practice to remove logging from production build. If you are using Timber, You can just add few Proguard rules and get rid of Log statements.

Conclusion

Let’s summarize what I have explained so far:

  • Create a wrapper class for Logging
  • Only log what is relevant
  • Add meaningful tags to your log statements. Use [Prefix] - [Feature] pattern.
  • Always log important things like lifecycle events, User actions and Config changes.
  • Avoid emojis and Unicode characters. Keep logs boring and simple.
  • Harness power of Terminal to track features individually
  • Write logs in a way that a non-technical person can understand what’s going on.
  • Remove logs from Production builds.

And that’s it. Thanks for reading!