YHDEV
YHDEV

Reputation: 477

Sbt test: class org.apache.logging.slf4j.Log4jLogger cannot be cast to class ch.qos.logback.classic.Logger (org.apache.logging.slf4j.Log4jLogger

I'm new to Scala and JVM, I want to write unit test for logger but I get this error when I ran sbt test from terminal.

java.lang.ClassCastException: class org.apache.logging.slf4j.Log4jLogger cannot be cast to class ch.qos.logback.classic.Logger (org.apache.logging.slf4j.Log4jLogger and ch.qos.logback.classic.Logger are in unnamed module of loader 'app') and this warning

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/apache/logging/log4j/log4j-slf4j-impl/2.16.0/log4j-slf4j-impl-2.16.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

But when I ran test from IntelliJ, it passes.

My test code is below

import org.scalatest._
import flatspec._

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
import org.slf4j.LoggerFactory

class MyAppSuite extends AnyFlatSpec {
  it should "Log message" in {
    object MyApp {
      val LOGGER = LoggerFactory.getLogger(classOf[ILoggingEvent])
    }
    class MyApp {
      def hello(word: String): Unit = {
        MyApp.LOGGER.info(s"Word is ${word}")
      }
    }

    val appender = new ListAppender[ILoggingEvent]
    appender.start()
    val logger = MyApp.LOGGER.asInstanceOf[Logger]
    logger.addAppender(appender)

    val myApp = new MyApp
    myApp.hello("wow")

    val logsList = appender.list
    assert(logsList.size() === 1)
    val logEvent = logsList.get(0)
    assert(logEvent.getLevel.levelStr === "INFO")
    assert(logEvent.getMessage === "Word is wow")
  }
}

My dependencies in build.sbt

...

scalaVersion := "2.12.14"

val log4jVersion = "2.16.0"

val loggingDependencies = Seq(
  "org.slf4j" % "slf4j-api" % "1.7.36",
  "org.apache.logging.log4j" % "log4j-api" % log4jVersion,
  "org.apache.logging.log4j" % "log4j-core" % log4jVersion,
  "org.apache.logging.log4j" % "log4j-slf4j-impl" % log4jVersion
)

val testingDependencies = Seq(
  "org.scalamock" %% "scalamock" % "5.2.0" % Test,
  "org.scalatest" %% "scalatest" % "3.2.12" % Test,
  "ch.qos.logback" % "logback-classic" % "1.2.11" % Test,
)

libraryDependencies ++= loggingDependencies ++ testingDependencies
excludeDependencies ++= Seq(
  "org.slf4j" % "slf4j-log4j12"
)

When I ran test from IntelliJ I see this warning

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/Users/yl3/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/org/apache/logging/log4j/log4j-slf4j-impl/2.16.0/log4j-slf4j-impl-2.16.0.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

I found similar logging tests in Java

So I assume it should work in Scala but not sure why casting doesn't work. Any suggestions or other ways to test logging using other libraries?

Upvotes: 0

Views: 2893

Answers (1)

Gaël J
Gaël J

Reputation: 15105

The warning messages from SLF4J are really helpful. In both cases it says there are several logging libraries loaded in the classpath and this might cause trouble.

When looking in details, it even says which library is chosen as the SLF4J implementation:

  • in your IntelliJ tests, it chose Logback: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
  • but in your SBT run it chose Log4j: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]

This explains that your code that expects the logging library to be Logback works in IntelliJ tests but not with SBT run.

When there are several logging implementation in the classpath, there's no way to know for sure which one SLF4J will use.


Now, let's clarify the dependencies you have in your build definition:

  • slf4j-api: the SlF4J API,
  • log4j-api: the log4j 2 API,
  • log4j-core: the log4j 2 implementation
  • log4j-slf4j-impl: the binding to make SLF4J write logs with log4j 2 implementation
  • logback-core and logback-classic: Logback implementation (Logback comes with a binding to make SLF4J write logs with Logback automatically) And you already excluded slf4j-log4j12 which is the binding to make SLF4J write logs with log4j 1.2 implementation.

Solution

If you don't need several logging libraries (which is 99% of the time the case), just choose the implementation you want to use, it seems to be Logback as you have Logback specific code. Then remove other libraries implementation and binding dependencies.

This would mean to remove log4j-core and log4j-slf4j-impl in your case.

But then you might still have code that rely on the log4j 2 API to log stuff (other dependencies for instance). Thus you might need to add the binding library to make log4j 2 logs be written through SlF4J. To do so, add the log4j-to-slf4j-2.x dependency.

Note: if for some strange reason you need several logging implementation, then do not assume in your code that it's one of them or handle it by handling cast exceptions.

Upvotes: 1

Related Questions