Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Debug logging does not include IMAP/POP3 commands in beta versions
### Checklist

- [x] I have used the search function to see if someone else has already submitted the same bug report.
- [x] I will describe the problem with as much detail as possible.

### App

Thunderbird for Android

### App version

12.0b4

### Where did you get the app from?

Other

### Android version

13 (xiaomi.eu 14.0.3.0.TJUMIXM)

### Device model

Xiaomi POCO X3 Pro (global)

### Steps to reproduce

1. Go to Settings -> "General settings" -> Debugging
2. Enable all options (debug logging, sync debug logging, log sensitive information)
3. Refresh a folder
4. Export logs or observe logcat output

### Expected behavior

Logs include IMAP/POP3 commands sent to the mail server and received responses, as well as other debugging information (number of unsynced messages, etc.)

### Actual behavior

There is no useful debugging information in debug logs (see below for an example). This makes debugging problems with message synchronisation much harder.

This problem does not exist in debug builds, but most people who submit bug reports will most likely run a release version. In my case a debug build (from beta branch) synchronises correctly, but the release (12.0b4) downloaded from archive.mozilla.org doesn't, so I only have debug logs from a version where everything is ok...

#9250 introduced logging changes that prevented messages with level lower than INFO (VERBOSE and DEBUG) from being logged at all. #9359 changed the minimum log level to VERBOSE, but only for debug builds. Most log messages enabled by the "Enable debug logging" option are DEBUG or VERBOSE, so enabling this option does almost nothing.

### Logs

```
08-04 14:22:58.350 32664 14487 I MessagingController: Running command 'synchronizeMailbox', seq = 119 (background priority)
08-04 14:22:58.352 32664 14487 I ImapSync: Synchronizing folder konto.pl:INBOX
08-04 14:22:58.731 32664 14458 I rd.android.beta: This is non sticky GC, maxfree is 8388608 minfree is 2097152
08-04 14:23:03.167 32664 14458 I rd.android.beta: This is sticky GC, maxfree is 8388608 minfree is 2097152
08-04 14:23:03.668 32664 14487 I ImapSync: Done synchronizing folder konto.pl:INBOX
08-04 14:23:03.668 32664 14487 I MessagingController:  Command 'synchronizeMailbox' completed
08-04 14:23:03.668 32664 14487 I MessagingController: Running command 'sendPendingMessages', seq = 120 (background priority)
08-04 14:23:03.669 32664 14487 I MessagingController:  Command 'sendPendingMessages' completed
```


Discussion hints:

Pull request: fix(logs): debug logs ignored when user enable it
Fixes #9561.

- Fixes the "Enable debug logging" preference not logging any useful data outside debuggable builds
- Refactor:
  - Move `CompositeLogSinkManager` to `:core:logging:api`
  - Replaced hardcoded string qualifier `syncDebug` with the `LogSync.Qualifier.SyncDebug` enum qualifier for improved discoverability.

Test command: ./gradlew :core:logging:impl-composite:testDebugUnitTest --tests "net.thunderbird.core.logging.composite.DefaultCompositeLogSinkTest"

Fix patch:
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
index 92e1c60d6b3..c9d1d85c7d9 100644
--- a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
@@ -2,9 +2,13 @@ package net.thunderbird.app.common.core
 
 import android.content.Context
 import kotlin.time.ExperimentalTime
-import net.thunderbird.app.common.BuildConfig
+import net.thunderbird.app.common.core.logging.DefaultLogLevelManager
+import net.thunderbird.core.common.inject.getList
+import net.thunderbird.core.common.inject.singleListOf
 import net.thunderbird.core.logging.DefaultLogger
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
@@ -13,25 +17,22 @@ import net.thunderbird.core.logging.file.AndroidFileSystemManager
 import net.thunderbird.core.logging.file.FileLogSink
 import org.koin.core.module.Module
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 
 val appCommonCoreModule: Module = module {
-    single<LogLevel> {
-        if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
-    }
+    single<LogLevelManager> {
+        DefaultLogLevelManager()
+    }.bind<LogLevelProvider>()
 
-    single<List<LogSink>> {
-        listOf(
-            ConsoleLogSink(
-                level = get(),
-            ),
-        )
-    }
+    singleListOf<LogSink>(
+        { ConsoleLogSink(level = LogLevel.VERBOSE) },
+    )
 
     single<CompositeLogSink> {
         CompositeLogSink(
-            level = get(),
-            sinks = get(),
+            logLevelProvider = get(),
+            sinks = getList(),
         )
     }
 
@@ -44,8 +45,8 @@ val appCommonCoreModule: Module = module {
 
     single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
         CompositeLogSink(
-            level = get(),
-            sinks = get(),
+            logLevelProvider = get(),
+            sinks = getList(),
         )
     }
 
@@ -58,7 +59,7 @@ val appCommonCoreModule: Module = module {
         )
     }
 
