Log4s

High-performance Scala wrapper for SLF4J. Leverages Scala's macros and value classes to provide idiomatic Scala facade without imposing runtime overhead. Frequently outperforms common usage patterns of JVM APIs.

LoggingScalaSLF4JHigh PerformanceMacrosValue Classes

Library

Log4s

Overview

Log4s is a high-performance Scala wrapper for SLF4J. Leveraging Scala's macros and value classes to provide idiomatic Scala facade without imposing runtime overhead. Frequently outperforms common usage patterns of JVM APIs, with adoption continuing in projects seeking both performance and Scala-like characteristics in 2025. Provides Scala convenience without compromising runtime performance through compile-time optimization, with a track record in medium to large-scale projects.

Details

The 2025 version of Log4s has established its position as a "performance-oriented SLF4J wrapper" in the Scala ecosystem. Its key feature is compile-time optimization leveraging the Scala macro system, resolving the cost of log message construction at compile time rather than runtime. This enables zero-cost logging that is difficult to achieve with traditional Java-based logging frameworks. String interpolations and complex expressions are automatically wrapped with isEnabled checks through macros, providing an intuitive API while maintaining performance.

Key Features

  • Macro-based Optimization: Elimination of runtime overhead through compile-time log level determination
  • Complete SLF4J Compatibility: Seamless integration with existing SLF4J configurations and libraries
  • Value Class Utilization: Minimization of object creation costs through Scala's value classes
  • Idiomatic API: Natural Scala-like notation for log descriptions
  • Automatic Guard Generation: Automatic isEnabled wrapping for complex expressions
  • MDC Support: Complete support for SLF4J's Mapped Diagnostic Context

Pros and Cons

Pros

  • Excellent runtime performance through compile-time optimization
  • Easy integration with existing infrastructure through complete SLF4J compatibility
  • Improved development experience through transparent optimization via Scala macros
  • Higher performance than traditional Java wrappers through value class usage
  • Automatic optimization of string interpolations and complex expressions
  • Rich track record in medium to large-scale projects

Cons

  • Differences between source and compiled code during debugging due to macro-based approach
  • Macro operation understanding can be difficult for Scala beginners
  • Potential slight increase in compile time due to macro expansion
  • Limited support for macro-generated code in some IDEs
  • Potential constraints on some macro features during Scala 3 migration
  • Error messages may become complex due to macro expansion

Reference Pages

Usage Examples

Installation and Setup

// build.sbt
libraryDependencies += "org.log4s" %% "log4s" % "1.10.0"

// SLF4J implementation required (e.g., Logback)
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.14"

// When using Maven
/*
<dependency>
    <groupId>org.log4s</groupId>
    <artifactId>log4s_2.13</artifactId>
    <version>1.10.0</version>
</dependency>
*/

Basic Logger Usage

import org.log4s._

class MyService {
  // Automatically generate logger name from class name
  private[this] val logger = getLogger
  
  def processData(data: String): Unit = {
    // Basic log output
    logger.info("Starting data processing")
    logger.debug(s"Processing target data: $data")
    
    try {
      // Business logic
      val result = transformData(data)
      logger.info(s"Data processing completed: $result")
      
    } catch {
      case ex: Exception =>
        logger.error(ex)("Error occurred during data processing")
    }
  }
  
  private def transformData(data: String): String = {
    // Processing simulation
    Thread.sleep(100)
    data.toUpperCase
  }
}

// Usage example
object Main extends App {
  val service = new MyService()
  service.processData("sample data")
}

Leveraging Automatic Optimization through Macros

import org.log4s._

class PerformanceOptimizedService {
  private[this] val logger = getLogger
  
  def expensiveOperation(userId: Long, requestId: String): Unit = {
    // String interpolation always available (automatically optimized by macros)
    logger.debug(s"Starting processing request $requestId for user $userId")
    
    // Complex calculations also automatically guarded by macros
    logger.trace(s"Detailed info: ${calculateComplexMetrics(userId)}")
    
    // Complex logs with exception handling
    logger.info(s"Processing status: ${getCurrentStatus()} - Progress: ${getProgress()}%")
  }
  
  // Computationally expensive function
  private def calculateComplexMetrics(userId: Long): String = {
    // This calculation is not executed when DEBUG level is disabled
    Thread.sleep(50) // Heavy processing simulation
    s"User $userId metrics: load=0.8, memory=60%"
  }
  
  private def getCurrentStatus(): String = "PROCESSING"
  private def getProgress(): Int = 75
}

// Example of optimized code generated at compile time
/*
logger.debug(s"Starting processing request $requestId for user $userId")
↓ After macro expansion
if (logger.isDebugEnabled) {
  logger.debug(s"Starting processing request $requestId for user $userId")
}
*/

