diff --git a/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt b/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt index fc350e4a..65c4e6f7 100644 --- a/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt +++ b/src/commonMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt @@ -2,6 +2,7 @@ package io.github.oshai.kotlinlogging import io.github.oshai.kotlinlogging.internal.KLoggerFactory import io.github.oshai.kotlinlogging.internal.KLoggerNameResolver +import kotlin.js.JsName public object KotlinLogging { /** @@ -10,6 +11,7 @@ public object KotlinLogging { * private val logger = KotlinLogging.logger {} * ``` */ + @JsName("kotlinLoggerByFunc") public fun logger(func: () -> Unit): KLogger = logger(KLoggerNameResolver.name(func)) /** @@ -21,5 +23,6 @@ public object KotlinLogging { * In most cases the name represents the package notation of the file that the logger is defined * in. */ + @JsName("kotlinLoggerByName") public fun logger(name: String): KLogger = KLoggerFactory.logger(name) } diff --git a/src/jsMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt b/src/jsMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt deleted file mode 100644 index 8a85526c..00000000 --- a/src/jsMain/kotlin/io/github/oshai/kotlinlogging/KotlinLogging.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.oshai.kotlinlogging - -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - -/** - * The JS way to define a logger without explicit name - * - * ``` - * class MyClass { - * private val logger by KotlinLogging.logger() - * } - * ``` - */ -public fun KotlinLogging.logger(): ReadOnlyProperty = LoggerDelegate() - -private class LoggerDelegate : ReadOnlyProperty { - private lateinit var logger: KLogger - - override fun getValue(thisRef: Any?, property: KProperty<*>): KLogger { - if (!::logger.isInitialized) { - logger = - thisRef.asDynamic()?.constructor?.name.unsafeCast()?.let { - KotlinLogging.logger(it) - } ?: KotlinLogging.logger("root-logger") - } - return logger - } -} diff --git a/src/jsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt b/src/jsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt index e83bbfb1..964f50f9 100644 --- a/src/jsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt +++ b/src/jsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt @@ -1,18 +1,45 @@ package io.github.oshai.kotlinlogging.internal internal actual object KLoggerNameResolver { + private const val DEFAULT_LOGGER_NAME = "root-logger" + private const val LOGGER_FUNCTION_NAME = "kotlinLoggerByFunc" + private const val COMPANION_GET_INSTANCE_SUFFIX = "_getInstance" + private val TOP_LEVEL_INIT_PROPERTIES_REGEX = Regex("_init_properties_(\\S+)_kt_") + private val CLASS_LEVEL_INIT_PROPERTIES_REGEX = Regex("new (\\S+)") internal actual fun name(func: () -> Unit): String { - var found = false - val exception = Exception() - for (line in exception.stackTraceToString().split("\n")) { - if (found) { - return line.substringBefore(".kt").substringAfterLast(".").substringAfterLast("/") - } - if (line.contains("at KotlinLogging")) { - found = true - } + return findLoggerCallerClassName() ?: DEFAULT_LOGGER_NAME + } + + private fun findLoggerCallerClassName(): String? { + val stackTrace = Throwable().stackTraceToString().split('\n') + val invokeLoggerLine = stackTrace.indexOfFirst { it.contains(LOGGER_FUNCTION_NAME) } + if (invokeLoggerLine == -1 || invokeLoggerLine + 1 >= stackTrace.size) return null + val callerLine = invokeLoggerLine + 1 + return resolveAsTopLevelProperty(stackTrace, callerLine) + ?: resolveAsClassLevelProperty(stackTrace, callerLine) + } + + private fun resolveAsTopLevelProperty(stackTrace: List, callerLine: Int): String? { + val found = TOP_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine]) ?: return null + return found.groupValues[1] + } + + private fun resolveAsClassLevelProperty(stackTrace: List, callerLine: Int): String? { + val found = CLASS_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine]) ?: return null + val className = found.groupValues[1] + // find enclosing class in case of Companion object: + // new MyCompanion() <- found class name + // MyCompanion_getInstance() + // new MyClass() <- enclosing class + if ( + callerLine + 2 >= stackTrace.size || + !stackTrace[callerLine + 1].contains("$className$COMPANION_GET_INSTANCE_SUFFIX") + ) { + return className } - return "" + val enclosingFound = + CLASS_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine + 2]) ?: return className + return enclosingFound.groupValues[1] } } diff --git a/src/jsTest/kotlin/io/github/oshai/kotlinlogging/SimpleJsTest.kt b/src/jsTest/kotlin/io/github/oshai/kotlinlogging/SimpleJsTest.kt index 4a684a21..0e5e80f8 100644 --- a/src/jsTest/kotlin/io/github/oshai/kotlinlogging/SimpleJsTest.kt +++ b/src/jsTest/kotlin/io/github/oshai/kotlinlogging/SimpleJsTest.kt @@ -2,7 +2,19 @@ package io.github.oshai.kotlinlogging import kotlin.test.* -private val logger = KotlinLogging.logger("SimpleJsTest") +val topLevelNamedLogger = KotlinLogging.logger("topLevelNamedLogger") +val topLevelLambdaLogger = KotlinLogging.logger {} + +class MyClass { + val classNamedLogger = KotlinLogging.logger("MyClass") + val classLambdaLogger = KotlinLogging.logger {} + + // check with non default "Companion" name also + companion object MyCompanion { + val companionNamedLogger = KotlinLogging.logger("MyClassCompanion") + val companionLambdaLogger = KotlinLogging.logger {} + } +} @Suppress("DEPRECATION") class SimpleJsTest { @@ -20,29 +32,158 @@ class SimpleJsTest { KotlinLoggingConfiguration.LOG_LEVEL = Level.INFO } + // TODO: use parameterized test? + + // TopLevelNamedLogger + @Test + fun checkTopLevelNamedLoggerName() { + checkLoggerName(topLevelNamedLogger, "topLevelNamedLogger") + } + + @Test + fun checkTopLevelNamedLoggerInfoMessage() { + checkLoggerInfoMessage(topLevelNamedLogger) + } + + @Test + fun checkTopLevelNamedLoggerErrorMessage() { + checkLoggerErrorMessage(topLevelNamedLogger) + } + + @Test + fun checkTopLevelNamedLoggerOffLevel() { + checkLoggerOffLevel(topLevelNamedLogger) + } + + // TopLevelLambdaLogger + @Test + fun checkTopLevelLambdaLoggerName() { + checkLoggerName(topLevelLambdaLogger, "SimpleJsTest") + } + + @Test + fun checkTopLevelLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(topLevelLambdaLogger) + } + + @Test + fun checkTopLevelLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(topLevelLambdaLogger) + } + + @Test + fun checkTopLevelLambdaLoggerOffLevel() { + checkLoggerOffLevel(topLevelLambdaLogger) + } + + // ClassNamedLogger + @Test + fun checkClassNamedLoggerName() { + checkLoggerName(MyClass().classNamedLogger, "MyClass") + } + @Test - fun simpleJsTest() { - assertEquals("SimpleJsTest", logger.name) + fun checkClassNamedLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass().classNamedLogger) + } + + @Test + fun checkClassNamedLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass().classNamedLogger) + } + + @Test + fun checkClassNamedLoggerOffLevel() { + checkLoggerOffLevel(MyClass().classNamedLogger) + } + + // ClassLambdaLogger + @Test + fun checkClassLambdaLoggerName() { + checkLoggerName(MyClass().classLambdaLogger, "MyClass") + } + + @Test + fun checkClassLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass().classLambdaLogger) + } + + @Test + fun checkClassLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass().classLambdaLogger) + } + + @Test + fun checkClassLambdaLoggerOffLevel() { + checkLoggerOffLevel(MyClass().classLambdaLogger) + } + + // CompanionNamedLogger + @Test + fun checkCompanionNamedLoggerName() { + checkLoggerName(MyClass.MyCompanion.companionNamedLogger, "MyClassCompanion") + } + + @Test + fun checkCompanionNamedLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass.MyCompanion.companionNamedLogger) + } + + @Test + fun checkCompanionNamedLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass.MyCompanion.companionNamedLogger) + } + + @Test + fun checkCompanionNamedLoggerOffLevel() { + checkLoggerOffLevel(MyClass.MyCompanion.companionNamedLogger) + } + + // CompanionLambdaLogger + @Test + fun checkCompanionLambdaLoggerName() { + checkLoggerName(MyClass.MyCompanion.companionLambdaLogger, "MyClass") + } + + @Test + fun checkCompanionLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass.MyCompanion.companionLambdaLogger) + } + + @Test + fun checkCompanionLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass.MyCompanion.companionLambdaLogger) + } + + @Test + fun checkCompanionLambdaLoggerOffLevel() { + checkLoggerOffLevel(MyClass.MyCompanion.companionLambdaLogger) + } + + // use cases + private fun checkLoggerName(logger: KLogger, expected: String) { + assertEquals(expected, logger.name) + } + + private fun checkLoggerInfoMessage(logger: KLogger) { logger.info { "info msg" } - assertEquals("INFO: [SimpleJsTest] info msg", appender.lastMessage) + assertEquals("INFO: [${logger.name}] info msg", appender.lastMessage) assertEquals("info", appender.lastLevel) } - @Test - fun logThrowableTest() { + private fun checkLoggerErrorMessage(logger: KLogger) { val errorLog = "Something Bad Happened" val outerMessage = "Outer Message" val innerMessage = "Inner Message" val throwable = Throwable(message = outerMessage, cause = Throwable(message = innerMessage)) logger.error(throwable) { errorLog } assertEquals( - "ERROR: [SimpleJsTest] $errorLog, Caused by: '$outerMessage', Caused by: '$innerMessage'", + "ERROR: [${logger.name}] $errorLog, Caused by: '$outerMessage', Caused by: '$innerMessage'", appender.lastMessage, ) } - @Test - fun offLevelJsTest() { + private fun checkLoggerOffLevel(logger: KLogger) { KotlinLoggingConfiguration.LOG_LEVEL = Level.OFF assertTrue(logger.isLoggingOff()) logger.error { "error msg" } @@ -50,11 +191,6 @@ class SimpleJsTest { assertEquals("NA", appender.lastLevel) } - @Test - fun loggerNameTest() { - assertEquals("MyClass", MyClass().logger2.name) - } - private fun createAppender(): SimpleAppender = SimpleAppender() class SimpleAppender : Appender { @@ -67,7 +203,3 @@ class SimpleJsTest { } } } - -class MyClass { - val logger2 by KotlinLogging.logger() -} diff --git a/src/wasmJsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt b/src/wasmJsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt index 412831cb..164d4252 100644 --- a/src/wasmJsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt +++ b/src/wasmJsMain/kotlin/io/github/oshai/kotlinlogging/internal/KLoggerNameResolver.kt @@ -1,25 +1,45 @@ package io.github.oshai.kotlinlogging.internal -private const val NO_CLASS = "" - internal actual object KLoggerNameResolver { - private val kotlinLoggingRegex = Regex("\\.KotlinLogging\\.logger\\s") - private val topLevelPropertyRegex = Regex("") - private val classPropertyRegex = Regex("\\.(\\S+)\\.") + private const val DEFAULT_LOGGER_NAME = "root-logger" + private const val LOGGER_FUNCTION_NAME = "KotlinLogging.logger" + private const val COMPANION_GET_INSTANCE_SUFFIX = "_getInstance" + private val TOP_LEVEL_INIT_PROPERTIES_REGEX = Regex("") + private val CLASS_LEVEL_INIT_PROPERTIES_REGEX = Regex("\\.([^.\\s]+)\\.") internal actual fun name(func: () -> Unit): String { - val stackTrace = Exception().stackTraceToString().split("\n") - val invokingClassLine = stackTrace.indexOfFirst(kotlinLoggingRegex::containsMatchIn) + 1 - return if (invokingClassLine in 1 ..< stackTrace.size) { - getInvokingClass(stackTrace[invokingClassLine]) - } else { - NO_CLASS - } + return findLoggerCallerClassName() ?: DEFAULT_LOGGER_NAME + } + + private fun findLoggerCallerClassName(): String? { + val stackTrace = Throwable().stackTraceToString().split('\n') + val invokeLoggerLine = stackTrace.indexOfFirst { it.contains(LOGGER_FUNCTION_NAME) } + if (invokeLoggerLine == -1 || invokeLoggerLine + 1 >= stackTrace.size) return null + val callerLine = invokeLoggerLine + 1 + return resolveAsTopLevelProperty(stackTrace, callerLine) + ?: resolveAsClassLevelProperty(stackTrace, callerLine) } - private fun getInvokingClass(line: String): String { - return topLevelPropertyRegex.find(line)?.let { it.groupValues[1].split(".").last() } - ?: classPropertyRegex.find(line)?.let { it.groupValues[1].split(".").last() } - ?: NO_CLASS + private fun resolveAsTopLevelProperty(stackTrace: List, callerLine: Int): String? { + val found = TOP_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine]) ?: return null + return found.groupValues[1] + } + + private fun resolveAsClassLevelProperty(stackTrace: List, callerLine: Int): String? { + val found = CLASS_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine]) ?: return null + val className = found.groupValues[1] + // find enclosing class in case of Companion object: + // MyCompanion.() <- found class name + // MyCompanion_getInstance() + // MyClass.() <- enclosing class + if ( + callerLine + 2 >= stackTrace.size || + !stackTrace[callerLine + 1].contains("$className$COMPANION_GET_INSTANCE_SUFFIX") + ) { + return className + } + val enclosingFound = + CLASS_LEVEL_INIT_PROPERTIES_REGEX.find(stackTrace[callerLine + 2]) ?: return className + return enclosingFound.groupValues[1] } } diff --git a/src/wasmJsTest/kotlin/io/github/oshai/kotlinlogging/SimpleWasmJsTest.kt b/src/wasmJsTest/kotlin/io/github/oshai/kotlinlogging/SimpleWasmJsTest.kt index 8a762fd7..b9712768 100644 --- a/src/wasmJsTest/kotlin/io/github/oshai/kotlinlogging/SimpleWasmJsTest.kt +++ b/src/wasmJsTest/kotlin/io/github/oshai/kotlinlogging/SimpleWasmJsTest.kt @@ -2,12 +2,22 @@ package io.github.oshai.kotlinlogging import kotlin.test.* -private val namedLogger = KotlinLogging.logger("SimpleWasmJsTest") -private val anonymousFilePropLogger = KotlinLogging.logger {} +val topLevelNamedLogger = KotlinLogging.logger("topLevelNamedLogger") +val topLevelLambdaLogger = KotlinLogging.logger {} + +class MyClass { + val classNamedLogger = KotlinLogging.logger("MyClass") + val classLambdaLogger = KotlinLogging.logger {} + + // check with non default "Companion" name also + companion object MyCompanion { + val companionNamedLogger = KotlinLogging.logger("MyClassCompanion") + val companionLambdaLogger = KotlinLogging.logger {} + } +} class SimpleWasmJsTest { private lateinit var appender: SimpleAppender - private val anonymousClassPropLogger = KotlinLogging.logger {} @BeforeTest fun setup() { @@ -21,33 +31,161 @@ class SimpleWasmJsTest { KotlinLoggingConfiguration.logLevel = Level.INFO } + // TODO: use parameterized test? + + // TopLevelNamedLogger + @Test + fun checkTopLevelNamedLoggerName() { + checkLoggerName(topLevelNamedLogger, "topLevelNamedLogger") + } + @Test - fun simpleWasmJsTest() { - assertEquals("SimpleWasmJsTest", namedLogger.name) - namedLogger.info { "info msg" } - assertEquals("INFO: [SimpleWasmJsTest] info msg", appender.lastMessage) - assertEquals("info", appender.lastLevel) + fun checkTopLevelNamedLoggerInfoMessage() { + checkLoggerInfoMessage(topLevelNamedLogger) + } + + @Test + fun checkTopLevelNamedLoggerErrorMessage() { + checkLoggerErrorMessage(topLevelNamedLogger) + } + + @Test + fun checkTopLevelNamedLoggerOffLevel() { + checkLoggerOffLevel(topLevelNamedLogger) + } + + // TopLevelLambdaLogger + @Test + fun checkTopLevelLambdaLoggerName() { + checkLoggerName(topLevelLambdaLogger, "SimpleWasmJsTest") + } + + @Test + fun checkTopLevelLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(topLevelLambdaLogger) + } + + @Test + fun checkTopLevelLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(topLevelLambdaLogger) } @Test - fun anonymousFilePropWasmJsTest() { - assertEquals("SimpleWasmJsTest", anonymousFilePropLogger.name) - anonymousFilePropLogger.info { "info msg" } - assertEquals("INFO: [SimpleWasmJsTest] info msg", appender.lastMessage) + fun checkTopLevelLambdaLoggerOffLevel() { + checkLoggerOffLevel(topLevelLambdaLogger) } + // ClassNamedLogger @Test - fun anonymousClassPropWasmJsTest() { - assertEquals("SimpleWasmJsTest", anonymousClassPropLogger.name) - anonymousFilePropLogger.info { "info msg" } - assertEquals("INFO: [SimpleWasmJsTest] info msg", appender.lastMessage) + fun checkClassNamedLoggerName() { + checkLoggerName(MyClass().classNamedLogger, "MyClass") } @Test - fun offLevelWasmJsTest() { + fun checkClassNamedLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass().classNamedLogger) + } + + @Test + fun checkClassNamedLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass().classNamedLogger) + } + + @Test + fun checkClassNamedLoggerOffLevel() { + checkLoggerOffLevel(MyClass().classNamedLogger) + } + + // ClassLambdaLogger + @Test + fun checkClassLambdaLoggerName() { + checkLoggerName(MyClass().classLambdaLogger, "MyClass") + } + + @Test + fun checkClassLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass().classLambdaLogger) + } + + @Test + fun checkClassLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass().classLambdaLogger) + } + + @Test + fun checkClassLambdaLoggerOffLevel() { + checkLoggerOffLevel(MyClass().classLambdaLogger) + } + + // CompanionNamedLogger + @Test + fun checkCompanionNamedLoggerName() { + checkLoggerName(MyClass.MyCompanion.companionNamedLogger, "MyClassCompanion") + } + + @Test + fun checkCompanionNamedLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass.MyCompanion.companionNamedLogger) + } + + @Test + fun checkCompanionNamedLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass.MyCompanion.companionNamedLogger) + } + + @Test + fun checkCompanionNamedLoggerOffLevel() { + checkLoggerOffLevel(MyClass.MyCompanion.companionNamedLogger) + } + + // CompanionLambdaLogger + @Test + fun checkCompanionLambdaLoggerName() { + checkLoggerName(MyClass.MyCompanion.companionLambdaLogger, "MyClass") + } + + @Test + fun checkCompanionLambdaLoggerInfoMessage() { + checkLoggerInfoMessage(MyClass.MyCompanion.companionLambdaLogger) + } + + @Test + fun checkCompanionLambdaLoggerErrorMessage() { + checkLoggerErrorMessage(MyClass.MyCompanion.companionLambdaLogger) + } + + @Test + fun checkCompanionLambdaLoggerOffLevel() { + checkLoggerOffLevel(MyClass.MyCompanion.companionLambdaLogger) + } + + // use cases + private fun checkLoggerName(logger: KLogger, expected: String) { + assertEquals(expected, logger.name) + } + + private fun checkLoggerInfoMessage(logger: KLogger) { + logger.info { "info msg" } + assertEquals("INFO: [${logger.name}] info msg", appender.lastMessage) + assertEquals("info", appender.lastLevel) + } + + private fun checkLoggerErrorMessage(logger: KLogger) { + val errorLog = "Something Bad Happened" + val outerMessage = "Outer Message" + val innerMessage = "Inner Message" + val throwable = Throwable(message = outerMessage, cause = Throwable(message = innerMessage)) + logger.error(throwable) { errorLog } + assertEquals( + "ERROR: [${logger.name}] $errorLog, Caused by: '$outerMessage', Caused by: '$innerMessage'", + appender.lastMessage, + ) + } + + private fun checkLoggerOffLevel(logger: KLogger) { KotlinLoggingConfiguration.logLevel = Level.OFF - assertTrue(namedLogger.isLoggingOff()) - namedLogger.error { "error msg" } + assertTrue(logger.isLoggingOff()) + logger.error { "error msg" } assertEquals("NA", appender.lastMessage) assertEquals("NA", appender.lastLevel) }