Collect useful logs in production using Crashlytics, File logging and Timber

Discover Firebase Crashlytics: Your real-time solution for collecting, analyzing, and prioritizing crash reports effortlessly.
Sep 29 2022 · 4 min read

Background

As a developer, it is our responsibility to provide crash/bug-free apps. However, sometimes it’s possible that accidentally we delivered a buggy app to the users.

In this case, there must be some ways with the help of which we can collect bug-related data (error logs) from the users so that we can fix them as early as possible.

So Today, we are going to see how we can use the Firebase Crashlytics tree and File logging tree to track the above-mentioned issues as soon as possible.

Firebase Crashlytics is a real-time crash reporting tool. It helps in automatically collecting, analyzing, and arranging crash reports. It also helps us identify which issues are most important so that we can fix them in priority.

File logging is basically the concept of adding a log file while any user reports any kind of crash or error.

Outline

Here’s what we’re going to implement Today.

  1. Collect logs using the Firebase Crashlytics tree
  2. Collect logs using a File logging tree

Alright, let’s get started!

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

Let’s get Started!

Collect logs using the Firebase Crashlytics tree

The first thing we need to do is create a Firebase project and add it to our Android app. If you have no idea how we can integrate firebase with our android app please refer to the official doc.

1. Add the Firebase plugin to the app:


buildscript {
    repositories {
        google()
    }
    
    dependencies {
        classpath 'com.google.gms:google-services:4.3.13'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1'
    }
}

allprojects {
    repositories {
        google()
    }
}

Here, we have added a firebase classpath in our project-level build.gradle file.

2. Add the below dependencies and plugin into the app-level build.gradle file:


plugins {
        ...
    id 'com.google.firebase.crashlytics'
}
       
dependencies {
        ...
    implementation 'com.google.firebase:firebase-crashlytics-ktx'
    implementation "com.jakewharton.timber:timber:5.0.1"
}

Here, you can observe that we have added dependency for timber and firebase crashlytics.

3. Plant the Timber for the crashlytics tree:


override fun onCreate() {
        super<Application>.onCreate() 
           Timber.plant(CrashlyticsLogTree(FirebaseCrashlytics.getInstance()))
    }

4. Record non-fatal exceptions for the crashlytics tree:


class CrashlyticsLogTree(private val firebaseCrashlytics: FirebaseCrashlytics) : Timber.Tree() {
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (t != null) {
            firebaseCrashlytics.recordException(CrashlyticsNonFatalError("$tag : $message", t))
        } else {
            firebaseCrashlytics.log("$priority/$tag: $message")
        }
    }
    
    class CrashlyticsNonFatalError constructor(message: String, cause: Throwable) :
        RuntimeException(message, cause)
}

So here we have implemented a class called CrashlyticsLogTree and inside the override method log we are recording the non-fatal exceptions in the if part and in the else part we are logging timber logs using crashlytics.

That’s all about how we can Integrate the Firebase Crashlytics tree into our app.

Collect logs using a File logging tree

So here we have implemented a class called FileLoggingTreeand inside the override method log we are performing various operations like creating files for the log, generating the logs, writing the logs, and maintaining the log size up to 5MB.

Snippet for override method log will look something like this:


  private val maxLogSize = 5 * 1024 * 1024 // 5 MB
  private val dateFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS", Locale.US)

override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (priority >= Log.DEBUG) {
            val log = generateLog(priority, tag, message)
            if (!logFile.exists()) {
                logFile.createNewFile()
            }
            writeLog(logFile, log)
            ensureLogSize(logFile)
        }
    }

Here you can observe the usage of the function generateLog() which will create logs, the typical snippets for functiongenerateLog() will look something like this:


 private fun generateLog(priority: Int, tag: String?, message: String): String {
        val logTimeStamp = dateFormat.format(Date())

        return StringBuilder().append(logTimeStamp).append(" ")
            .append(getPriorityString(priority)).append(": ")
            .append(tag).append(" - ")
            .append(message).append('\n').toString()
    }

Here we have agetPriorityString() function that will help us in getting the priority of logs, the snippet for the getPriorityString() function will look like this:


private fun getPriorityString(priority: Int): String {
        return when (priority) {
            Log.VERBOSE -> "VERBOSE"
            Log.DEBUG -> "DEBUG"
            Log.INFO -> "INFO"
            Log.WARN -> "WARN"
            Log.ERROR -> "ERROR"
            Log.ASSERT -> "ASSERT"
            else -> ""
        }
    }
view raw

You can observe in the override method log that we are generating logs only if the priority is greater or equal to DEBUG which means only debug, error and warnings.

After generating the logs we are writing the logs using writeLog() function and snippet for the same will look like:


  private fun writeLog(logFile: File, log: String) {
        val writer = FileWriter(logFile, true)
        writer.append(log)
        writer.flush()
        writer.close()
    }

Here we are using an inbuilt FileWriter() function to write the logs in the file.

The lasting is to ensure the size of the log is up to 5MB and for that, we are using ensureLogSize() function, the snippet for the function will look like this:


   @Throws(IOException::class)
    private fun ensureLogSize(logFile: File) {
        if (logFile.length() < maxLogSize) return

        // We remove first 25% part of logs
        val startIndex = logFile.length() / 4

        val randomAccessFile = RandomAccessFile(logFile, "r")
        randomAccessFile.seek(startIndex)

        val into = ByteArrayOutputStream()

        val buf = ByteArray(4096)
        var n: Int
        while (true) {
            n = randomAccessFile.read(buf)
            if (n < 0) break
            into.write(buf, 0, n)
        }

        randomAccessFile.close()

        val outputStream = FileOutputStream(logFile)
        into.writeTo(outputStream)

        outputStream.close()
        into.close()
    }

Here, we are ensuring that once the size of the log is higher than 5MB we are removing the top 25% of the logs.

That’s it.

Conclusion

Hope you have a basic idea of how to plant Crashlytics and FileLogging tree to record logs & non-fatal exceptions of your app.

Don’t worry if you didn’t get all the file logging steps.

The complete snippet of the FileLoggingTree is available on Github.

Keep logging!!


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
canopas-logo
We build products that customers can't help but love!
Get in touch

Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Follow us on
2024 Canopas Software LLP. All rights reserved.