Scribe

JVM上最速と謳われるロギングフレームワーク。Scalaでゼロから構築され、プログラマティック設定が可能。従来のJavaロギングフレームワークのラッパーではなく、マクロによりコンパイル時最適化を実現。リアルタイム設定変更をサポート。

ロギングScala高性能JVM最速プログラマティック設定マクロ

ライブラリ

Scribe

概要

ScribeはJVM上最速と謳われるロギングフレームワークです。Scalaでゼロから構築され、プログラマティック設定が可能。従来のJavaロギングフレームワークのラッパーではなく、マクロによりコンパイル時最適化を実現。リアルタイム設定変更をサポートし、2025年パフォーマンスが最重要なScalaアプリケーションでの採用が拡大。純Scala実装とマクロベースの最適化により、従来のJavaベースソリューションを上回る性能を実現し、高負荷システムでの選択例が増加しています。

詳細

Scribe 2025年版は、「JVM最速ロギングフレームワーク」として確固たる地位を築いています。従来のJavaロギングフレームワークに依存せず、Scalaでゼロから設計されており、ロギングに対する全く異なるアプローチを提供。設定ファイルや追加依存関係の必要なく、高速で効果的なロギングを実現します。Scalaマクロを活用してコンパイル時に可能な限りの最適化を行い、本番アプリケーションでのロギング処理がパフォーマンスに与える影響を最小限に抑制しています。

主な特徴

  • JVM最速性能: マクロベースのコンパイル時最適化による業界最高レベルの速度
  • 純Scala実装: Java依存のない完全Scala設計による最適化
  • プログラマティック設定: 設定ファイル不要の柔軟なコード内設定
  • リアルタイム再設定: 実行時での動的な設定変更サポート
  • ゼロ依存: 追加依存関係なしの軽量実装
  • クロスプラットフォーム: JVM、Scala.js、ScalaNativeでの動作対応

メリット・デメリット

メリット

  • JVM環境で最高レベルのロギング性能を実現
  • 純Scala実装によりScalaエコシステムとの完全統合
  • 設定ファイル不要でプログラマティックな柔軟性を提供
  • リアルタイム設定変更により運用時の動的調整が可能
  • 軽量で依存関係のない単体ライブラリとして導入容易
  • マクロ最適化により本番環境でのパフォーマンス影響を最小化

デメリット

  • 従来のJavaロギングフレームワークとの互換性がない
  • Scala初学者にはプログラマティック設定の学習コストが高い
  • 既存のJavaエコシステム(Logback、Log4j等)との統合が困難
  • 企業環境での実績がまだ限定的
  • マクロベースのため、デバッグ時の挙動理解が複雑
  • エラー処理やフォールバック機能が他のフレームワークより少ない

参考ページ

書き方の例

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

// build.sbt
libraryDependencies += "com.outr" %% "scribe" % "3.16.1"

// クロスプラットフォームプロジェクト(JVM, JS, Native)
libraryDependencies += "com.outr" %%% "scribe" % "3.16.1"

// SLF4J相互運用性が必要な場合
libraryDependencies += "com.outr" %% "scribe-slf4j" % "3.16.1"

// JSON出力サポートが必要な場合
libraryDependencies += "com.outr" %% "scribe-json" % "3.16.1"

ゼロインポート・ゼロミックスインロギング

// インポートやトレイトのミックスインなしで直接使用可能
class MyApplication {
  def start(): Unit = {
    scribe.info("アプリケーション開始")
    
    doSomethingImportant()
    
    scribe.info("アプリケーション処理完了")
  }
  
  def doSomethingImportant(): Unit = {
    scribe.debug("重要な処理を実行中")
    
    try {
      // ビジネスロジック
      processData()
      scribe.info("データ処理が正常に完了")
      
    } catch {
      case ex: Exception =>
        scribe.error("データ処理中にエラーが発生", ex)
        throw ex
    }
  }
  
  private def processData(): Unit = {
    scribe.trace("詳細なトレース情報")
    Thread.sleep(100) // 処理のシミュレーション
  }
}

// 使用例 - 設定なしで即座に動作
object ZeroConfigExample extends App {
  val app = new MyApplication()
  app.start()
}

// 出力例:
// 2025.01.02 19:05:47:342 [main] INFO MyApplication:5 - アプリケーション開始
// 2025.01.02 19:05:47.342 [main] DEBUG MyApplication.doSomethingImportant:12 - 重要な処理を実行中
// 2025.01.02 19:05:47.450 [main] INFO MyApplication:17 - データ処理が正常に完了
// 2025.01.02 19:05:47.451 [main] INFO MyApplication:7 - アプリケーション処理完了

伝統的なロガーアプローチ

import scribe.Logger

