Skip to content

hermannm/devlog-kotlin

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

devlog-kotlin

Logging library for Kotlin JVM, that thinly wraps SLF4J and Logback to provide a more ergonomic API.

Published on Maven Central: https://central.sonatype.com/artifact/dev.hermannm/devlog-kotlin

Contents:

Usage

The Logger class is the entry point to devlog-kotlin's logging API. You can get a Logger by calling getLogger {}, which automatically gives the logger the name of its containing class (or file, if defined at the top level).

// File Example.kt
package com.example

import dev.hermannm.devlog.getLogger

// Gets the name "com.example.Example"
private val log = getLogger {}

Logger provides methods for logging at various log levels (info, warn, error, debug and trace). The methods take a lambda to construct the log, which is only called if the log level is enabled (see Implementation for how this is done efficiently).

fun example() {
  log.info { "Example message" }
}

You can also add fields (structured key-value data) to your logs. The field method uses kotlinx.serialization to serialize the value.

import kotlinx.serialization.Serializable

@Serializable
data class Event(val id: Long, val type: String)

fun example() {
  val event = Event(id = 1001, type = "ORDER_UPDATED")

  log.info {
    field("event", event)
    "Processing event"
  }
}

When outputting logs as JSON, the key/value given to field is added to the logged JSON object (see below). This allows you to filter and query on the field in the log analysis tool of your choice, in a more structured manner than if you were to just use string concatenation.

{
  "message": "Processing event",
  "event": {
    "id": 1001,
    "type": "ORDER_UPDATED"
  },
  // ...timestamp etc.
}

If you want to add fields to all logs within a scope, you can use withLoggingContext:

import dev.hermannm.devlog.field
import dev.hermannm.devlog.withLoggingContext

fun processEvent(event: Event) {
  withLoggingContext(field("event", event)) {
    log.debug { "Started processing event" }
    // ...
    log.debug { "Finished processing event" }
  }
}

...giving the following output:

{ "message": "Started processing event", "event": { /* ... */ } }
{ "message": "Finished processing event", "event": { /* ... */ } }

Note that withLoggingContext uses a thread-local (SLF4J's MDC) to provide log fields to the scope, so it won't work with Kotlin coroutines and suspend functions. If you use coroutines, you can solve this with MDCContext from kotlinx-coroutines-slf4j.

Lastly, you can attach a cause exception to the log like this:

fun example() {
  try {
    callExternalService()
  } catch (e: Exception) {
    log.error(e) { "Request to external service failed" }
  }
}

Adding to your project

Like SLF4J, devlog-kotlin only provides a logging API, and you have to add a logging implementation to actually output logs. Any SLF4J logger implementation will work, but the library is specially optimized for Logback.

To set up devlog-kotlin with Logback and JSON output, add the following dependencies:

  • Gradle:
    dependencies {
      // Logger API
      implementation("dev.hermannm:devlog-kotlin:0.3.0")
      // Logger implementation
      implementation("ch.qos.logback:logback-classic:1.5.15")
      // JSON encoding of logs
      implementation("net.logstash.logback:logstash-logback-encoder:8.0")
    }
  • Maven:
    <dependencies>
      <!-- Logger API -->
      <dependency>
        <groupId>dev.hermannm</groupId>
        <artifactId>devlog-kotlin</artifactId>
        <version>0.3.0</version>
      </dependency>
      <!-- Logger implementation -->
      <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.5.15</version>
      </dependency>
      <!-- JSON encoding of logs -->
      <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
        <version>8.0</version>
      </dependency>
    </dependencies>

Then, configure Logback with a logback.xml file under src/main/resources:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <!-- Writes object values from logging context as actual JSON (not escaped) -->
      <mdcEntryWriter class="dev.hermannm.devlog.LoggingContextJsonFieldWriter"/>
    </encoder>
  </appender>

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

For more configuration options, see:

Implementation

  • All the methods on Logger take a lambda argument to build the log, which is only called if the log level is enabled - so you only pay for log field serialization and message concatenation if it's actually logged.
  • Logger's methods are also inline, so we avoid the cost of allocating a function object for the lambda argument.
  • Elsewhere in the library, we use inline value classes when wrapping Logback APIs, to get as close as possible to a zero-cost abstraction.

Credits

Credits to the kotlin-logging library by Ohad Shai (licensed under Apache 2.0), which inspired the getLogger {} syntax using a lambda to get the logger name. This kotlin-logging issue (by kosiakk) also inspired the implementation using inline methods for minimal overhead.