Skip to content

Commit 3cd9c9e

Browse files
authored
Remove runtime SLF4J implementation (#476)
Remove the bundled SLF4J simple logger implementation, allowing users to provide their own SLF4J implementation without conflicts. - Remove slf4j-simple runtime dependency and related logging configuration - Test logging behavior for scripts and projects through a Logback-based test - Update docs to guide users on logging for each use case Resolves #332. ### #332 copy > The library ships with `simple-logger` instead of relying on the consumer to provide SLF4J bindings. This was intentional to provide out-of-the-box logging using the library from notebooks and scripts. However, it's not obvious when using the library from projects. > > ### Fix options > > 1. Document it in the README and suggest excluding `simple-logger` in the example `dependencies` snippet (unintuitive for projects) > 2. Remove `simple-logger` and `logLevel` functionality (very unintuitive for notebooks and scripts)
1 parent 7753bab commit 3cd9c9e

File tree

20 files changed

+230
-189
lines changed

20 files changed

+230
-189
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ from the same OpenAPI spec.
165165

166166
## Optional setup
167167

168+
### Logging
169+
170+
See [docs/Logging.md](docs/Logging.md) for how to configure logging in projects, scripts, and notebooks.
171+
172+
### Code-based configuration
173+
168174
Creating a custom [`Config`][8] allows you to change library settings via code instead of
169175
environment variables. It also lets you share resources between the library's `OkHttpClient` and
170176
your own. For example:

docs/Logging.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Logging
2+
3+
This library uses [SLF4J][1] but does not bundle an SLF4J implementation.
4+
5+
- [Notebooks](#notebooks)
6+
- [Scripts](#scripts)
7+
- [`simple-logger`](#simple-logger)
8+
- [Projects](#projects)
9+
- [`simple-logger`](#simple-logger-1)
10+
11+
## Notebooks
12+
13+
1. Set `%logLevel <level>` in a code cell, e.g., `%logLevel debug`.
14+
15+
Logs appear in the Kotlin Jupyter kernel logs:
16+
17+
- In IntelliJ, view logs in the Kotlin Notebook logs tool window
18+
- In Jupyter and JupyterLab, logs appear in the shell that owns the Jupyter process
19+
20+
![IntelliJ Kotlin Notebook logs tool window](media/IntelliJKernelLogs.png)
21+
22+
⚠️ Older versions of the Kotlin Jupyter kernel had issues with logging. Kernel version `0.15.0-598` and higher are known to work. In IntelliJ, configure to use a later version than bundled. In pip and conda, update the kernel package.
23+
24+
![IntelliJ Kotlin Jupyter kernel version configuration](media/IntelliJKernelSettings.png)
25+
26+
## Scripts
27+
28+
1. Add an SLF4J implementation (e.g., `slf4j-simple`, `logback-classic`, etc.)
29+
2. Set the log level for the package `com.gabrielfeo.develocity.api` using your chosen logging framework's configuration
30+
31+
### `simple-logger`
32+
33+
Adding `simple-logger` to your classpath is the easiest way to get logging in scripts. You can do this by adding the following line to your script:
34+
35+
```kotlin
36+
@file:DependsOn("org.slf4j:slf4j-simple:2.0.17")
37+
```
38+
39+
Then set the log level for `com.gabrielfeo.develocity.api` using system properties. For example:
40+
41+
- from script code
42+
43+
```kotlin
44+
@file:DependsOn("com.gabrielfeo:develocity-api-kotlin:2024.3.0")
45+
@file:DependsOn("org.slf4j:slf4j-simple:2.0.17")
46+
47+
System.setProperty("org.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api", "debug")
48+
49+
// ...
50+
```
51+
52+
- from the shebang line
53+
54+
```kotlin
55+
#!/usr/bin/env kotlin -script -J-Dorg.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api=debug
56+
57+
@file:DependsOn("com.gabrielfeo:develocity-api-kotlin:2024.3.0")
58+
@file:DependsOn("org.slf4j:slf4j-simple:2.0.17")
59+
60+
// ...
61+
```
62+
63+
- from `JAVA_OPTS`
64+
65+
```bash
66+
export JAVA_OPTS="-Dorg.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api=debug"
67+
kotlin -script example-script.main.kts
68+
```
69+
70+
## Projects
71+
72+
1. Add an SLF4J implementation (e.g., `slf4j-simple`, `logback-classic`, etc.) to your classpath
73+
2. Set the log level for the package `com.gabrielfeo.develocity.api` using your chosen logging framework's configuration
74+
75+
### `simple-logger`
76+
77+
Adding `simple-logger` to your classpath is the easiest way to get logging in projects. You can do this by adding the following dependency to your build file:
78+
79+
```kotlin
80+
// build.gradle.kts
81+
dependencies {
82+
implementation("com.gabrielfeo:develocity-api-kotlin:2024.3.0")
83+
runtimeOnly("org.slf4j:slf4j-simple:2.0.17")
84+
}
85+
```
86+
87+
Then set the system property when running your application. If using the Gradle `run` task, it can be declared in your build:
88+
89+
```kotlin
90+
tasks.named<JavaExec>("run") {
91+
// ...
92+
systemProperty("org.slf4j.simpleLogger.log.com.gabrielfeo.develocity.api", "debug")
93+
}
94+
```
95+
96+
[0]: https://www.slf4j.org/

docs/media/IntelliJKernelLogs.png

1.26 MB
Loading
850 KB
Loading

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ retrofit = "3.0.0"
1111
kotlin-coroutines = "1.10.2"
1212
kotlin-binary-compatibility-validator = "0.18.1"
1313
slf4j = "2.0.17"
14+
logback = "1.5.18"
1415
guava = "33.4.8-jre"
1516

1617
[libraries]
@@ -35,5 +36,6 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
3536
dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
3637
openapi-generator-plugin = { module = "org.openapitools:openapi-generator-gradle-plugin", version.ref = "openapi-generator" }
3738
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
38-
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
39+
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
40+
logback-core = { module = "ch.qos.logback:logback-core", version.ref = "logback" }
3941
guava = { module = "com.google.guava:guava", version.ref = "guava" }

library/api/library.api

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,20 @@ public final class com/gabrielfeo/develocity/api/BuildsApi$DefaultImpls {
7878

7979
public final class com/gabrielfeo/develocity/api/Config {
8080
public fun <init> ()V
81-
public fun <init> (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)V
82-
public synthetic fun <init> (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
83-
public final fun component1 ()Ljava/lang/String;
84-
public final fun component2 ()Ljava/net/URI;
85-
public final fun component3 ()Lkotlin/jvm/functions/Function0;
86-
public final fun component4 ()Lokhttp3/OkHttpClient$Builder;
87-
public final fun component5 ()Ljava/lang/Integer;
88-
public final fun component6 ()J
89-
public final fun component7 ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
90-
public final fun copy (Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)Lcom/gabrielfeo/develocity/api/Config;
91-
public static synthetic fun copy$default (Lcom/gabrielfeo/develocity/api/Config;Ljava/lang/String;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILjava/lang/Object;)Lcom/gabrielfeo/develocity/api/Config;
81+
public fun <init> (Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)V
82+
public synthetic fun <init> (Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
83+
public final fun component1 ()Ljava/net/URI;
84+
public final fun component2 ()Lkotlin/jvm/functions/Function0;
85+
public final fun component3 ()Lokhttp3/OkHttpClient$Builder;
86+
public final fun component4 ()Ljava/lang/Integer;
87+
public final fun component5 ()J
88+
public final fun component6 ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
89+
public final fun copy (Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;)Lcom/gabrielfeo/develocity/api/Config;
90+
public static synthetic fun copy$default (Lcom/gabrielfeo/develocity/api/Config;Ljava/net/URI;Lkotlin/jvm/functions/Function0;Lokhttp3/OkHttpClient$Builder;Ljava/lang/Integer;JLcom/gabrielfeo/develocity/api/Config$CacheConfig;ILjava/lang/Object;)Lcom/gabrielfeo/develocity/api/Config;
9291
public fun equals (Ljava/lang/Object;)Z
9392
public final fun getAccessKey ()Lkotlin/jvm/functions/Function0;
9493
public final fun getCacheConfig ()Lcom/gabrielfeo/develocity/api/Config$CacheConfig;
9594
public final fun getClientBuilder ()Lokhttp3/OkHttpClient$Builder;
96-
public final fun getLogLevel ()Ljava/lang/String;
9795
public final fun getMaxConcurrentRequests ()Ljava/lang/Integer;
9896
public final fun getReadTimeoutMillis ()J
9997
public final fun getServer ()Ljava/net/URI;

library/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ dependencies {
2525
implementation(libs.moshi.kotlin)
2626
api(libs.kotlin.coroutines)
2727
implementation(libs.slf4j.api)
28-
runtimeOnly(libs.slf4j.simple)
2928
compileOnly(libs.kotlin.jupyter.api)
3029
testImplementation(libs.okhttp.mockwebserver)
3130
testImplementation(libs.okio.fakeFileSystem)
@@ -35,6 +34,8 @@ dependencies {
3534
integrationTestImplementation(libs.kotlin.coroutines.test)
3635
integrationTestImplementation(libs.guava)
3736
integrationTestImplementation(libs.kotlin.jupyter.testkit)
37+
integrationTestImplementation(libs.logback.core)
38+
integrationTestImplementation(libs.logback.classic)
3839
}
3940

4041
val libraryPom = Action<MavenPom> {
@@ -111,7 +112,6 @@ tasks.withType<Test>().configureEach {
111112
"junit.jupiter.tempdir.cleanup.mode.default",
112113
System.getProperty("junit.jupiter.tempdir.cleanup.mode.default") ?: "always",
113114
)
114-
environment("DEVELOCITY_API_LOG_LEVEL", "DEBUG")
115115
providers.environmentVariablesPrefixedBy("DEVELOCITY_API_").get().forEach { (name, value) ->
116116
inputs.property("${name}.hashCode", value.hashCode())
117117
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.gabrielfeo.develocity.api
2+
3+
import ch.qos.logback.classic.Logger as LogbackLogger
4+
import ch.qos.logback.classic.spi.ILoggingEvent
5+
import ch.qos.logback.core.AppenderBase
6+
import ch.qos.logback.core.ConsoleAppender
7+
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
8+
import com.gabrielfeo.develocity.api.internal.*
9+
import kotlinx.coroutines.test.runTest
10+
import org.junit.jupiter.api.io.TempDir
11+
import org.slf4j.Logger
12+
import org.slf4j.LoggerFactory
13+
import java.io.File
14+
import kotlin.test.*
15+
16+
class LoggingIntegrationTest {
17+
18+
private class LogRecorder : AppenderBase<ILoggingEvent>() {
19+
val logsByLoggerName = mutableListOf<Pair<String, String>>()
20+
override fun append(eventObject: ILoggingEvent) {
21+
with(eventObject) {
22+
logsByLoggerName += (loggerName to formattedMessage)
23+
}
24+
}
25+
}
26+
27+
@TempDir
28+
lateinit var tempDir: File
29+
30+
private val recorder = LogRecorder()
31+
32+
private lateinit var api: DevelocityApi
33+
34+
@BeforeTest
35+
fun setup() {
36+
(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as LogbackLogger).apply {
37+
detachAndStopAllAppenders()
38+
addAppender(recorder)
39+
addAppender(ConsoleAppender<ILoggingEvent>().apply {
40+
context = loggerContext
41+
encoder = PatternLayoutEncoder().apply {
42+
context = loggerContext
43+
pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
44+
start()
45+
}
46+
start()
47+
})
48+
}
49+
recorder.start()
50+
env = RealEnv
51+
api = DevelocityApi.newInstance(
52+
config = Config(
53+
cacheConfig = Config.CacheConfig(
54+
cacheEnabled = true,
55+
cacheDir = tempDir,
56+
)
57+
)
58+
)
59+
}
60+
61+
@AfterTest
62+
fun tearDown() {
63+
api.shutdown()
64+
}
65+
66+
@Test
67+
fun logsUnderLibraryPackage() = runTest {
68+
api.buildsApi.getBuilds(since = 0, maxBuilds = 1)
69+
with(recorder.logsByLoggerName) {
70+
assertTrue(isNotEmpty())
71+
assertTrue(any { (_, message) -> message.contains("cache dir", ignoreCase = true) })
72+
assertTrue(any { (_, message) -> message.contains("cache miss", ignoreCase = true) })
73+
assertTrue(any { (_, message) -> message.contains("get", ignoreCase = true) })
74+
forEach { (loggerName, message) ->
75+
assertTrue(
76+
loggerName.startsWith("com.gabrielfeo.develocity.api"),
77+
"Log from unexpected logger: '$loggerName' with message '$message'"
78+
)
79+
}
80+
}
81+
}
82+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<configuration>
2+
<logger name="com.gabrielfeo.develocity" level="DEBUG"/>
3+
<root level="INFO">
4+
<appender-ref ref="CONSOLE"/>
5+
</root>
6+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
7+
<encoder>
8+
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
9+
</encoder>
10+
</appender>
11+
</configuration>

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,6 @@ import kotlin.time.Duration.Companion.days
1515
@Suppress("MemberVisibilityCanBePrivate", "unused")
1616
data class Config(
1717

18-
/**
19-
* Changes minimum log level for library classes, including the HTTP
20-
* client, **when using `slf4j-simple`** (bundled with the library). If
21-
* replacing SLF4J bindings, this setting has no effect, and log level
22-
* must be changed in the chosen logging framework.
23-
*
24-
* Default value, by order of precedence:
25-
*
26-
* - `DEVELOCITY_API_LOG_LEVEL` environment variable
27-
* - `org.slf4j.simpleLogger.defaultLogLevel` system property
28-
* - `"off"`
29-
*
30-
* SLF4J valid log levels and their usage by the library:
31-
*
32-
* - "off" (default, no logs)
33-
* - "error"
34-
* - "warn"
35-
* - "info"
36-
* - "debug" (logs HTTP traffic: URLs and status codes only)
37-
* - "trace" (logs HTTP traffic: full request and response including body, excluding
38-
* authorization header)
39-
*/
40-
val logLevel: String =
41-
env["DEVELOCITY_API_LOG_LEVEL"]
42-
?: systemProperties.logLevel
43-
?: "off",
44-
4518
/**
4619
* Provides the URL of a Develocity server to use in API requests. By default, uses environment
4720
* variable `DEVELOCITY_URL`. Must be a valid URL with no path segments (trailing slash OK) or

0 commit comments

Comments
 (0)