Table of Contents
Kotlin Logging: A Complete Guide to Better Logging in Kotlin
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
- String Concatenation in Log Statements
// Bad
logger.debug("Processing user: " + user.email) // Eager evaluation
// Good
logger.debug { "Processing user: ${user.email}" } // Lazy evaluation
- Missing Context
// Bad
logger.error { "Operation failed" }
// Good
logger.error { "Operation failed: operation=$operationType, userId=$userId" }
- 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.