javascalaloggingakkaslf4j

Why am I getting "No SLF4J Providers found" in my Scala + AKKA Jar?


I have a JAR that I built using the assembly plugin for sbt.

When I run the code through the IDE, I can see the logs produced by my actors, alongside the logs from Hibernate.

When I run the jar on the command line (java -jar JAR_NAME.jar), I get the warning:

SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.

Followed by none of the logs mentioned above. This is making it very hard to debug issues in the production build.

I followed this: https://doc.akka.io/libraries/akka-core/current/typed/logging.html and included "ch.qos.logback" % "logback-classic" % "1.5.16" in my build file but no effect.

The closest I got was by adding:

  "org.slf4j"                       % "slf4j-api"                 % "2.0.16",
  "org.slf4j"                       % "slf4j-simple"              % "2.0.16",
  "org.slf4j"                       % "slf4j-jdk14"               % "2.0.16",

After which I got the Hibernate logs, but none of the logs from my Actors still, and still the original warning. Full output:

SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Jan 23, 2025 6:49:15 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate ORM core version 0.1.0-SNAPSHOT
Jan 23, 2025 6:49:16 PM org.hibernate.cache.internal.RegionFactoryInitiator initiateService
INFO: HHH000026: Second-level cache disabled

... more hibernate logs

What exactly am I missing here? I know the warning is because it cannot find a provider on the classpath, but surely that is solved by adding the dependencies to build.sbt like I have?

Some other important files:

logback.xml

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

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <pattern>%level[%thread] %logger{0} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>log/akka.log</file>
        <append>false</append>
        <encoder>
            <pattern>%date{yyyy-MM-dd} %X{akkaTimestamp} %-5level[%thread] %logger{1} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="TRACE">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

application.comf

akka {
    license-key = ??????????????????????????????????????????
    loggers = ["akka.event.slf4j.Slf4jLogger"]
    loglevel = "DEBUG"
    logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"

    use-slf4j = on
}

build.sbt

val akkaVersion = "2.8.8" // Latest stable Akka version
val akkaHttpVersion = "10.5.3" // Latest stable Akka-HTTP version

Compile / PB.targets := Seq(
  scalapb.gen() -> (Compile / sourceManaged).value / "scalapb"
)

Global / onChangedBuildSource := ReloadOnSourceChanges

libraryDependencies ++= Seq(
  "com.typesafe.akka"               %% "akka-http"                % akkaHttpVersion,
  "com.typesafe.akka"               %% "akka-http-spray-json"     % akkaHttpVersion,
  "com.typesafe.akka"               %% "akka-actor-typed"         % akkaVersion,
  "com.typesafe.akka"               %% "akka-stream"              % akkaVersion,

  "ch.qos.logback"                  % "logback-classic"           % "1.5.16",

  "commons-codec"                   % "commons-codec"             % "1.17.2",
  "com.typesafe.akka"               %% "akka-http-testkit"        % akkaHttpVersion                         % Test,
  "com.typesafe.akka"               %% "akka-actor-testkit-typed" % akkaVersion                             % Test,
  "org.scalatest"                   %% "scalatest"                % "3.2.19"                                % Test,
  "org.hibernate.orm"               % "hibernate-core"            % "6.6.3.Final",
  "org.hibernate.validator"         % "hibernate-validator"       % "8.0.0.Final",
  "org.glassfish"                   % "jakarta.el"                % "5.0.0-M1"                              % Test,
  "io.agroal"                       % "agroal-pool"               % "2.5",
  "org.hibernate.orm"               % "hibernate-agroal"          % "6.4.4.Final",
  "com.mysql"                       % "mysql-connector-j"         % "9.1.0",
  "jakarta.el"                      % "jakarta.el-api"            % "6.0.1",
  "com.sun.el"                      % "el-ri"                     % "3.0.4",
  "jakarta.el"                      % "jakarta.el-api"            % "6.0.1",
  "com.lihaoyi"                     %% "upickle"                  % "4.1.0",
  "com.thesamet.scalapb"            %% "scalapb-runtime"          % scalapb.compiler.Version.scalapbVersion % "protobuf"
)

ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "3.3.4"

lazy val root = (project in file("."))
  .settings(
    name := "Hydra",
    Compile / mainClass := Some("server.WebServer")
  )

assembly/assemblyMergeStrategy := {
  case PathList("META-INF", xs@_*) => MergeStrategy.discard
  case PathList("google", "protobuf", xs@_*) => MergeStrategy.first
  case PathList("module-info.class") => MergeStrategy.last
  case path if path.endsWith("/module-info.class") => MergeStrategy.last
  case PathList("reference.conf") => MergeStrategy.concat
  case x =>
    val oldStrategy = (assembly/assemblyMergeStrategy).value
    oldStrategy(x)
}

To build the FAT Jar, I run assembly in the sbt shell, which gives me the final Jar.


Solution

  • Let's read Class path contains SLF4J bindings targeting slf4j-api versions 1.7.x or earlier

    Planning for the advent of Jigsaw (Java 9), slf4j-api version 2.0.x and later use the ServiceLoader mechanism. Earlier versions of SLF4J relied on the static binder mechanism which is no longer honored by slf4j-api version 2.0.x.

    In case SLF4J 2.x finds no providers targeting SLF4J 2.x but finds instead bindings targeting SLF4J 1.7 or earlier, it will list the bindings it finds but otherwise will ignore them.

    This can be solved by placing an SLF4J provider on your classpath, such providers include logback version 1.3.x and later, as well as one of slf4j-reload4j, slf4j-jdk14, slf4j-simple version 2.0.0 or later.

    How do the service loader work?

    In your resources, in META-INF directory, there will be a file name.of.the.package.NameOfInterface. It will contain only 1 line with the full qualified name of the implementation.

    When ServiceLoader is asked about obtaining implementation for name.of.the.package.NameOfInterface it will find that file, read its content and ask ClassLoader to give it that class, and then instantiate it. It happens in the runtime.

    Your sbt explicitly states:

    assembly/assemblyMergeStrategy := {
      case PathList("META-INF", xs@_*) => MergeStrategy.discard
      ...
    

    so the uber JAR has all of these files used by ServiceLoader removed. So when you run the uberjar, the ServiceLoader cannot find them as if no implementation was available. (Not only for Slf4j, but any other library that relies on this mechanism).

    Replace it with MergeStrategy.first or MergeStrategy.last and it should work.