1. Introduction

Logging is one of the most basic and important parts of building a software system. It helps to troubleshoot our code more easily in case of any issues. As a result, in any programming language, there are a multitude of logging libraries.

In this tutorial, we’ll look at Scala-Logging, one of the most popular logging libraries in Scala. It’s a Scala wrapper over the popular SLF4J library.

2. Setup

Firstly, let’s add the scala-logging library dependency. Since it depends on SLF4J, we need to add a logger implementation library as well. We’ll use logback as the logging backend in this tutorial. Let’s add the necessary dependencies to build.sbt:

libraryDependencies ++= Seq(
    "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",
    "ch.qos.logback" % "logback-classic" % "1.3.5"
)

It’s worth noticing that we’re using logback-classic version 1.3.5 since 1.3.x supports JDK8. The versions from 1.4.x are supported only for JDK 11 and above. If the wrong versions are used, we’ll get a runtime exception.

Additionally, we’ll add basic configurations needed for the logback customization in logback.xml in the resources directory:

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
    </encoder>
  </appender>
  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

This configuration logs all messages of severity INFO and above. The lesser severity messages such as DEBUG and TRACE won’t be logged.

3. Basic Logging

Now that the dependencies are added, let’s look at how we can create a logger and use it.

3.1. Create a Logger Instance

Scala-Logger provides multiple ways to create logger instances.

We can pass in any name to create a logger with that name. This name is used along with the log messages:

val logger = Logger("BaeldungLogger")

We can also create a logger for the current class by using the getClass.getName() method:

val logger = Logger(getClass.getName)

Additionally, we can just pass the class to the logger instance to create the logger for that class:

val logger = Logger(classOf[ScalaLoggingSample])
val logger = Logger[ScalaLoggingSample]

It’s also possible to pass in an instance of SLF4J logger, which is then used by Scala-Logging as an underlying instance:

val logger = Logger(LoggerFactory.getLogger("FromSlf4jLogger"))

All the loggers created above are equivalent. As mentioned above, it’s a Scala wrapper over the SLF4J logger instance.

3.2. Using the Logger

Now that the logger instance is created, let’s look at how we can log messages. We can use the relevant level from the logger instance as per our requirement:

logger.trace("This is a trace message")
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warn("This is a warning message")
logger.error("This is an error message")

At runtime, the calls get delegated to the underlying logger implementation, in this case, logback-classic. Consequently, if the logger implementation library isn’t available at runtime, we get a runtime exception.

Any logger customization needs to be done in the underlying logger instance. So, in this particular case, we can configure logback.xml to make logging changes.

3.3. Conditional Logging

Another useful feature of this library is the ability to add conditional logs based on the log level:

logger.whenDebugEnabled {
    // This whole block is executed only if logger level is DEBUG
    // We can add additional calculation to be done during debugging
    logger.debug("additional information for debugging")
}

The code block within whenDebugEnabled() is executed only if the log level is DEBUG. This helps avoid unnecessary boilerplate code like ifelse checks to add logs based on the levels.

4. Lazy and Strict Loggers

In the previous sections, we instantiated loggers by using the Logger() constructor. Scala-Logging provides two additional traits, LazyLogging and StrictLogging, which can be mixed in with the required classes. These traits provide the logger instances where they are mixed in.

Let’s look examples where these traits get used:

class LazyLoggingSample extends LazyLogging {
  logger.info("This is from lazy logging")
}
class StrictLoggingSample extends StrictLogging {
  logger.info("This is from strict logging")
}

The logger instances are created as lazy val if we use the LazyLogging trait. This is particularly good if a lot of instances of the mixed-in class are created.

On the other hand, we can use StrictLogging if we’re creating a singleton instance or only have a few instances.

5. Advanced Logging Concepts

So far we’ve looked at the basic logging scenarios. However, Scala-Logging provides more advanced features that make logging operations easier and more efficient.

Let’s look at some of those advanced features in this section.

5.1. Logging With Context

Sometimes, it’s important to log the contextual information along with the log messages. This helps to identify the problem more clearly from the logs. We can, of course, pass the relevant information along with the log messages. However, it becomes too cumbersome to explicitly pass all the required information for every log statement.

Scala-Logging enables this using an implicit CanLog instance, which is then used by the logger while logging the messages.

Let’s implement a very simple contextual logger. For this example, let’s log the id of every request as contextual information.

Firstly, we need to add a case class to hold the requestId:

case class RequestId(id: String)

Secondly, let’s create an implicit case object that extends the CanLog type from Scala-Logging. We need to override the method logMessage() and can add requestId to the log message:

object LoggingImplicits {
  implicit case object WithRequestId extends CanLog[RequestId] {
    override def logMessage(originalMsg: String, a: RequestId): String =
      s"[REQ: ${a.id}] $originalMsg"
  }
}

We can now create an instance of a logger using the method Logger.takingImplicit() that takes the context as an implicit parameter:

import LoggingImplicits._
val ctxLogger = Logger.takingImplicit[RequestId]("ContextLogger")

Finally, we can invoke the logger using the relevant method, provided that the context RequestId is available in the implicit scope:

implicit val reqId = RequestId("user-1234")
val message = "This is a message with user request context"
ctxLogger.info(message)

This logs the content to the console:

Logger with context information

Notably, the context information is logged along with the original message.

5.2. String Interpolation

In SLF4J implementations, the interpolation is done using the placeholder {}. Since Scala’s string interpolation is easier to use than that of SLF4J, it makes sense to utilize it. However, when we use string interpolation, the entire interpolated content is passed as a single string to the logger instance, instead of varargs.

Some log aggregators use the SLF4J vararg parameters for grouping purposes, and hence s-interpolated strings won’t be processed correctly.

To avoid this problem, the s-interpolation within the log methods of Scala-Logger instances is converted into the equivalent SLF4J representation.

For instance, let’s use the log message in Scala-Logging:

logger.info(s"$capital is the capital of $country")

Scala-Logger converts this statement into the equivalent SLF4J-style interpolation:

slf4jLogger.info("{} is the capital of {}", capital, country)

6. Conclusion

In this article, we looked at the Scala-Logging library in detail. We discussed different ways in which we can create logger instances. We also saw how this library makes performing logging operations easier using different wrapper methods.

As always, the sample code used in this tutorial is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.