@@ -16,6 +16,7 @@ runtime overhead. Currently only supports the JVM platform, wrapping SLF4J and L
1616 - [ Performance] ( #performance )
1717 - [ Automatic logger names] ( #automatic-logger-names )
1818- [ Project Structure] ( #project-structure )
19+ - [ Why another logging library?] ( #why-another-logging-library )
1920- [ Credits] ( #credits )
2021
2122## Usage
@@ -133,7 +134,8 @@ library is specially optimized for Logback.
133134
134135To set up ` devlog-kotlin ` with
135136[ Logback] ( https://mvnrepository.com/artifact/ch.qos.logback/logback-classic ) and
136- [ Logstash Logback Encoder] ( https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder )
137+ [
138+ ` logstash-logback-encoder ` ] ( https://mvnrepository.com/artifact/net.logstash.logback/logstash-logback-encoder )
137139for JSON output, add the following dependencies:
138140
139141- ** Gradle:**
@@ -194,7 +196,8 @@ Then, configure Logback with a `logback.xml` file under `src/main/resources`:
194196For more configuration options, see:
195197
196198- [ The Configuration chapter of the Logback manual] ( https://logback.qos.ch/manual/configuration.html )
197- - [ The Usage docs for Logstash Logback Encoder] ( https://github.com/logfellow/logstash-logback-encoder#usage )
199+ - [ The Usage docs for
200+ ` logstash-logback-encoder ` ] ( https://github.com/logfellow/logstash-logback-encoder#usage )
198201
199202## Implementation
200203
@@ -229,38 +232,111 @@ future.
229232
230233Directory structure:
231234
232- - ` src/commonMain ` contains common, platform-neutral implementations
235+ - ` src/commonMain ` contains common, platform-neutral implementations.
233236 - This module implements the surface API of ` devlog-kotlin ` , namely ` Logger ` , ` LogBuilder ` and
234- ` LogField `
237+ ` LogField ` .
235238 - It declares ` expect ` classes and functions for the underlying APIs that must be implemented by
236- each platform, namely ` PlatformLogger ` , ` LogEvent ` and ` LoggingContext `
237- - ` src/jvmMain ` implements platform-specific APIs for the JVM
238- - It uses SLF4J, the de-facto standard JVM logging library, with extra optimizations for Logback
239+ each platform, namely ` PlatformLogger ` , ` LogEvent ` and ` LoggingContext ` .
240+ - ` src/jvmMain ` implements platform-specific APIs for the JVM.
241+ - It uses SLF4J, the de-facto standard JVM logging library, with extra optimizations for Logback.
239242 - It implements:
240- - ` PlatformLogger ` as a typealias for ` org.slf4j.Logger `
241- - ` LoggingContext ` using SLF4J's ` MDC ` (Mapped Diagnostic Context)
243+ - ` PlatformLogger ` as a typealias for ` org.slf4j.Logger ` .
244+ - ` LoggingContext ` using SLF4J's ` MDC ` (Mapped Diagnostic Context).
242245 - ` LogEvent ` with an SLF4J ` DefaultLoggingEvent ` , or a special-case optimization using
243- Logback's ` LoggingEvent ` if Logback is on the classpath
244- - ` src/commonTest ` contains the library's tests that apply to all platforms
246+ Logback's ` LoggingEvent ` if Logback is on the classpath.
247+ - ` src/commonTest ` contains the library's tests that apply to all platforms.
245248 - In order to keep as many tests as possible in the common module, we write most of our tests
246249 here, and delegate to platform-specific ` expect ` utilities where needed. This allows us to
247250 define a common test suite for all platforms, just switching out the parts where we need
248- platform-specific implementations
251+ platform-specific implementations.
249252- ` src/jvmTest ` contains JVM-specific tests, and implements the test utilities expected by
250- ` commonTest `
251- for the JVM
253+ ` commonTest ` for the JVM.
252254- ` integration-tests ` contains Gradle subprojects that load various SLF4J logger backends (Logback,
253255 Log4j and ` java.util.logging ` , a.k.a. ` jul ` ), and verify that they all work as expected with
254- ` devlog-kotlin `
256+ ` devlog-kotlin ` .
255257 - Since we do some special-case optimizations if Logback is loaded, this lets us test that these
256- Logback-specific optimizations do not interfere with other logger backends
258+ Logback-specific optimizations do not interfere with other logger backends.
259+
260+ ## Why another logging library?
261+
262+ The inspiration for this library mostly came from some inconveniencies and limitations I've
263+ experienced with the [ ` kotlin-logging ` ] ( https://github.com/oshai/kotlin-logging ) library (it's a
264+ great library, these are just subjective grievances!). Here are some of the things I wanted to
265+ improve with this library:
266+
267+ - ** Structured logging**
268+ - In ` kotlin-logging ` , going from a log _ without_ structured log fields to a log _ with_ them
269+ requires you to switch your logger method (` info ` -> ` atInfo ` ), use a different syntax
270+ (` message = ` instead of returning a string), and construct a map for the fields.
271+ - Having to switch syntax becomes a barrier for developers to do structured logging. In my
272+ experience, the key to making structured logging work in practice is to reduce such barriers.
273+ - So in ` devlog-kotlin ` , I wanted to make this easier: you use the same logger methods whether you
274+ are adding fields or not, and adding structured data to an existing log is as simple as just
275+ calling ` field ` in the scope of the log lambda.
276+ - ** Using ` kotlinx.serialization ` for log field serialization**
277+ - ` kotlin-logging ` also wraps SLF4J in the JVM implementation. It passes structured log fields as
278+ ` Map<String, Any?> ` , and leaves it to the logger backend to serialize them. Since most SLF4J
279+ logger implementations are Java-based, they typically use Jackson to serialize these fields (if
280+ they support structured logging at all).
281+ - But in Kotlin, we often use ` kotlinx.serialization ` instead of Jackson. There can be subtle
282+ differences between how Jackson and ` kotlinx ` serialize objects, so we would prefer to use
283+ ` kotlinx ` for our log fields, so that they serialize in the same way as in the rest of our
284+ application.
285+ - In ` devlog-kotlin ` , we solve this by serializing log fields _ before_ sending them to the logger
286+ backend, which allows us to control the serialization process with ` kotlinx.serialization ` .
287+ - Controlling the serialization process also lets us handle failures better. One of the issues
288+ I've experienced with Jackson serialization of log fields, is that ` logstash-logback-encoder `
289+ would drop an entire log line in some cases when one of the custom fields on that log failed
290+ to serialize. ` devlog-kotlin ` never drops logs on serialization failures, instead defaulting to
291+ ` toString() ` .
292+ - ** Inline logger methods**
293+ - One of the classic challenges for a logging library is how to handle calls to a logger method
294+ when the log level is disabled. We want this to have as little overhead as possible, so that
295+ we don't pay a runtime cost for a log that won't actually produce any output.
296+ - In Kotlin, we have the opportunity to create such zero-cost abstractions, using ` inline `
297+ functions with lambda parameters. This lets us implement logger methods that compile down to a
298+ simple ` if ` statement to check if the log level is enabled, and that do no work if the level is
299+ disabled. Great!
300+ - However, ` kotlin-logging ` does not use inline logger methods. This is partly because of how the
301+ library is structured: ` KLogger ` is an interface, with different implementations for various
302+ platforms - and interfaces can't have inline methods. So the methods that take lambdas won't be
303+ inlined, which means that they may allocate function objects, which are not zero-cost.
304+ [ This ` kotlin-logging ` issue] ( https://github.com/oshai/kotlin-logging/issues/34 ) discusses some
305+ of the performance implications.
306+ - ` devlog-kotlin ` solves this by dividing up the problem: we make our ` Logger ` a concrete class,
307+ with a single implementation in the ` common ` module. It wraps an internal ` PlatformLogger `
308+ interface (delegating to SLF4J in the JVM implementation). ` Logger ` provides the public API, and
309+ since it's a single concrete class, we can make its methods ` inline ` . We also make it a
310+ ` value class ` , so that it compiles down to just the underlying ` PlatformLogger ` at runtime. This
311+ makes the abstraction as close to zero-cost as possible.
312+ - One notable drawback of inline methods is that they don't work well with line numbers (i.e.,
313+ getting file location information inside an inlined lambda will show an incorrect line number).
314+ We deem this a worthy tradeoff for performance, because the class/file name + the log message is
315+ typically enough to find the source of a log. Also, ` logstash-logback-encoder `
316+ [ explicitly discourages enabling file locations] ( https://github.com/logfellow/logstash-logback-encoder/tree/logstash-logback-encoder-8.1#caller-info-fields ) ,
317+ due to the runtime cost. Still, this is something to be aware of if you want line numbers
318+ included in your logs. This limitation is documented on all the methods on ` Logger ` .
319+ - ** Supporting arbitrary types for logging context values**
320+ - SLF4J's ` MDC ` has a limitation: values must be ` String ` . And the ` withLoggingContext ` function
321+ from ` kotlin-logging ` , which uses ` MDC ` , inherits this limitation.
322+ - But when doing structured logging, it can be useful to attach more than just strings in the
323+ logging context - for example, attaching the JSON of an event in the scope that it's being
324+ processed. If you pass serialized JSON to ` MDC ` , the resulting log output will include the JSON
325+ as an escaped string. This defeats the purpose, as an escaped string will not be parsed
326+ automatically by log analysis platforms - what we want is to include actual, unescaped JSON in
327+ the logging context, so that we can filter and query on its fields.
328+ - ` devlog-kotlin ` solves this limitation by instead taking a ` LogField ` type, which can have an
329+ arbitrary serializable value, as the parameter to our ` withLoggingContext ` function. We then
330+ provide ` LoggingContextJsonFieldWriter ` for interoperability with ` MDC ` when using Logback +
331+ ` logstash-logback-encoder ` .
257332
258333## Credits
259334
260- Credits to the [ kotlin-logging library by Ohad Shai] ( https://github.com/oshai/kotlin-logging )
335+ Credits to the [ ` kotlin-logging ` library by Ohad Shai] ( https://github.com/oshai/kotlin-logging )
261336(licensed under
262337[ Apache 2.0] ( https://github.com/oshai/kotlin-logging/blob/c91fe6ab71b9d3470fae71fb28c453006de4e584/LICENSE ) ),
263- which inspired the ` getLogger() ` syntax using a lambda to get the logger name.
264- [ This kotlin-logging issue] ( https://github.com/oshai/kotlin-logging/issues/34 ) (by
265- [ kosiakk] ( https://github.com/kosiakk ) ) also inspired the implementation using ` inline ` methods for
266- minimal overhead.
338+ which was a great inspiration for this library.
339+
340+ Also credits to [ kosiakk] ( https://github.com/kosiakk ) for
341+ [ this ` kotlin-logging ` issue] ( https://github.com/oshai/kotlin-logging/issues/34 ) , which inspired the
342+ implementation using ` inline ` methods for minimal overhead.
0 commit comments