Logging
Overview of logging libraries/frameworks
You will find the following logging libraries being used in some way or another:
- io.klogging:klogging: Advanced logging library for server-side
- io.klogging:slf4j-klogging: SLF4J provider implemented with Klogging logging library
- io.github.oshai:kotlin-logging: Multiplatform logging; interfaces SLF4J on Java
- (SLF4J: The standard Java logging framework)
- org.slf4j:jul-to-slf4j: Bridging java.util.logging to SLF4J
Why
Requirements:
- Have a simple multiplatform-capable library for our core libraries (required as most core libraries are multiplatform)
- handled with
io.github.oshai:kotlin-logging
- handled with
- Support logs that come in via SLF4J (how most Java libraries that we use produce logs)
- handled with
io.klogging:slf4j-klogging
- handled with
- Support logs that come in via JUL (which a few Java libraries use)
- handled with
org.slf4j:jul-to-slf4j(alternative would be to directly go to klogging viaio.klogging:jdk-platform-klogging)
- handled with
- Advanced and high-performance logging framework supporting e.g. structured logs in our services/backends that support
all the above
- handled with
io.klogging:klogging
- handled with
Logging libraries in detail
SLF4J
SLF4J: Used by almost all Java libraries.
From Wikipedia:
Simple Logging Facade for Java (SLF4J) provides a Java logging API by means of a simple facade pattern. The underlying logging backend is determined at runtime by adding the desired binding to the classpath and may be the standard Sun Java logging package java.util.logging,2 Log4j, Reload4j, Logback3 or tinylog.
Basically, SLF4J is just a standardized interface over many different logging backends, and does not
do anything of its own. Note: slf4j-simple is one possible backend, not a or the standard one. It's
mainly used for basic / testing use cases.
The "underlying logging backend" we utilize is Klogging (io.klogging:klogging) vis slf4j-klogging (io.klogging: slf4j-klogging).
kotlin-logging
kotlin-logging: Kotlin multiplatform library that allows to do logging from common code, while targeting the preferred logging interface for the respective platforms (e.g. SLF4J on Java, native android Log, darwin os log, etc.).
This is currently used in the multiplatform libraries, as it allows for simple multiplatform access to platform native logging interfaces (as we cannot depend on SLF4J on iOS or similar, of course).
Bridges to SLF4J (on Java)
JUL & jul-to-slf4j
java.util.logging
Used by very few Java libraries, but still needs to be handled. We use jul-to-slf4j to bridge java.util.logging to SLF4J, so that logging by Java libraries that use JUL is being handled as if they were using SLF4J from the get-go.
Bridges to SLF4J
Klogging & slf4j-klogging
Klogging: Multiplatform Kotlin logging framework used in our backends / services.
Features
- Very powerful: Supports features like structured logs, execution scope information, and supports native
microsecond/nanosecond precision timestamps
- Beneficial in our case for backend / service logging
- High-performance (concurrent): Coroutine based, allows batching logs to different backends
- Beneficial in our case for backend / service logging
- Configuration: Supports runtime (in-code) configuration (unlike slf4j-simple), but can also be configured via JSON /
HOCON configuration
- This was initially one of the main points why we moved towards this system: Allow customers to customize the logging when running on their system
- Very flexible: Allows rendering logs in different formats, supports different APIs where logs can be sent to
- Highly beneficial in our case where customers run services within their own infrastructure, which might very well use a different log system compared to us
Rendering
Can render logs in various forms:
- Simple: Simple text format
- ANSI: Text format for consoles, with ANSI colouring of log levels, similar like Log4J2
- ISO8601: Basic text format with ISO timestamps
- CLEF: Compact Log Event Format (JSON-based format), as can be used for Seq
- ECS: Elastic Common Schema (JSON), to send data directly to ELK stack.
- ECSDotNet: JSON that mimics Elastic Common Schema for .NET
- GELF: JSON format for Graylog
- HEC: JSON event format for Splunk HTTP Event Collector
- etc...
Sending
Allows sending logs to various log collection systems:
- Console:
- Standard output stream
- Standard error stream
- Writes logs to console, as common for most Java frameworks (allows standard collection of logs on Kubernetes: the default Kubernetes logging framework recommends capturing the standard output (stdout) and standard error output ( stderr) from each container on the node)
- Logstash: Send logs to Logstash in the ECS format, as part of ELK stack
- Graylog: Send logs to Graylof in GELF format
- Seq: Sends logs to Seq in CLEF format
- Splunk: Sends logs to Splunk in HEC format
Keep in mind that different logging system might support the same format/API and can thus also be utilized.
Usage
Create structured logs
Use message template in logging code:

Klogging creates a structured log event:

Log information about execution scope
Put some information into coroutine scope:

Klogging includes the information in the log event:

Timestamp resolution
Klogging uses the finest resolution available: at least microsecond, and down to nanosecond if available.
This avoids this situation:

Asynchronous handling of log events
Usage in Code
Defining a logger
Three ways of defining a logger:
logger<T>()logger(Clazz::class)logger("package.Class")
For example:
package com.example
import io.klogging.logger
class BusyService {
private val loggerByReifiedType = logger<BusyService>()
private val loggerByClass = logger(BusyService::class)
private val loggerByName = logger("com.example.BusyService")
// or `val log = noCoLogger("Xyz")`, see below for NoCoLogger
}
Logging text messages
Use lazy-evaluated strings:
// BAD:
logger.debug("Message $x") // avoid
// GOOD:
logger.debug { "Message: $x" } // do this instead
This will cause the string to only be evaluated if the log level is enabled in the first place. In the example above, if the debug log level is not enabled, the message will not be evaluated (which, depending on what you are evaluating, could be a potentially expensive operation).
Logging outside a possible coroutine scope
Klogging will by default try to utilize coroutines for logging. This causes a significant performance improvement,
because logging no longer is a synchronous blocking task. However, in some cases you might not have access
to a usable coroutine context (you are within non-suspendable code) - in these cases you can only log using
a no-coroutine logger, which is called NoCoLogger in Klogging.
A no-coroutine logger can be defined like this: val log = noCoLogger("Xyz").
Its API is exactly identical no the normal logger.
Logging patterns
See Klogging docs
String message pattern
The simplest pattern is to send a string, for example:
logger.info("Processing started")
// meaningful code
val response = callOtherService()
logger.info("User response was ${response.text}")
However, this log is unstructured.
Message template pattern
Message templates provide a convenient way to both create meaningful messages and to create structured log messages:
The resulting log event contains both:
- the message populated with the value of userId; and
- an item called userId with the same value.
For example, if userId has the value wonti321 then:
- the message becomes User wonti321 signed in; and
- the log event contains "userId": "wonti321".
The log event displayed in Splunk may look like this:
logger.info("User {userId} signed in", userId)

Configuration
See here for an HOCON example. See here for a reference of config options.
Example file:
{
sinks = {
stdout = {
renderWith = RENDER_SIMPLE
sendTo = STDOUT
},
seq = {
seqServer = "http://localhost:5341"
}
},
logging = [
{
fromLoggerBase = com.example
levelRanges = [
{
fromMinLevel = INFO
toSinks = [stdout]
}
]
}
]
}
To load this file, you can either set the environment variable KLOGGING_CONFIG_PATH, or alternatively put the file
into the classpath as klogging.conf.
