Edit me on Github
Using ZIO Logging

Using ZIO Logging

September 28, 2020
ZIO
ZIO, Logging, Utilities

The complete source code used in this article can be found on github

Use ZIO logging #

Within Blended ZIO the service are kept clean of non functional requirements such as relying on a logging service being present within the environment.

For example, the Service within MBeanServerFacade is defined as follows.

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
  trait Service {

    /**
     * Retrieve the information for a MBean by it's object name. Any JMX related information will be
     * passed to the error channel without modification. If successful, A [[JmxBeanInfo]] will be
     * returned.
     */
    def mbeanInfo(objName: JmxObjectName): ZIO[Any, Throwable, JmxBeanInfo]

    /**
     * Retrieve the list of all MBeanNames known to the underlying MBean Server.
     * Any JMX exception that might occur will be passed onwards in the error
     * channel. If successful, a list of [[JmxObjectName]]s will be returned.
     */
    def allMbeanNames(): ZIO[Any, Throwable, List[JmxObjectName]] = mbeanNames(None)

    /**
     * Retrieve the list of all object names know to the underlying MBean Server.
     * Any JMX exception that might occur will be passed onwards in the error
     * channel. If successful, a list of [[JmxObjectName]]s will be returned.
     * @param objName If non-empty, the result will contain all object names that
     *                are in the same JMX domain and have all properties set within
     *                the parameter as additional name properties.
     *                If empty, no filtering will be applied.
     */
    def mbeanNames(objName: Option[JmxObjectName]): ZIO[Any, Throwable, List[JmxObjectName]]
  }

However, within the service’s implementation JvmMBeanServerFacade the corresponding methods leverage the API of zio-logging to produce some output while executing the effects.

73
74
75
76
77
78
79
80
81
82
83
84
85
86
  def mbeanInfo(objName: JmxObjectName): ZIO[Logging, Throwable, JmxBeanInfo] = for {
    on           <- ZIO.effect(new ObjectName(objName.objectName))
    _            <- doLog(LogLevel.Info)(s"Getting MBeanInfo [$objName]")
    info          = svr.getMBeanInfo(on)
    readableAttrs = info.getAttributes.filter(_.isReadable())
    mapped       <- mapAllAttributes(on, readableAttrs)
    result        = JmxBeanInfo(objName, mapped)
  } yield result

  private def doLog(level: LogLevel)(msg: => String): ZIO[Logging, Nothing, Unit] = for {
    _ <- log.locally(LogAnnotation.Name(getClass.getName :: Nil)) {
           log(level)(msg)
         }
  } yield ()

So, when we assemble the service

  • we need to provide a Logging service when building up the ZLayer
  • we need to make the Logging service available to the service implementation
  • the business service as such should not have any knowledge of the Logging service requirement

The code to construct the live service which requires Logging leverages ZLayer.fromFunction. We see that a Logging service is required within the environment and we can use the parameter to the fromFunction call in the provide operator so that the requirement of having a Logging service is eliminated and the sole business service interface remains.

47
48
49
50
51
52
53
54
55
56
57
  val live: ZLayer[Logging, Nothing, MBeanServerFacade] = ZLayer.fromFunction(log =>
    new Service {
      private val impl: JvmMBeanServerFacade = new JvmMBeanServerFacade(ManagementFactory.getPlatformMBeanServer)

      override def mbeanInfo(objName: JmxObjectName): ZIO[Any, Throwable, JmxBeanInfo] =
        impl.mbeanInfo(objName).provide(log)

      override def mbeanNames(objName: Option[JmxObjectName]): ZIO[Any, Throwable, List[JmxObjectName]] =
        impl.mbeanNames(objName).provide(log)
    }
  )

We might have other service implementations that do not require logging or use a different logging API while keeping the same business interface.

Finally, we can construct the environment for our program as we do in the testcase:

22
23
24
25
  private val logSlf4j = Slf4jLogger.make((_, message) => message)

  private val mbeanLayer: ZLayer[Any, Nothing, MBeanServerFacade.MBeanServerFacade] =
    logSlf4j >>> MBeanServerFacade.live