Kotlin DSL¶
Hermes provides idiomatic Kotlin extensions for a more natural logging experience in Kotlin projects.
Installation¶
Maven¶
<dependency>
<groupId>io.github.dotbrains</groupId>
<artifactId>hermes-kotlin</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
Gradle (Kotlin DSL)¶
Requirements¶
- Kotlin 2.1.10+
- Java 17+
Logger Creation¶
Extension Property¶
import io.github.dotbrains.kotlin.logger
class UserService {
private val log = UserService::class.logger
fun createUser(username: String) {
log.info { "Creating user: $username" }
}
}
Inline Logger¶
import io.github.dotbrains.kotlin.logger
class OrderService {
fun processOrder(orderId: Long) {
logger.info { "Processing order: $orderId" }
}
}
Lazy Evaluation¶
All logging methods accept lambda expressions for lazy evaluation:
// Message only evaluated if INFO level is enabled
log.info { "User details: ${user.toDetailedString()}" }
// Expensive computation avoided if DEBUG is disabled
log.debug { "Query result: ${complexQuery().formatResults()}" }
Traditional vs. Lazy¶
// ❌ Bad - string interpolation happens regardless of log level
log.debug("Result: ${expensiveOperation()}")
// ✅ Good - only executed if DEBUG is enabled
log.debug { "Result: ${expensiveOperation()}" }
MDC Extensions¶
Scoped MDC¶
Automatic MDC cleanup with withMDC:
import io.github.dotbrains.kotlin.withMDC
fun processRequest(requestId: String, userId: String) {
withMDC("requestId" to requestId, "userId" to userId) {
log.info { "Processing request" }
// MDC automatically cleared after block
}
}
MDC Builder¶
import io.github.dotbrains.kotlin.mdc
fun handleRequest(request: Request) {
mdc {
"requestId" to request.id
"method" to request.method
"path" to request.path
"userId" to request.userId
}.use {
log.info { "Handling ${request.method} ${request.path}" }
}
}
Suspending Functions¶
MDC propagation in coroutines:
import io.github.dotbrains.kotlin.withMDC
import kotlinx.coroutines.launch
suspend fun processAsync(requestId: String) {
withMDC("requestId" to requestId) {
launch {
log.info { "Async processing started" }
// MDC available in coroutine
}
}
}
Structured Logging¶
Using infoWith, debugWith, etc.¶
log.infoWith {
"message" to "User created"
"userId" to userId
"username" to username
"timestamp" to System.currentTimeMillis()
}
Output (with JsonLayout):
{
"timestamp": "2024-01-10T10:30:45.123Z",
"level": "INFO",
"logger": "com.example.UserService",
"message": "User created",
"fields": {
"userId": "user-123",
"username": "alice",
"timestamp": 1704882645123
}
}
All Levels¶
log.traceWith { /* fields */ }
log.debugWith { /* fields */ }
log.infoWith { /* fields */ }
log.warnWith { /* fields */ }
log.errorWith { /* fields */ }
Exception Logging¶
Lambda with Exception¶
try {
processPayment()
} catch (e: PaymentException) {
log.error(e) { "Payment processing failed for order $orderId" }
}
Structured Exception Logging¶
try {
executeTransaction()
} catch (e: Exception) {
log.errorWith(e) {
"message" to "Transaction failed"
"transactionId" to txId
"amount" to amount
"retryCount" to retries
}
}
Markers¶
Creating Markers¶
import io.github.dotbrains.kotlin.marker
val SECURITY = marker("SECURITY")
val AUDIT = marker("AUDIT")
val PERFORMANCE = marker("PERFORMANCE")
Using Markers¶
log.info(SECURITY) { "User $username logged in" }
log.warn(PERFORMANCE) { "Query took ${duration}ms" }
// With structured logging
log.warnWith(AUDIT) {
"message" to "Configuration changed"
"setting" to settingName
"oldValue" to oldValue
"newValue" to newValue
"changedBy" to adminId
}
Conditional Logging¶
Check Log Level¶
if (log.isDebugEnabled) {
val details = computeExpensiveDetails()
log.debug { "Details: $details" }
}
Extension Functions¶
Coroutines Support¶
Structured Concurrency¶
import kotlinx.coroutines.*
suspend fun processItems(items: List<Item>) = coroutineScope {
withMDC("batchId" to UUID.randomUUID().toString()) {
items.map { item ->
async {
withMDC("itemId" to item.id) {
log.info { "Processing item ${item.id}" }
processItem(item)
}
}
}.awaitAll()
}
}
Flow Logging¶
import kotlinx.coroutines.flow.*
fun observeOrders(): Flow<Order> = flow {
log.info { "Starting order observation" }
// ...
}.onEach { order ->
log.debug { "Order received: ${order.id}" }
}.catch { e ->
log.error(e) { "Order stream error" }
}
Extension Functions¶
Timing Operations¶
import io.github.dotbrains.kotlin.logTime
val result = log.logTime("Database query") {
database.query(sql)
}
// Logs: Database query completed in 45ms
Conditional Execution¶
import io.github.dotbrains.kotlin.ifDebug
log.ifDebug {
// Only executed if DEBUG is enabled
val diagnostics = generateDiagnostics()
log.debug { "Diagnostics: $diagnostics" }
}
Best Practices¶
1. Use Lazy Evaluation¶
// ✅ Good - lazy evaluation
log.debug { "Result: ${expensiveOp()}" }
// ❌ Bad - eager evaluation
log.debug("Result: ${expensiveOp()}")
2. Scope MDC Properly¶
// ✅ Good - automatic cleanup
withMDC("key" to "value") {
log.info { "Message" }
}
// ❌ Bad - manual cleanup required
MDC.put("key", "value")
log.info { "Message" }
MDC.remove("key")
3. Use Structured Logging¶
// ✅ Good - structured data
log.infoWith {
"event" to "user_created"
"userId" to user.id
"email" to user.email
}
// ❌ Bad - unstructured strings
log.info { "User created: ${user.id}, ${user.email}" }
4. Leverage Type Safety¶
// ✅ Good - type-safe field names
data class LogFields(
val userId: String,
val action: String,
val timestamp: Long
)
fun logEvent(fields: LogFields) {
log.infoWith {
"userId" to fields.userId
"action" to fields.action
"timestamp" to fields.timestamp
}
}
Complete Example¶
import io.github.dotbrains.kotlin.*
import kotlinx.coroutines.*
class OrderService {
private val log = OrderService::class.logger
private val AUDIT = marker("AUDIT")
private val PERFORMANCE = marker("PERFORMANCE")
suspend fun processOrder(order: Order) {
withMDC("orderId" to order.id, "customerId" to order.customerId) {
log.info { "Processing order ${order.id}" }
val duration = log.logTime("Order processing") {
try {
validateOrder(order)
log.debug { "Order validated" }
reserveInventory(order)
log.debug { "Inventory reserved" }
processPayment(order)
log.infoWith(AUDIT) {
"event" to "payment_processed"
"orderId" to order.id
"amount" to order.total
"currency" to order.currency
}
shipOrder(order)
log.info { "Order shipped successfully" }
} catch (e: ValidationException) {
log.warn(e) { "Order validation failed" }
throw e
} catch (e: PaymentException) {
log.errorWith(e) {
"event" to "payment_failed"
"orderId" to order.id
"reason" to e.reason
}
throw e
}
}
if (duration > 1000) {
log.warn(PERFORMANCE) {
"Order processing took ${duration}ms (threshold: 1000ms)"
}
}
}
}
private suspend fun validateOrder(order: Order) {
log.ifDebug {
val details = order.toDetailedString()
log.debug { "Validating order: $details" }
}
// validation logic
}
private suspend fun reserveInventory(order: Order) = coroutineScope {
order.items.map { item ->
async {
withMDC("itemId" to item.id) {
log.debug { "Reserving ${item.quantity}x ${item.name}" }
inventory.reserve(item)
}
}
}.awaitAll()
}
private suspend fun processPayment(order: Order) {
log.info { "Processing payment: ${order.total} ${order.currency}" }
payment.process(order)
}
private suspend fun shipOrder(order: Order) {
log.info { "Shipping order to ${order.shippingAddress}" }
shipping.ship(order)
}
}
Migration from Java¶
Before (Java)¶
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public void createUser(String username) {
log.info("Creating user: {}", username);
MDC.put("userId", userId);
try {
// ...
log.debug("User details: " + user.toDetailedString());
} finally {
MDC.remove("userId");
}
}
After (Kotlin)¶
private val log = UserService::class.logger
fun createUser(username: String) {
log.info { "Creating user: $username" }
withMDC("userId" to userId) {
// ...
log.debug { "User details: ${user.toDetailedString()}" }
}
}
Benefits:
- Concise logger creation
- Lazy evaluation by default
- Automatic MDC cleanup
- Idiomatic Kotlin syntax