Skip to content

Commit 08721de

Browse files
authored
fix(rt): slf4j 1.x compatability (#963)
1 parent f6504d5 commit 08721de

File tree

13 files changed

+371
-61
lines changed

13 files changed

+371
-61
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "d7863388-cf31-44d7-b613-c2376e853b97",
3+
"type": "bugfix",
4+
"description": "Provide SLF4J 1.x compatible fallback implementation",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#993"
7+
]
8+
}

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ apiValidation {
127127
"paginator-tests",
128128
"waiter-tests",
129129
"compile",
130+
"slf4j-1x-consumer",
131+
"slf4j-2x-consumer",
130132
),
131133
)
132134
}

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ okhttp-version = "5.0.0-alpha.11"
99
okio-version = "3.3.0"
1010
otel-version = "1.27.0"
1111
slf4j-version = "2.0.6"
12+
slf4j-v1x-version = "1.7.36"
1213
crt-kotlin-version = "0.7.1"
1314

1415
# codegen
@@ -51,6 +52,7 @@ okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version
5152
opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", version.ref = "otel-version" }
5253
opentelemetry-sdk-testing = {module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "otel-version" }
5354
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" }
55+
slf4j-api-v1x = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-v1x-version" }
5456
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" }
5557
crt-kotlin = { module = "aws.sdk.kotlin.crt:aws-crt-kotlin", version.ref = "crt-kotlin-version" }
5658

runtime/observability/logging-slf4j2/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
5-
description = "Logging provider based on SLF4J 2"
6-
extra["displayName"] = "Smithy :: Kotlin :: Observability :: SLF4J2 binding"
5+
description = "Logging provider based on SLF4J"
6+
extra["displayName"] = "Smithy :: Kotlin :: Observability :: SLF4J binding"
77
extra["moduleName"] = "aws.smithy.kotlin.runtime.telemetry"
88

