Logging in Scala
LoggingScalaLogging Problem Overview
What is a good way to do logging in a Scala application? Something that is consistent with the language philosophy, does not clutter the code, and is low-maintenance and unobtrusive. Here's a basic requirement list:
- simple
- does not clutter the code. Scala is great for its brevity. I don't want half of my code to be logging statements
- log format can be changed to fit the rest of my enterprise logs and monitoring software
- supports levels of logging (ie debug, trace, error)
- can log to disk as well as other destinations (i.e. socket, console, etc.)
- minimum configuration, if any
- works in containers (ie, web server)
- (optional, but nice to have) comes either as part of the language or as a maven artifact, so I don't have to hack my builds to use it
I know I can use the existing Java logging solutions, but they fail on at least two of the above, namely clutter and configuration.
Thanks for your replies.
Logging Solutions
Solution 1 - Logging
slf4j wrappers
Most of Scala's logging libraries have been some wrappers around a Java logging framework (slf4j, log4j etc), but as of March 2015, the surviving log libraries are all slf4j. These log libraries provide some sort of log
object to which you can call info(...)
, debug(...)
, etc. I'm not a big fan of slf4j, but it now seems to be the predominant logging framework. Here's the description of SLF4J:
> The Simple Logging Facade for Java or (SLF4J) serves as a simple facade or abstraction for various logging frameworks, e.g. java.util.logging, log4j and logback, allowing the end user to plug in the desired logging framework at deployment time.
The ability to change underlying log library at deployment time brings in unique characteristic to the entire slf4j family of loggers, which you need to be aware of:
- classpath as configuration approach. The way slf4j knows which underlying logging library you are using is by loading a class by some name. I've had issues in which slf4j not recognizing my logger when classloader was customized.
- Because the simple facade tries to be the common denominator, it's limited only to actual log calls. In other words, the configuration cannot be done via the code.
In a large project, it could actually be convenient to be able to control the logging behavior of transitive dependencies if everyone used slf4j.
Scala Logging
Scala Logging is written by Heiko Seeberger as a successor to his slf4s. It uses macro to expand calls into if expression to avoid potentially expensive log call.
> Scala Logging is a convenient and performant logging library wrapping logging libraries like SLF4J and potentially others.
Historical loggers
Solution 2 - Logging
With Scala 2.10+ Consider ScalaLogging by Typesafe. Uses macros to deliver a very clean API
https://github.com/typesafehub/scala-logging
Quoting from their wiki:
> Fortunately Scala macros can be used to make our lives easier: ScalaLogging offers the class Logger
with lightweight logging methods that will be expanded to the above idiom. So all we have to write is:
logger.debug(s"Some ${expensiveExpression} message!")
After the macro has been applied, the code will have been transformed into the above described idiom.
In addition ScalaLogging offers the trait Logging
which conveniently provides a Logger
instance initialized with the name of the class mixed into:
import com.typesafe.scalalogging.slf4j.LazyLogging
class MyClass extends LazyLogging {
logger.debug("This is very convenient ;-)")
}
Solution 3 - Logging
Using slf4j and a wrapper is nice but the use of it's built in interpolation breaks down when you have more than two values to interpolate, since then you need to create an Array of values to interpolate.
A more Scala like solution is to use a thunk or cluster to delay the concatenation of the error message. A good example of this is Lift's logger
Which looks like this:
class Log4JLogger(val logger: Logger) extends LiftLogger {
override def trace(msg: => AnyRef) = if (isTraceEnabled) logger.trace(msg)
}
Note that msg is a call-by-name and won't be evaluated unless isTraceEnabled is true so there's no cost in generating a nice message string. This works around the slf4j's interpolation mechanism which requires parsing the error message. With this model, you can interpolate any number of values into the error message.
If you have a separate trait that mixes this Log4JLogger into your class, then you can do
trace("The foobar from " + a + " doesn't match the foobar from " +
b + " and you should reset the baz from " + c")
instead of
info("The foobar from {0} doesn't match the foobar from {1} and you should reset the baz from {c},
Array(a, b, c))
Solution 4 - Logging
Don't use Logula
I've actually followed the recommendation of Eugene and tried it and found out that it has a clumsy configuration and is subjected to bugs, which don't get fixed (such as this one). It doesn't look to be well maintained and it doesn't support Scala 2.10.
Use slf4s + slf4j-simple
Key benefits:
- Supports latest Scala 2.10 (to date it's M7)
- Configuration is versatile but couldn't be simpler. It's done with system properties, which you can set either by appending something like
-Dorg.slf4j.simplelogger.defaultlog=trace
to execution command or hardcode in your script:System.setProperty("org.slf4j.simplelogger.defaultlog", "trace")
. No need to manage trashy config files! - Fits nicely with IDEs. For instance to set the logging level to "trace" in a specific run configuration in IDEA just go to
Run/Debug Configurations
and add-Dorg.slf4j.simplelogger.defaultlog=trace
toVM options
. - Easy setup: just drop in the dependencies from the bottom of this answer
Here's what you need to be running it with Maven:
<dependency>
<groupId>com.weiglewilczek.slf4s</groupId>
<artifactId>slf4s_2.9.1</artifactId>
<version>1.0.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.6</version>
</dependency>
Solution 5 - Logging
This is how I got Scala Logging working for me:
Put this in your build.sbt
:
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.7.2",
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
Then, after doing an sbt update
, this prints out a friendly log message:
import com.typesafe.scalalogging._
object MyApp extends App with LazyLogging {
logger.info("Hello there")
}
If you are using Play, you can of course simply import play.api.Logger
for writing log messages: Logger.debug("Hi")
.
See the docs for more info.
Solution 6 - Logging
I use SLF4J + Logback classic and apply it like this:
trait Logging {
lazy val logger = LoggerFactory.getLogger(getClass)
implicit def logging2Logger(anything: Logging): Logger = anything.logger
}
Then you can use it whichever fits your style better:
class X with Logging {
logger.debug("foo")
debug("bar")
}
but this approach of course uses a logger instance per class instance.
Solution 7 - Logging
I pulled a bit of work form the Logging
trait of scalax
, and created a trait that also integrated a MessageFormat-based
library.
Then stuff kind of looks like this:
class Foo extends Loggable {
info( "Dude, I'm an {0} with {1,number,#}", "Log message", 1234 )
}
We like the approach so far.
Implementation:
trait Loggable {
val logger:Logger = Logging.getLogger(this)
def checkFormat(msg:String, refs:Seq[Any]):String =
if (refs.size > 0) msgfmtSeq(msg, refs) else msg
def trace(msg:String, refs:Any*) = logger trace checkFormat(msg, refs)
def trace(t:Throwable, msg:String, refs:Any*) = logger trace (checkFormat(msg, refs), t)
def info(msg:String, refs:Any*) = logger info checkFormat(msg, refs)
def info(t:Throwable, msg:String, refs:Any*) = logger info (checkFormat(msg, refs), t)
def warn(msg:String, refs:Any*) = logger warn checkFormat(msg, refs)
def warn(t:Throwable, msg:String, refs:Any*) = logger warn (checkFormat(msg, refs), t)
def critical(msg:String, refs:Any*) = logger error checkFormat(msg, refs)
def critical(t:Throwable, msg:String, refs:Any*) = logger error (checkFormat(msg, refs), t)
}
/**
* Note: implementation taken from scalax.logging API
*/
object Logging {
def loggerNameForClass(className: String) = {
if (className endsWith "$") className.substring(0, className.length - 1)
else className
}
def getLogger(logging: AnyRef) = LoggerFactory.getLogger(loggerNameForClass(logging.getClass.getName))
}
Solution 8 - Logging
Logging in 2020
I was really surprised that Scribe logging framework that I use at work isn't even mentioned here. What is more, it doesn't even appear on the first page in Google after searching "scala logging". But this page appears when googling it! So let me leave that here.
Main advantages of Scribe:
- it is NOT a slf4j wrapper
- supports also Scala.js and Scala Native
- it is really fast, check comparison here: https://www.matthicks.com/2018/02/scribe-2-fastest-jvm-logger-in-world.html
Solution 9 - Logging
Writer
, Monoid
and a Monad
implementation.
Solution 10 - Logging
You should have a look at the scalax library : http://scalax.scalaforge.org/ In this library, there is a Logging trait, using sl4j as backend. By using this trait, you can log quite easily (just use the logger field in the class inheriting the trait).
Solution 11 - Logging
Haven't tried it yet, but Configgy looks promising for both configuration and logging:
Solution 12 - Logging
Quick and easy forms.
Scala 2.10 and older:
import com.typesafe.scalalogging.slf4j.Logger
import org.slf4j.LoggerFactory
val logger = Logger(LoggerFactory.getLogger("TheLoggerName"))
logger.debug("Useful message....")
And build.sbt:
libraryDependencies += "com.typesafe" %% "scalalogging-slf4j" % "1.1.0"
Scala 2.11+ and newer:
import import com.typesafe.scalalogging.Logger
import org.slf4j.LoggerFactory
val logger = Logger(LoggerFactory.getLogger("TheLoggerName"))
logger.debug("Useful message....")
And build.sbt:
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0"
Solution 13 - Logging
After using slf4s and logula for a while, I wrote loglady
, a simple logging trait wrapping slf4j.
It offers an API similar to that of Python's logging library, which makes the common cases (basic string, simple formatting) trivial and avoids formatting boilerplate.
Solution 14 - Logging
I find very convenient using some kind of java logger, sl4j for example, with simple scala wrapper, which brings me such syntax
val #! = new Logger(..) // somewhere deep in dsl.logging.
object User with dsl.logging {
#! ! "info message"
#! dbg "debug message"
#! trace "var a=true"
}
In my opinion very usefull mixin of java proven logging frameworks and scala's fancy syntax.