Store log files in subfolder based on the homeserver domain.

This commit is contained in:
Benoit Marty 2025-08-07 11:31:05 +02:00
parent 37786352ba
commit 18c325560b
9 changed files with 90 additions and 22 deletions

View file

@ -10,11 +10,10 @@ package io.element.android.x.initializer
import android.content.Context import android.content.Context
import android.system.Os import android.system.Os
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -34,7 +33,7 @@ class PlatformInitializer : Initializer<Unit> {
val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() } val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() }
val tracingConfiguration = TracingConfiguration( val tracingConfiguration = TracingConfiguration(
writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) }, writesToLogcat = runBlocking { featureFlagService.isFeatureEnabled(FeatureFlags.PrintLogsToLogcat) },
writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter), writesToFilesConfiguration = bugReporter.createWriteToFilesConfiguration(),
logLevel = logLevel, logLevel = logLevel,
extraTargets = listOf(ELEMENT_X_TARGET), extraTargets = listOf(ELEMENT_X_TARGET),
traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() }, traceLogPacks = runBlocking { preferencesStore.getTracingLogPacksFlow().first() },
@ -45,14 +44,5 @@ class PlatformInitializer : Initializer<Unit> {
Os.setenv("RUST_BACKTRACE", "1", true) Os.setenv("RUST_BACKTRACE", "1", true)
} }
private fun defaultWriteToDiskConfiguration(bugReporter: BugReporter): WriteToFilesConfiguration.Enabled {
return WriteToFilesConfiguration.Enabled(
directory = bugReporter.logDirectory().absolutePath,
filenamePrefix = "logs",
// Keep a maximum of 1 week of log files.
numberOfFiles = 7 * 24,
)
}
override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf() override fun dependencies(): List<Class<out Initializer<*>>> = mutableListOf()
} }

View file

@ -36,6 +36,7 @@ import io.element.android.appnav.root.RootView
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.features.viewfolder.api.ViewFolderEntryPoint import io.element.android.features.viewfolder.api.ViewFolderEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
@ -73,6 +74,7 @@ class RootFlowNode @AssistedInject constructor(
private val signedOutEntryPoint: SignedOutEntryPoint, private val signedOutEntryPoint: SignedOutEntryPoint,
private val intentResolver: IntentResolver, private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow, private val oidcActionFlow: OidcActionFlow,
private val bugReporter: BugReporter,
) : BaseFlowNode<RootFlowNode.NavTarget>( ) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.SplashScreen, initialElement = NavTarget.SplashScreen,
@ -123,6 +125,7 @@ class RootFlowNode @AssistedInject constructor(
private fun switchToNotLoggedInFlow(params: LoginParams?) { private fun switchToNotLoggedInFlow(params: LoginParams?) {
matrixSessionCache.removeAll() matrixSessionCache.removeAll()
bugReporter.setLogDirectorySubfolder(null)
backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
} }

View file

@ -16,5 +16,6 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
} }

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.rageshake.api.logs
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
fun BugReporter.createWriteToFilesConfiguration(): WriteToFilesConfiguration {
return WriteToFilesConfiguration.Enabled(
directory = logDirectory().absolutePath,
filenamePrefix = "logs",
// Keep a maximum of 1 week of log files.
numberOfFiles = 7 * 24,
)
}

View file

