diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt index e49e654826..79b05ea32e 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt @@ -45,5 +45,5 @@ interface BugReporter { /** * Save the logcat. */ - fun saveLogCat() + fun saveLogCat(): File? } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 7ce97a05f4..f51d699350 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -95,8 +95,6 @@ class DefaultBugReporter( private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") private var currentTracingLogLevel: String? = null - private val logCatErrFile: File - get() = File(logDirectory(), LOG_CAT_FILENAME) private val baseLogDirectory = File(context.cacheDir, LOG_DIRECTORY_NAME) private var currentLogDirectory: File = baseLogDirectory @@ -160,10 +158,14 @@ class DefaultBugReporter( } if (withCrashLogs || withDevicesLogs) { saveLogCat() - val gzippedLogcat = compressFile(logCatErrFile) - if (gzippedLogcat != null) { - gzippedFiles.add(0, gzippedLogcat) - } + ?.let { logCatFile -> + compressFile(logCatFile).also { + logCatFile.safeDelete() + } + } + ?.let { gzippedLogcat -> + gzippedFiles.add(0, gzippedLogcat) + } } val sessionData = sessionStore.getLatestSession() val numberOfAccounts = sessionStore.numberOfSessions() @@ -387,7 +389,8 @@ class DefaultBugReporter( onException = { Timber.e(it, "## getLogFiles() failed") } ) { val logDirectory = logDirectory() - logDirectory.listFiles()?.toList() + logDirectory.listFiles() + ?.filter { it.isFile && !it.name.endsWith(LOG_CAT_FILENAME) } }.orEmpty() } @@ -400,19 +403,19 @@ class DefaultBugReporter( * * @return the file if the operation succeeds */ - override fun saveLogCat() { - val file = logCatErrFile + override fun saveLogCat(): File? { + val file = File(baseLogDirectory, LOG_CAT_FILENAME) if (file.exists()) { file.safeDelete() } - try { + return try { file.writer().use { - getLogCatError(it) + getLogCatContent(it) } - } catch (error: OutOfMemoryError) { - Timber.e(error, "## saveLogCat() : fail to write logcat OOM") + file } catch (e: Exception) { Timber.e(e, "## saveLogCat() : fail to write logcat") + null } } @@ -421,15 +424,10 @@ class DefaultBugReporter( * * @param streamWriter the stream writer */ - private fun getLogCatError(streamWriter: OutputStreamWriter) { - val logcatProcess: Process - - try { - logcatProcess = Runtime.getRuntime().exec(logcatCommandDebug) - } catch (e1: IOException) { - return - } - + private fun getLogCatContent(streamWriter: OutputStreamWriter) { + val logcatProcess = tryOrNull { + Runtime.getRuntime().exec(logcatCommandDebug) + } ?: return try { val separator = System.lineSeparator() logcatProcess.inputStream @@ -440,7 +438,7 @@ class DefaultBugReporter( streamWriter.append(separator) } } catch (e: IOException) { - Timber.e(e, "getLog fails") + Timber.e(e, "getLogCatContent fails") } } } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt index e802646154..36cc185e86 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt @@ -59,7 +59,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter { // No op } - override fun saveLogCat() { - // No op + override fun saveLogCat(): File? { + return null } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/FileCompression.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/FileCompression.kt index 53e713c835..1dbf87a529 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/FileCompression.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/FileCompression.kt @@ -39,8 +39,5 @@ fun compressFile(file: File): File? { } catch (e: Exception) { Timber.e(e, "## compressFile() failed") null - } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## compressFile() failed") - null } } diff --git a/libraries/network/build.gradle.kts b/libraries/network/build.gradle.kts index cc9d0248f0..72e7eb05f2 100644 --- a/libraries/network/build.gradle.kts +++ b/libraries/network/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.preferences.api) implementation(platform(libs.network.okhttp.bom)) implementation(libs.network.okhttp) implementation(libs.network.okhttp.logging) diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt index 3881b1d752..d8809e042c 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt @@ -13,7 +13,7 @@ import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides import dev.zacsweers.metro.SingleIn -import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.network.interceptors.DynamicHttpLoggingInterceptor import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger import io.element.android.libraries.network.interceptors.UserAgentInterceptor import okhttp3.OkHttpClient @@ -26,21 +26,20 @@ object NetworkModule { @Provides @SingleIn(AppScope::class) fun providesOkHttpClient( - buildMeta: BuildMeta, userAgentInterceptor: UserAgentInterceptor, + dynamicHttpLoggingInterceptor: DynamicHttpLoggingInterceptor, ): OkHttpClient = OkHttpClient.Builder().apply { connectTimeout(30, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS) writeTimeout(60, TimeUnit.SECONDS) addInterceptor(userAgentInterceptor) - if (buildMeta.isDebuggable) addInterceptor(providesHttpLoggingInterceptor()) + addInterceptor(dynamicHttpLoggingInterceptor) }.build() -} -private fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { - val loggingLevel = HttpLoggingInterceptor.Level.BODY - val logger = FormattedJsonHttpLogger(loggingLevel) - val interceptor = HttpLoggingInterceptor(logger) - interceptor.level = loggingLevel - return interceptor + @Provides + @SingleIn(AppScope::class) + fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logger = FormattedJsonHttpLogger(HttpLoggingInterceptor.Level.BODY) + return HttpLoggingInterceptor(logger) + } } diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/DynamicHttpLoggingInterceptor.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/DynamicHttpLoggingInterceptor.kt new file mode 100644 index 0000000000..daaf922ce7 --- /dev/null +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/DynamicHttpLoggingInterceptor.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Element Creations 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.libraries.network.interceptors + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level + +/** + * HTTP logging interceptor that decides whether to display the HTTP logs or not based on the current log level. + */ +@Inject +@SingleIn(AppScope::class) +class DynamicHttpLoggingInterceptor( + private val appPreferencesStore: AppPreferencesStore, + private val loggingInterceptor: HttpLoggingInterceptor, +) : Interceptor by loggingInterceptor { + override fun intercept(chain: Interceptor.Chain): Response { + // This is called in a separate thread, so calling `runBlocking` here should be fine, it should be also instant after the value is cached + val logLevel = runBlocking { appPreferencesStore.getTracingLogLevelFlow().first() } + loggingInterceptor.level = if (logLevel >= LogLevel.DEBUG) Level.BODY else Level.NONE + return loggingInterceptor.intercept(chain) + } +} diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt index 53dd7746d7..11aa0a5cce 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt @@ -30,7 +30,7 @@ internal class FormattedJsonHttpLogger( */ @Synchronized override fun log(message: String) { - Timber.v(message.ellipsize(200_000)) + Timber.d(message.ellipsize(200_000)) // Try to log formatted Json only if there is a chance that [message] contains Json. // It can be only the case if we log the bodies of Http requests.