Skip to content

Koriit/slf4j-utils

Repository files navigation

SLF4J Utils

Build CodeFactor ktlint

Maven Central GitHub

Warning
From version 0.8.0 all package names have been renamed to match new artifact group id.

SLF4J utils for Kotlin.

Logger factory

With Kotlin lambdas, creating a logger object can be as simple as logger {}.

Please, note that this function is opinionated. Logger name is a fully qualified name of class/file where logger object is defined. This allows precise control over your loggers.

Bridges

This library also includes bridges that intercept JUL, JCL, and Log4J log statements and pass them to SLF4J.

This is handy as there are many libraries and frameworks that do not use SLF4J, and we usually want to see/control all of our logs.

Fatal Logs

SLF4J has limited levels of logging and does not allow creating any custom levels. Developers coming from many backgrounds tend to notice that certain errors - a.k.a. FATAL - are more important than the other and require a faster reaction.

According to SLF4J’s FAQ:

The Marker interface, part of the org.slf4j package, renders the FATAL level largely redundant. If a given error requires attention beyond that allocated for ordinary errors, simply mark the logging statement with a specially designated marker which can be named "FATAL" or any other name to your liking.

However, preparing such a marker and adding it to every fatal error can be a bit tedious. Thus this library provides a set of fatal extension functions that do this for you.

Warning
Fatal logs are still logged as ERROR. Whether this difference is visible in the output depends on your format, whether it includes markers in some way.

Exception Handling

This library provides watch and watched extension functions on the Logger to create a simple exception handler that just logs caught exception and eats it, or shuts down the JVM.

It is useful in concurrent, long-lived workers, for example:

Logger.watch
private val log = logger {}

private val jobQueue = Channel<Job>(UNLIMITED)

private val worker = launch(correlated()) {
    with(log) {
        info("Started worker")
        watch(fatal = true) {
            for (job in jobQueue) {
                continueCorrelation((job as Continuation<*>).context) {
                    watch(ignore = listOf(MyCancellation::class)) {
                        job.await()
                    }
                }
            }
        }
        jobQueue.close()
        info("Closed worker")
    }
}
Logger.watched
// Ktor config
parentCoroutineContext += continueCorrelation() + log.watched(shutdown = true)

Correlation

It may prove challenging to search for all logs related to a particular process, especially in applications that use any kind of concurrency as it leads to the interleaving of logs.

To solve this issue it is possible to add special correlation id to logs that belong to the same process.

The easiest way to achieve that is by using thread-local MDC provided by SLF4j.

However, it is a bit harder with Kotlin’s coroutines as usually they are not bound to any specific thread during its execution. The proper solution requires building correlated coroutine contexts.

The following functions ease the building of said coroutine contexts and correlating threads in general.

Correlated

// ...
// Threaded code (Correlation #1)
// ...

val job = launch(correlated()) {
    // (Correlation #2)
    while (isActive) {
        withCorrelation {
            // (Correlation #3)
        }
    }
    // (Correlation #2)
}

// ...
// Threaded code (Correlation #1)
// ...

SubCorrelated

Note
Sub-correlation can be nested only by one level. This is by design.
// ...
// Threaded code (Correlation #1, no SubCorrelation)
// ...

val job = launch(continueCorrelation() + subCorrelated()) {
    // (Correlation #1, SubCorrelation #1)
    while (isActive) {
        withSubCorrelation {
            // (Correlation #1, SubCorrelation #2)
        }
    }
    // (Correlation #1, SubCorrelation #2)
}

// ...
// Threaded code (Correlation #1, no SubCorrelation)
// ...

ContinueCorrelation

// ...
// Threaded code (Correlation #1, no SubCorrelation)
// ...

val job = async(continueCorrelation() + subCorrelated(), start=LAZY) {
    // (Correlation #1, SubCorrelation #1)
}

// ...
// Other threaded code (Correlation #2, no SubCorrelation)
// ...
launch(continueCorrelation() + subCorrelated()) {
    // (Correlation #2, SubCorrelation #2)
    continueCorrelation((job as Continuation<*>).context) {
        // (Correlation #1, SubCorrelation #1)
        val value = job.await()
        // (Correlation #1, SubCorrelation #1)
    }
    // (Correlation #2, SubCorrelation #2)
}

ThreadCorrelation

fun main() {
    // (no Correlation)
    val correlationId = correlateThread()
    // (Correlation #1)

    Thread {
        // (no Correlation)
        correlateThread(correlationId)
        // (Correlation #1)
    }.run()

    // (Correlation #1)

    Thread {
        // (no Correlation)
        correlateThread(correlationId = correlationId, subCorrelationId = newCorrelationId())
        // (Correlation #1, SubCorrelation #1)
    }.run()

    // (Correlation #1)

    Thread {
        // (no Correlation)
        correlateThread()
        // (Correlation #2)
    }.run()

    // (Correlation #1)
    // ...
}