@ -34,6 +34,14 @@ interface BugReporter {
*/ */
fun logDirectory(): File fun logDirectory(): File
/**
* Set the subfolder name for the log directory.
* This will create a subfolder in the log directory with the given name.
* It will also configure the Rust SDK to use this subfolder for its logs.
* If the name is null, the log files will be stored in the base folder for the logs.
*/
fun setLogDirectorySubfolder(subfolderName: String?)
/** /**
* Set the current tracing log level. * Set the current tracing log level.
*/ */

View file

@ -13,6 +13,7 @@ import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.RageshakeConfig import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.impl.crash.CrashDataStore import io.element.android.features.rageshake.impl.crash.CrashDataStore
@ -28,11 +29,14 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.SdkMetadata import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -71,6 +75,8 @@ class DefaultBugReporter @Inject constructor(
private val bugReporterUrlProvider: BugReporterUrlProvider, private val bugReporterUrlProvider: BugReporterUrlProvider,
private val sdkMetadata: SdkMetadata, private val sdkMetadata: SdkMetadata,
private val matrixClientProvider: MatrixClientProvider, private val matrixClientProvider: MatrixClientProvider,
private val tracingService: TracingService,
matrixAuthenticationService: MatrixAuthenticationService,
) : BugReporter { ) : BugReporter {
companion object { companion object {
// filenames // filenames
@ -81,7 +87,21 @@ class DefaultBugReporter @Inject constructor(
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
private var currentTracingLogLevel: String? = null private var currentTracingLogLevel: String? = null
private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME) private val logCatErrFile: File
get() = File(logDirectory(), LOG_CAT_FILENAME)
private val baseLogDirectory = File(context.cacheDir, LOG_DIRECTORY_NAME)
private var currentLogDirectory: File = baseLogDirectory
init {
val logSubfolder = runBlocking {
sessionStore.getLatestSession()
}?.userId?.substringAfter(":")
setCurrentLogDirectory(logSubfolder)
matrixAuthenticationService.listenToNewMatrixClients {
// When a new Matrix client is created, we update the tracing configuration to write to files
setLogDirectorySubfolder(it.userIdServerName())
}
}
override suspend fun sendBugReport( override suspend fun sendBugReport(
withDevicesLogs: Boolean, withDevicesLogs: Boolean,
@ -286,11 +306,24 @@ class DefaultBugReporter @Inject constructor(
} }
override fun logDirectory(): File { override fun logDirectory(): File {
return File(context.cacheDir, LOG_DIRECTORY_NAME).apply { return currentLogDirectory.apply {
mkdirs() mkdirs()
} }
} }
override fun setLogDirectorySubfolder(subfolderName: String?) {
setCurrentLogDirectory(subfolderName)
tracingService.updateWriteToFilesConfiguration(createWriteToFilesConfiguration())
}
private fun setCurrentLogDirectory(subfolderName: String?) {
currentLogDirectory = if (subfolderName == null) {
baseLogDirectory
} else {
File(baseLogDirectory, subfolderName)
}
}
suspend fun deleteAllFiles(predicate: (File) -> Boolean) { suspend fun deleteAllFiles(predicate: (File) -> Boolean) {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
getLogFiles() getLogFiles()
@ -325,11 +358,12 @@ class DefaultBugReporter @Inject constructor(
* @return the file if the operation succeeds * @return the file if the operation succeeds
*/ */
override fun saveLogCat() { override fun saveLogCat() {
if (logCatErrFile.exists()) { val file = logCatErrFile
logCatErrFile.safeDelete() if (file.exists()) {
file.safeDelete()
} }
try { try {
logCatErrFile.writer().use { file.writer().use {
getLogCatError(it) getLogCatError(it)
} }
} catch (error: OutOfMemoryError) { } catch (error: OutOfMemoryError) {

View file

@ -11,4 +11,6 @@ import timber.log.Timber
interface TracingService { interface TracingService {
fun createTimberTree(target: String): Timber.Tree fun createTimberTree(target: String): Timber.Tree
fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration)
} }

View file

@ -71,9 +71,9 @@ class RustMatrixAuthenticationService @Inject constructor(
private var currentClient: Client? = null private var currentClient: Client? = null
private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null) private var currentHomeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var newMatrixClientObserver: ((MatrixClient) -> Unit)? = null private val newMatrixClientObservers = mutableListOf<(MatrixClient) -> Unit>()
override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) { override fun listenToNewMatrixClients(lambda: (MatrixClient) -> Unit) {
newMatrixClientObserver = lambda newMatrixClientObservers.add(lambda)
} }
private fun rotateSessionPath(): SessionPaths { private fun rotateSessionPath(): SessionPaths {
@ -155,7 +155,8 @@ class RustMatrixAuthenticationService @Inject constructor(
passphrase = pendingPassphrase, passphrase = pendingPassphrase,
sessionPaths = currentSessionPaths, sessionPaths = currentSessionPaths,
) )
newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData) sessionStore.storeData(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
@ -246,7 +247,8 @@ class RustMatrixAuthenticationService @Inject constructor(
pendingOAuthAuthorizationData?.close() pendingOAuthAuthorizationData?.close()
pendingOAuthAuthorizationData = null pendingOAuthAuthorizationData = null
newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData) sessionStore.storeData(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary
@ -290,7 +292,8 @@ class RustMatrixAuthenticationService @Inject constructor(
passphrase = pendingPassphrase, passphrase = pendingPassphrase,
sessionPaths = emptySessionPaths, sessionPaths = emptySessionPaths,
) )
newMatrixClientObserver?.invoke(rustMatrixClientFactory.create(client)) val matrixClient = rustMatrixClientFactory.create(client)
newMatrixClientObservers.forEach { it.invoke(matrixClient) }
sessionStore.storeData(sessionData) sessionStore.storeData(sessionData)
// Clean up the strong reference held here since it's no longer necessary // Clean up the strong reference held here since it's no longer necessary

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import org.matrix.rustcomponents.sdk.TracingFileConfiguration import org.matrix.rustcomponents.sdk.TracingFileConfiguration
import org.matrix.rustcomponents.sdk.reloadTracingFileWriter
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -23,6 +24,12 @@ class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) :
override fun createTimberTree(target: String): Timber.Tree { override fun createTimberTree(target: String): Timber.Tree {
return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable) return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable)
} }
override fun updateWriteToFilesConfiguration(config: WriteToFilesConfiguration) {
config.toTracingFileConfiguration()?.let {
reloadTracingFileWriter(it)
}
}
} }
private fun LogLevel.toRustLogLevel(): org.matrix.rustcomponents.sdk.LogLevel { private fun LogLevel.toRustLogLevel(): org.matrix.rustcomponents.sdk.LogLevel {