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.

Logging LibraryKotlinCoroutinesSLF4JMDCAsynchronousTraceability

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

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")
}