Timber (Android)

Square社によるAndroid向けロギングライブラリ。Android標準ログAPIのラッパーとして機能し、タグの自動生成、本番ビルドでのログ無効化、カスタムログ出力先の追加が可能。Android開発で広く採用されている。

ロギングAndroidモバイルシンプルカスタマイズ可能

ライブラリ

Timber

概要

TimberはJake Wharton氏によって開発された「Android向けの小さく拡張可能なAPIを持つロガー」として、AndroidアプリケーションのAndroid標準Log クラスの上位互換機能を提供します。Tree インスタンスによる柔軟な動作カスタマイズ、自動タグ生成(クラス名・メソッド名・行番号)、lint ルールによるコード品質向上支援、Kotlin完全対応など、Android開発者にとって理想的なロギング体験を実現。DebugTreeによる開発時の詳細ログ出力とReleaseTree による本番時の制御されたロギングを組み合わせることで、効率的なAndroidアプリデバッグとプロダクション運用を支援します。

詳細

Timber 2025年版(v5.x)はKotlinで完全に書き直されながらも4.x系との完全な互換性を維持し、将来的なKotlin Multiplatform対応を目指している最新Android ロギングライブラリです。Jake Wharton氏の高い技術力により設計された拡張可能なTree システムにより、開発環境では詳細なスタックトレース付きログ、本番環境ではCrashlytics等への自動送信、カスタム フォーマット出力等を同一APIで実現可能。AndroidStudio のLint 統合により、文字列フォーマットエラー、冗長な記述、不適切なタグ長等を開発時に自動検出し、高品質なロギングコードの記述を支援します。

主な特徴

  • Tree システム: 拡張可能な出力先・フォーマット カスタマイズ機能
  • 自動タグ生成: クラス名・メソッド名・行番号の自動検出と設定
  • Lint 統合: 開発時のロギング品質向上支援機能
  • Kotlin 完全対応: Kotlin で書き直された最新コードベース
  • DebugTree: 開発用の詳細ログ出力機能
  • 軽量設計: 最小限のオーバーヘッドによる高性能ロギング

メリット・デメリット

メリット

  • Android Log クラスの上位互換で学習コスト最小、既存コード移行容易
  • Tree システムによる柔軟な出力先カスタマイズ(ファイル、クラッシュレポート等)
  • 自動タグ生成により手動タグ設定不要、ログ特定の効率化
  • Lint 統合による開発時品質向上とバグ早期発見
  • Jake Wharton 氏による高品質な設計とアクティブなメンテナンス
  • Kotlin Multiplatform 対応予定で将来性が高い

デメリット

  • Android プラットフォーム専用でクロスプラットフォーム開発には不向き
  • 高度な構造化ロギング機能は限定的(JSON出力等)
  • ログ レベル制御が Android Log ベースで細かな制御に制約
  • 非同期ロギング機能なくI/O パフォーマンス制限
  • Tree 設定を誤ると本番環境で意図しないログ出力リスク
  • サードパーティライブラリのため Android 標準APIでない

参考ページ

書き方の例

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

// build.gradle (Module: app) への依存関係追加
dependencies {
    implementation 'com.jakewharton.timber:timber:5.0.1'
}

// Mavenリポジトリ設定(通常は不要)
repositories {
    mavenCentral()
}

// スナップショット版を使用する場合
repositories {
    mavenCentral()
    maven {
        url 'https://oss.sonatype.org/content/repositories/snapshots/'
    }
}

dependencies {
    implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT'
}
// Application クラスでの初期化
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // Debug ビルドでの設定
        if (BuildConfig.DEBUG) {
            // DebugTree: 詳細な情報付きログ出力
            Timber.plant(Timber.DebugTree())
        } else {
            // Release ビルドでの設定(カスタムTree)
            Timber.plant(ReleaseTree())
        }
        
        Timber.d("Timber initialized successfully")
    }
}

// AndroidManifest.xml でApplication クラス指定
/*
<application
    android:name=".MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
    ...
</application>
*/

