2

I have a program using com.monovore.decline and org.typelevel.log4cats.

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

When I run this program, I expect to see JSON logs. But I see plain text (default ConsoleAppender with default PatternLayoutEncoder) instead:

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

I hope it's clear that I was expecting something like:

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

What am I doing wrong?

EDIT: It seems I misunderstood usage of resources/META-INF/services/.... I have edited the code snippet above to my latest attempt, but it still doesn't work as I expect so there is something else I am misunderstanding.

Daenyth
  • 35,856
  • 13
  • 85
  • 124
  • is the line `appender.setTarget("System.err")` ok? why not `"System.out"`? – Gastón Schabas Jun 24 '23 at 22:46
  • From logback manual, target can be either `"System.out"` or `"System.err"`. Former if I want logs to go to stdout and latter if I want the logs to go to stderr. I chose latter. I can change that, but that doesn't affect the issue, which is that Logback doesn't see my custom Configurator and continues with the builtin default config. – Zia Ur Rehman Jun 24 '23 at 23:14
  • You'd have to use JSON encoder, and set additional fields: https://stackoverflow.com/questions/22615311/is-there-a-logback-layout-that-creates-json-objects-with-message-parameters-as-a – Mateusz Kubuszok Jun 25 '23 at 08:35
  • I'm not sure I understand. In my example, I am trying to use `JsonEncoder` shipped with logback classic. Do you mean some other JSON encoder? I can try using the Logstash encoder instead. But the SO post you linked not relevant to my problem, which is that logback cannot see my custom `Configurator`. – Zia Ur Rehman Jun 25 '23 at 12:55

0 Answers0