Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -84,7 +94,8 @@ class DevCycleOptions(
disableAutomaticEventLogging,
disableCustomEventLogging,
apiProxyUrl,
eventsApiProxyUrl
eventsApiProxyUrl,
logLevel
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}