Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ba6db78
feat(logging): introduce thread-safe logging with new LogFileWriter i…
MohamadJaara Jun 18, 2025
6132313
quick change
MohamadJaara Jun 23, 2025
ac80293
refactor(logging): remove unused MEMORY_PRESSURE_MULTIPLIER constant
MohamadJaara Jun 23, 2025
948574f
remove not used value
MohamadJaara Jun 23, 2025
af9c37f
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Jun 23, 2025
bad4ee3
Merge remote-tracking branch 'origin/develop' into mo/feat/thred-safe…
MohamadJaara Jun 25, 2025
a63eaf0
detekt
MohamadJaara Jun 25, 2025
b4be3ee
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Jun 25, 2025
0c1cc7d
detekt
MohamadJaara Jun 25, 2025
816b392
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Jul 18, 2025
266003d
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Jul 18, 2025
d4068f8
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Aug 20, 2025
8268a73
detekt
MohamadJaara Aug 20, 2025
f1dfbae
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Aug 27, 2025
abf7c44
fix merge conflicts
MohamadJaara Aug 27, 2025
3849cbb
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Sep 15, 2025
106e806
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Sep 15, 2025
28844fc
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Sep 15, 2025
55b15c5
Merge remote-tracking branch 'origin/develop' into mo/feat/thred-safe…
MohamadJaara Sep 23, 2025
1e2c629
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Sep 25, 2025
106a7d1
Merge branch 'develop' into mo/feat/thred-safe-logging
MohamadJaara Oct 6, 2025
281988f
Merge remote-tracking branch 'origin/develop' into mo/feat/thred-safe…
MohamadJaara Oct 8, 2025
12b0720
Update app/src/main/kotlin/com/wire/android/util/logging/LogFileWrite…
MohamadJaara Oct 8, 2025
745718f
Update app/src/main/kotlin/com/wire/android/util/logging/LogFileWrite…
MohamadJaara Oct 8, 2025
e6cc9de
pr comments
MohamadJaara Oct 8, 2025
83c91d4
enable the async logger for for beta
MohamadJaara Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.wire.android.ui.debug
import androidx.compose.ui.test.junit4.createComposeRule
import com.wire.android.extensions.waitUntilExists
import com.wire.android.ui.WireTestTheme
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
Expand All @@ -40,6 +41,7 @@ class DebugScreenComposeTest {
onDeleteLogs = {},
onDatabaseLoggerEnabledChanged = {},
onShowFeatureFlags = {},
onFlushLogs = { CompletableDeferred(Unit) },
)
}
}
Expand Down
88 changes: 86 additions & 2 deletions app/src/main/kotlin/com/wire/android/WireApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import com.wire.android.util.AppNameUtil
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.DataDogLogger
import com.wire.android.util.LogFileWriter
import com.wire.android.util.logging.LogFileWriter
import com.wire.android.util.getGitBuildId
import com.wire.android.util.lifecycle.SyncLifecycleManager
import com.wire.android.workmanager.WireWorkerFactory
Expand All @@ -59,8 +59,10 @@
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import javax.inject.Inject

@Suppress("TooManyFunctions")
@HiltAndroidApp
class WireApplication : BaseApp() {

Expand Down Expand Up @@ -107,6 +109,8 @@

enableStrictMode()

setupGlobalExceptionHandler()

Check warning on line 112 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L112

Added line #L112 was not covered by tests

startActivityLifecycleCallback()

globalAppScope.launch {
Expand Down Expand Up @@ -160,6 +164,82 @@
}
}

private fun setupGlobalExceptionHandler() {
setupUncaughtExceptionHandler()
setupHistoricalExitMonitoring()

Check warning on line 169 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L168-L169

Added lines #L168 - L169 were not covered by tests
}

@Suppress("TooGenericExceptionCaught")
private fun setupUncaughtExceptionHandler() {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
flushLogsBeforeCrash()

Check warning on line 176 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L174-L176

Added lines #L174 - L176 were not covered by tests
defaultHandler?.uncaughtException(thread, exception)
}
}

