Skip to main content
Version: ZIO 2.x

Tutorial: How to Create a Custom Logger for a ZIO Application?

Introduction

As we have seen in the previous tutorial, ZIO has a variety of built-in logging facilities. Also, it has a default logger that can be used to print log messages to the console. When we go to production, we may want to use a different logger with a customized configuration. For example, we may want to log to a file or a database instead of the console.

In this tutorial, we are going to see how we can create a custom logger for a ZIO application.

Running the Examples

All the source code associated with this article is located on the custom-logger branch of this quickstart repository.

In this article, we enabled logging for UserApp http application. In this tutorial, we are going to create a custom logger for the UserApp.

To run the code, clone the repository and checkout the custom-logger branch:

$ git clone git@github.com:zio/zio-quickstart-restful-webservice.git 
$ cd zio-quickstart-restful-webservice
$ git checkout custom-logger

And finally, run the application using sbt:

$ sbt run

Creating a Custom Logger

To create a new logger for the ZIO application, we need to create a new ZLogger object. The ZLogger is a trait that defines the interface for a ZIO logger. The default logger has implemented this trait through the ZLogger.default object.

import zio._

val logger: ZLogger[String, Unit] =
new ZLogger[String, Unit] {
override def apply(
trace: Trace,
fiberId: FiberId,
logLevel: LogLevel,
message: () => String,
cause: Cause[Any],
context: FiberRefs,
spans: List[LogSpan],
annotations: Map[String, String]
): Unit =
println(s"${java.time.Instant.now()} - ${logLevel.label} - ${message()}")
}

So then, we can remove all the default loggers and replace them with our custom logger:

import zio._

object MainApp extends ZIOAppDefault {

val logger: ZLogger[String, Unit] =
new ZLogger[String, Unit] {
override def apply(
trace: Trace,
fiberId: FiberId,
logLevel: LogLevel,
message: () => String,
cause: Cause[Any],
context: FiberRefs,
spans: List[LogSpan],
annotations: Map[String, String]
): Unit =
println(s"${java.time.Instant.now()} - ${logLevel.label} - ${message()}")
}

override val bootstrap = Runtime.removeDefaultLoggers ++ Runtime.addLogger(logger)

def run =
for {
_ <- ZIO.log("Application started!")
_ <- ZIO.log("Another log message.")
_ <- ZIO.log("Application stopped!")
} yield ()
}

By running this application, the log messages will be printed like this:

2022-06-04T13:49:19.554648Z - INFO - Application started!
2022-06-04T13:49:19.567854Z - INFO - Another log message.
2022-06-04T13:49:19.568831Z - INFO - Application stopped!

Using SLF4J Logger in a ZIO Application

So far, we learned how to write a custom logger for a ZIO application. Now, in this section, we want to add SLF4J Logging support to the UserApp we have developed in the Restful Web Service quickstart.

SLF4J is a logging facade that decouples our application code from any underlying logging implementation. To enable SLF4J logging for a ZIO application, we need to implement the ZLogger trait using SLF4J. Fortunately, the ZIO Logging project has done this for us.

So we can simply add this library to our project by adding the following dependencies to our build.sbt file:

libraryDependencies += "dev.zio" %% "zio-logging"       % "2.0.0"
libraryDependencies += "dev.zio" %% "zio-logging-slf4j" % "2.0.0"

Now we can use the SLF4J.sl4j layer to enable SLF4J logging:

import zio._
import zio.logging.LogFormat
import zio.logging.backend.SLF4J

object MainApp extends ZIOAppDefault {
override val bootstrap = SLF4J.slf4j(LogLevel.Info, LogFormat.colored)

def run = ZIO.log("Application started!")
}

Let's run the application and see the output:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Oops! The SLF4J failed to find any binding in the classpath. To fix this, we need to add an SLF4J binding to our classpath.

Adding a Simple SLF4J Binding to the Classpath

The SLF4J has a simple binding that can be used by adding the slf4j-simple dependency to our build.sbt file:

libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.7.36"

And then we can run the application again:

[ZScheduler-Worker-7] INFO zio-slf4j-logger - timestamp=2022-06-04T19:36:43.768256+04:30 level=INFO thread=zio-fiber-6 message="Application started!"

It works! Now, our ZIO application uses SLF4J for its logging backend.

Similarly, we can bind our application to any other logging framework by adding the appropriate dependency to our build.sbt file:

  • slf4j-log4j12
  • slf4j-reload4j
  • slf4j-jdk14
  • slf4j-nop
  • slf4j-jcl
  • logback-classic

To switch to another logging framework, we need to provide one of the above dependencies instead of slf4j-simple. In the next section, we will learn how to switch to the reload4j logging framework.

Switching to the Reload4j Logging Framework

To use the reload4j logging framework, we need to add the following dependencies to our build.sbt file:

- libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.7.36"
+ libraryDependencies += "org.slf4j" % "slf4j-reload4j" % "1.7.36"

Now we can configure our logger by adding the log4j.properties to the resources directory:

log4j.rootLogger = Info, consoleAppender
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=[%p] %d %c %M - %m%n

By customizing the ConversionPattern we can control the format of the log messages.

Switching to the Logback Logging Framework

In the same way, we can switch to the logback-classic logging framework by adding the following dependencies to our build.sbt file:

- libraryDependencies += "org.slf4j"      % "slf4j-reload4j"  % "1.7.36"
+ libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11"

Then we can configure our logger by adding the logback.xml to the resources directory:

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

Conclusion

In this article, we have learned how to create a custom logger for a ZIO application. We also covered how to add SLF4J logging support instead of default ZIO logging.

The complete working example of this tutorial is available on the custom-logger branch of our ZIO Quickstart: Building RESTful Web Service quickstart on GitHub.