Scala Logging

repository·main·Indexed 21 days ago

https://github.com/scala-garden/scala-logging

A high-performance wrapper around SLF4J that uses Scala macros to automatically apply the 'check-enabled' idiom, ensuring expensive string interpolation only occurs if the target log level is active. It provides traits like LazyLogging, StrictLogging, and AnyLogging for automatic logger instantiation, as well as LoggerTakingImplicit and CanLog[A] for including contextual metadata in log messages.

Tokens
2.8K
Snippets
9
Records
10
Agent score
76%

What's inside scala-logging

  1. How Scala Logging handles string interpolation

    main

    Scala Logging uses macros to transform idiomatic Scala string interpolation (e.g., logger.error(s"msg $arg")) into efficient SLF4J parameterized logging (e.g., logger.error("msg {}", arg)). This ensures that expensive string construction only happens if the log level is enabled.

    Limitations:

    • This transformation only works when string interpolation is used directly inside the logging statement (the message must be static at compile time).
    • It does not work when you are logging an exception alongside string interpolation (due to SLF4J API limitations).
    // Idiomatic Scala (transformed to SLF4J style by macros)
    logger.error(s"my log message: $arg1 $arg2 $arg3")
    
    // Resulting SLF4J call
    logger.error("my log message: {} {} {}", arg1, arg2, arg3)
  2. Use LazyLogging, StrictLogging, or AnyLogging traits

    main

    Instead of manually creating a logger, you can mix in traits from com.typesafe.scalalogging to provide a logger member automatically. The logger name will default to the name of the class using the trait.

    • LazyLogging: Use when creating many objects repetitively (the logger is initialized lazily).
    • StrictLogging: Use by default, especially for singletons or when log methods are guaranteed to be called (the logger is initialized strictly).
    • AnyLogging: Use when writing a trait that needs access to a logger but doesn't want to decide on a specific implementation (defines an abstract logger).
    import com.typesafe.scalalogging.{LazyLogging, StrictLogging, AnyLogging}
    
    class LazyLoggingExample extends LazyLogging {
      logger.debug("This is Lazy Logging ;-)")
      logger.whenDebugEnabled {
        // This block only executes if debug is enabled
        println("Expensive operation")
      }
    }
    
    class StrictLoggingExample extends StrictLogging {
      logger.debug("This is Strict Logging ;-)")
    }
    
    class AnyLoggingExample extends AnyLogging {
      override protected val logger: Logger = Logger("name")
      logger.info("This is Any Logging ;-)")
    }
  3. Install Scala Logging

    main

    To use Scala Logging, add it to your build.sbt file. Ensure you also have a compatible SLF4J logging backend (like Logback) configured in your dependencies.

    Prerequisites:

    • Java 11 or higher (Scala Logging 3.x supports Java 8)
    • Scala 2.12, 2.13, or 3.0
    • An SLF4J-compatible backend (e.g., Logback)

    Note on Versions:

    • Use Scala Logging 3.x for SLF4J 1.x.
    • Use Scala Logging 2.x for Scala 2.10 compatibility.
    // Add Scala Logging
    libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "4.0.0-RC1"
    
    // Add a compatible backend like Logback
    libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.14"
  4. Enriched logging with implicit context in Scala 2

    main

    In Scala 2, LoggerTakingImplicitImpl provides a mechanism for enriched logging by accepting an implicit parameter of type A. This allows the logging macros to capture and include additional context (the implicit value) in the log messages.

    Supported log levels include:

    • error
    • warn
    • info
    • debug
    • trace

    Each level supports several method signatures:

    • Simple message: message: String
    • Message with cause: message: String, cause: Throwable
    • Message with arguments: message: String, args: Any*
    • Message with SLF4J Marker: marker: Marker, message: String (and variations with cause or args)

    Additionally, each level provides a when[Level]Enabled(body: Unit) method, which uses macros to execute the provided block only if that specific log level is enabled, avoiding unnecessary computation.

  5. Create a Logger instance

    main

    You can create a Logger instance from the com.typesafe.scalalogging package using several factory methods in the Logger companion object.

    import com.typesafe.scalalogging.Logger
    import org.slf4j.LoggerFactory
    
    // By name
    val logger = Logger("name")
    
    // By SLF4J logger instance
    val logger = Logger(LoggerFactory.getLogger("name"))
    
    // By class name
    val logger = Logger(getClass.getName)
    
    // By class
    val logger = Logger(classOf[MyClass])
    
    // By runtime class (using implicit class tag)
    val logger = Logger[MyClass]
  6. Include contextual information with LoggerTakingImplicit

    main

    The LoggerTakingImplicit class allows you to include contextual data (like a CorrelationId) in every log message by providing an implicit CanLog[A] instance. This is useful for passing correlation IDs or other metadata through call stacks.

    • Use Logger.takingImplicit[A](name) to create the logger.
    • Implement CanLog[A] to define how the context A is formatted into the message or how it interacts with the MDC (Mapped Diagnostic Context).
    • Use logger.canLogEv.getContext() to retrieve the current context object.
    import com.typesafe.scalalogging.{Logger, CanLog}
    import org.slf4j.MDC
    
    case class CorrelationId(value: String)
    
    implicit case object CanLogCorrelationId extends CanLog[CorrelationId] {
      override def logMessage(originalMsg: String, a: CorrelationId): String = {
        MDC.put("correlationId", a.value)
        originalMsg
      }
    
      override def afterLog(a: CorrelationId): Unit = {
        MDC.remove("correlationId")
      }
    }
    
    // Usage
    implicit val correlationId = CorrelationId("ID")
    val logger = Logger.takingImplicit[CorrelationId]("test")
    
    logger.info("Test") // Logs with the correlationId in MDC
  7. Log messages at different severity levels with LoggerImpl

    main

    The LoggerImpl class provides standard logging methods for various severity levels: trace, debug, info, warn, and error. Each level supports several method signatures to handle simple messages, messages with causes (Throwables), messages with variable arguments (args), and messages associated with an SLF4J Marker.

    // Basic message
    logger.info("Processing request")
    
    // Message with arguments
    logger.debug("User {} logged in", userId)
    
    // Message with a cause (Throwable)
    logger.error("Failed to connect to database", connectionException)
    
    // Message with a Marker
    logger.warn(myMarker, "Security threshold reached")
  8. Optimize performance using whenEnabled methods

    main

    To avoid the overhead of constructing log messages or executing code when a specific logging level is disabled, use the when[Level]Enabled methods. These methods accept a block of code (body: Unit) that will only be executed if the corresponding logging level is currently enabled in the underlying SLF4J configuration.

    // Only executes the block if DEBUG level is enabled
    logger.whenDebugEnabled {
      val complexData = performExpensiveCalculation()
      logger.debug(s"Result is: $complexData")
    }
  9. Use LoggerTakingImplicit for contextual logging

    main

    The takingImplicit method allows you to create a LoggerTakingImplicit[A] instance. This is used when you want to leverage implicit CanLog[A] evidence to attach specific context (like a CorrelationId) to your log messages. You can initialize this by providing a name, a class, or a ClassTag.

    import com.typesafe.scalalogging.Logger
    
    // Create a logger with implicit context for a specific name
    val logger = Logger.takingImplicit["application"]
    
    // Create a logger with implicit context for a specific class
    val logger = Logger.takingImplicit[MyClass]
    
    // Create a logger with implicit context for a specific type and class tag
    // Note: The syntax in the source suggests takingImplicit[T, A] for ClassTag and CanLog
    val logger = Logger.takingImplicit[MyClass, CorrelationId]
  10. Implement CanLog[A] for structured logging context

    main

    To use LoggerTakingImplicit with custom structured logging context, you must provide an implementation of the CanLog[A] trait. This trait defines how the logger should format the relationship between a log message and its implicit context A.

    Implement the following methods:

    • logMessage(originalMsg: String, context: A): String: Defines how to combine the log message string with the context object.
    • getContext()(implicit context: A): A: Retrieves the implicit context from the scope.
    • afterLog(context: A): Unit (optional): A hook called after a log event occurs, useful for side effects or telemetry.
    import com.typesafe.scalalogging.CanLog
    
    // Example implementation for a simple String context
    object StringCanLog extends CanLog[String] {
      override def logMessage(originalMsg: String, context: String): String = 
        s"[$context] $originalMsg"
    
      override def getContext()(implicit context: String): String = context
    }