-    single<Logger> (named(SYNC_DEBUG_LOG)) {
+    single<Logger>(named(SYNC_DEBUG_LOG)) {
         @OptIn(ExperimentalTime::class)
         DefaultLogger(
             sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt
new file mode 100644
index 00000000000..9dc4951226b
--- /dev/null
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/DefaultLogLevelManager.kt
@@ -0,0 +1,22 @@
+package net.thunderbird.app.common.core.logging
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import net.thunderbird.app.common.BuildConfig
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+
+class DefaultLogLevelManager : LogLevelManager {
+    private val defaultLevel = if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
+    private val logLevel = MutableStateFlow(defaultLevel)
+
+    override fun override(level: LogLevel) {
+        logLevel.update { level }
+    }
+
+    override fun restoreDefault() {
+        override(defaultLevel)
+    }
+
+    override fun current(): LogLevel = logLevel.value
+}
diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt
new file mode 100644
index 00000000000..4263f311885
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/inject/KoinMultibindingCollection.kt
@@ -0,0 +1,64 @@
+package net.thunderbird.core.common.inject
+
+import org.koin.core.definition.Definition
+import org.koin.core.module.KoinDslMarker
+import org.koin.core.module.Module
+import org.koin.core.parameter.parametersOf
+import org.koin.core.qualifier.Qualifier
+import org.koin.core.qualifier.named
+import org.koin.core.scope.Scope
+
+// This file must be deleted once https://github.com/InsertKoinIO/koin/pull/1951 is merged to
+// Koin and released on 4.2.0
+
+/**
+ * Defines a singleton list of elements of type [T].
+ *
+ * This function creates a singleton definition for a mutable list of elements.
+ * Each element in the list is resolved from the provided [items] definitions.
+ *
+ * @param T The type of elements in the list.
+ * @param items Vararg of [Definition]s that will be resolved and added to the list.
+ * @param qualifier Optional [Qualifier] to distinguish this list from others of the same type.
+ *                  If null, a default qualifier based on the type [T] will be used.
+ */
+@KoinDslMarker
+inline fun <reified T> Module.singleListOf(vararg items: Definition<T>, qualifier: Qualifier? = null) {
+    single(qualifier ?: defaultListQualifier<T>(), createdAtStart = true) {
+        items.map { definition -> definition(this, parametersOf()) }
+    }
+}
+
+/**
+ * Resolves a [List] of instances of type [T].
+ * This is a helper function for Koin's multibinding feature.
+ *
+ * It uses the [defaultListQualifier] if no [qualifier] is provided.
+ *
+ * @param T The type of instances in the list.
+ * @param qualifier An optional [Qualifier] to distinguish between different lists of the same type.
+ * @return The resolved [MutableList] of instances of type [T].
+ */
+inline fun <reified T> Scope.getList(qualifier: Qualifier? = null) =
+    get<List<T>>(qualifier ?: defaultListQualifier<T>())
+
+/**
+ * Creates a qualifier for a set of a specific type.
+ *
+ * This is used to differentiate between different sets of the same type when injecting dependencies.
+ *
+ * @param T The type of the elements in the set.
+ * @return A qualifier for the set.
+ */
+inline fun <reified T> defaultListQualifier() =
+    defaultCollectionQualifier<List<T>, T>()
+
+/**
+ * Creates a default [Qualifier] for a collection binding.
+ *
+ * @param TCollection The type of the collection (e.g., `List`, `List`).
+ * @param T The type of the elements in the collection.
+ * @return A [Qualifier] that can be used to identify the specific collection binding.
+ */
+inline fun <reified TCollection : Collection<T>, reified T> defaultCollectionQualifier() =
+    named("${TCollection::class.qualifiedName}<${T::class.qualifiedName}>")
diff --git a/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt
new file mode 100644
index 00000000000..1391f4352b6
--- /dev/null
+++ b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelManager.kt
@@ -0,0 +1,28 @@
+package net.thunderbird.core.logging
+
+/**
+ * Manages the log level for the application.
+ *
+ * This interface provides a way to update the log level dynamically.
+ * Implementations of this interface are responsible for persisting the log level
+ * and notifying listeners of changes.
+ */
+interface LogLevelManager : LogLevelProvider {
+    /**
+     * Overrides the current log level.
+     *
+     * This function allows for a temporary change in the log level,
+     * typically for debugging or specific operational needs.
+     * The original log level can be restored by calling [restoreDefault] function
+     *
+     * @param level The new log level to set
+     */
+    fun override(level: LogLevel)
+
+    /**
+     * Restores the log level to its default value.
+     *
+     * The default log level is defined by the specific implementation of this interface.
+     */
+    fun restoreDefault()
+}
diff --git a/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt
new file mode 100644
index 00000000000..34e46be9272
--- /dev/null
+++ b/core/logging/api/src/commonMain/kotlin/net/thunderbird/core/logging/LogLevelProvider.kt
@@ -0,0 +1,15 @@
+package net.thunderbird.core.logging
+
+/**
+ * Provides the current [LogLevel].
+ *
+ * This can be used to dynamically change the log level during runtime.
+ */
+fun interface LogLevelProvider {
+    /**
+     * Gets the current log level.
+     *
+     * @return The current log level.
+     */
+    fun current(): LogLevel
+}
diff --git a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
index 5db2a7ba766..77b981daa68 100644
--- a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
+++ b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/CompositeLogSink.kt
@@ -1,6 +1,7 @@
 package net.thunderbird.core.logging.composite
 
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink
 
 /**
@@ -22,15 +23,15 @@ interface CompositeLogSink : LogSink {
 /**
  * Creates a [CompositeLogSink] with the specified log level and manager.
  *
- * @param level The minimum [LogLevel] for messages to be logged.
+ * @param logLevelProvider The minimum [LogLevel] for messages to be logged.
  * @param manager The [CompositeLogSinkManager] that manages the collection of sinks.
  * @param sinks A list of [LogSink] instances to be managed by this composite sink.
  * @return A new instance of [CompositeLogSink].
  */
 fun CompositeLogSink(
-    level: LogLevel,
+    logLevelProvider: LogLevelProvider,
     manager: CompositeLogSinkManager = DefaultLogSinkManager(),
     sinks: List<LogSink> = emptyList(),
 ): CompositeLogSink {
-    return DefaultCompositeLogSink(level, manager, sinks)
+    return DefaultCompositeLogSink(logLevelProvider, manager, sinks)
 }
diff --git a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
index 481cc27e51f..5f398aed402 100644
--- a/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
+++ b/core/logging/impl-composite/src/commonMain/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSink.kt
@@ -2,13 +2,15 @@ package net.thunderbird.core.logging.composite
 
 import net.thunderbird.core.logging.LogEvent
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.LogSink
 
 internal class DefaultCompositeLogSink(
-    override val level: LogLevel,
+    private val logLevelProvider: LogLevelProvider,
     override val manager: CompositeLogSinkManager = DefaultLogSinkManager(),
     sinks: List<LogSink> = emptyList(),
 ) : CompositeLogSink {
+    override val level: LogLevel get() = logLevelProvider.current()
 
     init {
         manager.addAll(sinks)
diff --git a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
index 44756a57923..77cb628c942 100644
--- a/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
+++ b/core/preference/impl/src/commonMain/kotlin/net/thunderbird/core/preference/debugging/DefaultDebuggingSettingsPreferenceManager.kt
@@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.preference.storage.Storage
 import net.thunderbird.core.preference.storage.StorageEditor
@@ -20,6 +22,7 @@ class DefaultDebuggingSettingsPreferenceManager(
     private val logger: Logger,
     private val storage: Storage,
     private val storageEditor: StorageEditor,
+    private val logLevelManager: LogLevelManager,
     private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
     private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
 ) : DebuggingSettingsPreferenceManager {
@@ -32,7 +35,7 @@ class DefaultDebuggingSettingsPreferenceManager(
     override fun save(config: DebuggingSettings) {
         logger.debug(TAG) { "save() called with: config = $config" }
         writeConfig(config)
-        configState.update { config }
+        configState.update { config.also(::updateDebugLogLevel) }
     }
 
     private fun loadConfig(): DebuggingSettings = DebuggingSettings(
@@ -44,7 +47,7 @@ class DefaultDebuggingSettingsPreferenceManager(
             KEY_ENABLE_SYNC_DEBUG_LOGGING,
             DEBUGGING_SETTINGS_DEFAULT_IS_SYNC_LOGGING_ENABLED,
         ),
-    )
+    ).also(::updateDebugLogLevel)
 
     private fun writeConfig(config: DebuggingSettings) {
         logger.debug(TAG) { "writeConfig() called with: config = $config" }
@@ -58,4 +61,12 @@ class DefaultDebuggingSettingsPreferenceManager(
             }
         }
     }
+
+    private fun updateDebugLogLevel(config: DebuggingSettings) {
+        if (config.isDebugLoggingEnabled) {
+            logLevelManager.override(LogLevel.DEBUG)
+        } else {
+            logLevelManager.restoreDefault()
+        }
+    }
 }
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
index 2cda0b84a69..52c4d1fc170 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
@@ -71,6 +71,7 @@ val preferencesModule = module {
             logger = get(),
             storage = get<Preferences>().storage,
             storageEditor = get<Preferences>().createStorageEditor(),
+            logLevelManager = get(),
         )
     }
     single<DebugLogConfigurator> {


Test patch:
diff --git a/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt b/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
index ec3fed6ff8d..7b2f8aae487 100644
--- a/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
+++ b/core/logging/impl-composite/src/commonTest/kotlin/net/thunderbird/core/logging/composite/DefaultCompositeLogSinkTest.kt
@@ -19,7 +19,7 @@ class DefaultCompositeLogSinkTest {
 
         // Act
         DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
             sinks = listOf(sink1, sink2),
         )
@@ -38,7 +38,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
 
         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
         )
 
@@ -60,7 +60,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
 
         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.WARN,
+            logLevelProvider = { LogLevel.WARN },
             manager = sinkManager,
         )
 
@@ -80,7 +80,7 @@ class DefaultCompositeLogSinkTest {
         val sinkManager = FakeCompositeLogSinkManager(mutableListOf(sink1, sink2))
 
         val testSubject = DefaultCompositeLogSink(
-            level = LogLevel.INFO,
+            logLevelProvider = { LogLevel.INFO },
             manager = sinkManager,
         )
 
diff --git a/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt b/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt
new file mode 100644
index 00000000000..97b73330fe2
--- /dev/null
+++ b/core/logging/testing/src/commonMain/kotlin/net/thunderbird/core/logging/testing/TestLogLevelManager.kt
@@ -0,0 +1,17 @@
+package net.thunderbird.core.logging.testing
+
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+
+class TestLogLevelManager : LogLevelManager {
+    var logLevel = LogLevel.VERBOSE
+    override fun override(level: LogLevel) {
+        logLevel = level
+    }
+
+    override fun restoreDefault() {
+        logLevel = LogLevel.VERBOSE
+    }
+
+    override fun current(): LogLevel = logLevel
+}
diff --git a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
index c3772111eae..5d005e62b95 100644
--- a/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -17,15 +17,19 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import net.thunderbird.legacy.core.FakeAccountDefaultsProvider
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.kotlin.mock
 
@@ -52,7 +56,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = mock<FileLogSink>()
 
         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -61,6 +65,7 @@ class TestApp : Application() {
 
 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }
diff --git a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
index f7f9bf7d323..943cc1be6e1 100644
--- a/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
+++ b/legacy/storage/src/test/java/com/fsck/k9/storage/TestApp.kt
@@ -17,14 +17,18 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.kotlin.mock
 
@@ -50,7 +54,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = mock<FileLogSink>()
 
         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -59,6 +63,7 @@ class TestApp : Application() {
 
 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }
diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
index 644112cd9ae..1fb618518ca 100644
--- a/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
+++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/TestApp.kt
@@ -10,14 +10,18 @@ import net.thunderbird.core.featureflag.FeatureFlag
 import net.thunderbird.core.featureflag.FeatureFlagProvider
 import net.thunderbird.core.featureflag.InMemoryFeatureFlagProvider
 import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
 import net.thunderbird.core.logging.Logger
 import net.thunderbird.core.logging.composite.CompositeLogSink
 import net.thunderbird.core.logging.composite.CompositeLogSinkManager
 import net.thunderbird.core.logging.file.FileLogSink
 import net.thunderbird.core.logging.legacy.Log
+import net.thunderbird.core.logging.testing.TestLogLevelManager
 import net.thunderbird.core.logging.testing.TestLogger
 import net.thunderbird.core.preference.storage.StoragePersister
 import org.koin.core.qualifier.named
+import org.koin.dsl.bind
 import org.koin.dsl.module
 import org.mockito.Mockito.mock
 
@@ -44,7 +48,7 @@ class TestApp : Application() {
         val fileSink: FileLogSink = org.mockito.kotlin.mock<FileLogSink>()
 
         val compositeSink: CompositeLogSink = CompositeLogSink(
-            level = LogLevel.DEBUG,
+            logLevelProvider = { LogLevel.DEBUG },
             manager = sinkManager,
             sinks = listOf(fileSink),
         )
@@ -53,6 +57,7 @@ class TestApp : Application() {
 
 val testModule = module {
     single<Logger> { TestApp.logger }
+    single<LogLevelManager> { TestLogLevelManager() }.bind<LogLevelProvider>()
     single(named("syncDebug")) { TestApp.logger }
     single(named("syncDebug")) { TestApp.compositeSink }
     single(named("syncDebug")) { TestApp.fileSink }