Skip to content

Commit 48b0638

Browse files
committed
Replace use of "marker" with "log field"
"Marker" is an SLF4J-specific term, that was not originally designed for adding key-value data to logs in the way that we use it. Markers are more like "tags" on a log. But the logstash-logback-encoder library, which we use to attach structured key-value data to logs, use markers to pass this data. Initially, I thought that using the term "marker" made sense, since that is the underlying concept we use from logstash-logback-encoder. But this confuses the logstash-specific key-value marker concept with the more general SLF4j concept of a marker. In addition, no other languages that I know of use the term "marker" to describe key-value data added to logs. So with this in mind, I've decided to instead use the term "log field". After all, the way we use markers internally in the library is just to add key-value pairs to the log output - i.e., fields.
1 parent 213f1eb commit 48b0638

File tree

12 files changed

+596
-555
lines changed

12 files changed

+596
-555
lines changed

README.md

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# devlog-kotlin
22

3-
Logging library for Kotlin JVM, that thinly wraps SLF4J and Logback to provide a more ergonomic API,
4-
and to use `kotlinx.serialization` for log marker serialization instead of Jackson.
3+
Logging library for Kotlin JVM, that thinly wraps SLF4J and Logback to provide a more ergonomic API.
54

65
Published on Maven Central: https://central.sonatype.com/artifact/dev.hermannm/devlog-kotlin
76

@@ -38,7 +37,7 @@ fun example() {
3837
}
3938
```
4039

41-
You can also add _log markers_ (structured key-value data) to your logs. The `addMarker` method uses
40+
You can also add _log fields_ (structured key-value data) to your logs. The `addField` method uses
4241
`kotlinx.serialization` to serialize the value.
4342

4443
```kotlin
@@ -50,27 +49,36 @@ fun example() {
5049
val user = User(id = 1, name = "John Doe")
5150

5251
log.info {
53-
addMarker("user", user)
52+
addField("user", user)
5453
"Registered new user"
5554
}
5655
}
5756
```
5857

59-
This will give the following log output (if outputting logs as JSON with
60-
`logstash-logback-encoder`):
58+
When outputting logs as JSON (using [`logstash-logback-encoder`](#setting-up-with-logback)), the
59+
key/value given to `addField` is added to the logged JSON object (see below). This allows you to
60+
filter and query on the field in the log analysis tool of your choice, in a more structured manner
61+
than if you were to just use string concatenation.
6162

6263
```jsonc
63-
{ "message": "Registered new user", "user": { "id": 1, "name": "John Doe" } }
64+
{
65+
"message": "Registered new user",
66+
"user": {
67+
"id": 1,
68+
"name": "John Doe"
69+
}
70+
// ...timestamp etc.
71+
}
6472
```
6573

66-
If you want to add markers to all logs within a scope, you can use `withLoggingContext`:
74+
If you want to add fields to all logs within a scope, you can use `withLoggingContext`:
6775

6876
```kotlin
69-
import dev.hermannm.devlog.marker
77+
import dev.hermannm.devlog.field
7078
import dev.hermannm.devlog.withLoggingContext
7179

