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.
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.
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.
|
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:
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")
}
}
// Ktor config
parentCoroutineContext += continueCorrelation() + log.watched(shutdown = true)
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.
// ...
// Threaded code (Correlation #1)
// ...
val job = launch(correlated()) {
// (Correlation #2)
while (isActive) {
withCorrelation {
// (Correlation #3)
}
}
// (Correlation #2)
}
// ...
// Threaded code (Correlation #1)
// ...
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)
// ...
// ...
// 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)
}
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)
// ...
}