基本的なログ出力

import timber.log.Timber

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 基本的なログレベル別出力
        Timber.v("Verbose ログ - 最も詳細な情報")
        Timber.d("Debug ログ - デバッグ情報")
        Timber.i("Info ログ - 一般的な情報")
        Timber.w("Warning ログ - 警告情報")
        Timber.e("Error ログ - エラー情報")
        
        // フォーマット付きログ出力
        val userName = "田中太郎"
        val userId = 12345
        Timber.d("ユーザー情報: %s (ID: %d)", userName, userId)
        
        // Kotlin 文字列テンプレート(推奨)
        Timber.i("現在の Activity: ${this::class.simpleName}")
        Timber.d("画面サイズ: ${resources.displayMetrics.widthPixels}x${resources.displayMetrics.heightPixels}")
        
        // 例外付きログ出力
        try {
            riskyOperation()
        } catch (e: Exception) {
            Timber.e(e, "操作中にエラーが発生しました")
        }
        
        // タグ付きログ出力
        Timber.tag("CustomTag").d("カスタムタグ付きログ")
        Timber.tag("Network").i("API 呼び出し開始")
        
        // 複数引数での詳細情報出力
        val startTime = System.currentTimeMillis()
        performOperation()
        val duration = System.currentTimeMillis() - startTime
        Timber.d("操作完了: 実行時間=%dms, スレッド=%s", duration, Thread.currentThread().name)
    }
    
    private fun riskyOperation() {
        // リスクのある操作のシミュレーション
        if (Math.random() > 0.5) {
            throw RuntimeException("ランダムエラー")
        }
    }
    
    private fun performOperation() {
        // 操作のシミュレーション
        Thread.sleep(100)
    }
}

// Fragment でのログ使用例
class UserFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        Timber.d("UserFragment の View 作成開始")
        
        val view = inflater.inflate(R.layout.fragment_user, container, false)
        
        // ユーザーデータ取得
        val userId = arguments?.getInt("user_id") ?: -1
        Timber.i("ユーザーID: %d のデータを取得中", userId)
        
        if (userId > 0) {
            loadUserData(userId)
        } else {
            Timber.w("無効なユーザーID: %d", userId)
        }
        
        Timber.d("UserFragment の View 作成完了")
        return view
    }
    
    private fun loadUserData(userId: Int) {
        Timber.d("ユーザーデータ読み込み開始: ID=%d", userId)
        
        // データ読み込み処理(シミュレーション)
        val userData = mapOf(
            "id" to userId,
            "name" to "ユーザー$userId",
            "email" to "user$userId@example.com"
        )
        
        Timber.i("ユーザーデータ取得成功: %s", userData)
    }
}

条件付きロギング

import timber.log.Timber

class ConditionalLoggingExample {
    
    companion object {
        // 条件付きログ出力のヘルパー関数
        fun logIfDebug(message: String) {
            if (BuildConfig.DEBUG) {
                Timber.d(message)
            }
        }
        
        fun logNetworkInfo(enabled: Boolean, message: String) {
            if (enabled) {
                Timber.tag("Network").d(message)
            }
        }
    }
    
    fun performNetworkRequest(enableVerboseLogging: Boolean) {
        Timber.i("ネットワークリクエスト開始")
        
        // 詳細ログは条件付きで出力
        logNetworkInfo(enableVerboseLogging, "リクエストURL: https://api.example.com/users")
        logNetworkInfo(enableVerboseLogging, "リクエストヘッダー: Authorization=Bearer xxx")
        
        try {
            // ネットワーク処理のシミュレーション
            val response = simulateNetworkCall()
            
            Timber.i("ネットワークリクエスト成功: ステータス=%d", response.statusCode)
            logNetworkInfo(enableVerboseLogging, "レスポンスボディ: ${response.body}")
            
        } catch (e: Exception) {
            Timber.e(e, "ネットワークリクエスト失敗")
        }
    }
    
