Use ZIO Logging

In this article we will investigate how we can leverage zio-logging in our service implementations while avoiding to add a logging service requirement to the business interfaces.

note

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

Keep business interfaces free from non-business requirements#

Within Blended ZIO the services 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.

Service Definition
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.

Sample implementation
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.

Layer definition
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 test case:

Layer creation
private val logSlf4j = Slf4jLogger.make((_, message) => message)
private val mbeanLayer: ZLayer[Any, Nothing, MBeanServerFacade.MBeanServerFacade] =
logSlf4j >>> MBeanServerFacade.live