Structured Logging and MDC Usage

import org.log4s._
import org.slf4j.MDC
import scala.util.Using

class WebRequestHandler {
  private[this] val logger = getLogger
  
  def handleRequest(requestId: String, userId: Option[Long], action: String): Unit = {
    // Setup MDC (Mapped Diagnostic Context)
    Using.resource(setMDCContext(requestId, userId)) { _ =>
      logger.info(s"Request processing started: $action")
      
      try {
        processRequest(action)
        logger.info("Request processing completed")
        
      } catch {
        case ex: Exception =>
          logger.error(ex)(s"Request processing failed: $action")
          throw ex
      }
    }
  }
  
  private def setMDCContext(requestId: String, userId: Option[Long]): AutoCloseable = {
    MDC.put("requestId", requestId)
    userId.foreach(id => MDC.put("userId", id.toString))
    
    // AutoCloseable for resource management
    new AutoCloseable {
      override def close(): Unit = {
        MDC.remove("requestId")
        MDC.remove("userId")
      }
    }
  }
  
  private def processRequest(action: String): Unit = {
    action match {
      case "user_profile" => 
        logger.debug("Retrieving user profile")
        Thread.sleep(100)
        
      case "update_settings" =>
        logger.info("Executing settings update process")
        Thread.sleep(200)
        
      case _ =>
        logger.warn(s"Unknown action: $action")
    }
  }
}

// Example logback.xml configuration utilizing MDC
/*
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] [%X{userId}] %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  
  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
*/

// Usage example
object WebApp extends App {
  val handler = new WebRequestHandler()
  
  handler.handleRequest("req_001", Some(12345L), "user_profile")
  handler.handleRequest("req_002", None, "update_settings")
}

Advanced Logger Configuration and Performance Measurement

import org.log4s._
import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Success, Failure}
import java.time.Instant
import java.time.temporal.ChronoUnit

class AdvancedLoggingService(implicit ec: ExecutionContext) {
  // Explicit logger name specification
  private[this] val appLogger = getLogger("myapp.service")
  private[this] val perfLogger = getLogger("myapp.performance")
  private[this] val auditLogger = getLogger("myapp.audit")
  
  def performBusinessOperation(operationId: String, data: Map[String, Any]): Future[String] = {
    val startTime = Instant.now()
    
    appLogger.info(s"Business operation started: $operationId")
    
    val operation = Future {
      // Complex processing simulation
      validateData(data)
      processData(data)
      persistResult(operationId, data)
      
      s"Operation $operationId completed successfully"
    }
    
    operation.onComplete {
      case Success(result) =>
        val duration = ChronoUnit.MILLIS.between(startTime, Instant.now())
        appLogger.info(s"Business operation completed: $operationId")
        perfLogger.info(s"Operation performance: $operationId - ${duration}ms")
        auditLogger.info(s"Audit log: Operation success - ID:$operationId, User:${getCurrentUser()}")
        
      case Failure(exception) =>
        val duration = ChronoUnit.MILLIS.between(startTime, Instant.now())
        appLogger.error(exception)(s"Business operation failed: $operationId")
        perfLogger.warn(s"Operation failed: $operationId - ${duration}ms - ${exception.getClass.getSimpleName}")
        auditLogger.error(s"Audit log: Operation failed - ID:$operationId, Error:${exception.getMessage}")
    }
    
    operation
  }
  
  private def validateData(data: Map[String, Any]): Unit = {
    appLogger.debug(s"Validating data: ${data.keys.mkString(", ")}")
    
    if (data.isEmpty) {
      val error = new IllegalArgumentException("Data is empty")
      appLogger.error(error)("Data validation failed")
      throw error
    }
    
    appLogger.debug("Data validation completed")
  }
  
  private def processData(data: Map[String, Any]): Unit = {
    appLogger.debug("Data processing started")
    
    // Simulate processing time variations
    val processingTime = 100 + scala.util.Random.nextInt(200)
    Thread.sleep(processingTime)
    
    appLogger.debug(s"Data processing completed (${processingTime}ms)")
  }
  
  private def persistResult(operationId: String, data: Map[String, Any]): Unit = {
    appLogger.debug(s"Persisting result: $operationId")
    
    // Database operation simulation
    Thread.sleep(50)
    
    appLogger.debug("Result persistence completed")
  }
  
  private def getCurrentUser(): String = "user123" // User retrieval stub
}

// Test/Demo class
class LoggingDemo {
  private[this] val logger = getLogger
  
  def demonstrateLoggingLevels(): Unit = {
    logger.trace("Trace level: Most detailed debug information")
    logger.debug("Debug level: Development debug information")
    logger.info("Info level: General operational information")
    logger.warn("Warn level: Potential issues")
    logger.error("Error level: Error information")
  }
  