    // デバッグビルドでのみ実行される重い処理
    fun expensiveDebugOperation(data: Any) {
        if (BuildConfig.DEBUG) {
            val jsonData = convertToJson(data)
            Timber.d("詳細データ:\n%s", jsonData)
        }
    }
    
    private fun simulateNetworkCall(): NetworkResponse {
        Thread.sleep(500) // ネットワーク遅延シミュレーション
        return NetworkResponse(200, """{"users": [{"id": 1, "name": "田中太郎"}]}""")
    }
    
    private fun convertToJson(data: Any): String {
        // JSON変換処理(重い処理のシミュレーション)
        return """{"data": "$data", "timestamp": ${System.currentTimeMillis()}}"""
    }
    
    data class NetworkResponse(val statusCode: Int, val body: String)
}

// Fragment のライフサイクルでの条件付きログ
class LifecycleLoggingFragment : Fragment() {
    
    private val verboseLifecycleLogging = BuildConfig.DEBUG
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        logLifecycle("onCreate", "Bundle=${savedInstanceState != null}")
    }
    
    override fun onStart() {
        super.onStart()
        logLifecycle("onStart", "Fragment が表示開始")
    }
    
    override fun onResume() {
        super.onResume()
        logLifecycle("onResume", "Fragment がアクティブ")
    }
    
    override fun onPause() {
        super.onPause()
        logLifecycle("onPause", "Fragment が非アクティブ")
    }
    
    override fun onStop() {
        super.onStop()
        logLifecycle("onStop", "Fragment が非表示")
    }
    
    override fun onDestroy() {
        super.onDestroy()
        logLifecycle("onDestroy", "Fragment が破棄")
    }
    
    private fun logLifecycle(method: String, details: String) {
        if (verboseLifecycleLogging) {
            Timber.tag("Lifecycle").d("%s: %s", method, details)
        } else {
            Timber.d("Lifecycle: %s", method)
        }
    }
}

ログレベル設定

import timber.log.Timber

// カスタム Tree でログレベル制御
class FilteredDebugTree : Timber.DebugTree() {
    
    companion object {
        var minimumLogLevel = Log.VERBOSE
    }
    
    override fun isLoggable(tag: String?, priority: Int): Boolean {
        return priority >= minimumLogLevel
    }
    
    override fun createStackElementTag(element: StackTraceElement): String {
        // カスタムタグフォーマット: クラス名.メソッド名:行番号
        return "${element.className.substringAfterLast('.')}.${element.methodName}:${element.lineNumber}"
    }
}

// ログレベル制御の使用例
class LogLevelExample {
    
    fun demonstrateLogLevels() {
        // 通常のログレベル設定
        setLogLevel(Log.INFO)
        
        Timber.v("Verbose ログ") // 出力されない
        Timber.d("Debug ログ") // 出力されない
        Timber.i("Info ログ") // 出力される
        Timber.w("Warning ログ") // 出力される
        Timber.e("Error ログ") // 出力される
        
        // ログレベルを変更
        setLogLevel(Log.DEBUG)
        
        Timber.d("Debug ログ2") // 出力される
        Timber.v("Verbose ログ2") // 出力されない
    }
    
    private fun setLogLevel(level: Int) {
        FilteredDebugTree.minimumLogLevel = level
        
        val levelName = when (level) {
            Log.VERBOSE -> "VERBOSE"
            Log.DEBUG -> "DEBUG"
            Log.INFO -> "INFO"
            Log.WARN -> "WARN"
            Log.ERROR -> "ERROR"
            else -> "UNKNOWN"
        }
        
        Timber.i("ログレベル設定: %s", levelName)
    }
}

// タグ別ログレベル制御
class TagBasedLogLevel : Timber.DebugTree() {
    
    private val tagLogLevels = mapOf(
        "Network" to Log.INFO,
        "Database" to Log.DEBUG,
        "UI" to Log.WARN,
        "Performance" to Log.VERBOSE
    )
    
