Skip to content

Commit be2e531

Browse files
authored
feat: add wasmWasi target with console appender and tests (#551)
* feat: add wasmWasi target with console appender and tests * feat(wasmWasi): smarter logger name resolution with .kt stacktrace fallback; chore: remove changelog note
1 parent 96ea134 commit be2e531

File tree

5 files changed

+182
-0
lines changed

5 files changed

+182
-0
lines changed

build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ kotlin {
8787
}
8888
}
8989
}
90+
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
91+
wasmWasi { // console based WASI target
92+
// Use nodejs as runtime env for tests & distribution (WASI support via node --experimental-wasi-unstable-preview1)
93+
nodejs()
94+
}
9095
androidTarget {
9196
publishLibraryVariants("release", "debug")
9297
}
@@ -218,6 +223,14 @@ kotlin {
218223
implementation(kotlin("test-wasm-js"))
219224
}
220225
}
226+
val wasmWasiMain by getting {
227+
dependsOn(directMain)
228+
}
229+
val wasmWasiTest by getting {
230+
dependencies {
231+
implementation(kotlin("test-wasm-wasi"))
232+
}
233+
}
221234
val nativeMain by creating {
222235
dependsOn(directMain)
223236
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.oshai.kotlinlogging
2+
3+
// Simple console appender for WASI target relying on stdout
4+
public object ConsoleOutputAppender : FormattingAppender() {
5+
override fun logFormattedMessage(loggingEvent: KLoggingEvent, formattedMessage: Any?) {
6+
when (loggingEvent.level) {
7+
Level.TRACE,
8+
Level.DEBUG,
9+
Level.INFO,
10+
Level.WARN,
11+
Level.ERROR -> println(formattedMessage)
12+
Level.OFF -> Unit
13+
}
14+
}
15+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.github.oshai.kotlinlogging
2+
3+
public actual object KotlinLoggingConfiguration {
4+
public actual var logLevel: Level = Level.INFO
5+
public actual var formatter: Formatter = DefaultMessageFormatter(includePrefix = true)
6+
public actual var appender: Appender = ConsoleOutputAppender
7+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.github.oshai.kotlinlogging.internal
2+
3+
private const val NO_CLASS = ""
4+
5+
internal actual object KLoggerNameResolver {
6+
// In WASI the stacktrace is often empty; derive from lambda's synthetic class name when possible.
7+
internal actual fun name(func: () -> Unit): String {
8+
val qn = func::class.qualifiedName
9+
// Examples:
10+
// - "SimpleWasmWasiTest$anonymousClassPropLogger$lambda" -> "SimpleWasmWasiTest"
11+
// - "anonymousFilePropLogger$lambda" -> unknown (should NOT use this)
12+
if (qn != null) {
13+
val base = qn.substringBefore('$')
14+
// Heuristic: class names in Kotlin usually start with uppercase; avoid returning synthetic
15+
// top-level property names like 'anonymousFilePropLogger'.
16+
if (base.isNotEmpty() && base.first().isUpperCase()) {
17+
return base
18+
}
19+
}
20+
21+
// Fallback: Try to derive from stack by locating a .kt source file and using its base name.
22+
val fileName = tryExtractFileNameFromJsStack()
23+
return fileName ?: NO_CLASS
24+
}
25+
26+
private fun tryExtractFileNameFromJsStack(): String? {
27+
val st = Exception().stackTraceToString()
28+
if (st.isBlank()) return null
29+
// Try to find something like '/path/.../SimpleWasmWasiTest.kt' or 'SimpleWasmWasiTest.kt'
30+
val regex = Regex("""([A-Za-z0-9_]+)\.kt""")
31+
for (line in st.lineSequence()) {
32+
val match = regex.find(line)
33+
if (match != null) {
34+
val base = match.groupValues[1]
35+
if (base.isNotEmpty()) return base
36+
}
37+
}
38+
// Fallback similar to jsMain approach: take the token after last '/' and before '.kt'.
39+
for (line in st.lineSequence()) {
40+
if (".kt" in line) {
41+
val preKt = line.substringBefore(".kt")
42+
val afterSlash = preKt.substringAfterLast('/')
43+
val afterBackslash = afterSlash.substringAfterLast('\\')
44+
val afterDot = afterBackslash.substringAfterLast('.')
45+
if (afterDot.isNotEmpty()) return afterDot
46+
}
47+
}
48+
return null
49+
}
50+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package io.github.oshai.kotlinlogging
2+
3+
import kotlin.test.*
4+
5+
private val namedLogger = KotlinLogging.logger("SimpleWasmWasiTest")
6+
private val anonymousFilePropLogger = KotlinLogging.logger {}
7+
8+
// Create a logger inside a top-level function to exercise the stacktrace-based fallback
9+
private fun createAnonymousFunctionLogger() = KotlinLogging.logger {}
10+
11+
class SimpleWasmWasiTest {
12+
private lateinit var appender: SimpleAppender
13+
private val anonymousClassPropLogger = KotlinLogging.logger {}
14+
15+
@BeforeTest
16+
fun setup() {
17+
appender = createAppender()
18+
KotlinLoggingConfiguration.appender = appender
19+
}
20+
21+
@AfterTest
22+
fun cleanup() {
23+
KotlinLoggingConfiguration.appender = ConsoleOutputAppender
24+
KotlinLoggingConfiguration.logLevel = Level.INFO
25+
}
26+
27+
@Test
28+
fun simpleWasiTest() {
29+
assertEquals("SimpleWasmWasiTest", namedLogger.name)
30+
namedLogger.info { "info msg" }
31+
assertEquals("INFO: [SimpleWasmWasiTest] info msg", appender.lastMessage)
32+
assertEquals("info", appender.lastLevel)
33+
}
34+
35+
@Test
36+
fun anonymousFilePropWasiTest() {
37+
// On WASI, stack traces are often unavailable; top-level property name may be empty.
38+
val n = anonymousFilePropLogger.name
39+
if (n.isNotEmpty()) {
40+
assertEquals("SimpleWasmWasiTest", n)
41+
}
42+
anonymousFilePropLogger.info { "info msg" }
43+
val expected =
44+
if (n.isNotEmpty()) {
45+
"INFO: [SimpleWasmWasiTest] info msg"
46+
} else {
47+
"INFO: [] info msg"
48+
}
49+
assertEquals(expected, appender.lastMessage)
50+
}
51+
52+
@Test
53+
fun anonymousClassPropWasiTest() {
54+
assertEquals("SimpleWasmWasiTest", anonymousClassPropLogger.name)
55+
anonymousClassPropLogger.info { "info msg" }
56+
assertEquals("INFO: [SimpleWasmWasiTest] info msg", appender.lastMessage)
57+
}
58+
59+
@Test
60+
fun anonymousFunctionWasiTest_stacktraceFallback() {
61+
// This aims to cover the new fallback that parses the .kt filename from the stacktrace.
62+
val logger = createAnonymousFunctionLogger()
63+
val n = logger.name
64+
if (n.isNotEmpty()) {
65+
assertEquals("SimpleWasmWasiTest", n)
66+
}
67+
logger.info { "function msg" }
68+
val expected =
69+
if (n.isNotEmpty()) {
70+
"INFO: [SimpleWasmWasiTest] function msg"
71+
} else {
72+
"INFO: [] function msg"
73+
}
74+
assertEquals(expected, appender.lastMessage)
75+
}
76+
77+
@Test
78+
fun offLevelWasiTest() {
79+
KotlinLoggingConfiguration.logLevel = Level.OFF
80+
assertTrue(namedLogger.isLoggingOff())
81+
namedLogger.error { "error msg" }
82+
assertEquals("NA", appender.lastMessage)
83+
assertEquals("NA", appender.lastLevel)
84+
}
85+
86+
private fun createAppender(): SimpleAppender = SimpleAppender()
87+
88+
class SimpleAppender : Appender {
89+
var lastMessage: String = "NA"
90+
var lastLevel: String = "NA"
91+
92+
override fun log(loggingEvent: KLoggingEvent) {
93+
lastMessage = DefaultMessageFormatter(includePrefix = true).formatMessage(loggingEvent)
94+
lastLevel = loggingEvent.level.name.lowercase()
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)