KotlinX Coroutines SLF4J
Library providing integration between Kotlin coroutines and SLF4J's MDC (Mapped Diagnostic Context). Adds MDC to coroutine context, enabling preservation of logging context across asynchronous processing.
Library
KotlinX Coroutines SLF4J
Overview
KotlinX Coroutines SLF4J is an official library providing integration between Kotlin coroutines and SLF4J's MDC (Mapped Diagnostic Context). It adds MDC to coroutine context, enabling preservation of logging context across asynchronous processing. This library plays a crucial role in improving traceability in distributed systems and microservice environments, and is essential for coroutine-based server-side Kotlin development.
Details
KotlinX Coroutines SLF4J has been gaining importance in 2025 as an official module of the Kotlin coroutines ecosystem. Traditional Java logging implementations primarily used thread-local variables for MDC context management, but coroutines' asynchronous nature and thread switching caused MDC information loss. This library preserves MDC information within coroutine context through MDCContext and automatically inherits MDC context in coroutine builders like launch and async.
Key Features
- MDC Context Integration: Automatic integration between SLF4J's MDC and Kotlin coroutine context
- Asynchronous Logging: Automatic propagation of traceability information across coroutines
- Thread Switching Support: MDC information retention across dispatchers
- Easy Integration: Full compatibility with existing SLF4J/logback setups
- Distributed Tracing: Management of tracking information like request IDs and user IDs
- Performance Optimization: Lightweight coroutine context implementation
Pros & Cons
Pros
- Reliable MDC context management in coroutine-based applications
- Improved request tracing and debuggability in distributed systems
- Full compatibility with existing SLF4J/logback ecosystem
- Reliability and long-term support as an official JetBrains module
- Minimal API and simple integration process
- Good integration with web frameworks like Spring Boot and Ktor
Cons
- Dependency on SLF4J ecosystem increases library size
- Limited functionality compared to modern coroutine-native loggers like Klogging
- Memory overhead and performance impact when using MDC
- Complexity in understanding context inheritance in complex coroutine hierarchies
- Limited structured logging support
- Complexity in MDC cleanup during coroutine cancellation
Reference Pages
- KotlinX Coroutines SLF4J Official Documentation
- kotlinx.coroutines GitHub Repository
- SLF4J Official Site
Code Examples
Installation and Setup
// build.gradle.kts
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("ch.qos.logback:logback-classic:1.4.14")
}
// Logback configuration (logback.xml)
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%X{requestId}] %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>app.log</file>
<encoder>
<pattern>[%X{requestId}] [%X{userId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
Basic MDCContext Usage
import kotlinx.coroutines.*
import kotlinx.coroutines.slf4j.MDCContext
import org.slf4j.LoggerFactory
import org.slf4j.MDC
private val logger = LoggerFactory.getLogger("Application")
suspend fun main() = runBlocking {
// Set MDC context and launch coroutine
MDC.put("requestId", "req-123456")
MDC.put("userId", "user-789")
launch(MDCContext()) {
logger.info("Main task started") // [req-123456] [user-789] Main task started
processData()
}.join()
// Clear MDC
MDC.clear()
}
suspend fun processData() {
logger.info("Data processing started") // MDC context is automatically preserved
// MDC is inherited even in concurrent processing
val results = coroutineScope {
listOf(
async { processStep("Step 1") },
async { processStep("Step 2") },
async { processStep("Step 3") }
)
}
logger.info("All processing completed: ${results.awaitAll()}")
}
suspend fun processStep(step: String): String {
delay(100) // Asynchronous processing simulation
logger.info("$step executing") // MDC context is preserved
return "$step completed"
}
Request Tracing in Web Applications
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.async
import kotlinx.coroutines.slf4j.MDCContext
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import java.util.*
private val logger = LoggerFactory.getLogger("WebApp")
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/api/users/{id}") {
// Generate unique ID for each request
val requestId = UUID.randomUUID().toString()
val userId = call.parameters["id"] ?: "unknown"
// Set tracing information in MDC
MDC.put("requestId", requestId)
MDC.put("userId", userId)
MDC.put("endpoint", "/api/users/$userId")
try {
// Launch coroutines with MDCContext
val userInfo = async(MDCContext()) {
fetchUserInfo(userId)
}
val preferences = async(MDCContext()) {
fetchUserPreferences(userId)
}
// Combine results and return
val response = mapOf(
"user" to userInfo.await(),
"preferences" to preferences.await()
)
logger.info("User information retrieval completed")
call.respond(response)
} catch (e: Exception) {
logger.error("User information retrieval error", e)
call.respond(mapOf("error" to "Internal Server Error"))
} finally {
// Clear MDC (on request completion)
MDC.clear()
}
}
}
}.start(wait = true)
}
suspend fun fetchUserInfo(userId: String): Map<String, Any> {
logger.info("User basic information retrieval started")
// Database access simulation
delay(200)
val userInfo = mapOf(
"id" to userId,
"name" to "User$userId",
"email" to "user$userId@example.com"
)
logger.info("User basic information retrieval completed")
return userInfo
}
suspend fun fetchUserPreferences(userId: String): Map<String, Any> {
logger.info("User preferences retrieval started")
// External API call simulation
delay(150)
val preferences = mapOf(
"theme" to "dark",
"language" to "en",
"notifications" to true
)
logger.info("User preferences retrieval completed")
return preferences
}
Spring Boot Integration and Log Filters
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Component
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import java.util.*
@SpringBootApplication
class LoggingApplication
fun main(args: Array<String>) {
runApplication<LoggingApplication>(*args)
}
// WebFilter for MDC setup
@Component
class MDCWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val requestId = UUID.randomUUID().toString()
val path = exchange.request.path.value()
// Set MDC on request start
MDC.put("requestId", requestId)
MDC.put("path", path)
MDC.put("method", exchange.request.method.name())
return chain.filter(exchange)
.doFinally {
// Clear MDC on request completion
MDC.clear()
}
}
}
@RestController
class UserController(private val userService: UserService) {
private val logger = LoggerFactory.getLogger(UserController::class.java)
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: String): Map<String, Any> {
logger.info("User retrieval request: ID=$id")
return runBlocking {
// Call service with MDCContext
launch(MDCContext()) {
userService.getUserWithDetails(id)
}.let { it.join(); userService.lastResult }
}
}
}
@Service
class UserService {
private val logger = LoggerFactory.getLogger(UserService::class.java)
var lastResult: Map<String, Any> = emptyMap()
private set
suspend fun getUserWithDetails(userId: String): Map<String, Any> {
logger.info("User detail information service started")
// MDC context is automatically inherited
val basicInfo = fetchBasicInfo(userId)
val permissions = fetchPermissions(userId)
val activity = fetchRecentActivity(userId)
lastResult = mapOf(
"basic" to basicInfo,
"permissions" to permissions,
"activity" to activity
)
logger.info("User detail information service completed")
return lastResult
}
private suspend fun fetchBasicInfo(userId: String): Map<String, String> {
logger.info("Basic information retrieval started")
delay(100)
logger.info("Basic information retrieval completed")
return mapOf("id" to userId, "name" to "User$userId")
}
private suspend fun fetchPermissions(userId: String): List<String> {
logger.info("Permission information retrieval started")
delay(50)
logger.info("Permission information retrieval completed")
return listOf("read", "write")
}
private suspend fun fetchRecentActivity(userId: String): List<String> {
logger.info("Activity retrieval started")
delay(75)
logger.info("Activity retrieval completed")
return listOf("login", "view_profile", "update_settings")
}
}
Error Handling and Log Aggregation
import kotlinx.coroutines.*
import kotlinx.coroutines.slf4j.MDCContext
import org.slf4j.LoggerFactory
import org.slf4j.MDC
private val logger = LoggerFactory.getLogger("ErrorHandling")
// Custom exception classes
class BusinessException(message: String, val errorCode: String) : Exception(message)
class ValidationException(message: String, val field: String) : Exception(message)
suspend fun main() = runBlocking {
// Demo of error handling and log aggregation
val requests = listOf("valid-123", "invalid-", "", "error-456", "valid-789")
requests.forEach { requestId ->
launch(MDCContext()) {
try {
MDC.put("requestId", requestId)
MDC.put("operation", "processRequest")
processRequest(requestId)
} catch (e: Exception) {
handleError(e, requestId)
} finally {
MDC.clear()
}
}
}
}
suspend fun processRequest(requestId: String) {
logger.info("Request processing started")
try {
// Validation
validateRequest(requestId)
// Execute business logic
val result = executeBusinessLogic(requestId)
// Save result
saveResult(requestId, result)
logger.info("Request processing completed successfully")
} catch (e: ValidationException) {
// Add MDC information for validation errors
MDC.put("errorType", "validation")
MDC.put("field", e.field)
logger.warn("Validation error: ${e.message}")
throw e
} catch (e: BusinessException) {
// Add MDC information for business errors
MDC.put("errorType", "business")
MDC.put("errorCode", e.errorCode)
logger.error("Business logic error: ${e.message}")
throw e
} catch (e: Exception) {
// Add MDC information for system errors
MDC.put("errorType", "system")
logger.error("System error", e)
throw e
}
}
suspend fun validateRequest(requestId: String) {
logger.debug("Request validation started")
when {
requestId.isEmpty() -> {
throw ValidationException("Request ID is empty", "requestId")
}
requestId.contains("invalid") -> {
throw ValidationException("Invalid request ID format", "requestId")
}
requestId.length < 3 -> {
throw ValidationException("Request ID is too short", "requestId")
}
}
logger.debug("Request validation completed")
}
suspend fun executeBusinessLogic(requestId: String): String {
logger.info("Business logic execution started")
// Error case simulation
if (requestId.contains("error")) {
throw BusinessException("Business rule violation", "BUSINESS_001")
}
// Processing time simulation
delay(100)
val result = "Processing result-$requestId"
logger.info("Business logic execution completed")
return result
}
suspend fun saveResult(requestId: String, result: String) {
logger.info("Result saving started")
// Database save simulation
delay(50)
logger.info("Result saving completed: $result")
}
fun handleError(exception: Exception, requestId: String) {
when (exception) {
is ValidationException -> {
logger.info("Validation error handled appropriately")
// Return 400 error to client
}
is BusinessException -> {
logger.info("Business error handled appropriately")
// Return 422 error to client
}
else -> {
logger.error("Unexpected error occurred", exception)
// Return 500 error to client
}
}
}
// Utility functions for log aggregation
object LogUtils {
fun addTransactionContext(transactionId: String, userId: String? = null) {
MDC.put("transactionId", transactionId)
userId?.let { MDC.put("userId", it) }
}
fun addPerformanceContext(operation: String, startTime: Long) {
MDC.put("operation", operation)
MDC.put("startTime", startTime.toString())
}
fun logPerformance(operation: String, startTime: Long) {
val duration = System.currentTimeMillis() - startTime
MDC.put("duration", duration.toString())
logger.info("Performance: $operation execution time=${duration}ms")
}
}
Advanced Microservice Integration
import kotlinx.coroutines.*
import kotlinx.coroutines.slf4j.MDCContext
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import java.util.*
private val logger = LoggerFactory.getLogger("MicroserviceIntegration")
// Context for distributed tracing
data class TraceContext(
val traceId: String,
val spanId: String,
val parentSpanId: String? = null,
val userId: String? = null,
val serviceName: String
)
object DistributedTracing {
fun createRootTrace(userId: String? = null, serviceName: String = "main"): TraceContext {
return TraceContext(
traceId = UUID.randomUUID().toString(),
spanId = UUID.randomUUID().toString(),
userId = userId,
serviceName = serviceName
)
}
fun createChildSpan(parent: TraceContext, serviceName: String): TraceContext {
return parent.copy(
spanId = UUID.randomUUID().toString(),
parentSpanId = parent.spanId,
serviceName = serviceName
)
}
fun setupMDC(context: TraceContext) {
MDC.put("traceId", context.traceId)
MDC.put("spanId", context.spanId)
context.parentSpanId?.let { MDC.put("parentSpanId", it) }
context.userId?.let { MDC.put("userId", it) }
MDC.put("serviceName", context.serviceName)
}
}
// Microservice communication simulation
class OrderService {
suspend fun processOrder(userId: String, orderId: String): String = coroutineScope {
val traceContext = DistributedTracing.createRootTrace(userId, "order-service")
DistributedTracing.setupMDC(traceContext)
logger.info("Order processing started: orderId=$orderId")
try {
// Call microservices concurrently
val userInfoDeferred = async(MDCContext()) {
callUserService(traceContext, userId)
}
val inventoryDeferred = async(MDCContext()) {
callInventoryService(traceContext, orderId)
}
val paymentDeferred = async(MDCContext()) {
callPaymentService(traceContext, userId, orderId)
}
// Wait for all service calls to complete
val userInfo = userInfoDeferred.await()
val inventoryResult = inventoryDeferred.await()
val paymentResult = paymentDeferred.await()
// Final order confirmation processing
val finalResult = finalizeOrder(traceContext, orderId, userInfo, inventoryResult, paymentResult)
logger.info("Order processing completed successfully: result=$finalResult")
finalResult
} catch (e: Exception) {
logger.error("Order processing error", e)
throw e
}
}
private suspend fun callUserService(parentContext: TraceContext, userId: String): String {
val childContext = DistributedTracing.createChildSpan(parentContext, "user-service")
DistributedTracing.setupMDC(childContext)
logger.info("User service call started")
// External service call simulation
delay(150)
if (userId == "error-user") {
throw RuntimeException("User service error")
}
val result = "User info retrieval completed:$userId"
logger.info("User service call completed")
return result
}
private suspend fun callInventoryService(parentContext: TraceContext, orderId: String): String {
val childContext = DistributedTracing.createChildSpan(parentContext, "inventory-service")
DistributedTracing.setupMDC(childContext)
logger.info("Inventory service call started")
delay(100)
val result = "Inventory check completed:$orderId"
logger.info("Inventory service call completed")
return result
}
private suspend fun callPaymentService(parentContext: TraceContext, userId: String, orderId: String): String {
val childContext = DistributedTracing.createChildSpan(parentContext, "payment-service")
DistributedTracing.setupMDC(childContext)
logger.info("Payment service call started")
delay(200)
val result = "Payment processing completed:$userId-$orderId"
logger.info("Payment service call completed")
return result
}
private suspend fun finalizeOrder(
context: TraceContext,
orderId: String,
userInfo: String,
inventoryResult: String,
paymentResult: String
): String {
DistributedTracing.setupMDC(context)
logger.info("Order finalization started")
// Order finalization processing simulation
delay(75)
val finalResult = "Order confirmed:$orderId"
logger.info("Order finalization completed")
return finalResult
}
}
// Demo execution
suspend fun main() = runBlocking {
val orderService = OrderService()
// Process multiple orders concurrently
val orders = listOf(
"user1" to "order-001",
"user2" to "order-002",
"error-user" to "order-003",
"user3" to "order-004"
)
orders.map { (userId, orderId) ->
async {
try {
orderService.processOrder(userId, orderId)
} catch (e: Exception) {
logger.error("Order processing failed: userId=$userId, orderId=$orderId", e)
"Failed:$orderId"
} finally {
MDC.clear()
}
}
}.awaitAll()
logger.info("All order processing completed")
}