Logging in Scala

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.


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:

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

  • Logula, a Log4J wrapper written by Coda Hale. Used to like this one, but now it's abandoned.
  • configgy, a java.util.logging wrapper that used to be popular in the earlier days of Scala. Now abandoned.

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 ;-)")
}

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

Log.scala Slf4jLog.scala

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

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 to VM 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>