Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ kotlin {
}
}
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmWasi { // console based WASI target
// Use nodejs as runtime env for tests & distribution (WASI support via node --experimental-wasi-unstable-preview1)
nodejs()
}
androidTarget {
publishLibraryVariants("release", "debug")
}
Expand Down Expand Up @@ -218,6 +223,14 @@ kotlin {
implementation(kotlin("test-wasm-js"))
}
}
val wasmWasiMain by getting {
dependsOn(directMain)
}
val wasmWasiTest by getting {
dependencies {
implementation(kotlin("test-wasm-wasi"))
}
}
val nativeMain by creating {
dependsOn(directMain)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.oshai.kotlinlogging

// Simple console appender for WASI target relying on stdout
public object ConsoleOutputAppender : FormattingAppender() {
override fun logFormattedMessage(loggingEvent: KLoggingEvent, formattedMessage: Any?) {
when (loggingEvent.level) {
Level.TRACE,
Level.DEBUG,
Level.INFO,
Level.WARN,
Level.ERROR -> println(formattedMessage)
Level.OFF -> Unit
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.oshai.kotlinlogging

public actual object KotlinLoggingConfiguration {
public actual var logLevel: Level = Level.INFO
public actual var formatter: Formatter = DefaultMessageFormatter(includePrefix = true)
public actual var appender: Appender = ConsoleOutputAppender
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.github.oshai.kotlinlogging.internal

private const val NO_CLASS = ""

internal actual object KLoggerNameResolver {
// In WASI the stacktrace is often empty; derive from lambda's synthetic class name when possible.
internal actual fun name(func: () -> Unit): String {
val qn = func::class.qualifiedName
// Examples:
// - "SimpleWasmWasiTest$anonymousClassPropLogger$lambda" -> "SimpleWasmWasiTest"
// - "anonymousFilePropLogger$lambda" -> unknown (should NOT use this)
if (qn != null) {
val base = qn.substringBefore('$')
// Heuristic: class names in Kotlin usually start with uppercase; avoid returning synthetic
// top-level property names like 'anonymousFilePropLogger'.
if (base.isNotEmpty() && base.first().isUpperCase()) {
return base
}
}

// Fallback: Try to derive from stack by locating a .kt source file and using its base name.
val fileName = tryExtractFileNameFromJsStack()
return fileName ?: NO_CLASS
}

private fun tryExtractFileNameFromJsStack(): String? {
val st = Exception().stackTraceToString()
if (st.isBlank()) return null
// Try to find something like '/path/.../SimpleWasmWasiTest.kt' or 'SimpleWasmWasiTest.kt'
val regex = Regex("""([A-Za-z0-9_]+)\.kt""")
for (line in st.lineSequence()) {
val match = regex.find(line)
if (match != null) {
val base = match.groupValues[1]
if (base.isNotEmpty()) return base
}
}
// Fallback similar to jsMain approach: take the token after last '/' and before '.kt'.
for (line in st.lineSequence()) {
if (".kt" in line) {
val preKt = line.substringBefore(".kt")
val afterSlash = preKt.substringAfterLast('/')
val afterBackslash = afterSlash.substringAfterLast('\\')
val afterDot = afterBackslash.substringAfterLast('.')
if (afterDot.isNotEmpty()) return afterDot
}
}
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.github.oshai.kotlinlogging

import kotlin.test.*

private val namedLogger = KotlinLogging.logger("SimpleWasmWasiTest")
private val anonymousFilePropLogger = KotlinLogging.logger {}

// Create a logger inside a top-level function to exercise the stacktrace-based fallback
private fun createAnonymousFunctionLogger() = KotlinLogging.logger {}

class SimpleWasmWasiTest {
private lateinit var appender: SimpleAppender
private val anonymousClassPropLogger = KotlinLogging.logger {}

@BeforeTest
fun setup() {
appender = createAppender()
KotlinLoggingConfiguration.appender = appender
}

@AfterTest
fun cleanup() {
KotlinLoggingConfiguration.appender = ConsoleOutputAppender
KotlinLoggingConfiguration.logLevel = Level.INFO
}

@Test
fun simpleWasiTest() {
assertEquals("SimpleWasmWasiTest", namedLogger.name)
namedLogger.info { "info msg" }
assertEquals("INFO: [SimpleWasmWasiTest] info msg", appender.lastMessage)
assertEquals("info", appender.lastLevel)
}

@Test
fun anonymousFilePropWasiTest() {
// On WASI, stack traces are often unavailable; top-level property name may be empty.
val n = anonymousFilePropLogger.name
if (n.isNotEmpty()) {
assertEquals("SimpleWasmWasiTest", n)
}
anonymousFilePropLogger.info { "info msg" }
val expected =
if (n.isNotEmpty()) {
"INFO: [SimpleWasmWasiTest] info msg"
} else {
"INFO: [] info msg"
}
assertEquals(expected, appender.lastMessage)
}

@Test
fun anonymousClassPropWasiTest() {
assertEquals("SimpleWasmWasiTest", anonymousClassPropLogger.name)
anonymousClassPropLogger.info { "info msg" }
assertEquals("INFO: [SimpleWasmWasiTest] info msg", appender.lastMessage)
}

@Test
fun anonymousFunctionWasiTest_stacktraceFallback() {
// This aims to cover the new fallback that parses the .kt filename from the stacktrace.
val logger = createAnonymousFunctionLogger()
val n = logger.name
if (n.isNotEmpty()) {
assertEquals("SimpleWasmWasiTest", n)
}
logger.info { "function msg" }
val expected =
if (n.isNotEmpty()) {
"INFO: [SimpleWasmWasiTest] function msg"
} else {
"INFO: [] function msg"
}
assertEquals(expected, appender.lastMessage)
}

@Test
fun offLevelWasiTest() {
KotlinLoggingConfiguration.logLevel = Level.OFF
assertTrue(namedLogger.isLoggingOff())
namedLogger.error { "error msg" }
assertEquals("NA", appender.lastMessage)
assertEquals("NA", appender.lastLevel)
}

private fun createAppender(): SimpleAppender = SimpleAppender()

class SimpleAppender : Appender {
var lastMessage: String = "NA"
var lastLevel: String = "NA"

override fun log(loggingEvent: KLoggingEvent) {
lastMessage = DefaultMessageFormatter(includePrefix = true).formatMessage(loggingEvent)
lastLevel = loggingEvent.level.name.lowercase()
}
}
}
Loading