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.
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.