    override fun isLoggable(tag: String?, priority: Int): Boolean {
        val minLevel = tagLogLevels[tag] ?: Log.VERBOSE
        return priority >= minLevel
    }
}

// Application クラスでの詳細設定
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        if (BuildConfig.DEBUG) {
            // デバッグビルド: 詳細ログ
            Timber.plant(FilteredDebugTree())
            FilteredDebugTree.minimumLogLevel = Log.DEBUG
        } else {
            // リリースビルド: エラーログのみ
            Timber.plant(ReleaseTree())
        }
    }
}

// リリース用 Tree (エラーログのみ)
class ReleaseTree : Timber.Tree() {
    
    override fun isLoggable(tag: String?, priority: Int): Boolean {
        // エラーと警告のみログ出力
        return priority >= Log.WARN
    }
    
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (isLoggable(tag, priority)) {
            // Crashlytics やログ収集システムに送信
            when (priority) {
                Log.ERROR -> {
                    // エラーログをクラッシュレポートに送信
                    logErrorToCrashlytics(tag, message, t)
                }
                Log.WARN -> {
                    // 警告ログを分析システムに送信
                    logWarningToAnalytics(tag, message)
                }
            }
        }
    }
    
    private fun logErrorToCrashlytics(tag: String?, message: String, throwable: Throwable?) {
        // Crashlytics SDK を使用してエラーログ送信
        // FirebaseCrashlytics.getInstance().recordException(throwable ?: Exception(message))
        println("Crashlytics: [$tag] $message")
    }
    
    private fun logWarningToAnalytics(tag: String?, message: String) {
        // Analytics SDK を使用して警告ログ送信
        // Firebase Analytics や Google Analytics にイベント送信
        println("Analytics: [$tag] $message")
    }
}

Android固有の機能

import timber.log.Timber
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*

// Android 固有のカスタム Tree 実装
class AndroidFileTree(private val context: Context) : Timber.Tree() {
    
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
    
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        val priorityString = when (priority) {
            Log.VERBOSE -> "V"
            Log.DEBUG -> "D"
            Log.INFO -> "I"
            Log.WARN -> "W"
            Log.ERROR -> "E"
            else -> "?"
        }
        
        val timestamp = dateFormat.format(Date())
        val logLine = "$timestamp $priorityString/$tag: $message\n"
        
        // 内部ストレージにログファイル保存
        saveLogToFile(logLine)
        
        // 例外情報がある場合は追加保存
        t?.let {
            val stackTrace = Log.getStackTraceString(it)
            saveLogToFile("$timestamp $priorityString/$tag: Exception:\n$stackTrace\n")
        }
    }
    
    private fun saveLogToFile(logLine: String) {
        try {
            val logDir = File(context.filesDir, "logs")
            if (!logDir.exists()) {
                logDir.mkdirs()
            }
            
            val logFile = File(logDir, "app_log_${getCurrentDate()}.txt")
            FileWriter(logFile, true).use { writer ->
                writer.write(logLine)
            }
        } catch (e: Exception) {
            // ファイル保存エラーの場合は標準ログに出力
            Log.e("AndroidFileTree", "ログファイル保存エラー", e)
        }
    }
    
    private fun getCurrentDate(): String {
        return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
    }
}

// SharedPreferences を使用したログ設定管理
class LoggingPreferencesManager(private val context: Context) {
    
    private val preferences: SharedPreferences = 
        context.getSharedPreferences("logging_prefs", Context.MODE_PRIVATE)
    
    var enableFileLogging: Boolean
        get() = preferences.getBoolean("enable_file_logging", false)
        set(value) = preferences.edit().putBoolean("enable_file_logging", value).apply()
    
    var logLevel: Int
        get() = preferences.getInt("log_level", Log.DEBUG)
        set(value) = preferences.edit().putInt("log_level", value).apply()
    
    var enableNetworkLogging: Boolean
        get() = preferences.getBoolean("enable_network_logging", true)
        set(value) = preferences.edit().putBoolean("enable_network_logging", value).apply()
    
