Log4s

SLF4J向けの高性能Scalaラッパー。Scalaのマクロと値クラスを活用し、ランタイムオーバーヘッドを課さずにイディオムatic Scalaファサードを提供。JVM APIの一般的な使用パターンを頻繁に上回る性能を実現。

ロギングScalaSLF4J高性能マクロ値クラス

ライブラリ

Log4s

概要

Log4sはSLF4J向けの高性能Scalaラッパーです。Scalaのマクロと値クラスを活用し、ランタイムオーバーヘッドを課さずにイディオムatic Scalaファサードを提供。JVM APIの一般的な使用パターンを頻繁に上回る性能を実現し、2025年パフォーマンスとScalaらしさの両立を求めるプロジェクトでの採用が継続。コンパイル時最適化により、ランタイムパフォーマンスを損なうことなくScalaの利便性を提供する中規模から大規模プロジェクトでの実績を持つライブラリです。

詳細

Log4s 2025年版は、Scalaエコシステムにおいて「パフォーマンス志向のSLF4Jラッパー」としての地位を確立しています。最大の特徴は、Scalaマクロシステムを活用したコンパイル時最適化により、ログメッセージ構築のコストを実行時ではなくコンパイル時に解決することです。これにより、従来のJavaベースロギングフレームワークでは実現困難な、ゼロコストロギングを可能にしています。文字列補間や複雑な式もマクロで自動的にisEnabledチェックでラップされ、パフォーマンスを維持しながら直感的なAPIを提供しています。

主な特徴

  • マクロベース最適化: コンパイル時のログレベル判定による実行時オーバーヘッド排除
  • SLF4J完全互換: 既存のSLF4J設定とライブラリとのシームレス統合
  • 値クラス活用: Scalaの値クラスによるオブジェクト生成コストの最小化
  • イディオムatic API: Scalaらしい自然な記法でのログ記述
  • 自動ガード生成: 複雑な式の自動isEnabledラップ機能
  • MDC対応: SLF4JのMapped Diagnostic Context完全サポート

メリット・デメリット

メリット

  • コンパイル時最適化により実行時パフォーマンスが優秀
  • SLF4Jとの完全互換性により既存インフラとの統合が容易
  • Scalaマクロによる透明な最適化で開発体験が向上
  • 値クラス使用により従来のJavaラッパーより高性能
  • 文字列補間や複雑な式の自動最適化
  • 中規模から大規模プロジェクトでの豊富な実績

デメリット

  • マクロベースのため、デバッグ時にコンパイル後コードとの差異が生じる
  • Scala初学者にはマクロの動作理解が困難な場合がある
  • コンパイル時間がマクロ展開により若干増加する可能性
  • IDEによってはマクロ生成コードのサポートが限定的
  • Scala 3への移行で一部のマクロ機能に制約の可能性
  • エラーメッセージがマクロ展開により複雑になる場合がある

参考ページ

書き方の例

インストールとセットアップ

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

// SLF4J実装が必要(例:Logback)
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.4.14"

// Maven使用の場合
/*
<dependency>
    <groupId>org.log4s</groupId>
    <artifactId>log4s_2.13</artifactId>
    <version>1.10.0</version>
</dependency>
*/

基本的なロガー使用方法

import org.log4s._

class MyService {
  // クラス名から自動的にロガー名を生成
  private[this] val logger = getLogger
  
  def processData(data: String): Unit = {
    // 基本的なログ出力
    logger.info("データ処理を開始します")
    logger.debug(s"処理対象データ: $data")
    
    try {
      // ビジネスロジック
      val result = transformData(data)
      logger.info(s"データ処理が完了しました: $result")
      
    } catch {
      case ex: Exception =>
        logger.error(ex)("データ処理中にエラーが発生しました")
    }
  }
  
  private def transformData(data: String): String = {
    // 処理のシミュレーション
    Thread.sleep(100)
    data.toUpperCase
  }
}

