Skip to content

Commit e31347a

Browse files
committed
Mark logger lambda parameters as crossinline
This prevents accidental non-local returns inside the lambda, which would drop the log.
1 parent c0c0e48 commit e31347a

File tree

4 files changed

+93
-47
lines changed

4 files changed

+93
-47
lines changed

src/commonMain/kotlin/dev/hermannm/devlog/LogLevel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ public class LogLevel private constructor() {
103103
* - We make the method `inline`, so we don't pay a cost for the lambdas
104104
* - In the unreachable else branch, we throw an exception
105105
*
106-
* We may be able to use an enum/sealed class instead if this issue moves along:
107-
* https://youtrack.jetbrains.com/issue/KT-38750/Support-declaration-site-nonexhaustiveness-for-enums-and-sealed-classes
106+
* We may be able to use an enum/sealed class instead if
107+
* [this issue moves along](https://youtrack.jetbrains.com/issue/KT-38750/Support-declaration-site-nonexhaustiveness-for-enums-and-sealed-classes).
108108
*/
109109
@Suppress("LocalVariableName") // We want the argument names here to be the same as the constants
110110
internal inline fun <ReturnT> match(

src/commonMain/kotlin/dev/hermannm/devlog/Logger.kt

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,14 @@ internal constructor(
178178
*
179179
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
180180
* to add structured key-value data to the log.
181+
*
182+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
183+
* which would drop the log.
181184
*/
182-
public inline fun info(cause: Throwable? = null, buildLog: LogBuilder.() -> String) {
185+
public inline fun info(
186+
cause: Throwable? = null,
187+
crossinline buildLog: LogBuilder.() -> String,
188+
) {
183189
if (isInfoEnabled) {
184190
log(LogLevel.INFO, cause, buildLog)
185191
}
@@ -225,8 +231,14 @@ internal constructor(
225231
*
226232
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
227233
* to add structured key-value data to the log.
234+
*
235+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
236+
* which would drop the log.
228237
*/
229-
public inline fun warn(cause: Throwable? = null, buildLog: LogBuilder.() -> String) {
238+
public inline fun warn(
239+
cause: Throwable? = null,
240+
crossinline buildLog: LogBuilder.() -> String,
241+
) {
230242
if (isWarnEnabled) {
231243
log(LogLevel.WARN, cause, buildLog)
232244
}
@@ -272,8 +284,14 @@ internal constructor(
272284
*
273285
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
274286
* to add structured key-value data to the log.
287+
*
288+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
289+
* which would drop the log.
275290
*/
276-
public inline fun error(cause: Throwable? = null, buildLog: LogBuilder.() -> String) {
291+
public inline fun error(
292+
cause: Throwable? = null,
293+
crossinline buildLog: LogBuilder.() -> String,
294+
) {
277295
if (isErrorEnabled) {
278296
log(LogLevel.ERROR, cause, buildLog)
279297
}
@@ -315,8 +333,14 @@ internal constructor(
315333
*
316334
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
317335
* to add structured key-value data to the log.
336+
*
337+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
338+
* which would drop the log.
318339
*/
319-
public inline fun debug(cause: Throwable? = null, buildLog: LogBuilder.() -> String) {
340+
public inline fun debug(
341+
cause: Throwable? = null,
342+
crossinline buildLog: LogBuilder.() -> String,
343+
) {
320344
if (isDebugEnabled) {
321345
log(LogLevel.DEBUG, cause, buildLog)
322346
}
@@ -358,8 +382,14 @@ internal constructor(
358382
*
359383
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
360384
* to add structured key-value data to the log.
385+
*
386+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
387+
* which would drop the log.
361388
*/
362-
public inline fun trace(cause: Throwable? = null, buildLog: LogBuilder.() -> String) {
389+
public inline fun trace(
390+
cause: Throwable? = null,
391+
crossinline buildLog: LogBuilder.() -> String,
392+
) {
363393
if (isTraceEnabled) {
364394
log(LogLevel.TRACE, cause, buildLog)
365395
}
@@ -409,17 +439,36 @@ internal constructor(
409439
*
410440
* The [LogBuilder] receiver lets you call [field][LogBuilder.field] in the scope of the lambda,
411441
* to add structured key-value data to the log.
442+
*
443+
* We mark the lambda as `crossinline`, so you don't accidentally do a non-local return in it,
444+
* which would drop the log.
412445
*/
413446
public inline fun at(
414447
level: LogLevel,
415448
cause: Throwable? = null,
416-
buildLog: LogBuilder.() -> String
449+
crossinline buildLog: LogBuilder.() -> String,
417450
) {
418451
if (isEnabledFor(level)) {
419452
log(level, cause, buildLog)
420453
}
421454
}
422455

456+
@PublishedApi
457+
internal inline fun log(
458+
level: LogLevel,
459+
cause: Throwable?,
460+
crossinline buildLog: LogBuilder.() -> String,
461+
) {
462+
val builder = LogBuilder(createLogEvent(level, cause, underlyingLogger))
463+
val message = builder.buildLog()
464+
if (cause != null) {
465+
// Call this after buildLog(), so cause exception fields don't overwrite LogBuilder fields
466+
builder.addFieldsFromCauseException(cause)
467+
}
468+
469+
builder.logEvent.log(message, underlyingLogger)
470+
}
471+
423472
/**
424473
* Returns true if [LogLevel.ERROR] is enabled for this logger.
425474
*
@@ -486,18 +535,6 @@ internal constructor(
486535
TRACE = { isTraceEnabled },
487536
)
488537
}
489-
490-
@PublishedApi
491-
internal inline fun log(level: LogLevel, cause: Throwable?, buildLog: LogBuilder.() -> String) {
492-
val builder = LogBuilder(createLogEvent(level, cause, underlyingLogger))
493-
val message = builder.buildLog()
494-
if (cause != null) {
495-
// Call this after buildLog(), so cause exception fields don't overwrite LogBuilder fields
496-
builder.addFieldsFromCauseException(cause)
497-
}
498-
499-
builder.logEvent.log(message, underlyingLogger)
500-
}
501538
}
502539

503540
/**
@@ -508,11 +545,11 @@ internal constructor(
508545
internal expect interface PlatformLogger {
509546
fun getName(): String
510547

511-
fun isInfoEnabled(): Boolean
548+
fun isErrorEnabled(): Boolean
512549

513550
fun isWarnEnabled(): Boolean
514551

515-
fun isErrorEnabled(): Boolean
552+
fun isInfoEnabled(): Boolean
516553

517554
fun isDebugEnabled(): Boolean
518555

src/commonTest/kotlin/dev/hermannm/devlog/LoggerTest.kt

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -311,29 +311,6 @@ internal class LoggerTest {
311311
disabledLogger.isErrorEnabled.shouldBeFalse()
312312
disabledLogger.isEnabledFor(LogLevel.ERROR).shouldBeFalse()
313313
}
314-
315-
@Test
316-
fun `non-local return works in all logger methods`() {
317-
// This won't compile if the methods aren't inline, and we want to verify that
318-
log.info {
319-
return
320-
}
321-
log.warn {
322-
return
323-
}
324-
log.error {
325-
return
326-
}
327-
log.debug {
328-
return
329-
}
330-
log.trace {
331-
return
332-
}
333-
log.at(LogLevel.INFO) {
334-
return
335-
}
336-
}
337314
}
338315

339316
private val loggerOutsideClass = getLogger()

src/jvmTest/kotlin/dev/hermannm/devlog/LoggerTest.jvm.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.kotest.matchers.date.shouldBeBetween
1616
import io.kotest.matchers.nulls.shouldBeNull
1717
import io.kotest.matchers.shouldBe
1818
import io.kotest.matchers.types.shouldBeInstanceOf
19+
import java.lang.invoke.MethodHandles
1920
import java.time.Instant
2021
import kotlin.test.AfterTest
2122
import kotlin.test.Test
@@ -98,8 +99,8 @@ internal class LoggerJvmTest {
9899
companion object {
99100
/** We use a ListAppender from Logback here so we can inspect log events after logging. */
100101
val logAppender = ListAppender<ILoggingEvent>()
101-
const val TEST_LOGGER_NAME = "LoggerTest"
102-
val logbackLogger = Slf4jLoggerFactory.getLogger(TEST_LOGGER_NAME) as LogbackLogger
102+
val logbackLogger = Slf4jLoggerFactory.getLogger("LoggerTest") as LogbackLogger
103+
val log = Logger(logbackLogger)
103104

104105
init {
105106
logAppender.start()
@@ -146,4 +147,35 @@ internal class LoggerJvmTest {
146147
fun `Logback is loaded in tests`() {
147148
LOGBACK_IS_ON_CLASSPATH shouldBe true
148149
}
150+
151+
@Test
152+
fun `lambda arguments to logger methods are inlined`() {
153+
// We verify that the lambdas are inlined by calling `lookupClass()` inside of them.
154+
// If they are inlined, then the calling class should be this test class - otherwise, the
155+
// calling class would be a generated class for the lambda.
156+
log.info {
157+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
158+
"Test"
159+
}
160+
log.warn {
161+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
162+
"Test"
163+
}
164+
log.error {
165+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
166+
"Test"
167+
}
168+
log.debug {
169+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
170+
"Test"
171+
}
172+
log.trace {
173+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
174+
"Test"
175+
}
176+
log.at(LogLevel.INFO) {
177+
MethodHandles.lookup().lookupClass() shouldBe LoggerJvmTest::class.java
178+
"Test"
179+
}
180+
}
149181
}

0 commit comments

Comments
 (0)