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
  • Support logs that come in via SLF4J (how most Java libraries that we use produce logs)
    • handled with io.klogging:slf4j-klogging
  • 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 via io.klogging:jdk-platform-klogging)
  • 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

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

JUL bridge

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:

Message template in logging code

Klogging creates a structured log event:

Klogging creates a structured log event

Log information about execution scope

Put some information into coroutine scope:

Put some information into coroutine scope

Klogging includes the information in the log event:

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:

avoids this situation
Where you no longer know the order of log events because they all came in the same millisecond into the aggregator.

Asynchronous handling of log events

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)

structured log

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.