    fun configureTimber() {
        // 既存の Tree をクリア
        Timber.uprootAll()
        
        if (BuildConfig.DEBUG) {
            // デバッグビルド設定
            Timber.plant(FilteredDebugTree().apply {
                FilteredDebugTree.minimumLogLevel = logLevel
            })
            
            if (enableFileLogging) {
                Timber.plant(AndroidFileTree(context))
            }
        } else {
            // リリースビルド設定
            Timber.plant(ReleaseTree())
        }
        
        Timber.i("Timber 設定完了: FileLogging=%s, LogLevel=%d, NetworkLogging=%s", 
                enableFileLogging, logLevel, enableNetworkLogging)
    }
}

// Activity での Android 固有機能使用例
class MainActivity : AppCompatActivity() {
    
    private lateinit var loggingPrefs: LoggingPreferencesManager
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        loggingPrefs = LoggingPreferencesManager(this)
        loggingPrefs.configureTimber()
        
        // Activity ライフサイクルログ
        Timber.tag("ActivityLifecycle").i("MainActivity.onCreate")
        
        // システム情報ログ
        logSystemInfo()
        
        // ネットワーク状態ログ
        logNetworkState()
        
        // アプリ情報ログ
        logAppInfo()
    }
    
    private fun logSystemInfo() {
        Timber.tag("SystemInfo").apply {
            d("Android バージョン: ${android.os.Build.VERSION.RELEASE}")
            d("API レベル: ${android.os.Build.VERSION.SDK_INT}")
            d("デバイス: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
            d("画面密度: ${resources.displayMetrics.density}")
            d("利用可能メモリ: ${getAvailableMemory()}MB")
        }
    }
    
    private fun logNetworkState() {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkInfo = connectivityManager.activeNetworkInfo
        
        Timber.tag("Network").apply {
            if (networkInfo?.isConnected == true) {
                i("ネットワーク接続: %s", networkInfo.typeName)
            } else {
                w("ネットワーク未接続")
            }
        }
    }
    
    private fun logAppInfo() {
        try {
            val packageInfo = packageManager.getPackageInfo(packageName, 0)
            Timber.tag("AppInfo").apply {
                i("アプリバージョン: %s (%d)", packageInfo.versionName, packageInfo.versionCode)
                i("パッケージ名: %s", packageName)
                i("デバッグビルド: %s", BuildConfig.DEBUG)
            }
        } catch (e: Exception) {
            Timber.e(e, "アプリ情報取得エラー")
        }
    }
    
    private fun getAvailableMemory(): Long {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val memoryInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memoryInfo)
        return memoryInfo.availMem / (1024 * 1024) // MB に変換
    }
}

パフォーマンス考慮事項

import timber.log.Timber

// パフォーマンス最適化のためのログ制御
class PerformanceOptimizedLogging {
    
    companion object {
        // 頻繁な呼び出しでのログ間引き
        private var lastLogTime = 0L
        private const val LOG_INTERVAL_MS = 1000L // 1秒間隔
        
        fun logWithThrottling(tag: String, message: String) {
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastLogTime >= LOG_INTERVAL_MS) {
                Timber.tag(tag).d(message)
                lastLogTime = currentTime
            }
        }
        
