SLF4ZIO
Integrates SLF4J with ZIO in a simple manner.
When to Use
If your code is based on ZIO and you want to log with SLF4J without additional abstractions getting in your way.
How to Use
The library supports three different coding styles, that you can mix and match according to your needs. They are listed here in the order of my personal preference:
1. Using the LoggingSupport Convenience Trait
import zio._
import zio.random
import zio.random.Random
import zio.clock.Clock
import zio.duration.durationInt
import com.github.mlangc.slf4zio.api._
object SomeObject extends LoggingSupport {
def doStuff: RIO[Random with Clock, Unit] = {
for {
_ <- logger.warnIO("What the heck")
_ <- ZIO.ifM(random.nextBoolean)(
logger.infoIO("Uff, that was close"),
logger.errorIO("Game over", new IllegalStateException("This is the end"))
)
_ <- Task {
// logger is just a plain SLF4J logger; you can therefore use it from
// effectful code directly:
logger.trace("Wink wink nudge nudge")
}
_ <- ZIO.sleep(8.millis).as(23).perfLog(
// See below for more examples with `LogSpec`
LogSpec.onSucceed[Int]((d, i) => debug"Finally done with $i after ${d.render}")
.withThreshold(5.millis)
)
} yield ()
}
}
Side Note
Note that the logger
field in the LoggingSupport
trait is lazy. Since the implicit class that implements the various **IO
methods, like debugIO
, infoIO
and so forth, wraps the logger using a by-name parameter, logger initialization won't happen before unsafeRun
, if you don't access the logger directly from non effectful code. To ensure referential transparency for creating an object of a class that inherits the LoggingSupport
trait even with outright broken or strange logger implementations, you have wrap the creation of the object in an effect of its own. It might make more sense to use another logger implementation though. For practical purposes, I would consider obtaining a logger to be a pure operation as soon as the logging framework has finished its initialization, and not care too much about this subtlety.
2. Creating Loggers as Needed
import com.github.mlangc.slf4zio.api._
import zio.duration.durationInt
import zio.clock.Clock
import zio.RIO
import zio.ZIO
import zio.Task
val effect: RIO[Clock, Unit] = {
// ...
class SomeClass
// ...
for {
logger <- makeLogger[SomeClass]
_ <- logger.debugIO("Debug me tender")
// ...
_ <- Task {
// Note that makeLogger just returns a plain SLF4J logger; you can therefore use it from
// effectful code directly:
logger.info("Don't be shy")
// ...
logger.warn("Please take me home")
// ...
}
// ...
// Generate highly configurable performance logs with ease:
_ <- logger.perfLogZIO(ZIO.sleep(10.millis)) {
LogSpec.onSucceed(d => info"Feeling relaxed after sleeping ${d.render}") ++
LogSpec.onTermination((d, c) => error"Woke up after ${d.render}: ${c.prettyPrint}")
}
} yield ()
}
3. Using the Logging Service
import com.github.mlangc.slf4zio.api._
import zio.RIO
import zio.ZIO
import zio.Task
import zio.clock.Clock
val effect: RIO[Logging with Clock, Unit] =
for {
_ <- logging.warnIO("Surprise, surprise")
plainLogger <- logging.logger
_ <- Task {
plainLogger.debug("Shhh...")
plainLogger.warn("The devil always comes in disguise")
}
_ <- logging.traceIO("...")
getNumber = ZIO.succeed(42)
_ <- getNumber.perfLogZ(LogSpec.onSucceed(d => debug"Got number after ${d.render}"))
} yield ()
Using Markers
import com.github.mlangc.slf4zio.api._
import zio.{RIO, Task}
import zio.clock.Clock
val effect: RIO[Logging with Clock, Unit] =
for {
marker <- getMarker("[MARKER]")
_ <- logging.infoIO(marker, "Here we are")
logger <- logging.logger
_ <- logger.debugIO(marker, "Wat?")
_ <- Task {
logger.warn(marker, "Don't worry")
}
} yield ()
Performance Logging
Apart from providing ZIO aware wrappers for SLF4J, the library might also help you with performance related logging. The examples from above are meant to give you the overall idea. Here is another snippet, that is meant to illustrate how to build complex LogSpec
s from simple ones, utilizing the underlying monoidial structure:
import com.github.mlangc.slf4zio.api._
// Simple specs can be combined using the `++` to obtain more complex specs
val logSpec1: LogSpec[Throwable, Int] =
LogSpec.onSucceed[Int]((d, a) => info"Succeeded after ${d.render} with $a") ++
LogSpec.onError[Throwable]((d, th) => error"Failed after ${d.render} with $th") ++
LogSpec.onTermination((d, c) => error"Fatal failure after ${d.render}: ${c.prettyPrint}")
// A threshold can be applied to a LogSpec. Nothing will be logged, unless the threshold is exceeded.
val logSpec2: LogSpec[Any, Any] =
LogSpec.onSucceed(d => warn"Operation took ${d.render}")
.withThreshold(1.milli)
// Will behave like logSpec1 and eventually log a warning as specified in logSpec2
val logSpec3: LogSpec[Throwable, Int] = logSpec1 ++ logSpec2
Using MDC convenience APIs
SLF4ZIO also ships with a set of convenience APIs for org.slf4j.MDC
. Note however, that traditional MDC implementations are based on thread local data, which doesn't work at all with ZIO, where a single zio.Fiber
might run on different threads during its lifetime, and a single thread might accommodate multiple fibers. If you want to use MDC logging in your ZIO based application, it is critical to use a fiber aware MDC implementation, as provided for example by zio-interop-log4j2. MDZIO
is just a collection of convenience APIs for interacting with org.slf4j.MDC
that doesn't add any functionality of its own.
Alternatives
If you want to track logging effects using the ZIO Environment exclusively, consider using zio-logging. If you are into Tagless Final, take a look at log4cats.