// 使用例
object Main extends App {
  val service = new MyService()
  service.processData("sample data")
}

マクロによる自動最適化の活用

import org.log4s._

class PerformanceOptimizedService {
  private[this] val logger = getLogger
  
  def expensiveOperation(userId: Long, requestId: String): Unit = {
    // 常に文字列補間を使用可能(マクロが自動的に最適化)
    logger.debug(s"ユーザー $userId のリクエスト $requestId を処理開始")
    
    // 複雑な計算もマクロで自動ガード
    logger.trace(s"詳細情報: ${calculateComplexMetrics(userId)}")
    
    // 例外処理付きの複雑なログ
    logger.info(s"処理状況: ${getCurrentStatus()} - 進捗: ${getProgress()}%")
  }
  
  // 計算コストの高い関数
  private def calculateComplexMetrics(userId: Long): String = {
    // この計算はDEBUGレベルが無効の場合実行されない
    Thread.sleep(50) // 重い処理のシミュレーション
    s"User $userId metrics: load=0.8, memory=60%"
  }
  
  private def getCurrentStatus(): String = "PROCESSING"
  private def getProgress(): Int = 75
}

// コンパイル時に生成される最適化コード例
/*
logger.debug(s"ユーザー $userId のリクエスト $requestId を処理開始")
↓ マクロ展開後
if (logger.isDebugEnabled) {
  logger.debug(s"ユーザー $userId のリクエスト $requestId を処理開始")
}
*/

構造化ログとMDC使用

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 = {
    // MDC(Mapped Diagnostic Context)の設定
    Using.resource(setMDCContext(requestId, userId)) { _ =>
      logger.info(s"リクエスト処理開始: $action")
      
      try {
        processRequest(action)
        logger.info("リクエスト処理完了")
        
      } catch {
        case ex: Exception =>
          logger.error(ex)(s"リクエスト処理失敗: $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
    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("ユーザープロファイル取得中")
        Thread.sleep(100)
        
      case "update_settings" =>
        logger.info("設定更新処理実行中")
        Thread.sleep(200)
        
      case _ =>
        logger.warn(s"未知のアクション: $action")
    }
  }
}

// logback.xmlで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>
*/

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

高度なロガー設定とパフォーマンス測定

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) {
  // 明示的なロガー名指定
  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"ビジネス操作開始: $operationId")
    
    val operation = Future {
      // 複雑な処理のシミュレーション
      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"ビジネス操作完了: $operationId")
        perfLogger.info(s"操作性能: $operationId - ${duration}ms")
        auditLogger.info(s"監査ログ: 操作成功 - ID:$operationId, ユーザー:${getCurrentUser()}")
        
      case Failure(exception) =>
        val duration = ChronoUnit.MILLIS.between(startTime, Instant.now())
        appLogger.error(exception)(s"ビジネス操作失敗: $operationId")
        perfLogger.warn(s"操作失敗: $operationId - ${duration}ms - ${exception.getClass.getSimpleName}")
        auditLogger.error(s"監査ログ: 操作失敗 - ID:$operationId, エラー:${exception.getMessage}")
    }
    
    operation
  }
  
  private def validateData(data: Map[String, Any]): Unit = {
    appLogger.debug(s"データ検証中: ${data.keys.mkString(", ")}")
    
    if (data.isEmpty) {
      val error = new IllegalArgumentException("データが空です")
      appLogger.error(error)("データ検証失敗")
      throw error
    }
    
    appLogger.debug("データ検証完了")
  }
  
  private def processData(data: Map[String, Any]): Unit = {
    appLogger.debug("データ処理開始")
    
    // 処理時間のばらつきをシミュレート
    val processingTime = 100 + scala.util.Random.nextInt(200)
    Thread.sleep(processingTime)
    
    appLogger.debug(s"データ処理完了 (${processingTime}ms)")
  }
  
  private def persistResult(operationId: String, data: Map[String, Any]): Unit = {
    appLogger.debug(s"結果永続化: $operationId")
    
    // データベース操作のシミュレーション
    Thread.sleep(50)
    
    appLogger.debug("結果永続化完了")
  }
  
  private def getCurrentUser(): String = "user123" // ユーザー取得のスタブ
}

