mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
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 libs.firebase.analytics
|
||||
implementation libs.firebase.crashlytics
|
||||
implementation libs.firebase.storage
|
||||
|
||||
implementation platform(libs.okhttp.bom)
|
||||
implementation libs.okhttp
|
||||
|
||||
@@ -8,6 +8,7 @@ import app.affine.pro.utils.dataStore
|
||||
import app.affine.pro.utils.get
|
||||
import app.affine.pro.utils.logger.AffineDebugTree
|
||||
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.setCustomKeys
|
||||
import com.google.firebase.ktx.Firebase
|
||||
@@ -26,7 +27,12 @@ class AffineApp : Application() {
|
||||
super.onCreate()
|
||||
_context = applicationContext
|
||||
// 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
|
||||
CapacitorConfig.init(baseContext)
|
||||
// init crashlytics
|
||||
@@ -50,6 +56,7 @@ class AffineApp : Application() {
|
||||
?: error("Parse user id cookie fail:[ cookie = $userIdCookieStr ]"),
|
||||
)
|
||||
CookieStore.saveCookies(BuildConfig.BASE_URL.toHttpUrl().host, cookies)
|
||||
FileTree.get()?.checkAndUploadOldLogs()
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e, "[init] load persistent cookies fail.")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package app.affine.pro.plugin
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import app.affine.pro.AffineApp
|
||||
import app.affine.pro.CapacitorConfig
|
||||
import app.affine.pro.service.CookieStore
|
||||
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.Plugin
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user