Skip to content

Commit

Permalink
Logback backend for kotlin-logging (#452)
Browse files Browse the repository at this point in the history
* Logback backend for kotlin-logging
* Introduce internalCompilerData field
* Populate caller information from compiler data, if available
* Document that "internalCompilerData" field is not meant for public consumption
* Improve naming and remove misleading JavaDoc.

---------

Co-authored-by: Neeme Praks <[email protected]>
  • Loading branch information
neeme-praks-sympower and Neeme Praks authored Nov 3, 2024
1 parent c128ae4 commit 1f9ecdd
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ kotlin {
dependsOn(javaMain)
dependencies {
compileOnly("org.slf4j:slf4j-api:${extra["slf4j_version"]}")
compileOnly("ch.qos.logback:logback-classic:${extra["logback_version"]}")
compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:${extra["coroutines_version"]}")
}
}
Expand All @@ -132,6 +133,7 @@ kotlin {
implementation("org.apache.logging.log4j:log4j-core:${extra["log4j_version"]}")
implementation("org.apache.logging.log4j:log4j-slf4j2-impl:${extra["log4j_version"]}")
implementation("org.slf4j:slf4j-api:${extra["slf4j_version"]}")
implementation("ch.qos.logback:logback-classic:${extra["logback_version"]}")
// our jul test just forward the logs jul -> slf4j -> log4j
implementation("org.slf4j:jul-to-slf4j:${extra["slf4j_version"]}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:${extra["coroutines_version"]}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,18 @@ public class KLoggingEventBuilder {
public var message: String? = null
public var cause: Throwable? = null
public var payload: Map<String, Any?>? = null

/**
* Internal data that is used by compiler plugin to provide additional information about the log
* site. Not intended for use by user code, API stability is not guaranteed.
*/
public var internalCompilerData: InternalCompilerData? = null

public class InternalCompilerData(
public val messageTemplate: String? = null,
public val className: String? = null,
public val methodName: String? = null,
public val lineNumber: Int? = null,
public val fileName: String? = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.github.oshai.kotlinlogging.internal

import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.jul.internal.JulLoggerFactory
import io.github.oshai.kotlinlogging.logback.internal.LogbackLoggerFactory
import io.github.oshai.kotlinlogging.slf4j.internal.Slf4jLoggerFactory

/** factory methods to obtain a [KLogger] */
Expand All @@ -11,6 +12,8 @@ internal actual object KLoggerFactory {
internal actual fun logger(name: String): KLogger {
if (System.getProperty("kotlin-logging-to-jul") != null) {
return JulLoggerFactory.wrapJLogger(JulLoggerFactory.jLogger(name))
} else if (System.getProperty("kotlin-logging-to-logback") != null) {
return LogbackLoggerFactory.wrapLogbackLogger(LogbackLoggerFactory.logbackLogger(name))
}
// default to slf4j
return Slf4jLoggerFactory.wrapJLogger(Slf4jLoggerFactory.jLogger(name))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.oshai.kotlinlogging.logback

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.spi.LogbackServiceProvider
import io.github.oshai.kotlinlogging.Level.DEBUG
import io.github.oshai.kotlinlogging.Level.ERROR
import io.github.oshai.kotlinlogging.Level.INFO
import io.github.oshai.kotlinlogging.Level.OFF
import io.github.oshai.kotlinlogging.Level.TRACE
import io.github.oshai.kotlinlogging.Level.WARN
import io.github.oshai.kotlinlogging.Marker
import io.github.oshai.kotlinlogging.slf4j.internal.Slf4jMarker

public fun io.github.oshai.kotlinlogging.Level.toLogbackLevel(): Level {
val logbackLevel: Level =
when (this) {
TRACE -> Level.TRACE
DEBUG -> Level.DEBUG
INFO -> Level.INFO
WARN -> Level.WARN
ERROR -> Level.ERROR
OFF -> Level.OFF
}
return logbackLevel
}

public fun Marker.toLogback(logbackServiceProvider: LogbackServiceProvider): org.slf4j.Marker =
when (this) {
is Slf4jMarker -> marker
else -> logbackServiceProvider.markerFactory.getMarker(getName())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.oshai.kotlinlogging.logback.internal

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.LoggingEvent
import io.github.oshai.kotlinlogging.KLoggingEventBuilder
import io.github.oshai.kotlinlogging.Level
import io.github.oshai.kotlinlogging.logback.toLogbackLevel

public class LogbackLogEvent(
fqcn: String,
logger: Logger,
level: Level,
private val kLoggingEvent: KLoggingEventBuilder,
) :
LoggingEvent(
fqcn,
logger,
level.toLogbackLevel(),
kLoggingEvent.internalCompilerData?.messageTemplate ?: kLoggingEvent.message,
kLoggingEvent.cause,
emptyArray(),
) {

override fun getFormattedMessage(): String? {
return kLoggingEvent.message
}

override fun getCallerData(): Array<StackTraceElement> =
if (kLoggingEvent.internalCompilerData?.fileName != null) {
arrayOf(
StackTraceElement(
kLoggingEvent.internalCompilerData?.className,
kLoggingEvent.internalCompilerData?.methodName,
kLoggingEvent.internalCompilerData?.fileName,
kLoggingEvent.internalCompilerData?.lineNumber ?: 0,
)
)
} else {
super.getCallerData()
}

override fun hasCallerData(): Boolean =
if (kLoggingEvent.internalCompilerData?.fileName != null) {
true
} else {
super.hasCallerData()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.oshai.kotlinlogging.logback.internal

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.spi.LogbackServiceProvider
import io.github.oshai.kotlinlogging.KLogger

internal object LogbackLoggerFactory {

private val logbackServiceProvider = createLogbackServiceProvider()

private fun createLogbackServiceProvider(): LogbackServiceProvider {
val logbackServiceProvider = LogbackServiceProvider()
logbackServiceProvider.initialize()
return logbackServiceProvider
}

/** Get a Logback logger by name. Logback relies on SLF4J logger factory */
internal fun logbackLogger(name: String): Logger =
logbackServiceProvider.loggerFactory.getLogger(name) as Logger

internal fun wrapLogbackLogger(logbackLogger: Logger): KLogger =
LogbackLoggerWrapper(logbackLogger, logbackServiceProvider)

fun getLoggerContext() = logbackServiceProvider.loggerFactory as LoggerContext
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.github.oshai.kotlinlogging.logback.internal

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.LogbackServiceProvider
import io.github.oshai.kotlinlogging.DelegatingKLogger
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KLoggingEventBuilder
import io.github.oshai.kotlinlogging.Level
import io.github.oshai.kotlinlogging.Marker
import io.github.oshai.kotlinlogging.logback.toLogback
import io.github.oshai.kotlinlogging.logback.toLogbackLevel
import io.github.oshai.kotlinlogging.slf4j.internal.LocationAwareKLogger
import org.slf4j.event.KeyValuePair

internal class LogbackLoggerWrapper(
override val underlyingLogger: Logger,
private val logbackServiceProvider: LogbackServiceProvider,
) : KLogger, DelegatingKLogger<Logger> {

override val name: String
get() = underlyingLogger.name

private val fqcn: String = LocationAwareKLogger::class.java.name

override fun at(level: Level, marker: Marker?, block: KLoggingEventBuilder.() -> Unit) {
if (isLoggingEnabledFor(level, marker)) {
KLoggingEventBuilder().apply(block).run {
val logbackEvent =
LogbackLogEvent(
fqcn = fqcn,
logger = underlyingLogger,
level = level,
kLoggingEvent = this,
)
marker?.toLogback(logbackServiceProvider)?.let { logbackEvent.addMarker(it) }
payload?.forEach { (key, value) -> logbackEvent.addKeyValuePair(KeyValuePair(key, value)) }
underlyingLogger.callAppenders(logbackEvent)
}
}
}

override fun isLoggingEnabledFor(level: Level, marker: Marker?) =
underlyingLogger.isEnabledFor(marker?.toLogback(logbackServiceProvider), level.toLogbackLevel())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.github.oshai.kotlinlogging.logback.internal

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.OutputStreamAppender
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import java.io.ByteArrayOutputStream
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class LogbackLoggerWrapperTest {

companion object {
private lateinit var logger: KLogger
private lateinit var warnLogger: KLogger
private lateinit var errorLogger: KLogger
private lateinit var logOutputStream: ByteArrayOutputStream
private lateinit var appender: OutputStreamAppender<ILoggingEvent>
private lateinit var rootLogger: Logger

@BeforeAll
@JvmStatic
fun init() {
val loggerContext = LogbackLoggerFactory.getLoggerContext()
loggerContext.reset()
System.setProperty("kotlin-logging-to-logback", "true")

val encoder = PatternLayoutEncoder()
encoder.context = loggerContext
encoder.pattern = "%-5p %c %marker - %m%n"
encoder.charset = Charsets.UTF_8
encoder.start()

logOutputStream = ByteArrayOutputStream()
appender = OutputStreamAppender<ILoggingEvent>()
appender.context = loggerContext
appender.encoder = encoder
appender.outputStream = logOutputStream
appender.start()

rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME)
rootLogger.addAppender(appender)
rootLogger.level = Level.TRACE

logger = KotlinLogging.logger {}
warnLogger = KotlinLogging.logger("warnLogger")
loggerContext.getLogger("warnLogger").level = Level.WARN
errorLogger = KotlinLogging.logger("errorLogger")
loggerContext.getLogger("errorLogger").level = Level.ERROR
}

@AfterAll
@JvmStatic
fun teardown() {
System.clearProperty("kotlin-logging-to-logback")
val loggerContext = LogbackLoggerFactory.getLoggerContext()
loggerContext.reset()
}
}

@Test
fun testLogbackLogger() {
assertTrue(logger is LogbackLoggerWrapper)
assertTrue(warnLogger is LogbackLoggerWrapper)
assertTrue(errorLogger is LogbackLoggerWrapper)
logger.info { "simple logback info message" }
warnLogger.warn { "simple logback warn message" }
errorLogger.error { "simple logback error message" }
val lines =
logOutputStream
.toByteArray()
.toString(Charsets.UTF_8)
.trim()
.replace("\r", "\n")
.replace("\n\n", "\n")
.split("\n")
assertEquals(
"INFO io.github.oshai.kotlinlogging.logback.internal.LogbackLoggerWrapperTest - simple logback info message",
lines[0],
)
assertEquals("WARN warnLogger - simple logback warn message", lines[1])
assertEquals("ERROR errorLogger - simple logback error message", lines[2])
}
}
1 change: 1 addition & 0 deletions versions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ extra["coroutines_version"] = "1.8.0"
extra["log4j_version"] = "2.22.0"
extra["mockito_version"] = "4.11.0"
extra["junit_version"] = "5.9.2"
extra["logback_version"] = "1.5.11"

0 comments on commit 1f9ecdd

Please sign in to comment.