class TraditionalLoggingService {
  // 明示的なロガー作成
  val logger: Logger = Logger("TraditionalLoggingService")
  
  def performOperation(operationId: String): Unit = {
    logger.info(s"操作開始: $operationId")
    
    val startTime = System.currentTimeMillis()
    
    try {
      executeBusinessLogic(operationId)
      
      val duration = System.currentTimeMillis() - startTime
      logger.info(s"操作完了: $operationId (実行時間: ${duration}ms)")
      
    } catch {
      case ex: Exception =>
        logger.error(s"操作失敗: $operationId", ex)
        throw ex
    }
  }
  
  private def executeBusinessLogic(operationId: String): Unit = {
    logger.debug(s"ビジネスロジック実行中: $operationId")
    
    // 複雑な処理のシミュレーション
    Thread.sleep(scala.util.Random.nextInt(200) + 50)
    
    if (scala.util.Random.nextDouble() < 0.1) {
      throw new RuntimeException(s"処理エラー: $operationId")
    }
    
    logger.debug(s"ビジネスロジック完了: $operationId")
  }
}

// 使用例
object TraditionalExample extends App {
  val service = new TraditionalLoggingService()
  
  for (i <- 1 to 5) {
    try {
      service.performOperation(s"op_$i")
    } catch {
      case _: Exception =>
        // エラーは既にログ出力済み
    }
  }
}

プログラマティック設定とカスタマイズ

import scribe._
import scribe.format._
import scribe.writer.FileWriter

object ConfigurableLoggingExample extends App {
  
  // ログレベル設定
  Logger.root
    .clearHandlers()
    .clearModifiers()
    .withHandler(minimumLevel = Some(Level.Debug))
    .replace()
  
  // カスタムフォーマッター作成
  val customFormatter: Formatter = formatter"[$threadName] $positionAbbreviated - $message$newLine"
  
  // コンソール出力用の設定
  Logger.root
    .clearHandlers()
    .withHandler(formatter = customFormatter)
    .replace()
  
  // ファイル出力の設定
  val fileLogger = Logger("file-logger")
    .withHandler(
      writer = FileWriter("logs" / ("app-" % year % "-" % month % "-" % day % ".log")),
      formatter = formatter"$date $levelPaddedRight $classNameAbbreviated.$methodName:$line - $message$newLine"
    )
    .replace()
  
  // 複数の出力先を持つロガー設定
  val multiOutputLogger = Logger("multi-output")
    .withHandler(
      writer = scribe.writer.ConsoleWriter,
      formatter = formatter"[CONSOLE] $levelPaddedRight - $message$newLine"
    )
    .withHandler(
      writer = FileWriter("logs" / "debug.log"),
      formatter = formatter"$date [FILE] $levelPaddedRight $classNameAbbreviated - $message$newLine",
      minimumLevel = Some(Level.Debug)
    )
    .replace()
  
  // テストログ出力
  scribe.info("プログラマティック設定によるログ出力")
  scribe.debug("デバッグレベルの情報")
  scribe.warn("警告メッセージ")
  scribe.error("エラーメッセージ")
  
  // ファイルロガーでの出力
  fileLogger.info("ファイルに記録される情報")
  
  // マルチアウトプットロガーでの出力
  multiOutputLogger.info("コンソールとファイル両方に出力")
  multiOutputLogger.debug("デバッグ情報もファイルに記録")
}

リアルタイム設定変更と動的制御

import scribe._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}

class DynamicLoggingService {
  private var currentLogLevel: Level = Level.Info
  
  def adjustLogLevel(newLevel: Level): Unit = {
    currentLogLevel = newLevel
    
    // リアルタイムでログレベル変更
    Logger.root
      .clearHandlers()
      .withHandler(minimumLevel = Some(newLevel))
      .replace()
    
    scribe.info(s"ログレベルを${newLevel.name}に変更しました")
  }
  
  def simulateHighLoadOperation(): Future[String] = {
    scribe.info("高負荷操作開始")
    
    // 高負荷中はログレベルを上げてパフォーマンス優先
    val originalLevel = currentLogLevel
    adjustLogLevel(Level.Warn)
    
    val operation = Future {
      for (i <- 1 to 10000) {
        // 大量の処理(デバッグログは出力されない)
        scribe.debug(s"処理中: $i")
        
        if (i % 1000 == 0) {
          scribe.info(s"進捗: $i/10000")
        }
      }
      
      "高負荷操作完了"
    }
    
    operation.onComplete {
      case Success(result) =>
        scribe.info(result)
        // 元のログレベルに戻す
        adjustLogLevel(originalLevel)
        
      case Failure(exception) =>
        scribe.error("高負荷操作失敗", exception)
        adjustLogLevel(originalLevel)
    }
    
    operation
  }
  
