Skip to content

Commit fc05dd6

Browse files
authored
Fix SLF4J conflict with Kotlin scripting, default log level (#203)
In #178, logging was changed to SLF4J and logback-classic. To keep it possible to set the log level via environment variables (`Config.logLevel` / `DEVELOCITY_API_LOG_LEVEL`), a small hack was in place. Kotlin scripts, [which have `slf4j-simple` in the classpath by default][1], crashed due to this hack. ``` java.lang.ClassCastException: class org.slf4j.impl.SimpleLogger cannot be cast to class ch.qos.logback.classic.Logger (org.slf4j.impl.SimpleLogger is in unnamed module of loader java.net.URLClassLoader @3a0f0552; ch.qos.logback.classic.Logger is in unnamed module of loader java.net.URLClassLoader @47b530e0) at com.gabrielfeo.develocity.api.internal.RealLoggerFactory.newLogger(LoggerFactory.kt:17) at com.gabrielfeo.develocity.api.internal.OkHttpClientKt.addNetworkInterceptors(OkHttpClient.kt:61) at com.gabrielfeo.develocity.api.internal.OkHttpClientKt.buildOkHttpClient(OkHttpClient.kt:35) at com.gabrielfeo.develocity.api.RealDevelocityApi$okHttpClient$2.invoke(DevelocityApi.kt:72) at com.gabrielfeo.develocity.api.RealDevelocityApi$okHttpClient$2.invoke(DevelocityApi.kt:71) at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) at com.gabrielfeo.develocity.api.RealDevelocityApi.getOkHttpClient(DevelocityApi.kt:71) at com.gabrielfeo.develocity.api.RealDevelocityApi.access$getOkHttpClient(DevelocityApi.kt:67) at com.gabrielfeo.develocity.api.RealDevelocityApi$retrofit$2.invoke(DevelocityApi.kt:78) at com.gabrielfeo.develocity.api.RealDevelocityApi$retrofit$2.invoke(DevelocityApi.kt:75) at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) at com.gabrielfeo.develocity.api.RealDevelocityApi.getRetrofit(DevelocityApi.kt:75) at com.gabrielfeo.develocity.api.RealDevelocityApi.access$getRetrofit(DevelocityApi.kt:67) at com.gabrielfeo.develocity.api.RealDevelocityApi$buildsApi$2.invoke(DevelocityApi.kt:84) at com.gabrielfeo.develocity.api.RealDevelocityApi$buildsApi$2.invoke(DevelocityApi.kt:84) at kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74) at com.gabrielfeo.develocity.api.RealDevelocityApi.getBuildsApi(DevelocityApi.kt:84) at Example_script_main$builds$1.invokeSuspend(example-script.main.kts:39) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:277) at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:95) at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:69) at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source) at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:48) at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source) at Example_script_main.<init>(example-script.main.kts:38) ``` Also, the default log level was 'info', but it was supposed to be 'off'. - Remove logback and any reference to SLF4J impls - Add `slf4j-simple` instead with a simpler hack ✨ to set the log level from environment variables - Fix the default log level to be 'off', unless it'd conflict with the user's own SimpleLogger default - Document what the llbrary logs on each log level [1]: https://github.com/JetBrains/kotlin/blob/27c6d00cb2a5f9c0e9df62b021600e1263f5201a/libraries/tools/kotlin-main-kts/build.gradle.kts#L44
1 parent d27f4b8 commit fc05dd6

File tree

5 files changed

+68
-21
lines changed

5 files changed

+68
-21
lines changed

library/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ dependencies {
4343
implementation("com.squareup.retrofit2:converter-scalars:2.11.0")
4444
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
4545
implementation("org.slf4j:slf4j-api:2.0.11")
46-
implementation("ch.qos.logback:logback-classic:1.4.14")
46+
runtimeOnly("org.slf4j:slf4j-simple:2.0.11")
4747
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
4848
testImplementation("com.squareup.okio:okio:3.9.0")
4949
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")

library/src/main/kotlin/com/gabrielfeo/develocity/api/Config.kt

+15-6
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@ import kotlin.time.Duration.Companion.days
1414
data class Config(
1515

1616
/**
17-
* Forces a log level for internal library classes. By default, the library includes
18-
* logback-classic with logging disabled, aimed at notebook and script usage. Logging may be
19-
* enabled for troubleshooting by changing log level to "INFO", "DEBUG", etc.
17+
* Changes the default log level for library classes, such as the HTTP client. By default,
18+
* log level is the value of
19+
* [org.slf4j.simpleLogger.defaultLogLevel](https://www.slf4j.org/api/org/slf4j/simple/SimpleLogger.html)
20+
* system property or `"off"`.
2021
*
21-
* To use different SLF4J bindings, simply exclude the logback dependency.
22+
* Possible values:
23+
* - "off" (default)
24+
* - "error"
25+
* - "warn"
26+
* - "info"
27+
* - "debug" (logs HTTP traffic: URLs and status codes only)
28+
* - "trace" (logs HTTP traffic: full request and response including body, excluding
29+
* authorization header)
2230
*/
23-
val logLevel: String? =
24-
env["DEVELOCITY_API_LOG_LEVEL"],
31+
val logLevel: String =
32+
env["DEVELOCITY_API_LOG_LEVEL"]
33+
?: "off",
2534

2635
/**
2736
* Provides the URL of a Develocity API instance REST API. By default, uses

library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/LoggerFactory.kt

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.gabrielfeo.develocity.api.internal
22

3-
import ch.qos.logback.classic.Level
43
import com.gabrielfeo.develocity.api.Config
54
import org.slf4j.Logger
65
import kotlin.reflect.KClass
@@ -14,9 +13,16 @@ class RealLoggerFactory(
1413
) : LoggerFactory {
1514

1615
override fun newLogger(cls: KClass<*>): Logger {
17-
val impl = org.slf4j.LoggerFactory.getLogger(cls.java) as ch.qos.logback.classic.Logger
18-
return impl.apply {
19-
level = Level.valueOf(config.logLevel)
16+
setLogLevel()
17+
return org.slf4j.LoggerFactory.getLogger(cls.java)
18+
}
19+
20+
private fun setLogLevel() {
21+
if (System.getProperty(SIMPLE_LOGGER_LOG_LEVEL) != null) {
22+
return
2023
}
24+
System.setProperty(SIMPLE_LOGGER_LOG_LEVEL, config.logLevel)
2125
}
2226
}
27+
28+
internal const val SIMPLE_LOGGER_LOG_LEVEL = "org.slf4j.simpleLogger.defaultLogLevel"

library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClient.kt

+3-10
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,12 @@ private fun OkHttpClient.Builder.addNetworkInterceptors(
5858
if (config.cacheConfig.cacheEnabled) {
5959
addNetworkInterceptor(buildCacheEnforcingInterceptor(config))
6060
}
61-
val httpLogger = loggerFactory.newLogger(HttpLoggingInterceptor::class)
62-
getHttpLoggingInterceptorForLogger(httpLogger)?.let {
63-
addNetworkInterceptor(it)
64-
}
61+
val logger = loggerFactory.newLogger(HttpLoggingInterceptor::class)
62+
addNetworkInterceptor(HttpLoggingInterceptor(logger = logger::debug).apply { level = BASIC })
63+
addNetworkInterceptor(HttpLoggingInterceptor(logger = logger::trace).apply { level = BODY })
6564
addNetworkInterceptor(HttpBearerAuth("bearer", config.apiToken()))
6665
}
6766

68-
private fun getHttpLoggingInterceptorForLogger(logger: Logger): Interceptor? = when {
69-
logger.isDebugEnabled -> HttpLoggingInterceptor(logger = logger::debug).apply { level = BASIC }
70-
logger.isTraceEnabled -> HttpLoggingInterceptor(logger = logger::debug).apply { level = BODY }
71-
else -> null
72-
}
73-
7467
internal fun buildCache(
7568
config: Config,
7669
loggerFactory: LoggerFactory,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.gabrielfeo.develocity.api.internal
2+
3+
import kotlin.test.Test
4+
import com.gabrielfeo.develocity.api.Config
5+
import kotlin.test.AfterTest
6+
import kotlin.test.BeforeTest
7+
import kotlin.test.assertEquals
8+
9+
class LoggerFactoryTest {
10+
11+
@BeforeTest
12+
@AfterTest
13+
fun cleanup() {
14+
System.clearProperty(SIMPLE_LOGGER_LOG_LEVEL)
15+
env = FakeEnv("DEVELOCITY_API_URL" to "https://example.com/")
16+
}
17+
18+
@Test
19+
fun `Logging off by default`() {
20+
val loggerFactory = RealLoggerFactory(Config())
21+
loggerFactory.newLogger(LoggerFactoryTest::class)
22+
assertEquals("off", System.getProperty(SIMPLE_LOGGER_LOG_LEVEL))
23+
}
24+
25+
@Test
26+
fun `Pre-existing defaultLogLevel is honored`() {
27+
val loggerFactory = RealLoggerFactory(Config(logLevel = "bar"))
28+
System.setProperty(SIMPLE_LOGGER_LOG_LEVEL, "foo")
29+
loggerFactory.newLogger(LoggerFactoryTest::class)
30+
assertEquals("foo", System.getProperty(SIMPLE_LOGGER_LOG_LEVEL))
31+
}
32+
33+
@Test
34+
fun `Logging can be set from config`() {
35+
val loggerFactory = RealLoggerFactory(Config(logLevel = "foo"))
36+
loggerFactory.newLogger(LoggerFactoryTest::class)
37+
assertEquals("foo", System.getProperty(SIMPLE_LOGGER_LOG_LEVEL))
38+
}
39+
}

0 commit comments

Comments
 (0)