Configure logback in Typelevel Scala app programmatically

I am attempting to use LoggingConfigurator to configure a ConsoleAppender with a JsonEncoder to print JSON logs. However, the program prints plain text logs instead.

$ scala-cli .
16:57:51.531 [io-compute-5] DEBUG myapp.App -- Hello, world!

I expected the output to be in JSON format:

$ scala-cli .
{"timestamp": "2023-06-24T16:57:51.531+0500", "level": "DEBUG", "name": "myapp.App", "msg": "Hello, world!"}

What am I doing wrong?

app.scala

//> using scala 3
//> using resourceDir ./resources
//> using dep org.typelevel::log4cats-slf4j:2.6.0
//> using dep ch.qos.logback:logback-classic:1.4.8
//> using dep org.typelevel::cats-core:2.9.0
//> using dep org.typelevel::cats-effect:3.5.1
//> using dep com.monovore::decline:2.4.1
//> using dep com.monovore::decline-effect:2.4.1

package myapp

import cats.effect.{ExitCode, IO, Sync}
import cats.implicits._
import org.typelevel.log4cats.slf4j.Slf4jFactory
import org.typelevel.log4cats.{LoggerFactory, SelfAwareStructuredLogger}
import com.monovore.decline.effect.CommandIOApp
import com.monovore.decline.Opts
import org.typelevel.log4cats.syntax._

class LoggingConfigurator
    extends ch.qos.logback.core.spi.ContextAwareBase
    with ch.qos.logback.classic.spi.Configurator {
    override final def configure(
        loggerContext: ch.qos.logback.classic.LoggerContext,
    ): ch.qos.logback.classic.spi.Configurator.ExecutionStatus = {
        val encoder = new ch.qos.logback.classic.encoder.JsonEncoder
        encoder.setContext(loggerContext)
        encoder.start()

        val appender = new ch.qos.logback.core.ConsoleAppender[ch.qos.logback.classic.spi.ILoggingEvent]
        appender.setContext(loggerContext)
        appender.setTarget("System.err")
        appender.setEncoder(encoder)
        appender.start()

        val logger = loggerContext.getLogger("ROOT")
        logger.addAppender(appender)
        logger.setLevel(ch.qos.logback.classic.Level.DEBUG)
        logger.setAdditive(false)

        ch.qos.logback.classic.spi.Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY
    }
}

trait Logging[F[_]: Sync] {
    private given LoggerFactory[F]                      = Slf4jFactory.create[F]
    inline protected given SelfAwareStructuredLogger[F] = LoggerFactory[F].getLogger
}

object App extends CommandIOApp("myapp", "an app", true, "0.0.1") with Logging[IO] {
    override final def main: Opts[IO[ExitCode]] =
        Opts.unit map { _ =>
            for {
                _ <- debug"Hello, world!"
            } yield ExitCode.Success
        }
}

resources/META-INF/services/ch.qos.logback.classic.spi.Configurator

myapp.LoggingConfigurator

I am attempting to configure a ConsoleAppender with a JsonEncoder to print JSON logs. However, the program prints plain text logs instead:

$ scala-cli .
16:57:51.531 [io-compute-5] DEBUG myapp.App -- Hello, world!

I expected the output to be in JSON format:

$ scala-cli .
{"timestamp": "2023-06-24T16:57:51.531+0500", "level": "DEBUG", "name": "myapp.App", "msg": "Hello, world!"}

What am I doing wrong?

The issue seems to be with the configuration of the LoggingConfigurator class. The configure method in the LoggingConfigurator class is not being invoked.

To fix the issue, you can try the following steps:

  1. Move the LoggingConfigurator class to a separate file, let’s say LoggingConfigurator.scala.

  2. Update the resources/META-INF/services/ch.qos.logback.classic.spi.Configurator file to include the fully qualified name of the LoggingConfigurator class.

    myapp.LoggingConfigurator
    
  3. Import the ch.qos.logback.classic.spi.Configurator and ch.qos.logback.core.spi.ContextAwareBase classes in the LoggingConfigurator file.

    import ch.qos.logback.classic.spi.Configurator
    import ch.qos.logback.core.spi.ContextAwareBase
    
  4. Modify the LoggingConfigurator class to extend the Configurator trait and implement the configure method.

    class LoggingConfigurator extends ContextAwareBase with Configurator {
      override def configure(loggerContext: LoggerContext): Unit = {
        // Your existing configuration code here
      }
    }
    

Make sure to update the LoggingConfigurator class path in the resources/META-INF/services/ch.qos.logback.classic.spi.Configurator file to match the new location if you move the class to a separate file.

After making these changes, the configure method should be invoked, and the program should print logs in the desired JSON format.