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