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.
Here’s what we’re going to implement Today.
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!
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.
So here we have implemented a class called FileLoggingTree
and 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.
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!!