ZIO Logging
ZIO logging
Section titled “ZIO logging”To use logging functional adapted for ZIO users, first add the following dependency:
Then import the package:
import tofu.logging.zlogs._This contains some useful stuff:
-
tofu.logging.Loggingtype aliases
These services do logs.ULogging— is a type alias forLogging[UIO]. Logging methods have no environment likedef info(message: String, values: LoggedValue*): UIO[Unit]ZLogging[R]— is a type alias forLogging[URIO[R, *]]. Logging methods require a ZIO environmentR:def info(message: String, values: LoggedValue*): URIO[R, Unit]
-
ZLogging.Maketype
Use this instead oftofu.logging.Loging.Make.Makeis a factory creatingLogginginstances with no side effects.ZLogging.Make— is a type alias forLogs[Id, UIO], produces plain instances ofULogging.ZLogging.ZMake[R]— is a type alias forLogs[Id, URIO[R, *]], produces contextualZLogging[R].
Read more about logging factory in core concepts.
-
ZLogging.Makeobject
Provides several methods for creating ZIO layers withMakeinstances.layerPlaincreates layer contains simple implementation ofZLogging.MakelayerContextual[R: Loggable]makes a fabricZMake[R]of contextualZLogging[R]retrieving a context from the ZIO environment of the logging methodslayerPlainWithContext[C: Loggable, ContextService](f: ContextService => UIO[C])creates layers with an implementation ofZLogging.Makeencapsulated context inside. Every logging methods will call functionfon theContextServiceto get a context which will be added to the logs. TheContextServiceis supposed to be provided at the app creation point via ZLayer environment.
Example
Section titled “Example”Let’s write a simple service, which logs a current date.
import tofu.logging.zlogs._import tofu.syntax.logging._import zio._import zio.clock._
trait BarService { def foo: Task[Unit]}
class BarServiceImpl(clock: Clock.Service, logs: ZLogging.Make) extends BarService {
private implicit val log: ULogging = logs.forService[BarServiceImpl]
override def foo: Task[Unit] = for { _ <- log.info("Start program") dt <- clock.localDateTime date = dt.toLocalDate _ <- debug"Got current date $date" } yield ()}
object BarService { val live: URLayer[Clock with TofuLogs, Has[BarService]] = (new BarServiceImpl(_, _)).toLayer}What can we learn from this code?
- According to ZIO Module Pattern 2.0
class constructors are used to define service dependencies. At the end of the day the class constructor
is lifted to ZLayer:
(new BarServiceImpl(_, _)).toLayer TofuLogsis a type alias forHas[ZLogging.Make], but ZIO encourages us to use explicitly theHaswrapper whenever we want to specify the dependency on a service.- Logging methods can be used:
- explicitly like
log.info("Start getting datetime") - via string interpolation provided by tofu.syntax.logging. For this purpose
log service was defined as
implicit.
- explicitly like
- To log object Tofu must know how to present it in the log message. This way is described by instances of
Loggabletype class. We did provide no one because Tofu already hasjava.timeLoggableinstances.
Now look at the main app:
import tofu.logging.zlogs.ZLoggingimport zio._import zio.magic._
object Main extends zio.App { override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { for { barService <- ZIO.service[BarService] _ <- barService.foo } yield () }.injectCustom( BarService.live, ZLogging.Make.layerPlain ).exitCode}Thanks to zio-magic we can just list all dependencies in
the injectCustom method, not design layers manually. Output of the program will be:
{"level":"INFO","message":"Start program"}{"level":"DEBUG","message":"Got current date 2021-09-20"}Note: for simplicity here and further extra fields (e.g. timestamp, threadName) were removed.
Custom Loggable
Section titled “Custom Loggable”Default Loggable[LocalDate] is based on stringValue instance, hence the date was logged as a plain string, not key-value.
What if you want the date to be a separated field in json? Well, you can customize Loggable. Add the following line into the BarServiceImpl:
private implicit val customDateLoggable = Loggable[LocalDate].named(name="date")Now the output looks like:
{"level":"DEBUG","message":"Got current date 2021-09-20","date":"2021-09-20"}Loggable.apply[LocalDate] summons an instance from the global scope, .named converts logged object into a single field named name.
There are several methods to create and modify Loggable instances, read more in Loggable section.
Context logging
Section titled “Context logging”Let’s consider how to log the context along with actual log message. If you have an instance of Loggable for your context,
you can have it logged automagically. For example:
import derevo.deriveimport tofu.logging.derivation.loggable
@derive(loggable)final case class Context(requestId: Int)Here we use Tofu Derevo for automatic derivation Loggable instance.
One possible way to add a context to your logs is to use layerPlainWithContext which encapsulates dealing with the context inside
(otherwise you can use layerContextual retrieving the context from a ZIO environment R, but we won’t cover it here).
The main idea of this approach is to store your context in ZIO FiberRef.
It provides all the power of State Monad. Unlike Java’s ThreadLocal, FiberRef has copy-on-fork semantic:
a child Fiber starts with FiberRef values of its parent.
When the child set a new value of FiberRef, the change is visible only to the child itself. This means if we set requestId value to 117
(e.g. at the start of the request) and pass the FiberRef to a child fiber, it sees the value 117.
Let’s modify Main app. We have to define a context service and how it provides the context.
object Main extends zio.App { val contextLayer: ULayer[Has[FiberRef[Context]]] = FiberRef.make(Context(-10)).toLayer
val logsLayer: URLayer[Has[FiberRef[Context]], TofuLogs] = ZLogging.Make.layerPlainWithContext(_.get)
override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { for { (barService, ref) <- ZIO.services[BarService, FiberRef[Context]] f1 <- barService.foo.fork _ <- ref.set(Context(117)) f2 <- barService.foo.fork _ <- f1.join <&> f2.join } yield () }.injectCustom( BarService.live, logsLayer, contextLayer ).exitCode}Run the program and look at the output:
{"level":"INFO","message":"Start program","requestId":-10}{"level":"DEBUG","message":"Got current date 2021-09-20","requestId":-10,"date":"2021-09-20"}{"level":"INFO","message":"Start program","requestId":117}{"level":"DEBUG","message":"Got current date 2021-09-20","requestId":117,"date":"2021-09-20"}As you can see, the context has been added to log messages without any changes to the service calling the logging methods.