with Logging Context
Adds the given log fields to every log made by a Logger in the context of the given block.
Use the field/rawJsonField functions to construct log fields.
If an exception is thrown from block, then the given log fields are attached to the exception, and included in the log output when the exception is logged. That way, we don't lose logging context when an exception escapes the context scope.
An example of when this is useful is when processing an event, and you want to trace all the logs made in the context of the event. Instead of manually attaching the event ID to each log, you can wrap the event processing in withLoggingContext
, with the event ID as a log field. All the logs inside that context will then include the event ID as a structured log field, that you can filter on in your log analysis tool.
Field value encoding with SLF4J
The JVM implementation uses MDC
from SLF4J, which only supports String values by default. But we want to encode object values in the logging context as actual JSON (not escaped strings), so that log analysis tools can parse the fields. If you're using Logback and logstash-logback-encoder
for JSON output, you can add support for this by configuring dev.hermannm.devlog.output.logback.JsonContextFieldWriter
as an mdcEntryWriter
:
<!-- Example Logback config (in src/main/resources/logback.xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- Writes object values from logging context as actual JSON (not escaped) -->
<mdcEntryWriter class="dev.hermannm.devlog.output.logback.JsonContextFieldWriter"/>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration
This requires that you have added ch.qos.logback:logback-classic
and net.logstash.logback:logstash-logback-encoder
as dependencies.
Note on coroutines
SLF4J's MDC
uses a thread-local, so it won't work by default with Kotlin coroutines and suspend
functions. If you use coroutines, you can solve this with MDCContext
from kotlinx-coroutines-slf4j
.
Example
import dev.hermannm.devlog.field
import dev.hermannm.devlog.getLogger
import dev.hermannm.devlog.withLoggingContext
private val log = getLogger()
fun example(event: Event) {
withLoggingContext(field("eventId", event.id)) {
log.debug { "Started processing event" }
// ...
log.debug { "Finished processing event" }
}
}
If you have configured dev.hermannm.devlog.output.logback.JsonContextFieldWriter
, the field from withLoggingContext
will then be attached to every log as follows:
{ "message": "Started processing event", "eventId": "..." }
{ "message": "Finished processing event", "eventId": "..." }
Adds the fields from an existing logging context to all logs made by a Logger in the context of the given block. This overload is designed to be used with getCopyOfLoggingContext, to pass logging context between threads. If you want to add fields to the current thread's logging context, you should instead construct log fields with the field/rawJsonField functions, and pass them to one of the withLoggingContext overloads that take LogFields.
If you spawn threads using a java.util.concurrent.ExecutorService
, you may instead use the ExecutorService.inheritLoggingContext
extension function from this library, which passes logging context from parent to child for you.
If an exception is thrown from block, then the given logging context is attached to the exception, and included in the log output when the exception is logged. That way, we don't lose logging context when an exception escapes the context scope.
Example
Scenario: We store an updated order in a database, and then want to asynchronously update statistics for the order.
import dev.hermannm.devlog.field
import dev.hermannm.devlog.getCopyOfLoggingContext
import dev.hermannm.devlog.getLogger
import dev.hermannm.devlog.withLoggingContext
import kotlin.concurrent.thread
private val log = getLogger()
class OrderService(
private val orderRepository: OrderRepository,
private val statisticsService: StatisticsService,
) {
fun updateOrder(order: Order) {
// This is the default withLoggingContext overload, adding context to the current thread
withLoggingContext(field("order", order)) {
orderRepository.update(order)
updateStatistics(order)
}
}
// In this scenario, we don't want updateStatistics to block updateOrder, so we spawn a thread.
//
// But we want to log if it fails, and include the logging context from the parent thread.
private fun updateStatistics(order: Order) {
// We call getCopyOfLoggingContext here, to copy the context fields from the parent thread
val parentContext = getCopyOfLoggingContext()
thread {
// We then pass the parent context to withLoggingContext here in the child thread.
// This uses the overload that takes an existing logging context
withLoggingContext(parentContext) {
try {
statisticsService.orderUpdated(order)
} catch (e: Exception) {
// This log will get the "order" field from the parent logging context
log.error(e) { "Failed to update order statistics" }
}
}
}
}
}