ADT support
slog4s
provides built-in support for automatic derivation of LogEncoder
typeclass, which allows one to use
case class
or sealed trait
as additional arguments for logging. It supports both fully automatic derivation, and semi
automatic derivation.
Installation
libraryDependencies ++= Seq("com.avast" %% "slog4s-generic" % "0.6.1+7-cb5b3c2d-SNAPSHOT")
Example
Suppose we have following case class:
case class Foo(fooValue: String)
case class Bar(barValue: Int, foo: Foo)
val bar = Bar(42, Foo("Hello!"))
Automatic derivation
With automatic derivation you just need to include proper import.
import slog4s.generic.auto._
logger.info
.withArg("bar", bar)
.log("Logging bar instance")
.unsafeRunSync()
Output:
{
"@timestamp" : "2023-02-24T17:57:18.704+01:00",
"@version" : "1",
"bar" : {
"barValue" : 42,
"foo" : {
"fooValue" : "Hello!"
}
},
"file" : "adt.md",
"level" : "INFO",
"level_value" : 20000,
"line" : 43,
"logger_name" : "test-logger",
"message" : "Logging bar instance",
"thread_name" : "Thread-17"
}
Semi automatic derivation
Sometimes it might be more convenient to have LogEncoder
instance have defined directly in a code. You can use
semi automatic derivation for that:
import slog4s.generic.semi._
object Foo {
implicit val fooEncoder: LogEncoder[Foo] = logEncoder[Foo]
}
object Bar {
implicit val barEncoder: LogEncoder[Bar] = logEncoder[Bar]
}
logger.info
.withArg("bar", bar)
.log("Logging bar instance")
.unsafeRunSync()
Output:
{
"@timestamp" : "2023-02-24T17:57:18.725+01:00",
"@version" : "1",
"bar" : {
"barValue" : 42,
"foo" : {
"fooValue" : "Hello!"
}
},
"file" : "adt.md",
"level" : "INFO",
"level_value" : 20000,
"line" : 65,
"logger_name" : "test-logger",
"message" : "Logging bar instance",
"thread_name" : "Thread-17"
}
Map support
There is built-in support for representing Map
s. Generally we try to represent them as a map in the target encoding
(think JSON dictionary) whenever possible. However it might be impossible for cases where a key is not a primitive type
or a String
. So there is a simple rule: if the key implements cats.Show
typeclass, we represent the whole Map
as a real map/dictionary. Otherwise we represent it as an array of key/value pairs.
import cats.Show
import cats.instances.all._
case class MyKey(value: String)
object MyKey {
implicit val showInstance: Show[MyKey] = _.value
}
logger.info
.withArg("foo", Map("key" -> "value"))
.withArg("bar", Map(42 -> "value"))
.withArg("baz", Map(MyKey("my_key") -> "value"))
.log("Hello world")
.unsafeRunSync()
Output:
{
"@timestamp" : "2023-02-24T17:57:18.9+01:00",
"@version" : "1",
"bar" : {
"42" : "value"
},
"baz" : {
"my_key" : "value"
},
"file" : "adt.md",
"foo" : {
"key" : "value"
},
"level" : "INFO",
"level_value" : 20000,
"line" : 90,
"logger_name" : "test-logger",
"message" : "Hello world",
"thread_name" : "Thread-17"
}
case class OtherKey(x: Int, y: Int)
logger.info
.withArg("foo", Map(OtherKey(1,2) -> "value"))
.log("Hello world")
.unsafeRunSync()
Output:
{
"@timestamp" : "2023-02-24T17:57:18.903+01:00",
"@version" : "1",
"file" : "adt.md",
"foo" : [
[
{
"x" : 1,
"y" : 2
},
"value"
]
],
"level" : "INFO",
"level_value" : 20000,
"line" : 102,
"logger_name" : "test-logger",
"message" : "Hello world",
"thread_name" : "Thread-17"
}