Kotlin Logging: A Complete Guide to Better Logging in Kotlin

December 21, 2024 3 min read Intermediate

Kotlin-logging is a lightweight yet powerful logging library for Kotlin, built on top of SLF4J. It provides an elegant, Kotlin-idiomatic way to handle logging in your applications. In this guide, we'll explore why you should use kotlin-logging, its advantages over traditional logging approaches, and how to implement it effectively.

Why Choose Kotlin Logging?

1. Kotlin-First Design

Unlike traditional Java logging frameworks, kotlin-logging is designed specifically for Kotlin, taking advantage of language features like:

  • Extension functions
  • String templates
  • Null safety
  • Lambda expressions

2. Performance Benefits

  • No overhead when logging is disabled
  • Lazy evaluation of log statements
  • Minimal memory footprint

3. Clean Syntax

  • More concise than traditional logging
  • Better readability
  • Type-safe logging

Getting Started

Setup

Add the dependency to your build.gradle.kts:

dependencies {
    implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
    // Choose one logging backend (e.g., Logback)
    implementation("ch.qos.logback:logback-classic:1.4.11")
}

Basic Usage

Here's a simple example of how to use kotlin-logging:

import mu.KotlinLogging

class UserService {
    private val logger = KotlinLogging.logger {}
    
    fun createUser(user: User) {
        logger.info { "Creating new user: ${user.email}" }
        try {
            // User creation logic
            logger.debug { "User created successfully" }
        } catch (e: Exception) {
            logger.error(e) { "Failed to create user" }
        }
    }
}

Advanced Features

1. Structured Logging

Kotlin-logging supports structured logging, which is crucial for log aggregation and analysis:

data class LogContext(
    val userId: String,
    val action: String,
    val duration: Long
)

class OrderProcessor {
    private val logger = KotlinLogging.logger {}
    
    fun processOrder(orderId: String, userId: String) {
        val startTime = System.currentTimeMillis()
        
        try {
            // Process order
            val duration = System.currentTimeMillis() - startTime
            
            logger.info {
                val context = LogContext(userId, "process_order", duration)
                "Order processed successfully: $orderId [context=$context]"
            }
        } catch (e: Exception) {
            logger.error(e) { 
                "Order processing failed for orderId=$orderId, userId=$userId" 
            }
        }
    }
}

2. Custom Log Formatting

Configure Logback (the backend) with custom patterns in logback.xml:

<configuration>
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMDC>true</includeMDC>
            <includeCallerData>true</includeCallerData>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="JSON" />
    </root>
</configuration>

3. Performance Optimization

Kotlin-logging shines in performance-critical applications:

class PerformanceCriticalService {
    private val logger = KotlinLogging.logger {}
    
    fun processLargeDataSet(data: List<String>) {
        // Lazy evaluation - string interpolation only happens if DEBUG is enabled
        logger.debug { "Processing ${data.size} records" }
        
        data.forEach { item ->
            // Expensive operation only executed if TRACE is enabled
            logger.trace { "Calculating hash for item: ${calculateHash(item)}" }
            process(item)
        }
    }
    
    private fun calculateHash(item: String): String {
        // Expensive operation
        return item.hashCode().toString()
    }
}

4. Testing with Logs

Here's how to test your logging in unit tests:

class LoggingTest {
    private val logger = KotlinLogging.logger {}
    
    @Test
    fun `test logging output`() {
        // Configure test appender
        val testAppender = ListAppender<ILoggingEvent>()
        val logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger
        logger.addAppender(testAppender)
        testAppender.start()
        
        // Execute code that logs
        processWithLogging()
        
        // Verify logs
        assertThat(testAppender.list)
            .extracting<String> { it.formattedMessage }
            .contains("Expected log message")
    }
}

Best Practices

1. Log Level Guidelines

class BestPracticesExample {
    private val logger = KotlinLogging.logger {}
    
    fun demonstrate() {
        // ERROR: Use for unrecoverable failures
        logger.error { "Database connection failed" }
        
        // WARN: Use for recoverable issues
        logger.warn { "Rate limit reached, retrying in 5s" }
        
        // INFO: Use for significant events
        logger.info { "User logged in successfully" }
        
        // DEBUG: Use for detailed information
        logger.debug { "Cache hit ratio: 0.85" }
        
        // TRACE: Use for very detailed debugging
        logger.trace { "Entering method with params: x=$x, y=$y" }
    }
}

2. Context Enrichment

class ContextualLogging {
    private val logger = KotlinLogging.logger {}
    
    fun processWithContext(userId: String) {
        MDC.put("userId", userId)
        try {
            logger.info { "Processing request" }
            // The log will automatically include userId
        } finally {
            MDC.remove("userId")
        }
    }
}

3. Exception Handling

class ExceptionHandling {
    private val logger = KotlinLogging.logger {}
    
    fun demonstrateExceptionLogging(data: String?) {
        try {
            processData(data!!)
        } catch (e: Exception) {
            // Log with exception context
            logger.error(e) { "Failed to process data" }
            
            // Or with custom error details
            logger.error(e) {
                "Failed to process data. Context: ${
                    mapOf(
                        "dataLength" to (data?.length ?: 0),
                        "errorType" to e.javaClass.simpleName
                    )
                }"
            }
        }
    }
}

Common Pitfalls to Avoid

  1. String Concatenation in Log Statements
// Bad
logger.debug("Processing user: " + user.email) // Eager evaluation

// Good
logger.debug { "Processing user: ${user.email}" } // Lazy evaluation
  1. Missing Context
// Bad
logger.error { "Operation failed" }

// Good
logger.error { "Operation failed: operation=$operationType, userId=$userId" }
  1. Overlogging
// Bad
list.forEach { item ->
    logger.debug { "Processing $item" } // Too verbose
}

// Good
logger.debug { "Processing ${list.size} items" }

Performance Comparison

Here's a simple benchmark comparing kotlin-logging with traditional logging:

class LoggingBenchmark {
    private val kotlinLogger = KotlinLogging.logger {}
    private val javaLogger = LoggerFactory.getLogger(javaClass)
    
    fun benchmark() {
        val iterations = 1_000_000
        
        measureTimeMillis {
            repeat(iterations) {
                kotlinLogger.debug { "Test message with param: $it" }
            }
        }.also { println("Kotlin-logging took: ${it}ms") }
        
        measureTimeMillis {
            repeat(iterations) {
                if (javaLogger.isDebugEnabled) {
                    javaLogger.debug("Test message with param: {}", it)
                }
            }
        }.also { println("Traditional logging took: ${it}ms") }
    }
}

Conclusion

Kotlin-logging offers a superior logging experience for Kotlin applications through:

  • Type-safe, null-safe logging
  • Better performance through lazy evaluation
  • Clean, idiomatic Kotlin syntax
  • Powerful integration with existing logging frameworks

By following the best practices and utilizing the advanced features, you can create maintainable, performant, and informative logging in your Kotlin applications.

Remember to:

  • Choose appropriate log levels
  • Provide relevant context
  • Use structured logging when possible
  • Take advantage of lazy evaluation
  • Test your logging implementation

With these tools and practices, you'll be well-equipped to implement effective logging in your Kotlin applications.

Latest Articles