7280
fun processEvent(event: Event) {
73-
withLoggingContext(marker("event", event)) {
81+
withLoggingContext(field("event", event)) {
7482
log.debug { "Started processing event" }
7583
// ...
7684
log.debug { "Finished processing event" }
@@ -85,21 +93,20 @@ fun processEvent(event: Event) {
8593
{ "message": "Finished processing event", "event": { /* ... */ } }
8694
```
8795

88-
Note that `withLoggingContext` uses a thread-local to provide markers to the scope, so it won't work
89-
with Kotlin coroutines and `suspend` functions (though it does work with Java virtual threads). An
90-
alternative that supports coroutines may be added in a future version of the library.
96+
Note that `withLoggingContext` uses a thread-local to provide log fields to the scope, so it won't
97+
work with Kotlin coroutines and `suspend` functions (though it does work with Java virtual threads).
98+
An alternative that supports coroutines may be added in a future version of the library.
9199

92100
Finally, you can attach a `cause` exception to logs:
93101

94102
```kotlin
95-
fun example(user: User) {
103+
fun example( {
96104
try {
97-
storeUser(user)
105+
callExternalService()
98106
} catch (e: Exception) {
99107
log.error {
100108
cause = e
101-
addMarker("user", user)
102-
"Failed to store user in database"
109+
"Request to external service failed"
103110
}
104111
}
105112
}
@@ -130,9 +137,10 @@ See the [Usage docs](https://github.com/logfellow/logstash-logback-encoder#usage
130137
## Implementation
131138

132139
All the methods on `Logger` are `inline`, and don't do anything if the log level is disabled - so
133-
you only pay for marker serialization and log message concatenation if it's actually logged.
140+
you only pay for log field serialization and message concatenation if it's actually logged. Inlining
141+
the logger methods avoids having to allocate a function object for the lambda argument.
134142

135-
Elsewhere in the library, we use inline value classes to wrap Logback APIs, to get as close as
143+
Elsewhere in the library, we use inline value classes when wrapping Logback APIs, to get as close as
136144
possible to a zero-cost abstraction.
137145

138146
## Credits

src/main/kotlin/dev/hermannm/devlog/ExceptionWithLogMarkers.kt renamed to src/main/kotlin/dev/hermannm/devlog/ExceptionWithLogFields.kt

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
package dev.hermannm.devlog
22

33
/**
4-
* Interface to allow you to attach [log markers][LogMarker] to exceptions. When passing a `cause`
4+
* Interface to allow you to attach [log fields][LogField] to exceptions. When passing a `cause`
55
* exception to one of the methods on [Logger], it will check if the given exception implements this
6-
* interface, and if it does, these log markers will be attached to the log.
6+
* interface, and if it does, these fields will be added to the log.
77
*
88
* This is useful when you are throwing an exception from somewhere down in the stack, but do
9-
* logging further up the stack, and you have structured data that you want to attach to the logged
10-
* exception. In this case, one may typically resort to string concatenation, but this interface
9+
* logging further up the stack, and you have structured data that you want to attach to the
10+
* exception log. In this case, one may typically resort to string concatenation, but this interface
1111
* allows you to have the benefits of structured logging for exceptions as well.
1212
*
1313
* ### Example
1414
*
1515
* ```
1616
* import dev.hermannm.devlog.Logger
17-
* import dev.hermannm.devlog.WithLogMarkers
18-
* import dev.hermannm.devlog.marker
17+
* import dev.hermannm.devlog.WithLogFields
18+
* import dev.hermannm.devlog.field
1919
*
20-
* class InvalidUserData(user: User) : RuntimeException(), WithLogMarkers {
20+
* class InvalidUserData(user: User) : RuntimeException(), WithLogFields {
2121
* override val message = "Invalid user data"
22-
* override val logMarkers = listOf(marker("user", user))
22+
* override val logFields = listOf(field("user", user))
2323
* }
2424
*
2525
* fun storeUser(user: User) {
@@ -43,7 +43,7 @@ package dev.hermannm.devlog
4343
* ```
4444
*
4545
* The `log.error` would then give the following log output (using `logstash-logback-encoder`), with
46-
* the `user` log marker from `InvalidUserData` attached:
46+
* the `user` log field from `InvalidUserData` attached:
4747
* ```
4848
* {
4949
* "message": "Failed to store user",
@@ -56,18 +56,18 @@ package dev.hermannm.devlog
5656
* }
5757
* ```
5858
*/
59-
interface WithLogMarkers {
60-
/** Will be attached to a log if this is passed through a `cause` parameter to [Logger]. */
61-
val logMarkers: List<LogMarker>
59+
interface WithLogFields {
60+
/** Will be attached to the log when passed through `cause` to one of [Logger]'s methods. */
61+
val logFields: List<LogField>
6262
}
6363

6464
/**
65-
* Base exception class implementing the [WithLogMarkers] interface for attaching structured data to
65+
* Base exception class implementing the [WithLogFields] interface for attaching structured data to
6666
* the exception when it's logged. If you don't want to create a custom exception and implement
67-
* [WithLogMarkers] on it, you can use this class instead.
67+
* [WithLogFields] on it, you can use this class instead.
6868
*/
69-
open class ExceptionWithLogMarkers(
69+
open class ExceptionWithLogFields(
7070
override val message: String?,
71-
override val logMarkers: List<LogMarker>,
71+
override val logFields: List<LogField>,
7272
override val cause: Throwable? = null,
73-
) : RuntimeException(), WithLogMarkers
73+
) : RuntimeException(), WithLogFields

src/main/kotlin/dev/hermannm/devlog/LogBuilder.kt

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,30 @@ import ch.qos.logback.classic.spi.ThrowableProxy
55
import kotlinx.serialization.SerializationStrategy
66
import net.logstash.logback.marker.SingleFieldAppendingMarker
77

8-
/** Class used in the logging methods on [Logger] to add markers/cause exception to logs. */
8+
/**
9+
* Class used in the logging methods on [Logger], allowing you to set a [cause] exception and
10+
* [add structured key-value data][addField] to a log.
11+
*
12+
* ### Example
13+
*
14+
* ```
15+
* private val log = Logger {}
16+
*
17+
* fun example(user: User) {
18+
* try {
19+
* storeUser(user)
20+
* } catch (e: Exception) {
21+
* // The lambda argument passed to this logger method has a LogBuilder as its receiver, which
22+
* // means that you can set `LogBuilder.cause` and call `LogBuilder.addField` in this scope.
23+
* log.error {
24+
* cause = e
25+
* addField("user", user)
26+
* "Failed to store user in database"
27+
* }
28+
* }
29+
* }
30+
* ```
31+
*/
932
@JvmInline // Inline value class, since we just wrap a Logback logging event
1033
value class LogBuilder
1134
internal constructor(
@@ -20,14 +43,14 @@ internal constructor(
2043
get() = (logEvent.throwableProxy as? ThrowableProxy)?.throwable
2144

2245
/**
23-
* Adds a [log marker][LogMarker] with the given key and value to the log.
46+
* Adds a [log field][LogField] (structured key-value data) to the log.
2447
*
2548
* The value is serialized using `kotlinx.serialization`, so if you pass an object here, you
2649
* should make sure it is annotated with [@Serializable][kotlinx.serialization.Serializable].
2750
* Alternatively, you can pass your own serializer for the value. If serialization fails, we fall
2851
* back to calling `toString()` on the value.
2952
*
30-
* If you have a value that is already serialized, you should use [addRawJsonMarker] instead.
53+
* If you have a value that is already serialized, you should use [addRawJsonField] instead.
3154
*
3255
* ### Example
3356
*
@@ -41,15 +64,15 @@ internal constructor(
4164
* val user = User(id = 1, name = "John Doe")
4265
*
4366
* log.info {
44-
* addMarker("user", user)
67+
* addField("user", user)
4568
* "Registered new user"
4669
* }
4770
* }
4871
*
4972
* @Serializable data class User(val id: Long, val name: String)
5073
* ```
5174
*
52-
* This gives the following output using `logstash-logback-encoder`:
75+
* This gives the following output (using `logstash-logback-encoder`):
5376
* ```json
5477
* {
5578
* "message": "Registered new user",
@@ -61,18 +84,19 @@ internal constructor(
6184
* }
6285
* ```
6386
*/
64-
inline fun <reified ValueT> addMarker(
87+
inline fun <reified ValueT> addField(
6588
key: String,
6689
value: ValueT,
6790
serializer: SerializationStrategy<ValueT>? = null,
6891
) {
69-
if (!markerKeyAdded(key)) {
70-
logEvent.addMarker(createLogstashMarker(key, value, serializer))
92+
if (!keyAdded(key)) {
93+
logEvent.addMarker(createLogstashField(key, value, serializer))
7194
}
7295
}
7396

7497
/**
75-
* Adds a [log marker][LogMarker] with the given key and pre-serialized JSON value to the log.
98+
* Adds a [log field][LogField] (structured key-value data) to the log, with the given
99+
* pre-serialized JSON value.
76100
*
77101
* By default, this function checks that the given JSON string is actually valid JSON. The reason
78102
* for this is that giving raw JSON to our log encoder when it is not in fact valid JSON can break
@@ -90,59 +114,58 @@ internal constructor(
90114
* val userJson = """{"id":1,"name":"John Doe"}"""
91115
*
92116
* log.info {
93-
* addRawJsonMarker("user", userJson)
117+
* addRawJsonField("user", userJson)
94118
* "Registered new user"
95119
* }
96120
* }
97121
* ```
98122
*
99-
* This gives the following output using `logstash-logback-encoder`:
123+
* This gives the following output (using `logstash-logback-encoder`):
100124
* ```json
101125
* {"message":"Registered new user","user":{"id":1,"name":"John Doe"},/* ...timestamp etc. */}
102126
* ```
103127
*/
104-
fun addRawJsonMarker(key: String, json: String, validJson: Boolean = false) {
105-
if (!markerKeyAdded(key)) {
106-
logEvent.addMarker(createRawJsonLogstashMarker(key, json, validJson))
128+
fun addRawJsonField(key: String, json: String, validJson: Boolean = false) {
129+
if (!keyAdded(key)) {
130+
logEvent.addMarker(createRawJsonLogstashField(key, json, validJson))
107131
}
108132
}
109133

110134
/**
111-
* Adds the given [log marker][LogMarker] to the log. This is useful when you have a previously
112-
* constructed log marker, from the [marker]/[rawJsonMarker] functions.
113-
* - If you want to create a new marker and add it to the log, you should instead call [addMarker]
114-
* - If you want to add the marker to all logs within a scope, you should instead use
135+
* Adds the given [log field][LogField] to the log. This is useful when you have a previously
136+
* constructed field from the [field][dev.hermannm.devlog.field]/[rawJsonField] functions.
137+
* - If you want to create a new field and add it to the log, you should instead call [addField]
138+
* - If you want to add the field to all logs within a scope, you should instead use
115139
* [withLoggingContext]
116140
*/
117-
fun addExistingMarker(marker: LogMarker) {
118-
if (!markerKeyAdded(marker.logstashMarker.fieldName)) {
119-
logEvent.addMarker(marker.logstashMarker)
141+
fun addPreconstructedField(field: LogField) {
142+
if (!keyAdded(field.key)) {
143+
logEvent.addMarker(field.logstashField)
120144
}
121145
}
122146

123-
/** Adds log markers from [withLoggingContext]. */
124-
internal fun addMarkersFromContext() {
147+
/** Adds log fields from [withLoggingContext]. */
148+
internal fun addFieldsFromContext() {
125149
// loggingContext will be null if withLoggingContext has not been called in this thread
126-
val contextMarkers = loggingContext.get() ?: return
150+
val contextFields = loggingContext.get() ?: return
127151

128-
// Add context markers in reverse, so newest marker shows first
129-
contextMarkers.forEachReversed { logstashMarker ->
130-
// Don't add marker keys that have already been added
131-
if (!markerKeyAdded(logstashMarker.fieldName)) {
132-
logEvent.addMarker(logstashMarker)
152+
// Add context fields in reverse, so newest field shows first
153+
contextFields.forEachReversed { logstashField ->
154+
// Don't add fields with keys that have already been added
155+
if (!keyAdded(logstashField.fieldName)) {
156+
logEvent.addMarker(logstashField)
133157
}
134158
}
135159
}
136160

137161
/**
138162
* Checks if the log [cause] exception (or any of its own cause exceptions) implements the
139-
* [WithLogMarkers] interface, and if so, adds those markers.
163+
* [WithLogFields] interface, and if so, adds those fields.
140164
*/
141-
internal fun addMarkersFromCauseException() {
165+
internal fun addFieldsFromCauseException() {
142166
// The `cause` here is the log event cause exception. But this exception may itself have a
143167
// `cause` exception, and that may have another one, and so on. We want to go through all these
144-
// exceptions to look for log markers, so we re-assign this local variable as we iterate
145-
// through.
168+
// exceptions to look for log field, so we re-assign this local variable as we iterate through.
146169
var exception = cause
147170
// Limit the depth of cause exceptions, so we don't expose ourselves to infinite loops.
148171
// This can happen if:
@@ -152,11 +175,11 @@ internal constructor(
152175
// We set max depth to 10, which should be high enough to not affect real users.
153176
var depth = 0
154177
while (exception != null && depth < 10) {
155-
if (exception is WithLogMarkers) {
156-
exception.logMarkers.forEach { marker ->
157-
// Don't add marker keys that have already been added
158-
if (!markerKeyAdded(marker.logstashMarker.fieldName)) {
159-
logEvent.addMarker(marker.logstashMarker)
178+
if (exception is WithLogFields) {
179+
exception.logFields.forEach { field ->
180+
// Don't add fields with keys that have already been added
181+
if (!keyAdded(field.key)) {
182+
logEvent.addMarker(field.logstashField)
160183
}
161184
}
162185
}
@@ -167,12 +190,12 @@ internal constructor(
167190
}
168191

169192
@PublishedApi
170-
internal fun markerKeyAdded(key: String): Boolean {
171-
/** [LogbackEvent.markerList] can be null if no markers have been added yet. */
172-
val markers = logEvent.markerList ?: return false
193+
internal fun keyAdded(key: String): Boolean {
194+
/** [LogbackEvent.markerList] can be null if no fields have been added yet. */
195+
val addedFields = logEvent.markerList ?: return false
173196

174-
return markers.any { existingMarker ->
175-
existingMarker is SingleFieldAppendingMarker && existingMarker.fieldName == key
197+
return addedFields.any { logstashField ->
198+
logstashField is SingleFieldAppendingMarker && logstashField.fieldName == key
176199
}
177200
}
178201
}

0 commit comments

Comments
 (0)