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