This note is based on the ZIO Logging doc. It supports context across fibers. It comes with a console implementation and a SLf4j intergration. The code is not elegant and the sample code is a mess.

1 LogAnnotation

A value of LogAnnotation describes a piece of meta-info of a log entry. It has a name, an initial value, a value combination function and a render function. The combine function merges two values. The render function generates a string from the value.

A value of LogAnnotation is used as the map key of a LogContext.

Its companion object defines the following LogAnnotation values:

  • CorrelationId: a value of Option[java.util.UUID]
  • Level: a value of LogLevel
  • Name: a value of List[String]
  • Throwable: a value of Option[Throwable]
  • Cause: a value of [Option[Cause[Any]]]

An important method is the apply method that returns a function of LogContext => LogContext. The function calls the combine field of the current LogAnnotation to combine two values.

2 LogContext

A LogContext stores conext for a logging operation. It wraps a map of a key of type LogAnnotation and a value of a type specified by the LogAnnotation type.

It defines the following methods:

  • ++ and merge: merge two values of type LogContext.
  • annotate: add an annotation and return a new LogContext.
  • apply: render the value of a given annotation.
  • get: retrieve the specified annotation.
  • renderContext: rend all annotation name and rendered value as a map.

3 Logger

The Logger[A] trait defines the log methods. A is the type of the logged value. It has three abstract methods that are to be implemented by any Logger object.

  • def log(line: => A): UIO[Unit]: the core function of a logger is to log a value of type A.
  • def logContext: UIO[LogContext]: the log context.
  • def locally[R1, E, A1](f: LogContext => LogContext)(zio: ZIO[R1, E, A1]): ZIO[R1, E, A1]: modify the log context in the scope of the specified effect zio – the second parameter is a result of the log method defined above. The f is usually defined by the apply method of LogAnnotation – the function is to combine annotation vlues.

Based on the three abastract methods, the Logger trait implements the following help methods:

  • contramap: log a different value with a contramap function parameter f: A1 => A.
  • def log(level: LogLevel)(line: => A): UIO[Unit] = locally(_.annotate(LogAnnotation.Level, level))(log(line)): log with log level. Some shortcuts are: trace, debug, info, warn, error, error(line, cause), and throwable(line, throwable).
  • def derive(f: LogContext => LogContext): Logger[A]: create a new logger by new LogContext.
  • def named(name: String): Logger[A]: produce a named logger

4 Logging

The Logging object defines a make[R] method to create a ZLayer from a logger and a rootLoggerName. The logger parameter has a type of (LogContext, => String) => URIO[R, Unit]. It is used to write a line with the log context. The make methods creates a FiberRef that wraps a LogContext value. Therefore the created logging object has a context in its FiberRef.

The make method is used to create a console method that create a value of ZLayer[Console with Clock, Nothing, Logging]. The Logging type is defined as Has[Logger[String]], i.e, a Logger type that logs string values.

The rest of the Logging object defines all methods of Logger that returns a type of URIO[Logging, LogContext].

5 log

The log object repeats all methods defined in Logging using the corresponding Logging methods . The sample code use the Loggin.console to create an environment and use the log methods to write log.

6 Slf4j Integration

The Slf4jLogger defines a make method that takes a logFormat: (LogContext, => String) => String and a root logger name. It maps each log level into the Slf4jLogger level. The log context can have a LogAnnotation.Throwable value.

Another method makeWithAnnotationAsMdc takes an additional parameter mdcAnnotations: List[LogAnnotation[_]] that allows the logging of mapped diagnostic context (MDC).

7 Sample Code

The ZIO logging document comes with several examples. The build file is as the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION`

import mill.Agg
import mill.scalalib.{DepSyntax, ScalaModule}

object hi extends ScalaModule {
  def scalaVersion = "2.13.1"
  def ivyDeps = Agg(
    ivy"dev.zio::zio:1.0.0-RC18-2",
    ivy"dev.zio::zio-logging:0.2.8"
  )
}

7.1 Simple Console Log

The console logging example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import zio.logging.{Logging, log}

object Simple extends zio.App {

  val env =
    Logging.console(
      format = (_, logEntry) => logEntry,
      rootLoggerName = Some("default-logger")
    )

  override def run(args: List[String]) =
    log.info("Hello from ZIO logger").provideCustomLayer(env).as(0)
}

7.2 Slf4j Log

The build.sc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION`

import mill.Agg
import mill.scalalib.{DepSyntax, ScalaModule}

object hi extends ScalaModule {
  def scalaVersion = "2.13.1"
  def ivyDeps = Agg(
    ivy"dev.zio::zio:1.0.0-RC18-2",
    ivy"dev.zio::zio-logging:0.2.8",
    ivy"dev.zio::zio-logging-slf4j:0.2.8",
    ivy"ch.qos.logback:logback-classic:1.2.3"
  )
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import zio.logging.{Logging, log, LogAnnotation}
import zio.logging.slf4j.Slf4jLogger

object Slf4jAndCorrelationId extends zio.App {
  val logFormat = "[correlation-id = %s] %s"

  val env =
    Slf4jLogger.make { (context, message) =>
      val correlationId = LogAnnotation.CorrelationId.render(
        context.get(LogAnnotation.CorrelationId)
      )
      logFormat.format(correlationId, message)
    }

  def generateCorrelationId =
    Some(java.util.UUID.randomUUID())

  override def run(args: List[String]) =
    (for {
      fiber <- log
        .locally(LogAnnotation.CorrelationId(generateCorrelationId))(
          zio.ZIO.unit
        )
        .fork
      _ <- log.info("info message without correlation id")
      _ <- fiber.join
      _ <- log.locally(LogAnnotation.CorrelationId(generateCorrelationId)) {
        log.info("info message with correlation id") *>
          log
            .throwable(
              "another info message with correlation id",
              new RuntimeException("error message")
            )
            .fork
      }
    } yield 1).provideLayer(env)
}

7.3 SLF4j with MDC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import zio.logging.{Logging, log, LogAnnotation}
import zio.logging.slf4j.Slf4jLogger
import java.util.UUID
import zio.{ZIO, UIO, clock}
import zio.duration._

object Slf4jMdc extends zio.App {

  val userId = LogAnnotation[UUID](
    name = "user-id",
    initialValue = UUID.fromString("0-0-0-0-0"),
    combine = (_, newValue) => newValue,
    render = _.toString
  )

  val logLayer = Slf4jLogger.makeWithAnnotationsAsMdc(List(userId))
  val users = List.fill(2)(UUID.randomUUID())

  override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] =
    (for {
      correlationId <- UIO(Some(UUID.randomUUID()))
      _ <- ZIO.foreachPar(users) { uId =>
        log.locally(
          _.annotate(userId, uId)
            .annotate(LogAnnotation.CorrelationId, correlationId)
        ) {
          log.info("Starting operation") *>
            ZIO.sleep(500.millis) *>
            log.info("Stopping operation")
        }
      }
    } yield 0).provideSomeLayer[clock.Clock](logLayer)
}

When run with mill -i hi, the messages are incorrect: 1) no mdc 2) logging call during initialization phase.