diff --git a/build.gradle.kts b/build.gradle.kts index 7717221a..8a234766 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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"]}") } } @@ -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"]}") diff --git a/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KLoggingEventBuilder.kt b/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KLoggingEventBuilder.kt index c1d77428..3fc3115d 100644 --- a/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KLoggingEventBuilder.kt +++ b/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KLoggingEventBuilder.kt @@ -4,4 +4,18 @@ public class KLoggingEventBuilder { public var message: String? = null public var cause: Throwable? = null public var payload: Map? = 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, + ) } diff --git a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerFactory.kt b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerFactory.kt index 21eabead..ce582c5f 100644 --- a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerFactory.kt +++ b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerFactory.kt @@ -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] */ @@ -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)) diff --git a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/LogbackExtensions.kt b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/LogbackExtensions.kt new file mode 100644 index 00000000..4986eefe --- /dev/null +++ b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/LogbackExtensions.kt @@ -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()) + } diff --git a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLogEvent.kt b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLogEvent.kt new file mode 100644 index 00000000..985c9961 --- /dev/null +++ b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLogEvent.kt @@ -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 = + 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() + } +} diff --git a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerFactory.kt b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerFactory.kt new file mode 100644 index 00000000..114814f5 --- /dev/null +++ b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerFactory.kt @@ -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 +} diff --git a/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapper.kt b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapper.kt new file mode 100644 index 00000000..4ea0d30e --- /dev/null +++ b/src/jvmMain/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapper.kt @@ -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 { + + 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()) +} diff --git a/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapperTest.kt b/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapperTest.kt new file mode 100644 index 00000000..d1553e13 --- /dev/null +++ b/src/jvmTest/kotlin/io/github/oshai/kotlinlogging/logback/internal/LogbackLoggerWrapperTest.kt @@ -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 + 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() + 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]) + } +} diff --git a/versions.gradle.kts b/versions.gradle.kts index 5beb6b05..174d96a0 100644 --- a/versions.gradle.kts +++ b/versions.gradle.kts @@ -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"