  def demonstrateConditionalLogging(): Unit = {
    val isProduction = false // 環境設定
    
    if (isProduction) {
      // 本番環境では最小限のログ
      Logger.root
        .clearHandlers()
        .withHandler(minimumLevel = Some(Level.Warn))
        .replace()
    } else {
      // 開発環境では詳細ログ
      Logger.root
        .clearHandlers()
        .withHandler(minimumLevel = Some(Level.Trace))
        .replace()
    }
    
    scribe.trace("詳細なトレース情報")
    scribe.debug("デバッグ情報")
    scribe.info("一般情報")
    scribe.warn("警告")
    scribe.error("エラー")
  }
}

object DynamicLoggingExample extends App {
  val service = new DynamicLoggingService()
  
  // ログレベルの動的変更デモ
  service.adjustLogLevel(Level.Debug)
  service.demonstrateConditionalLogging()
  
  // 高負荷操作でのパフォーマンス優先設定
  import scala.concurrent.duration._
  import scala.concurrent.Await
  
  val future = service.simulateHighLoadOperation()
  Await.result(future, 10.seconds)
  
  println("動的ロギング設定デモ完了")
}

構造化ログとオブジェクトロギング

import scribe._
import scribe.format._

// カスタムオブジェクトのロギング対応
case class User(id: Long, name: String, email: String, role: String)
case class RequestInfo(method: String, path: String, duration: Long, statusCode: Int)

// Loggable instance for custom objects
implicit val userLoggable: Loggable[User] = new Loggable[User] {
  override def apply(user: User): LogOutput = {
    LogOutput.empty
      .add("id" -> user.id)
      .add("name" -> user.name)
      .add("email" -> user.email)
      .add("role" -> user.role)
  }
}

implicit val requestInfoLoggable: Loggable[RequestInfo] = new Loggable[RequestInfo] {
  override def apply(request: RequestInfo): LogOutput = {
    LogOutput.empty
      .add("method" -> request.method)
      .add("path" -> request.path)
      .add("duration" -> s"${request.duration}ms")
      .add("status" -> request.statusCode)
  }
}

class StructuredLoggingService {
  def processUserRequest(user: User, requestInfo: RequestInfo): Unit = {
    // オブジェクト全体をログ出力
    scribe.info(s"ユーザーリクエスト処理開始", user)
    
    // 複数のオブジェクトをログに含める
    scribe.info("リクエスト詳細", 
      Map(
        "user" -> user,
        "request" -> requestInfo
      )
    )
    
    // パフォーマンス計測
    val startTime = System.currentTimeMillis()
    
    try {
      handleBusinessLogic(user, requestInfo)
      
      val actualDuration = System.currentTimeMillis() - startTime
      val updatedRequest = requestInfo.copy(duration = actualDuration, statusCode = 200)
      
      scribe.info("リクエスト処理完了", updatedRequest)
      
    } catch {
      case ex: Exception =>
        val actualDuration = System.currentTimeMillis() - startTime
        val errorRequest = requestInfo.copy(duration = actualDuration, statusCode = 500)
        
        scribe.error("リクエスト処理エラー", Map(
          "request" -> errorRequest,
          "error" -> ex.getMessage,
          "user" -> user
        ))
    }
  }
  
  private def handleBusinessLogic(user: User, request: RequestInfo): Unit = {
    scribe.debug(s"ビジネスロジック実行: ${request.method} ${request.path}")
    
    request.path match {
      case "/api/profile" =>
        scribe.info("プロファイル取得処理", Map("userId" -> user.id))
        Thread.sleep(50)
        
      case "/api/settings" =>
        scribe.info("設定更新処理", Map("userId" -> user.id, "role" -> user.role))
        Thread.sleep(100)
        
      case path if path.startsWith("/api/admin") =>
        if (user.role != "admin") {
          throw new SecurityException("管理者権限が必要です")
        }
        scribe.warn("管理者操作実行", Map("userId" -> user.id, "path" -> path))
        Thread.sleep(200)
        
      case _ =>
        scribe.warn("未知のエンドポイント", Map("path" -> request.path))
    }
  }
}

object StructuredLoggingExample extends App {
  // JSON形式での出力設定
  Logger.root
    .clearHandlers()
    .withHandler(
      formatter = formatter"$date $level $classNameAbbreviated.$methodName:$line - $message$mdc$newLine"
    )
    .replace()
  
  val service = new StructuredLoggingService()
  
  val users = List(
    User(1001, "田中太郎", "[email protected]", "user"),
    User(1002, "佐藤花子", "[email protected]", "admin"),
    User(1003, "鈴木一郎", "[email protected]", "user")
  )
  
