Skip to content

Commit c0c0e48

Browse files
committed
Add 'Why another logging library?' section to README
1 parent 24ad8d0 commit c0c0e48

File tree

1 file changed

+97
-21
lines changed

1 file changed

+97
-21
lines changed

README.md

Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

134135
To 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)
137139
for 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`:
194196
For 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

230233
Directory 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

Comments
 (0)