Skip to content

feat: reduced IO logger #4078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 = {},
onEnableWireCellsFeature = {},
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 @@ -164,6 +168,82 @@
}
}

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

Check warning on line 173 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#L172-L173

Added lines #L172 - L173 were not covered by tests
}

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

Check warning on line 180 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#L178-L180

Added lines #L178 - L180 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 191 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-L191

Added lines #L189 - L191 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 194 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#L193-L194

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

Check warning on line 197 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#L196-L197

Added lines #L196 - L197 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 199 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#L199

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

Check warning on line 202 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#L202

Added line #L202 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 212 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#L210-L212

Added lines #L210 - L212 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 220 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#L216-L220

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

Check warning on line 223 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#L222-L223

Added lines #L222 - L223 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 232 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#L232

Added line #L232 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 235 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#L235

Added line #L235 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 238 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#L238

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

Check warning on line 241 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#L241

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

@Suppress("EmptyFunctionBlock")
private fun startActivityLifecycleCallback() {
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
Expand Down Expand Up @@ -293,7 +373,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 377 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#L376-L377

Added lines #L376 - L377 were not covered by tests
}
}

private companion object {
Expand All @@ -316,5 +398,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 @@ -33,7 +33,7 @@ import com.wire.android.notification.CallNotificationManager
import com.wire.android.notification.NotificationIds
import com.wire.android.services.CallService.Action
import com.wire.android.util.dispatchers.DispatcherProvider
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.logic.CoreLogic
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 @@ -60,6 +61,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

@RootNavGraph
Expand All @@ -74,6 +79,7 @@ fun DebugScreen(
state = userDebugViewModel.state,
onLoggingEnabledChange = userDebugViewModel::setLoggingEnabledState,
onDeleteLogs = userDebugViewModel::deleteLogs,
onFlushLogs = userDebugViewModel::flushLogs,
onDatabaseLoggerEnabledChanged = userDebugViewModel::setDatabaseLoggerEnabledState,
onEnableWireCellsFeature = userDebugViewModel::enableWireCellsFeature,
)
Expand All @@ -86,6 +92,7 @@ internal fun UserDebugContent(
onLoggingEnabledChange: (Boolean) -> Unit,
onDatabaseLoggerEnabledChanged: (Boolean) -> Unit,
onDeleteLogs: () -> Unit,
onFlushLogs: () -> Deferred<Unit>,
onEnableWireCellsFeature: (Boolean) -> Unit,
) {
val debugContentState: DebugContentState = rememberDebugContentState(state.logPath)
Expand All @@ -111,7 +118,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 @@ -182,13 +189,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 @@ -197,7 +206,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 @@ -208,18 +218,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 @@ -234,6 +248,7 @@ internal fun PreviewUserDebugContent() = WireTheme {
onNavigationPressed = {},
onLoggingEnabledChange = {},
onDeleteLogs = {},
onFlushLogs = { CompletableDeferred(Unit) },
onDatabaseLoggerEnabledChanged = {},
onEnableWireCellsFeature = {},
)
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 @@ -94,6 +96,12 @@
logFileWriter.deleteAllLogFiles()
}

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

Check warning on line 101 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#L100-L101

Added lines #L100 - L101 were not covered by tests
}
}

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