  val requests = List(
    RequestInfo("GET", "/api/profile", 0, 0),
    RequestInfo("POST", "/api/settings", 0, 0),
    RequestInfo("DELETE", "/api/admin/users", 0, 0),
    RequestInfo("GET", "/api/unknown", 0, 0)
  )
  
  for {
    user <- users
    request <- requests.take(2) // 各ユーザーに2つのリクエスト
  } {
    service.processUserRequest(user, request)
  }
  
  println("構造化ログデモ完了")
}

パフォーマンステストとベンチマーク

import scribe._
import org.slf4j.LoggerFactory
import java.util.concurrent.{Executors, TimeUnit}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Random

class ScribePerformanceBenchmark {
  
  def benchmarkSingleThreaded(iterations: Int = 1000000): Unit = {
    println(s"単一スレッドベンチマーク ($iterations 回)")
    
    // ウォームアップ
    warmup()
    
    // Scribeベンチマーク
    val scribeTime = measureTime {
      for (i <- 1 to iterations) {
        scribe.debug(s"Scribe message $i with value ${Random.nextInt(1000)}")
      }
    }
    
    // SLF4J比較用
    val slf4jLogger = LoggerFactory.getLogger("benchmark")
    val slf4jTime = measureTime {
      for (i <- 1 to iterations) {
        if (slf4jLogger.isDebugEnabled) {
          slf4jLogger.debug(s"SLF4J message $i with value ${Random.nextInt(1000)}")
        }
      }
    }
    
    println(f"Scribe:    ${scribeTime}%,d ms")
    println(f"SLF4J:     ${slf4jTime}%,d ms")
    
    if (slf4jTime > 0) {
      val improvement = ((slf4jTime.toDouble - scribeTime.toDouble) / slf4jTime * 100)
      println(f"Scribeの性能向上: ${improvement}%.1f%%")
    }
  }
  
  def benchmarkMultiThreaded(threads: Int = 10, iterations: Int = 100000): Unit = {
    println(s"マルチスレッドベンチマーク (${threads}スレッド, 1スレッドあたり$iterations 回)")
    
    val executor = Executors.newFixedThreadPool(threads)
    implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(executor)
    
    // Scribeマルチスレッドテスト
    val scribeStartTime = System.currentTimeMillis()
    val scribeFutures = (1 to threads).map { threadId =>
      Future {
        for (i <- 1 to iterations) {
          scribe.info(s"Thread $threadId message $i")
        }
      }
    }
    
    import scala.concurrent.Await
    import scala.concurrent.duration._
    
    Await.result(Future.sequence(scribeFutures), 30.seconds)
    val scribeTime = System.currentTimeMillis() - scribeStartTime
    
    println(f"Scribeマルチスレッド: ${scribeTime}%,d ms")
    println(f"スループット: ${(threads * iterations * 1000.0 / scribeTime)}%,.0f messages/sec")
    
    executor.shutdown()
    executor.awaitTermination(5, TimeUnit.SECONDS)
  }
  
  def benchmarkAsyncLogging(): Unit = {
    println("非同期ロギングベンチマーク")
    
    // 非同期設定
    Logger.root
      .clearHandlers()
      .withHandler(
        writer = scribe.writer.ConsoleWriter,
        minimumLevel = Some(Level.Info)
      )
      .replace()
    
    val iterations = 500000
    val startTime = System.currentTimeMillis()
    
    for (i <- 1 to iterations) {
      scribe.info(s"Async message $i")
    }
    
    // 全てのログが処理されるまで待機
    Thread.sleep(1000)
    
    val asyncTime = System.currentTimeMillis() - startTime
    println(f"非同期ロギング: ${asyncTime}%,d ms")
    println(f"スループット: ${(iterations * 1000.0 / asyncTime)}%,.0f messages/sec")
  }
  
  private def warmup(): Unit = {
    for (_ <- 1 to 10000) {
      scribe.debug("warmup")
    }
  }
  
  private def measureTime(operation: => Unit): Long = {
    val startTime = System.currentTimeMillis()
    operation
    System.currentTimeMillis() - startTime
  }
}

object PerformanceBenchmarkRunner extends App {
  // DEBUGレベルを無効にしてパフォーマンス測定
  Logger.root
    .clearHandlers()
    .withHandler(minimumLevel = Some(Level.Info))
    .replace()
  
  val benchmark = new ScribePerformanceBenchmark()
  
  benchmark.benchmarkSingleThreaded(2000000)
  println()
  
  benchmark.benchmarkMultiThreaded(threads = 8, iterations = 250000)
  println()
  
  benchmark.benchmarkAsyncLogging()
  
  println("\nScribeパフォーマンステスト完了")
}