99
kotlin {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.logging.slf4j
6+
7+
import aws.smithy.kotlin.runtime.telemetry.logging.LogLevel
8+
import aws.smithy.kotlin.runtime.telemetry.logging.Logger
9+
10+
/**
11+
* Common functionality across SLF4J 1.x and 2.x
12+
* @param delegate the underlying slf4j logger instance
13+
*/
14+
internal abstract class AbstractSlf4jLoggerAdapter(protected val delegate: org.slf4j.Logger) : Logger {
15+
override fun trace(t: Throwable?, msg: () -> String) {
16+
if (!isEnabledFor(LogLevel.Trace)) return
17+
if (t != null) {
18+
delegate.trace(msg(), t)
19+
} else {
20+
delegate.trace(msg())
21+
}
22+
}
23+
override fun debug(t: Throwable?, msg: () -> String) {
24+
if (!isEnabledFor(LogLevel.Debug)) return
25+
if (t != null) {
26+
delegate.debug(msg(), t)
27+
} else {
28+
delegate.debug(msg())
29+
}
30+
}
31+
override fun info(t: Throwable?, msg: () -> String) {
32+
if (!isEnabledFor(LogLevel.Info)) return
33+
if (t != null) {
34+
delegate.info(msg(), t)
35+
} else {
36+
delegate.info(msg())
37+
}
38+
}
39+
override fun warn(t: Throwable?, msg: () -> String) {
40+
if (!isEnabledFor(LogLevel.Warning)) return
41+
if (t != null) {
42+
delegate.warn(msg(), t)
43+
} else {
44+
delegate.warn(msg())
45+
}
46+
}
47+
override fun error(t: Throwable?, msg: () -> String) {
48+
if (!isEnabledFor(LogLevel.Error)) return
49+
if (t != null) {
50+
delegate.error(msg(), t)
51+
} else {
52+
delegate.error(msg())
53+
}
54+
}
55+
override fun isEnabledFor(level: LogLevel): Boolean = when (level) {
56+
LogLevel.Trace -> delegate.isTraceEnabled
57+
LogLevel.Debug -> delegate.isDebugEnabled
58+
LogLevel.Info -> delegate.isInfoEnabled
59+
LogLevel.Warning -> delegate.isWarnEnabled
60+
LogLevel.Error -> delegate.isErrorEnabled
61+
}
62+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.logging.slf4j
6+
7+
import aws.smithy.kotlin.runtime.telemetry.context.Context
8+
import aws.smithy.kotlin.runtime.telemetry.logging.LogLevel
9+
import aws.smithy.kotlin.runtime.telemetry.logging.LogRecordBuilder
10+
import aws.smithy.kotlin.runtime.telemetry.logging.LoggerProvider
11+
import org.slf4j.MDC
12+
13+
private val noOpLogRecordBuilder = LoggerProvider.None.getOrCreateLogger("NoOpLogger").atLevel(LogLevel.Error)
14+
15+
/**
16+
* SLF4J 1.x based logger
17+
*/
18+
internal class Slf4j1xLoggerAdapter(logger: org.slf4j.Logger) : AbstractSlf4jLoggerAdapter(logger) {
19+
override fun atLevel(level: LogLevel): LogRecordBuilder = if (isEnabledFor(level)) {
20+
Slf4j1xLogRecordBuilderAdapter(this, level)
21+
} else {
22+
noOpLogRecordBuilder
23+
}
24+
}
25+
26+
private class Slf4j1xLogRecordBuilderAdapter(
27+
private val delegate: Slf4j1xLoggerAdapter,
28+
private val level: LogLevel,
29+
) : LogRecordBuilder {
30+
31+
private var msg: (() -> String)? = null
32+
private var cause: Throwable? = null
33+
private val kvp by lazy { mutableMapOf<String, Any>() }
34+
override fun setCause(ex: Throwable) {
35+
cause = ex
36+
}
37+
38+
override fun setMessage(message: String) {
39+
msg = { message }
40+
}
41+
42+
override fun setMessage(message: () -> String) {
43+
msg = message
44+
}
45+
46+
override fun setKeyValuePair(key: String, value: Any) {
47+
kvp[key] = value
48+
}
49+
50+
override fun setContext(context: Context) {
51+
// TODO - add a way to get the current trace context and set the key/value pair on it?
52+
}
53+
54+
override fun emit() {
55+
val message = requireNotNull(msg) { "no message provided to LogRecordBuilder" }
56+
val logMethod = when (level) {
57+
LogLevel.Error -> delegate::error
58+
LogLevel.Warning -> delegate::warn
59+
LogLevel.Info -> delegate::info
60+
LogLevel.Debug -> delegate::debug
61+
LogLevel.Trace -> delegate::trace
62+
}
63+
64+
if (kvp.isNotEmpty()) {
65+
val prevCtx = MDC.getCopyOfContextMap()
66+
try {
67+
kvp.forEach { (k, v) ->
68+
MDC.put(k, v.toString())
69+
}
70+
logMethod(cause, message)
71+
} finally {
72+
MDC.setContextMap(prevCtx)
73+
}
74+
} else {
75+
logMethod(cause, message)
76+
}
77+
}
78+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.telemetry.logging.slf4j
6+
7+
import aws.smithy.kotlin.runtime.telemetry.context.Context
8+
import aws.smithy.kotlin.runtime.telemetry.logging.LogLevel
9+
import aws.smithy.kotlin.runtime.telemetry.logging.LogRecordBuilder
10+
import org.slf4j.event.Level
11+
import org.slf4j.spi.LoggingEventBuilder
12+
13+
/**
14+
* SLF4J 2.x based logger
15+
*/
16+
internal class Slf4j2xLoggerAdapter(logger: org.slf4j.Logger) : AbstractSlf4jLoggerAdapter(logger) {
17+
override fun atLevel(level: LogLevel): LogRecordBuilder =
18+
Slf4j2xLogRecordBuilderAdapter(delegate.atLevel(level.slf4jLevel))
19+
}
20+
21+
private class Slf4j2xLogRecordBuilderAdapter(
22+
private val delegate: LoggingEventBuilder,
23+
) : LogRecordBuilder {
24+
override fun setCause(ex: Throwable) {
25+
delegate.setCause(ex)
26+
}
27+
28+
override fun setMessage(message: String) {
29+
delegate.setMessage(message)
30+
}
31+
32+
override fun setMessage(message: () -> String) {
33+
delegate.setMessage(message)
34+
}
35+
36+
override fun setKeyValuePair(key: String, value: Any) {
37+
delegate.addKeyValue(key, value)
38+
}
39+
40+
override fun setContext(context: Context) {
41+
// TODO - add a way to get the current trace context and set the key/value pair on it?
42+
}
43+
44+
override fun emit() = delegate.log()
45+
}
46+
47+
private val LogLevel.slf4jLevel: org.slf4j.event.Level
48+
get() = when (this) {
49+
LogLevel.Error -> Level.ERROR
50+
LogLevel.Warning -> Level.WARN
51+
LogLevel.Info -> Level.INFO
52+
LogLevel.Debug -> Level.DEBUG
53+
LogLevel.Trace -> Level.TRACE
54+
}

runtime/observability/logging-slf4j2/jvm/src/aws/smithy/kotlin/runtime/telemetry/logging/slf4j/Slf4jLoggerProvider.kt

Lines changed: 14 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,73 +5,28 @@
55

66
package aws.smithy.kotlin.runtime.telemetry.logging.slf4j
77

8-
import aws.smithy.kotlin.runtime.telemetry.context.Context
98
import aws.smithy.kotlin.runtime.telemetry.logging.*
109
import org.slf4j.LoggerFactory
11-
import org.slf4j.event.Level
12-
import org.slf4j.spi.LoggingEventBuilder
1310

1411
/**
15-
* SLF4J 2 based logger provider
12+
* SLF4J based logger provider
1613
*/
1714
public object Slf4jLoggerProvider : LoggerProvider {
18-
override fun getOrCreateLogger(name: String): Logger {
19-
val sl4fjLogger = LoggerFactory.getLogger(name)
20-
return Slf4JLoggerAdapter(sl4fjLogger)
21-
}
22-
}
23-
24-
private class Slf4JLoggerAdapter(private val delegate: org.slf4j.Logger) : Logger {
25-
private fun logWith(t: Throwable?, msg: () -> String, builder: LoggingEventBuilder) =
26-
builder.setMessage(msg)
27-
.apply {
28-
if (t != null) {
29-
setCause(t)
30-
}
31-
}.log()
32-
override fun trace(t: Throwable?, msg: () -> String) = logWith(t, msg, delegate.atTrace())
33-
override fun debug(t: Throwable?, msg: () -> String) = logWith(t, msg, delegate.atDebug())
34-
override fun info(t: Throwable?, msg: () -> String) = logWith(t, msg, delegate.atInfo())
35-
override fun warn(t: Throwable?, msg: () -> String) = logWith(t, msg, delegate.atWarn())
36-
override fun error(t: Throwable?, msg: () -> String) = logWith(t, msg, delegate.atError())
37-
override fun isEnabledFor(level: LogLevel): Boolean =
38-
delegate.isEnabledForLevel(level.slf4jLevel)
39-
40-
override fun atLevel(level: LogLevel): LogRecordBuilder =
41-
Slf4jLogRecordBuilderAdapter(delegate.atLevel(level.slf4jLevel))
42-
}
43-
44-
private class Slf4jLogRecordBuilderAdapter(
45-
private val delegate: LoggingEventBuilder,
46-
) : LogRecordBuilder {
47-
override fun setCause(ex: Throwable) {
48-
delegate.setCause(ex)
49-
}
5015

51-
override fun setMessage(message: String) {
52-
delegate.setMessage(message)
16+
private val useSlf4j2x = try {
17+
Class.forName("org.slf4j.spi.LoggingEventBuilder")
18+
true
19+
} catch (ex: ClassNotFoundException) {
20+
LoggerFactory.getLogger(Slf4jLoggerProvider::class.java).warn("falling back to SLF4J 1.x compatible binding")
21+
false
5322
}
5423

55-
override fun setMessage(message: () -> String) {
56-
delegate.setMessage(message)
57-
}
58-
59-
override fun setKeyValuePair(key: String, value: Any) {
60-
delegate.addKeyValue(key, value)
61-
}
62-
63-
override fun setContext(context: Context) {
64-
// TODO - add a way to get the current trace context and set the key/value pair on it?
24+
override fun getOrCreateLogger(name: String): Logger {
25+
val sl4fjLogger = LoggerFactory.getLogger(name)
26+
return if (useSlf4j2x) {
27+
Slf4j2xLoggerAdapter(sl4fjLogger)
28+
} else {
29+
Slf4j1xLoggerAdapter(sl4fjLogger)
30+
}
6531
}
66-
67-
override fun emit() = delegate.log()
6832
}
69-
70-
private val LogLevel.slf4jLevel: org.slf4j.event.Level
71-
get() = when (this) {
72-
LogLevel.Error -> Level.ERROR
73-
LogLevel.Warning -> Level.WARN
74-
LogLevel.Info -> Level.INFO
75-
LogLevel.Debug -> Level.DEBUG
76-
LogLevel.Trace -> Level.TRACE
77-
}

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,5 @@ include(":tests:benchmarks:serde-benchmarks")
7878
include(":tests:compile")
7979
include(":tests:codegen:paginator-tests")
8080
include(":tests:codegen:waiter-tests")
81+
include(":tests:integration:slf4j-1x-consumer")
82+
include(":tests:integration:slf4j-2x-consumer")
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import aws.sdk.kotlin.gradle.dsl.skipPublishing
6+
7+
plugins {
8+
kotlin("jvm")
9+
}
10+
11+
skipPublishing()
12+
13+
dependencies {
14+
implementation(project(":runtime:observability:logging-slf4j2"))
15+
implementation(libs.slf4j.api) {
16+
version {
17+
strictly(libs.versions.slf4j.v1x.version.get())
18+
}
19+
}
20+
implementation(libs.slf4j.simple) {
21+
version {
22+
strictly(libs.versions.slf4j.v1x.version.get())
23+
}
24+
}
25+
26+
testImplementation(libs.junit.jupiter)
27+
testImplementation(libs.kotlin.test)
28+
testImplementation(libs.kotlin.test.junit5)
29+
}
30+
31+
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
32+
kotlinOptions.jvmTarget = "1.8"
33+
}
34+
35+
tasks.test {
36+
useJUnitPlatform()
37+
testLogging {
38+
events("passed", "skipped", "failed")
39+
showStandardStreams = false
40+
showStackTraces = true
41+
showExceptions = true
42+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
43+
}
44+
}

0 commit comments

Comments
 (0)