I wanted to perform remote Android logging for specific users and conditions for release debugging purpose.
Options
- Crashlytics Custom Logging - log messages are grouped together with a specific crash/exception, thus need to log an non fatal exception (via
Crashlytics.logException(e)
) and all previous logging shall be shown in this specific crash report. - Firebase Analytics Log Events - the Analytics console UI is more for Events statistics rather than log messages. There is a DebugView for realtime event debugging, but won't work well to view previous events in time chronological order.
- Stackdriver / Google Cloud Logging Client Libraries is not supported on Android.
Solution
By default, Timber support custom output handlers. Firestore is the easiest method to send data to remote server without the need to write a RESTful API, and there is a simple console UI to view the data.
Remote logging handler for Timber. Only log for
- Specific user (using Firebase Auth)
- Specific tag
Firestore
- Use
user_log
collection withuser id
as document id. (allow me to debug log messages by user easily) - Using single document to store all log messages per user, storing them in map using Timestamp as the key.
- Using updateOrCreate to make sure partial update is successful even when document does not exist.
- Since firestore map key must be String, I am using Timestamp.now().toLocalDateTime() and convert it to device local timezone datetime string. I could use UTC as well.
// class RemoteLoggingTree: Timber.DebugTree() {class RemoteLoggingTree: Timber.Tree() { private val watchUids = listOf( "QYHqCKCGSbY3qqMSliqNhR3j9QD2", // Desmond "CHqMRFyD6OeavAIz1xKx2yLEM3Z2", // Mei Ru "iDCeXV2lFCSNBKx94euaTczicyp1" // JackJack ) private val watchTags = listOf( "ReminderWorker" ) override fun isLoggable(tag: String?, priority: Int): Boolean { return priority >= Log.INFO } @SuppressLint("LogNotTimber") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { if (priority == Log.VERBOSE || priority == Log.DEBUG) { return } val user = FirebaseAuth.getInstance().currentUser // tag is null unless .tag() is called // override Timber.DebugTree will have performance penalty // https://github.com/JakeWharton/timber/issues/122 if (user != null && watchUids.contains(user.uid) && watchTags.contains(tag)) { val db = FirebaseFirestore.getInstance() val ref = db.collection("user_log").document(user.uid) val priorityString = when(priority) { Log.INFO -> "I" Log.WARN -> "W" Log.ERROR -> "E" else -> "" } // Log.i("RemoteLoggingTree", "log to firestore") val logString = "$priorityString/$tag: $message" // firestore map key must be String - use device local timestamp val key = Timestamp.now().toLocalDateTime().toString() ref.updateOrCreate(mapOf( "logs" to mapOf( key to logString ) )) } else { // Log.i("RemoteLoggingTree", "skip log -> $message") // super.log(priority, tag, message, t) } }}
Enable remote logging for RELEASE only.
class LuaApp: Application() { override fun onCreate() { if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) } else { Timber.plant(RemoteLoggingTree()) } }}
Logging
val TAG = "ReminderWorker"Timber.tag(TAG).i("doWork: show notification")// e = ExceptionTimber.tag(TAG).e(e)
View logs from Firebase Database Console.