// テスト・デモ用のクラス
class LoggingDemo {
  private[this] val logger = getLogger
  
  def demonstrateLoggingLevels(): Unit = {
    logger.trace("トレースレベル: 最も詳細なデバッグ情報")
    logger.debug("デバッグレベル: 開発時のデバッグ情報")
    logger.info("情報レベル: 一般的な動作情報")
    logger.warn("警告レベル: 潜在的な問題")
    logger.error("エラーレベル: エラー情報")
  }
  
  def demonstrateExceptionLogging(): Unit = {
    try {
      throw new RuntimeException("テスト例外")
    } catch {
      case ex: Exception =>
        // 例外オブジェクトとメッセージの両方をログ
        logger.error(ex)("例外が発生しました")
        
        // 例外詳細情報付きログ
        logger.error(ex)(s"詳細情報: ${ex.getClass.getSimpleName} - ${ex.getMessage}")
    }
  }
  
  def demonstrateConditionalLogging(): Unit = {
    val complexData = generateComplexData()
    
    // 条件付きログ(マクロで自動最適化)
    logger.debug(s"複雑なデータ: ${complexData.mkString(", ")}")
    
    // 手動でのレベルチェック(通常は不要)
    if (logger.isInfoEnabled) {
      val summary = summarizeData(complexData)
      logger.info(s"データサマリー: $summary")
    }
  }
  
  private def generateComplexData(): List[String] = {
    // 計算コストの高い処理のシミュレーション
    (1 to 1000).map(i => s"item_$i").toList
  }
  
  private def summarizeData(data: List[String]): String = {
    s"件数: ${data.length}, 先頭: ${data.headOption.getOrElse("N/A")}"
  }
}

// 使用例とテスト
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()
  
  // ログレベルのデモ
  demo.demonstrateLoggingLevels()
  
  // 例外ログのデモ
  demo.demonstrateExceptionLogging()
  
  // 条件付きログのデモ
  demo.demonstrateConditionalLogging()
  
  // 非同期処理でのログ使用例
  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) // エラーを意図的に発生
  
  try {
    Await.result(Future.sequence(List(future1, future2)), 5.seconds)
  } catch {
    case _: Exception => 
      // 一部の操作が失敗することを想定
      println("一部の操作が失敗しましたが、これは意図的なデモです")
  }
  
  println("Log4sロギングデモが完了しました")
}

パフォーマンス比較とベンチマーク

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

class PerformanceBenchmark {
  // Log4s使用
  private[this] val log4sLogger = getLogger
  
  // 標準SLF4J使用
  private[this] val slf4jLogger = LoggerFactory.getLogger(this.getClass)
  
  def benchmarkLogging(iterations: Int = 100000): Unit = {
    println(s"$iterations 回のログ出力パフォーマンステスト")
    
    // ウォームアップ
    warmup()
    
    // Log4sベンチマーク
    val log4sTime = measureTime {
      for (i <- 1 to iterations) {
        val randomValue = Random.nextInt(1000)
        log4sLogger.debug(s"Log4s iteration $i with value $randomValue")
      }
    }
    
    // SLF4Jベンチマーク
    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(最適化なし)ベンチマーク
    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 (最適化済み):      ${slf4jTime}%,d ms") 
    println(f"SLF4J (最適化なし):      ${slf4jUnoptimizedTime}%,d ms")
    println(f"Log4s vs SLF4J改善率:   ${((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
  }
}

// ベンチマーク実行
object BenchmarkRunner extends App {
  val benchmark = new PerformanceBenchmark()
  
  // DEBUGレベルを無効にしてパフォーマンス測定
  // logback.xmlでレベルをINFOに設定することを想定
  benchmark.benchmarkLogging(1000000)
}