        // 遅延ログ評価(重い処理がある場合)
        inline fun logIfEnabled(priority: Int, tag: String, lazyMessage: () -> String) {
            if (Timber.isLoggable(tag, priority)) {
                when (priority) {
                    Log.DEBUG -> Timber.tag(tag).d(lazyMessage())
                    Log.INFO -> Timber.tag(tag).i(lazyMessage())
                    Log.WARN -> Timber.tag(tag).w(lazyMessage())
                    Log.ERROR -> Timber.tag(tag).e(lazyMessage())
                }
            }
        }
    }
    
    fun demonstratePerformanceOptimizations() {
        // ✓ 良い例: シンプルな文字列ログ
        Timber.d("処理開始")
        
        // ✓ 良い例: 軽量な文字列テンプレート
        val userId = 123
        Timber.d("ユーザーID: %d", userId)
        
        // ✗ 避けるべき例: 重い処理を直接含む
        // Timber.d("重い処理結果: ${expensiveOperation()}")
        
        // ✓ 良い例: 条件付きで重い処理実行
        if (BuildConfig.DEBUG) {
            val result = expensiveOperation()
            Timber.d("重い処理結果: %s", result)
        }
        
        // ✓ 良い例: 遅延評価
        logIfEnabled(Log.DEBUG, "Performance") {
            "複雑な計算結果: ${complexCalculation()}"
        }
        
        // 大量データのパフォーマンステスト
        performanceBenchmark()
    }
    
    private fun expensiveOperation(): String {
        // 重い処理のシミュレーション
        Thread.sleep(100)
        return "処理完了"
    }
    
    private fun complexCalculation(): Double {
        // 複雑な計算のシミュレーション
        return (1..1000).map { Math.random() }.average()
    }
    
    private fun performanceBenchmark() {
        val startTime = System.currentTimeMillis()
        
        // 大量ログ出力のテスト
        repeat(1000) { index ->
            // ログの間引き
            if (index % 100 == 0) {
                Timber.d("進捗: %d/1000", index)
            }
            
            // 軽量な処理
            processItem(index)
        }
        
        val duration = System.currentTimeMillis() - startTime
        Timber.i("パフォーマンステスト完了: %dms", duration)
    }
    
    private fun processItem(index: Int) {
        // アイテム処理のシミュレーション
        if (index < 10) {
            Timber.v("Item %d 処理", index)
        }
    }
}

// メモリ効率的なログ管理
class MemoryEfficientLogging {
    
    private val logBuffer = mutableListOf<String>()
    private val maxBufferSize = 100
    
    fun addLog(message: String) {
        logBuffer.add(message)
        
        // バッファサイズ制限
        if (logBuffer.size > maxBufferSize) {
            logBuffer.removeAt(0) // 古いログを削除
        }
    }
    
    fun flushLogs() {
        if (logBuffer.isNotEmpty()) {
            Timber.d("バッファログ出力: %d件", logBuffer.size)
            logBuffer.forEach { Timber.d(it) }
            logBuffer.clear()
        }
    }
    
    // WeakReference を使用したログコンテキスト保持
    private val contextReferences = mutableMapOf<String, WeakReference<Any>>()
    
    fun logWithContext(key: String, context: Any, message: String) {
        contextReferences[key] = WeakReference(context)
        Timber.tag(key).d("%s [Context: %s]", message, context::class.simpleName)
        
        // 無効な参照をクリーンアップ
        cleanupReferences()
    }
    
    private fun cleanupReferences() {
        val iterator = contextReferences.iterator()
        while (iterator.hasNext()) {
            val entry = iterator.next()
            if (entry.value.get() == null) {
                iterator.remove()
            }
        }
    }
}

// カスタム Tree でパフォーマンス測定
class PerformanceTree : Timber.DebugTree() {
    
    private val logCounts = mutableMapOf<String, Int>()
    private val logTimes = mutableMapOf<String, Long>()
    
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        val startTime = System.nanoTime()
        
        super.log(priority, tag, message, t)
        
        val endTime = System.nanoTime()
        val duration = endTime - startTime
        
        // パフォーマンス統計収集
        tag?.let {
            logCounts[it] = logCounts.getOrDefault(it, 0) + 1
            logTimes[it] = logTimes.getOrDefault(it, 0L) + duration
        }
    }
    
    fun printStatistics() {
        Timber.d("=== ログパフォーマンス統計 ===")
        logCounts.forEach { (tag, count) ->
            val totalTime = logTimes[tag] ?: 0L
            val averageTime = if (count > 0) totalTime / count else 0L
            Timber.d("Tag: %s, Count: %d, Average: %d ns", tag, count, averageTime)
        }
    }
}