feat(android): t+1 file log system with firebase storage (#11945)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced file-based logging that saves daily log files within the app. - Old log files are now automatically uploaded to cloud storage for backup. - Enhanced logging initialization to include file-based logging alongside crash reporting. - **Chores** - Added Firebase Storage as a new dependency to support log file uploads. - **Style** - Cleaned up unused imports in the authentication plugin. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -124,6 +124,7 @@ dependencies {
|
|||||||
implementation platform(libs.firebase.bom)
|
implementation platform(libs.firebase.bom)
|
||||||
implementation libs.firebase.analytics
|
implementation libs.firebase.analytics
|
||||||
implementation libs.firebase.crashlytics
|
implementation libs.firebase.crashlytics
|
||||||
|
implementation libs.firebase.storage
|
||||||
|
|
||||||
implementation platform(libs.okhttp.bom)
|
implementation platform(libs.okhttp.bom)
|
||||||
implementation libs.okhttp
|
implementation libs.okhttp
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import app.affine.pro.utils.dataStore
|
|||||||
import app.affine.pro.utils.get
|
import app.affine.pro.utils.get
|
||||||
import app.affine.pro.utils.logger.AffineDebugTree
|
import app.affine.pro.utils.logger.AffineDebugTree
|
||||||
import app.affine.pro.utils.logger.CrashlyticsTree
|
import app.affine.pro.utils.logger.CrashlyticsTree
|
||||||
|
import app.affine.pro.utils.logger.FileTree
|
||||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||||
import com.google.firebase.crashlytics.setCustomKeys
|
import com.google.firebase.crashlytics.setCustomKeys
|
||||||
import com.google.firebase.ktx.Firebase
|
import com.google.firebase.ktx.Firebase
|
||||||
@@ -26,7 +27,12 @@ class AffineApp : Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
_context = applicationContext
|
_context = applicationContext
|
||||||
// init logger
|
// init logger
|
||||||
Timber.plant(if (BuildConfig.DEBUG) AffineDebugTree() else CrashlyticsTree())
|
if (BuildConfig.DEBUG) {
|
||||||
|
Timber.plant(AffineDebugTree())
|
||||||
|
} else {
|
||||||
|
Timber.plant(CrashlyticsTree(), FileTree(applicationContext))
|
||||||
|
}
|
||||||
|
Timber.i("Application started.")
|
||||||
// init capacitor config
|
// init capacitor config
|
||||||
CapacitorConfig.init(baseContext)
|
CapacitorConfig.init(baseContext)
|
||||||
// init crashlytics
|
// init crashlytics
|
||||||
@@ -50,6 +56,7 @@ class AffineApp : Application() {
|
|||||||
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
|
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
|
||||||
)
|
)
|
||||||
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
|
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
|
||||||
|
FileTree.get()?.checkAndUploadOldLogs()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.w(e, "[init] load persistent cookies fail.")
|
Timber.w(e, "[init] load persistent cookies fail.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package app.affine.pro.plugin
|
package app.affine.pro.plugin
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import app.affine.pro.AffineApp
|
|
||||||
import app.affine.pro.CapacitorConfig
|
import app.affine.pro.CapacitorConfig
|
||||||
import app.affine.pro.service.CookieStore
|
import app.affine.pro.service.CookieStore
|
||||||
import app.affine.pro.service.OkHttp
|
import app.affine.pro.service.OkHttp
|
||||||
import app.affine.pro.utils.clear
|
|
||||||
import app.affine.pro.utils.dataStore
|
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package app.affine.pro.utils.logger
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import app.affine.pro.BuildConfig
|
||||||
|
import app.affine.pro.service.CookieStore
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import com.google.firebase.storage.ktx.storage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class FileTree(context: Context) : Timber.Tree() {
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
private val logDirectory: File = File(context.filesDir, "logs")
|
||||||
|
private val currentLogFile: File
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!logDirectory.exists()) {
|
||||||
|
logDirectory.mkdirs()
|
||||||
|
}
|
||||||
|
val today = dateFormat.format(Date())
|
||||||
|
currentLogFile = File(logDirectory, "$today.log")
|
||||||
|
if (!currentLogFile.exists()) {
|
||||||
|
try {
|
||||||
|
currentLogFile.createNewFile()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e, "Create log file fail.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun checkAndUploadOldLogs() {
|
||||||
|
val today = dateFormat.format(Date())
|
||||||
|
logDirectory.listFiles()?.forEach { file ->
|
||||||
|
val fileName = file.name
|
||||||
|
if (fileName.endsWith(".log") && !fileName.startsWith(today)) {
|
||||||
|
uploadLogToFirebase(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun uploadLogToFirebase(file: File) =
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
val user = CookieStore.getCookie(BuildConfig.BASE_URL, CookieStore.AFFINE_USER_ID)
|
||||||
|
?: return@suspendCancellableCoroutine
|
||||||
|
val storageRef = Firebase.storage.reference
|
||||||
|
val logFileRef = storageRef.child("android_log/$user/${file.name}")
|
||||||
|
|
||||||
|
val uploadTask = logFileRef.putFile(Uri.fromFile(file))
|
||||||
|
uploadTask.addOnSuccessListener {
|
||||||
|
if (file.delete()) {
|
||||||
|
if (continuation.isActive) continuation.resume(true) { _, _, _ -> }
|
||||||
|
} else {
|
||||||
|
if (continuation.isActive) continuation.resume(false) { _, _, _ -> }
|
||||||
|
}
|
||||||
|
}.addOnFailureListener { e ->
|
||||||
|
if (continuation.isActive) continuation.resume(false) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
if (uploadTask.isInProgress) {
|
||||||
|
uploadTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
|
if (priority < Log.INFO) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val level = when (priority) {
|
||||||
|
Log.ASSERT -> "[assert]"
|
||||||
|
Log.ERROR -> "[error]"
|
||||||
|
Log.WARN -> "[warn]"
|
||||||
|
else -> "[info]"
|
||||||
|
}
|
||||||
|
val log = StringBuilder(level)
|
||||||
|
.append(tag?.let { "[$tag]" } ?: "")
|
||||||
|
.append(" ")
|
||||||
|
.append(message)
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
MainScope().launch {
|
||||||
|
try {
|
||||||
|
val timestamp =
|
||||||
|
SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
|
||||||
|
val logMessage = "[$timestamp] $log\n"
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
FileOutputStream(currentLogFile, true).use {
|
||||||
|
it.write(logMessage.toByteArray())
|
||||||
|
t?.stackTraceToString()?.let { stacktrace ->
|
||||||
|
it.write(stacktrace.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Timber.e(e, "Failed to write to log file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun get() = Timber.forest().filterIsInstance<FileTree>().firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,7 @@ apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.r
|
|||||||
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
|
||||||
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
|
||||||
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
|
||||||
|
firebase-storage = { module = "com.google.firebase:firebase-storage" }
|
||||||
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
google-services = { module = "com.google.gms:google-services", version.ref = "google-services" }
|
||||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||||
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" }
|
||||||
|
|||||||
Reference in New Issue
Block a user