diff --git a/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt b/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt index bf5d69ff..69b0ba44 100644 --- a/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt +++ b/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleClient.kt @@ -631,7 +631,14 @@ class DevCycleClient private constructor( require(sdkKey.isNotEmpty()) { "SDK key must be set" } val dvcUser = requireNotNull(dvcUser) { "User must be set" } - if (logLevel.value > 0) { + // Choose the most verbose (lowest value) log level between options and builder + val effectiveLogLevel = listOfNotNull(options?.logLevel, logLevel).minByOrNull { it.value } ?: LogLevel.ERROR + + // Set the minimum log level in DevCycleLogger + DevCycleLogger.setMinLogLevel(effectiveLogLevel) + + // Start the logger if logging is enabled + if (effectiveLogLevel != LogLevel.NO_LOGGING) { DevCycleLogger.start(logger) } diff --git a/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleOptions.kt b/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleOptions.kt index 1f80ef53..1fd0bd08 100644 --- a/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleOptions.kt +++ b/android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DevCycleOptions.kt @@ -1,5 +1,7 @@ package com.devcycle.sdk.android.api +import com.devcycle.sdk.android.util.LogLevel + class DevCycleOptions( val flushEventsIntervalMs: Long, val disableEventLogging: Boolean, @@ -10,7 +12,8 @@ class DevCycleOptions( val disableAutomaticEventLogging : Boolean, val disableCustomEventLogging : Boolean, val apiProxyUrl: String?, - val eventsApiProxyUrl: String? + val eventsApiProxyUrl: String?, + val logLevel: LogLevel? ) { class DevCycleOptionsBuilder internal constructor() { private var flushEventsIntervalMs = 0L @@ -23,6 +26,7 @@ class DevCycleOptions( private var disableCustomEventLogging = false private var apiProxyUrl: String? = null private var eventsApiProxyUrl: String? = null + private var logLevel: LogLevel? = null fun flushEventsIntervalMs(flushEventsIntervalMs: Long): DevCycleOptionsBuilder { this.flushEventsIntervalMs = flushEventsIntervalMs @@ -73,6 +77,12 @@ class DevCycleOptions( this.eventsApiProxyUrl = eventsApiProxyUrl return this } + + fun logLevel(logLevel: LogLevel): DevCycleOptionsBuilder { + this.logLevel = logLevel + return this + } + fun build(): DevCycleOptions { return DevCycleOptions( flushEventsIntervalMs, @@ -84,7 +94,8 @@ class DevCycleOptions( disableAutomaticEventLogging, disableCustomEventLogging, apiProxyUrl, - eventsApiProxyUrl + eventsApiProxyUrl, + logLevel ) } } diff --git a/android-client-sdk/src/main/java/com/devcycle/sdk/android/util/DevCycleLogger.kt b/android-client-sdk/src/main/java/com/devcycle/sdk/android/util/DevCycleLogger.kt index 83a73633..a9bd0f52 100644 --- a/android-client-sdk/src/main/java/com/devcycle/sdk/android/util/DevCycleLogger.kt +++ b/android-client-sdk/src/main/java/com/devcycle/sdk/android/util/DevCycleLogger.kt @@ -136,10 +136,16 @@ class DevCycleLogger private constructor() { /** Return whether a message at `priority` should be logged. */ @Deprecated("Use isLoggable(String, int)", ReplaceWith("this.isLoggable(null, priority)")) - protected open fun isLoggable(priority: Int) = true + protected open fun isLoggable(priority: Int) = isLoggable(null, priority) /** Return whether a message at `priority` or `tag` should be logged. */ - protected open fun isLoggable(tag: String?, priority: Int) = isLoggable(priority) + protected open fun isLoggable(tag: String?, priority: Int): Boolean { + val minLogLevel = Loggers.minLogLevel + return when (minLogLevel) { + LogLevel.NO_LOGGING -> false + else -> priority >= minLogLevel.value + } + } private fun prepareLog(priority: Int, t: Throwable?, message: String?, vararg args: Any?) { // Consume tag even when message is not loggable so that next message is correctly tagged. @@ -270,6 +276,17 @@ class DevCycleLogger private constructor() { } companion object Loggers : Logger() { + /** The minimum log level that will be logged. Defaults to ERROR. */ + @Volatile + var minLogLevel: LogLevel = LogLevel.ERROR + private set + + /** Set the minimum log level that will be logged. */ + @JvmStatic + fun setMinLogLevel(logLevel: LogLevel) { + minLogLevel = logLevel + } + /** Log a verbose message with optional format args. */ @JvmStatic override fun v(@NonNls message: String?, vararg args: Any?) { loggerArray.forEach { it.v(message, *args) } diff --git a/android-client-sdk/src/test/java/com/devcycle/sdk/android/util/DevCycleLoggerTests.kt b/android-client-sdk/src/test/java/com/devcycle/sdk/android/util/DevCycleLoggerTests.kt new file mode 100644 index 00000000..7a6f15d2 --- /dev/null +++ b/android-client-sdk/src/test/java/com/devcycle/sdk/android/util/DevCycleLoggerTests.kt @@ -0,0 +1,91 @@ +package com.devcycle.sdk.android.util + +import android.util.Log +import com.devcycle.sdk.android.helpers.TestDVCLogger +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* + +class DevCycleLoggerTests { + + private val logger = TestDVCLogger() + + @BeforeEach + fun setUp() { + DevCycleLogger.setMinLogLevel(LogLevel.ERROR) + try { + DevCycleLogger.stop(logger) + } catch (e: IllegalArgumentException) { + // Logger wasn't started, which is fine + } + logger.logs.clear() + } + + @AfterEach + fun tearDown() { + try { + DevCycleLogger.stop(logger) + } catch (e: IllegalArgumentException) { + // Logger wasn't started, which is fine + } + DevCycleLogger.setMinLogLevel(LogLevel.ERROR) + } + + @Test + fun `test log level filtering works correctly`() { + DevCycleLogger.start(logger) + + // Test DEBUG level - should log DEBUG and above + DevCycleLogger.setMinLogLevel(LogLevel.DEBUG) + DevCycleLogger.v("Verbose message") + DevCycleLogger.d("Debug message") + DevCycleLogger.e("Error message") + + assertEquals(2, logger.logs.size) // Should see DEBUG and ERROR, not VERBOSE + assertEquals(Log.DEBUG, logger.logs[0].first) + assertEquals(Log.ERROR, logger.logs[1].first) + + // Clear and test ERROR level - should only log ERROR + logger.logs.clear() + DevCycleLogger.setMinLogLevel(LogLevel.ERROR) + DevCycleLogger.d("Debug message") + DevCycleLogger.e("Error message") + + assertEquals(1, logger.logs.size) // Should only see ERROR + assertEquals(Log.ERROR, logger.logs[0].first) + } + + @Test + fun `test NO_LOGGING disables all logging`() { + DevCycleLogger.setMinLogLevel(LogLevel.NO_LOGGING) + DevCycleLogger.start(logger) + + DevCycleLogger.v("Verbose message") + DevCycleLogger.d("Debug message") + DevCycleLogger.i("Info message") + DevCycleLogger.w("Warning message") + DevCycleLogger.e("Error message") + + assertEquals(0, logger.logs.size) // Should log nothing + } + + @Test + fun `test setMinLogLevel updates the log level`() { + assertEquals(LogLevel.ERROR, DevCycleLogger.minLogLevel) + + DevCycleLogger.setMinLogLevel(LogLevel.DEBUG) + assertEquals(LogLevel.DEBUG, DevCycleLogger.minLogLevel) + + DevCycleLogger.setMinLogLevel(LogLevel.NO_LOGGING) + assertEquals(LogLevel.NO_LOGGING, DevCycleLogger.minLogLevel) + } + + @Test + fun `test LogLevel values are ordered correctly for verbosity`() { + // Verify that lower values are more verbose (VERBOSE=2, DEBUG=3, etc.) + assertTrue(LogLevel.VERBOSE.value < LogLevel.DEBUG.value) + assertTrue(LogLevel.DEBUG.value < LogLevel.INFO.value) + assertTrue(LogLevel.INFO.value < LogLevel.WARN.value) + assertTrue(LogLevel.WARN.value < LogLevel.ERROR.value) + assertEquals(0, LogLevel.NO_LOGGING.value) // Special case + } +} \ No newline at end of file