  def demonstrateExceptionLogging(): Unit = {
    try {
      throw new RuntimeException("Test exception")
    } catch {
      case ex: Exception =>
        // Log both exception object and message
        logger.error(ex)("An exception occurred")
        
        // Log with exception details
        logger.error(ex)(s"Details: ${ex.getClass.getSimpleName} - ${ex.getMessage}")
    }
  }
  
  def demonstrateConditionalLogging(): Unit = {
    val complexData = generateComplexData()
    
    // Conditional logging (automatically optimized by macros)
    logger.debug(s"Complex data: ${complexData.mkString(", ")}")
    
    // Manual level check (usually unnecessary)
    if (logger.isInfoEnabled) {
      val summary = summarizeData(complexData)
      logger.info(s"Data summary: $summary")
    }
  }
  
  private def generateComplexData(): List[String] = {
    // Computationally expensive processing simulation
    (1 to 1000).map(i => s"item_$i").toList
  }
  
  private def summarizeData(data: List[String]): String = {
    s"Count: ${data.length}, First: ${data.headOption.getOrElse("N/A")}"
  }
}

// Usage examples and tests
object AdvancedLoggingExample extends App {
  import scala.concurrent.ExecutionContext.Implicits.global
  import scala.concurrent.duration._
  import scala.concurrent.Await
  
  val service = new AdvancedLoggingService()
  val demo = new LoggingDemo()
  
  // Log level demo
  demo.demonstrateLoggingLevels()
  
  // Exception logging demo
  demo.demonstrateExceptionLogging()
  
  // Conditional logging demo
  demo.demonstrateConditionalLogging()
  
  // Asynchronous processing log usage example
  val testData = Map(
    "userId" -> 12345,
    "action" -> "update_profile",
    "timestamp" -> System.currentTimeMillis()
  )
  
  val future1 = service.performBusinessOperation("op_001", testData)
  val future2 = service.performBusinessOperation("op_002", Map.empty) // Intentionally trigger error
  
  try {
    Await.result(Future.sequence(List(future1, future2)), 5.seconds)
  } catch {
    case _: Exception => 
      // Expecting some operations to fail
      println("Some operations failed, but this is an intentional demo")
  }
  
  println("Log4s logging demo completed")
}

Performance Comparison and Benchmarking

import org.log4s._
import org.slf4j.LoggerFactory
import scala.util.Random

class PerformanceBenchmark {
  // Using Log4s
  private[this] val log4sLogger = getLogger
  
  // Using standard SLF4J
  private[this] val slf4jLogger = LoggerFactory.getLogger(this.getClass)
  
  def benchmarkLogging(iterations: Int = 100000): Unit = {
    println(s"Performance test for $iterations log outputs")
    
    // Warmup
    warmup()
    
    // Log4s benchmark
    val log4sTime = measureTime {
      for (i <- 1 to iterations) {
        val randomValue = Random.nextInt(1000)
        log4sLogger.debug(s"Log4s iteration $i with value $randomValue")
      }
    }
    
    // SLF4J benchmark
    val slf4jTime = measureTime {
      for (i <- 1 to iterations) {
        val randomValue = Random.nextInt(1000)
        if (slf4jLogger.isDebugEnabled) {
          slf4jLogger.debug(s"SLF4J iteration $i with value $randomValue")
        }
      }
    }
    
    // SLF4J (unoptimized) benchmark
    val slf4jUnoptimizedTime = measureTime {
      for (i <- 1 to iterations) {
        val randomValue = Random.nextInt(1000)
        slf4jLogger.debug(s"SLF4J unoptimized iteration $i with value $randomValue")
      }
    }
    
    println(f"Log4s:                ${log4sTime}%,d ms")
    println(f"SLF4J (optimized):    ${slf4jTime}%,d ms") 
    println(f"SLF4J (unoptimized):  ${slf4jUnoptimizedTime}%,d ms")
    println(f"Log4s vs SLF4J improvement: ${((slf4jTime.toDouble - log4sTime.toDouble) / slf4jTime * 100)}%.1f%%")
  }
  
  private def warmup(): Unit = {
    for (_ <- 1 to 10000) {
      log4sLogger.debug("warmup")
      slf4jLogger.debug("warmup")
    }
  }
  
  private def measureTime(operation: => Unit): Long = {
    val startTime = System.currentTimeMillis()
    operation
    System.currentTimeMillis() - startTime
  }
}

// Benchmark execution
object BenchmarkRunner extends App {
  val benchmark = new PerformanceBenchmark()
  
  // Measure performance with DEBUG level disabled
  // Assumes level is set to INFO in logback.xml
  benchmark.benchmarkLogging(1000000)
}