@Suppress("TooGenericExceptionCaught")
private fun flushLogsBeforeCrash() {
// Use fire-and-forget approach to avoid blocking the crash handler
// which could lead to ANRs. We attempt a quick flush but don't wait for it.
try {
globalAppScope.launch(Dispatchers.IO) {
try {

Check warning on line 187 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L185-L187

Added lines #L185 - L187 were not covered by tests
// Use a very short timeout to avoid delaying the crash
withTimeout(CRASH_FLUSH_TIMEOUT_MS) {
logFileWriter.get().forceFlush()

Check warning on line 190 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L189-L190

Added lines #L189 - L190 were not covered by tests
}
appLogger.i("Logs flushed before crash")
} catch (e: Exception) {

Check warning on line 193 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L192-L193

Added lines #L192 - L193 were not covered by tests
// Log errors but don't block the crash handler
appLogger.e("Failed to flush logs before crash", e)

Check warning on line 195 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L195

Added line #L195 was not covered by tests
}
}
} catch (e: Exception) {

Check warning on line 198 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L198

Added line #L198 was not covered by tests
// Ignore any launch failures - we don't want to interfere with crash handling
}
}

@Suppress("TooGenericExceptionCaught")
private fun setupHistoricalExitMonitoring() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val activityManager = getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager
activityManager.setProcessStateSummary(ByteArray(0))

Check warning on line 208 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L206-L208

Added lines #L206 - L208 were not covered by tests

// This will be called after the app exits, so we can't flush here,
// but we log it for diagnostics
globalAppScope.launch {
activityManager.getHistoricalProcessExitReasons(packageName, 0, MAX_HISTORICAL_EXIT_REASONS)
.forEach { info ->
logPreviousExitReason(info)
}

Check warning on line 216 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L212-L216

Added lines #L212 - L216 were not covered by tests
}
} catch (e: Exception) {
appLogger.e("Failed to setup app exit monitoring", e)

Check warning on line 219 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L218-L219

Added lines #L218 - L219 were not covered by tests
}
}
}

private fun logPreviousExitReason(info: android.app.ApplicationExitInfo) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
when (info.reason) {
android.app.ApplicationExitInfo.REASON_ANR -> {
appLogger.w("Previous app exit was due to ANR at ${info.timestamp}")

Check warning on line 228 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L228

Added line #L228 was not covered by tests
}
android.app.ApplicationExitInfo.REASON_CRASH -> {
appLogger.w("Previous app exit was due to crash at ${info.timestamp}")

Check warning on line 231 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L231

Added line #L231 was not covered by tests
}
android.app.ApplicationExitInfo.REASON_LOW_MEMORY -> {
appLogger.w("Previous app exit was due to low memory at ${info.timestamp}")

Check warning on line 234 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L234

Added line #L234 was not covered by tests
}
else -> {
appLogger.i("Previous app exit reason: ${info.reason} at ${info.timestamp}")

Check warning on line 237 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L237

Added line #L237 was not covered by tests
}
}
}
}

@Suppress("EmptyFunctionBlock")
private fun startActivityLifecycleCallback() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
Expand Down Expand Up @@ -290,7 +370,9 @@
override fun onLowMemory() {
super.onLowMemory()
appLogger.w("onLowMemory called - Stopping logging, buckling the seatbelt and hoping for the best!")
logFileWriter.get().stop()
globalAppScope.launch {
logFileWriter.get().stop()

Check warning on line 374 in app/src/main/kotlin/com/wire/android/WireApplication.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/WireApplication.kt#L373-L374

Added lines #L373 - L374 were not covered by tests
}
}

private companion object {
Expand All @@ -313,5 +395,7 @@
}

