A ZIO 2 logging example, with a custom log format

If you’re interested in logging in a ZIO application, the following example shows a collection of different ways you can write log messages. I also show how to create your own custom log format, so the output logging from this application looks like this:

2024-11-28T16:09:08.669276-05:00 | INFO | zio-fiber-249876708 | Basic log message (defaults to 'info') | 
2024-11-28T16:09:08.673841-05:00 | INFO | zio-fiber-249876708 | Info level message | 
2024-11-28T16:09:08.674505-05:00 | WARN | zio-fiber-249876708 | Warning level message | 
2024-11-28T16:09:08.675342-05:00 | ERROR | zio-fiber-249876708 | Error level message | 
2024-11-28T16:09:08.678958-05:00 | INFO | zio-fiber-249876708 | inside logSpan-1 | 
2024-11-28T16:09:08.679334-05:00 | INFO | zio-fiber-249876708 | inside logSpan-2 | 
2024-11-28T16:09:08.68125-05:00 | INFO | zio-fiber-249876708 | Starting customer operation | 
2024-11-28T16:09:08.682087-05:00 | INFO | zio-fiber-249876708 | Completed customer operation | 
2024-11-28T16:09:08.686092-05:00 | INFO | zio-fiber-249876708 | This log message will include the user-id annotation | user-id=12345
2024-11-28T16:09:08.690269-05:00 | INFO | zio-fiber-249876708 | Processing value: some-value | 
2024-11-28T16:09:08.69066-05:00 | INFO | zio-fiber-249876708 | Successfully processed: some-value |

An example ZIO 2 logging application

Given that introduction, here’s my example ZIO 2 logging application:

//> using scala "3"
//> using dep "dev.zio::zio::2.1.13"
//> using dep "dev.zio::zio-logging:2.4.0"
// //> using dep "dev.zio::zio-logging-slf4j:2.4.0"
// //> using dep "ch.qos.logback:logback-classic:1.5.12"

import zio.*
// needed for configuring:
import zio.logging.consoleLogger
import zio.logging.ConsoleLoggerConfig
import zio.logging.LogFilter
import zio.logging.LogFormat

object ZioLoggingExample extends ZIOAppDefault:

    // define custom log format
    val logSeparator = LogFormat.text(" | ")
    val customLogFormat = 
        LogFormat.timestamp + logSeparator +
        LogFormat.level     + logSeparator +
        LogFormat.fiberId   + logSeparator +
        LogFormat.line      + logSeparator +  // our ‘message’
        LogFormat.annotations                 // ZIO.logAnnotate values

    // configure logger with custom format and INFO as root level
    val loggerConfig = ConsoleLoggerConfig(
        format = customLogFormat,
        filter = LogFilter.LogLevelByNameConfig(
            rootLevel = LogLevel.Info,
            mappings = Map[String, LogLevel]()
        )
    )

    // remove default loggers and replace them with the custom console logger.
    // note that >>> is for composing multiple ZLayer values in sequence.
    override val bootstrap = Runtime.removeDefaultLoggers >>> 
        consoleLogger(config = loggerConfig)

    val program = for
        _ <- ZIO.log("Basic log message (defaults to 'info')")
        _ <- ZIO.logInfo("Info level message")
        _ <- ZIO.logWarning("Warning level message")
        _ <- ZIO.logError("Error level message")
        _ <- ZIO.logDebug("Debug message: won’t show with Info level")
        _ <- ZIO.logTrace("Trace message: won’t show with Info level")
        // use logSpan to group related log messages (not 100% working)
        _ <- ZIO.logSpan("operation-name") {
                 ZIO.log("inside logSpan-1") *> ZIO.log("inside logSpan-2")
             }
        _ <- ZIO.logSpan("customer-operation") {
            for
                _ <- ZIO.log("Starting customer operation")
                // do your ‘customer operation’ here
                _ <- ZIO.log("Completed customer operation")
            yield ()
        }

        // Log with annotations
        // todo: this does not work with my customizations (yet)
        _ <- ZIO.logAnnotate("user-id", "12345") {
                 ZIO.log("This log message will include the user-id annotation")
             }

        // using `tap` and `tapError`
        _ <- ZIO.attempt("some-value")
                .tap(value => ZIO.log(s"Processing value: $value"))
                .tap(value => ZIO.logInfo(s"Successfully processed: $value"))
                .tapError(err => ZIO.logError(s"Failed processing: ${err.getMessage}"))

    yield
        ()

    val run = program

I hope those ZIO logging examples and custom log formatting are helpful!