diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt index 7aeaa1c67eb..00514af944e 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/LogLevel.kt @@ -30,5 +30,26 @@ public enum class LogLevel { WARN, /** Do not log anything. */ - NONE, + NONE; + + internal companion object { + + /** + * Returns one of the two given log levels, the one that is "noisier" (i.e. that logs more). + * + * It can be useful to figure out which of two log levels are noisier on log level change, to + * emit a message about the log level change at the noisiest level. + */ + fun noisiestOf(logLevel1: LogLevel, logLevel2: LogLevel): LogLevel = + when (logLevel1) { + DEBUG -> DEBUG + NONE -> logLevel2 + WARN -> + when (logLevel2) { + DEBUG -> DEBUG + WARN -> WARN + NONE -> WARN + } + } + } } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt index 77639e94838..1d759735848 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/Logger.kt @@ -20,9 +20,14 @@ import android.util.Log import com.google.firebase.dataconnect.BuildConfig import com.google.firebase.dataconnect.LogLevel import com.google.firebase.dataconnect.core.LoggerGlobals.LOG_TAG +import com.google.firebase.dataconnect.core.LoggerGlobals.Logger import com.google.firebase.util.nextAlphanumericString import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch internal interface Logger { val name: String @@ -59,7 +64,11 @@ private class LoggerImpl(override val name: String) : Logger { internal object LoggerGlobals { const val LOG_TAG = "FirebaseDataConnect" - val logLevel = MutableStateFlow(LogLevel.WARN) + val logLevel = + MutableStateFlow(LogLevel.WARN).also { logLevelFlow -> + val logger = Logger("LogLevelChange") + @OptIn(DelicateCoroutinesApi::class) logger.logChanges(logLevelFlow, GlobalScope) + } inline fun Logger.debug(message: () -> Any?) { if (logLevel.value <= LogLevel.DEBUG) debug("${message()}") @@ -86,4 +95,21 @@ internal object LoggerGlobals { } fun Logger(name: String): Logger = LoggerImpl(name) + + // Log a message each time the log level changes. This is intended to provide context when debug + // logging is enabled and no logs are produced, to at least confirm that debug logging has been + // enabled. Also, it will leave a "mark" in the logs when debug logging is _disabled_ to explain + // why the debug logs stop. + private fun Logger.logChanges(flow: MutableStateFlow, coroutineScope: CoroutineScope) { + var previousLogLevel = flow.value + coroutineScope.launch { + flow.collect { newLogLevel: LogLevel -> + if (newLogLevel != previousLogLevel) { + val emitLogLevel = LogLevel.noisiestOf(newLogLevel, previousLogLevel) + log(null, emitLogLevel, "Log level changed to $newLogLevel (was $previousLogLevel)") + previousLogLevel = newLogLevel + } + } + } + } } diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt new file mode 100644 index 00000000000..20073b7556d --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/LogLevelUnitTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoggerUnitTest { + + @Test + fun `noisiestOf()`() = runTest { + assertSoftly { + verifyNoisiestOf(LogLevel.NONE, LogLevel.NONE, LogLevel.NONE) + verifyNoisiestOf(LogLevel.NONE, LogLevel.WARN, LogLevel.WARN) + verifyNoisiestOf(LogLevel.NONE, LogLevel.DEBUG, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.WARN, LogLevel.NONE, LogLevel.WARN) + verifyNoisiestOf(LogLevel.WARN, LogLevel.WARN, LogLevel.WARN) + verifyNoisiestOf(LogLevel.WARN, LogLevel.DEBUG, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.NONE, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.WARN, LogLevel.DEBUG) + verifyNoisiestOf(LogLevel.DEBUG, LogLevel.DEBUG, LogLevel.DEBUG) + } + } + + private companion object { + + fun verifyNoisiestOf(logLevel1: LogLevel, logLevel2: LogLevel, expected: LogLevel) { + withClue("noisiestOf($logLevel1, $logLevel2)") { + LogLevel.noisiestOf(logLevel1, logLevel2) shouldBe expected + } + } + } +}