private const val TAG = "WireApplication"
private const val CRASH_FLUSH_TIMEOUT_MS = 1000L
private const val MAX_HISTORICAL_EXIT_REASONS = 5
}
}
14 changes: 11 additions & 3 deletions app/src/main/kotlin/com/wire/android/di/LogWriterModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
package com.wire.android.di

import android.content.Context
import com.wire.android.util.LogFileWriter
import com.wire.android.BuildConfig
import com.wire.android.util.logging.LogFileWriterV1Impl
import com.wire.android.util.logging.LogFileWriter
import com.wire.android.util.logging.LogFileWriterV2Impl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -34,7 +37,12 @@ class LogWriterModule {
@Singleton
@Provides
fun provideKaliumFileWriter(@ApplicationContext context: Context): LogFileWriter {
val logsDirectory = LogFileWriter.logsDirectory(context)
return LogFileWriter(logsDirectory)
if (BuildConfig.USE_ASYNC_FLUSH_LOGGING) {
val logsDirectory = LogFileWriter.logsDirectory(context)
return LogFileWriterV2Impl(logsDirectory)
} else {
val logsDirectory = LogFileWriter.logsDirectory(context)
return LogFileWriterV1Impl(logsDirectory)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import com.ramcosta.composedestinations.spec.Direction
import com.wire.android.BuildConfig
import com.wire.android.R
import com.wire.android.util.EmailComposer
import com.wire.android.util.LogFileWriter
import com.wire.android.util.getDeviceIdString
import com.wire.android.util.getGitBuildId
import com.wire.android.util.getUrisOfFilesInDirectory
import com.wire.android.util.logging.LogFileWriter
import com.wire.android.util.multipleFileSharingIntent
import com.wire.android.util.sha256

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.lifecycle.SyncLifecycleManager
import com.wire.android.util.logIfEmptyUserName
import com.wire.android.util.logging.logIfEmptyUserName
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.id.ConversationId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.wire.android.appLogger
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.notification.CallNotificationData
import com.wire.android.services.CallService.Action
import com.wire.android.util.logIfEmptyUserName
import com.wire.android.util.logging.logIfEmptyUserName
import com.wire.kalium.common.functional.Either
import com.wire.kalium.common.functional.fold
import com.wire.kalium.common.functional.left
Expand Down
45 changes: 30 additions & 15 deletions app/src/main/kotlin/com/wire/android/ui/debug/DebugScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ClipboardManager
Expand Down Expand Up @@ -61,6 +62,10 @@ import com.wire.android.util.AppNameUtil
import com.wire.android.util.getMimeType
import com.wire.android.util.getUrisOfFilesInDirectory
import com.wire.android.util.multipleFileSharingIntent
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.launch
import java.io.File

@WireDestination
Expand All @@ -74,6 +79,7 @@ fun DebugScreen(
state = userDebugViewModel.state,
onLoggingEnabledChange = userDebugViewModel::setLoggingEnabledState,
onDeleteLogs = userDebugViewModel::deleteLogs,
onFlushLogs = userDebugViewModel::flushLogs,
onDatabaseLoggerEnabledChanged = userDebugViewModel::setDatabaseLoggerEnabledState,
onShowFeatureFlags = {
navigator.navigate(NavigationCommand(DebugFeatureFlagsScreenDestination))
Expand All @@ -88,6 +94,7 @@ internal fun UserDebugContent(
onLoggingEnabledChange: (Boolean) -> Unit,
onDatabaseLoggerEnabledChanged: (Boolean) -> Unit,
onDeleteLogs: () -> Unit,
onFlushLogs: () -> Deferred<Unit>,
onShowFeatureFlags: () -> Unit,
) {
val debugContentState: DebugContentState = rememberDebugContentState(state.logPath)
Expand All @@ -113,7 +120,7 @@ internal fun UserDebugContent(
isLoggingEnabled = isLoggingEnabled,
onLoggingEnabledChange = onLoggingEnabledChange,
onDeleteLogs = onDeleteLogs,
onShareLogs = debugContentState::shareLogs,
onShareLogs = { debugContentState.shareLogs(onFlushLogs) },
isDBLoggerEnabled = state.isDBLoggingEnabled,
onDBLoggerEnabledChange = onDatabaseLoggerEnabledChanged,
isPrivateBuild = BuildConfig.PRIVATE_BUILD,
Expand Down Expand Up @@ -181,13 +188,15 @@ fun rememberDebugContentState(logPath: String): DebugContentState {
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()

return remember {
DebugContentState(
context,
clipboardManager,
logPath,
scrollState
scrollState,
coroutineScope
)
}
}
Expand All @@ -196,7 +205,8 @@ data class DebugContentState(
val context: Context,
val clipboardManager: ClipboardManager,
val logPath: String,
val scrollState: ScrollState
val scrollState: ScrollState,
val coroutineScope: CoroutineScope
) {
fun copyToClipboard(text: String) {
clipboardManager.setText(AnnotatedString(text))
Expand All @@ -207,18 +217,22 @@ data class DebugContentState(
).show()
}

fun shareLogs() {
val dir = File(logPath).parentFile
val fileUris =
if (dir != null && dir.exists()) context.getUrisOfFilesInDirectory(dir) else arrayListOf()
val intent = context.multipleFileSharingIntent(fileUris)
// The first log file is simply text, not compressed. Get its mime type separately
// and set it as the mime type for the intent.
intent.type = fileUris.firstOrNull()?.getMimeType(context) ?: "text/plain"
// Get all other mime types and add them
val mimeTypes = fileUris.drop(1).mapNotNull { it.getMimeType(context) }
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toSet().toTypedArray())
context.startActivity(intent)
fun shareLogs(onFlushLogs: () -> Deferred<Unit>) {
coroutineScope.launch {
// Flush any buffered logs before sharing to ensure completeness
onFlushLogs().await()
val dir = File(logPath).parentFile
val fileUris =
if (dir != null && dir.exists()) context.getUrisOfFilesInDirectory(dir) else arrayListOf()
val intent = context.multipleFileSharingIntent(fileUris)
// The first log file is simply text, not compressed. Get its mime type separately
// and set it as the mime type for the intent.
intent.type = fileUris.firstOrNull()?.getMimeType(context) ?: "text/plain"
// Get all other mime types and add them
val mimeTypes = fileUris.drop(1).mapNotNull { it.getMimeType(context) }
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toSet().toTypedArray())
context.startActivity(intent)
}
}
}

Expand All @@ -233,6 +247,7 @@ internal fun PreviewUserDebugContent() = WireTheme {
onNavigationPressed = {},
onLoggingEnabledChange = {},
onDeleteLogs = {},
onFlushLogs = { CompletableDeferred(Unit) },
onDatabaseLoggerEnabledChanged = {},
onShowFeatureFlags = {},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.CurrentAccount
import com.wire.android.util.EMPTY
import com.wire.android.util.LogFileWriter
import com.wire.android.util.logging.LogFileWriter
import com.wire.kalium.common.logger.CoreLogger
import com.wire.kalium.logger.KaliumLogLevel
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.client.ObserveCurrentClientIdUseCase
import com.wire.kalium.logic.feature.debug.ChangeProfilingUseCase
import com.wire.kalium.logic.feature.debug.ObserveDatabaseLoggerStateUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand Down Expand Up @@ -87,6 +89,12 @@
logFileWriter.deleteAllLogFiles()
}

fun flushLogs(): Deferred<Unit> {
return viewModelScope.async {
logFileWriter.forceFlush()

Check warning on line 94 in app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/ui/debug/UserDebugViewModel.kt#L93-L94

Added lines #L93 - L94 were not covered by tests
}
}

fun setLoggingEnabledState(isEnabled: Boolean) {
viewModelScope.launch {
globalDataStore.setLoggingEnabled(isEnabled)
Expand Down
Loading
Loading