
【2026年8月版】
Rustロギングライブラリの選定 ― tracing・fastrace・fast_log の使い分け
公開日: 2026/08/09
読了時間: 約 39分
代表的なRustのロギング/トレーシング系クレート — log、tracing、slog、fast_log、fastrace、spdlog-rs、defmt — と、そのユースケースを調べました。この記事では、各クレートが担当する層の整理、主要クレートの紹介と比較、実行環境ごとの制約、OpenTelemetryとの接続、性能を測るときの条件をまとめます。
結論を先に
調査の結果を用途とクレートの対応として先にまとめます。それぞれの根拠は以降の節で扱います。
| 用途 | 代表的なクレート |
|---|---|
| Tokioベースの非同期サーバー | tracing + tracing-subscriber |
| 複数サービスをまたぐ観測 | tracing + OpenTelemetry bridge |
| 低オーバーヘッドの分散トレース | fastrace |
| 小規模CLI、汎用ライブラリの計装 | log + env_logger |
| ローカルファイルの高速出力 | fast_log、spdlog-rs |
| ファイルのローテーション運用 | flexi_logger |
| ベアメタルの組み込み | defmt + RTT |
| Linuxカーネルモジュール | カーネル提供の pr_info! 系マクロ |
何を記録し、どこへ送るかによって構成が決まります。ログは記録が残ることを前提に組みますが、高頻度の分散トレースではサンプリングを前提にし、マイコンではリングバッファの上書きや混雑時のdropを織り込む構成もあります。
ログの三層構造
Rustのロギング関連クレートは、次の層に分かれています。
アプリケーション / ライブラリの計装
│
▼
計装API(facade) log / tracing / fastrace
│
▼
構造化・レベル判定・Span管理 tracing-subscriber / slog
│
▼
出力・転送 tracing-appender / flexi_logger / OTel exporterイベントを発生させるAPIの層にあるのが log、tracing、fastrace です。構造化とレベル判定とSpan管理を受け持つのが tracing-subscriber や slog、stderrやファイルやOTLPへ送り出すのが tracing-appender、OTel exporter、flexi_logger になります。
tracing 単体と fast_log も、厳密には同じレイヤーではありません。tracing は計装APIを含むフレームワークで、実際の速度はsubscriber、フォーマッター、出力方法、バッファ、OTLP exporterの構成に大きく左右されます。
速さという言葉が指す性質も、どの層の話かで変わります。層ごとにボトルネックと、その層で許容できるものが違います。
| 層 | 主なボトルネック | 縮められるもの | 許容の対象 |
|---|---|---|---|
| 計装API | ログ1回・Span1個あたりに呼び出し側で走る処理 | 無効レベルの早期return、Span生成の削減 | 記録の粒度(サンプリング) |
| 構造化・レベル判定・Span管理 | 整形とserializationのCPU・メモリ | フィールド数、JSONをやめる、コンパイル時フィルタ | 出力の情報量 |
| 出力・転送 | I/O待ちと転送帯域 | 非同期化、バッチ、リングバッファ | 遅延、あるいはキュー満杯時の欠損 |
計装APIの層を削っても、出力の層が同期I/Oのままなら呼び出し元は待たされます。逆に出力を非同期にしても、Spanを大量に生成していれば計装APIの層が残ります。「速いロガー」を選ぶことは、どの層のコストを縮め、代わりにどの層で粒度や欠損を受け入れるかを決めることになります。続く個別のクレートの紹介と比較は、この区別を前提にしています。
ユースケースと制約
層の整理と並んで、実行環境がクレートの選択を制約します。カーネルではカーネルログバッファ、ベアメタルのマイコンではRTTやUARTというように、使える出力先は環境ごとに決まっています。ヒープやスレッドの有無、割り込み文脈から呼べるかどうかも、その環境で扱える構成を絞り込みます。
判断軸になるのは、その環境で安全に記録できること、記録処理が本体を止めないこと、障害時にログを回収できることの三つです。
| 環境 | 主な制約 | 現実的な出力先 | 主な候補 |
|---|---|---|---|
| CLI | 起動時間、依存サイズ、人が読む端末表示 | stderr、stdout | log + env_logger、tracing-subscriber |
| デスクトップ・単体アプリ | ファイル管理、長時間稼働、障害解析 | 端末、ローテーションファイル、OSログ | flexi_logger、spdlog-rs、tracing |
| 非同期サーバー | タスクをまたぐ因果関係、高負荷、遠隔収集 | JSON、journald、Collector | tracing、必要ならOpenTelemetry |
| 高頻度ローカルログ | 呼び出し側の遅延、書き込み量 | 非同期ファイル、リングバッファ | fast_log、spdlog-rs、tracing-appender |
| Linuxカーネル | no_std、カーネルAPI、割り込み文脈 | カーネルログバッファ | pr_info! などカーネル提供マクロ |
| ベアメタル組み込み | 小容量、通信帯域、割り込み、ヒープなし | RTT、UART、ITM、semihosting | defmt、rtt-target、log + 独自backend |
| RTOS・リアルタイム制御 | 実行時間の上限、優先度逆転、停止禁止 | 固定長リングバッファ、専用転送タスク | 環境専用backend、defmt、静的イベント記録 |
OTelへ対応しているかどうかは、この表では最上位の判断基準になっていません。
主要クレート紹介
tracing ― EventとSpanの統合API
現在のRustサーバーやTokio上の非同期処理では、tracing が事実上の標準候補になっています。単純な文字列ログだけでなく、構造化された Event と、処理区間を表す Span を同じAPIから扱える点が中心にあります。非同期関数をまたぐ処理の関係を保ちやすく、ログと分散トレースを同じ計装から派生させられます。
導入は、計装APIの tracing と、購読側の tracing-subscriber を組で入れます。
cargo add tracing tracing-subscriber#[tracing::instrument(skip(db))]
async fn get_user(db: &Database, user_id: u64) -> Result<User> {
tracing::info!(user_id, "loading user");
db.get(user_id).await
}強みはTokio系ライブラリとの親和性の高さです。構造化フィールドを持たせられ、非同期処理をまたぐSpanを扱え、subscriberのLayerを重ねて複数の出力を組み合わせられます。tracing-subscriberのLayerを使えば、同じEventやSpanをターミナルやJSON、ファイル、OTelを同じイベントから生成でき、tracing-log を挟めば既存の log イベントも同じ経路に取り込めます。
代償は構成の複雑さとSpanのコストです。subscriberの組み立ては層が増えるほど読みにくくなり、Spanを大量に生成すれば無視できないオーバーヘッドが乗ります。OTLPへの変換、属性の生成、時刻の取得、コンテキストの管理はいずれも軽い処理ではなく、ログ専用ライブラリと比べれば依存関係も増えます。高頻度ループの反復ごとにSpanを生成して大量の属性を記録すれば、コンテキスト管理と時刻取得と割り当てと整形が負荷になります。通常の業務処理は tracing、極端に高頻度な区間は fastrace、集計値はメトリクスというように、記録の粒度を分ける形が現実的です。
なお tracing を採用する理由はOTelだけではありません。非同期タスクをまたぐコンテキストと、構造化された診断イベントそのものに価値があります。default-features = false によりno_stdでも利用できますが、allocを必要とし、std依存の機能には制限が残ります。no_std対応というチェックだけで組み込みやカーネルへの適性は判断できず、対象環境に合うsubscriberと転送経路まで含めた評価が要ります。
tracing-appender ― ファイル出力とOTelブリッジ
tracing 本体が受け持つのはイベントの発生までで、ファイルへの書き出しは tracing-appender の担当です。OTelへの送信は、さらに別のクレート群が受け持ちます。
ファイル出力だけなら tracing-appender の一つで足ります。
cargo add tracing-appenderOTelへ送る場合は、SDKとexporter、そしてbridgeを加えます。
cargo add opentelemetry opentelemetry_sdk opentelemetry-otlp tracing-opentelemetry opentelemetry-appender-tracingファイル出力の要になるのは、整形と書き込みを専用スレッドへ逃がす non_blocking writer です。これがアプリケーションの実行パスからI/Oを外します。
let file = tracing_appender::rolling::daily("./logs", "server.log");
let (writer, guard) = tracing_appender::non_blocking(file);
tracing_subscriber::fmt()
.json()
.with_writer(writer)
.init();注意点は二つあります。WorkerGuard をアプリケーション終了まで保持しないと、終了時のログが失われます。また、lossyモードは混雑時にログを捨てるため、捨てた行数を ErrorCounter で監視していない構成では、速度と引き換えに許した欠損が誰にも見えないまま残ります。
OTelとの接続は tracing-opentelemetry と opentelemetry-appender-tracing に分かれます。前者は tracing のSpanをOpenTelemetryのTraceへ接続し、後者は tracing::Event をOTelの LogRecord へ変換します。前者を入れただけではOTel Logsを送信したことにはなりません。OpenTelemetryのRust向け入門ガイドも、既存のロギングAPIとして tracing を使い、bridgeを介してOTelへ送る構成を紹介しています。
fastrace ― 極低オーバーヘッドの分散トレーサ
速度を前面に出すクレートは、狙っている出力先で性格が分かれます。fastrace は極めて高速な分散トレーシングを目的としたTiKV系 minitrace の後継プロジェクトで、minitrace 側のリポジトリも fastrace への移行を案内しています。
本体と、OTLPへ送るためのexporterを合わせて導入します。
cargo add fastrace fastrace-opentelemetry設計としては、Span生成のホットパスを小さく保ち、OTelへの変換やexportをバックグラウンド側へ寄せる設計で、サンプリングを前提に組みやすくなっています。fastrace-opentelemetry でOTLPへ接続でき、fastrace-tracing を使えば tracing で計装済みのライブラリも取り込めます。
公開されているベンチマークでは tracing-opentelemetry より大幅に高速とされますが、比較条件は揃っていません。OTelオブジェクトを生成する時点、非同期化の有無、drop、batch、export処理をどこまで含めるかが違えば、ns/opの数字を並べても比較になりません。
位置づけは高速ロガーではなく、低オーバーヘッドの分散トレーサです。テキストログ、ファイルローテーション、運用ログをまとめて任せる相手ではありません。分散データベースのような高頻度処理では、QUICのリクエスト経路、Raftメッセージの処理、ストレージI/OのSpan、分散問い合わせの親子関係、サンプリングした遅延分析といった部分が対象になります。
fast_log / spdlog-rs ― 高速なローカルファイルログ
この二つは、OTelよりもローカルファイルへの書き込み速度を優先する場面の候補です。いずれも log facade互換で、既存の log ベースのコードから乗り換えられます。
Crossbeam channelとバッチ書き込みで待機を減らす fast_log は、log facade互換の高性能非同期ロガーです。チャネルとバッチ書き込みを使い、呼び出し元スレッドの待機を減らします。
cargo add fast_logappender単位で処理を分けることで、呼び出し元スレッドが書き込みを待つ時間を削っています。
向くのは大量のテキストログをローカルファイルへ非同期に書き出す用途で、RBatis系を含む log ベースのアプリケーションや、OTelより書き込み速度を優先する処理が対象です。OTelとの統合を中心に据える構成では第一候補になりません。高速なログファイルと分散トレースを別系統で運用すると、trace IDとの対応やフィルタ設定が二重化します。
spdlog-rs はC++のspdlogに影響を受けたRust実装で、コンパイル時フィルタ、非同期sink、ローテーション、複数sink、構造化ログ、log 互換を備えます。C++のspdlogに慣れた開発者には理解しやすい設計です。
cargo add spdlog-rsspdlog-rs の公式ドキュメントが示すとおり、高速なローカルロギングライブラリとしての設計は明快です。一方、Rustの非同期サーバーやOTelエコシステムとの統合度では tracing に及びません。独立した高速ロガーが必要で、spdlog型の設計に馴染みがある場合の候補です。
log / flexi_logger ― facadeを残す構成
log はRustで最も基本的なロギングfacadeです。標準ライブラリには含まれず、rust-lang 組織が管理する外部クレートとして提供されます。
facadeの log と、backendとなる flexi_logger を組で入れます。facade単体では出力先が決まらないため、backendの指定が必要です。
cargo add log flexi_loggerlog::info!("server started on {}", address);フィルタで無効化された呼び出しは、log の公式ドキュメントによれば整数のロードと比較と分岐程度の小さい処理に落ちます。広く使われていてライブラリ側の依存先として安定しており、backendはアプリケーション側で選べます。概念が単純な代わりに、Spanや非同期コンテキストは直接扱わず、OTelへ自然に接続するにはbridgeが要ります。型付きフィールドや階層的なコンテキストの表現力も tracing より弱くなります。
汎用ライブラリを公開する側では、log APIに依存して利用者にbackendを選ばせる設計が現在も有効です。システム全体を自分で制御する場合は tracing の方が機能的です。
再利用されるライブラリが勝手にファイルを開いたり、グローバルloggerを初期化したりすると、利用側が出力方法を選べなくなります。facadeだけを使う構成が基本で、非同期処理のSpanや構造化フィールドがAPI上の意味を持つライブラリなら tracing が適します。両方へ二重に出力するのではなく、必要な場合はアプリケーション側で tracing-log などのbridgeを挟みます。
log は標準ライブラリを必須としない構成を取れるため、no_stdライブラリの共通facadeとしても使えます。ただしno_stdでコンパイルできることと、その環境で安全に出力できることは別の話です。実際のbackendはUART、リングバッファ、ホスト通信など、対象機器に合わせた実装が要ります。
backendとしての flexi_logger は log facade用として成熟していて、ファイル運用の機能が厚いのが特徴です。同期・非同期・バッファ出力を選べ、サイズと時間によるローテーション、圧縮と古いファイルの削除まで面倒を見ます。実行中のログレベル変更、複数writer、tracing 連携も備えています。WriteMode の公式ドキュメントも、Direct 出力が最も遅く、バッファリングがI/O負荷を下げると説明しています。OTelネイティブではないものの、組み込み機器や単体プロセスでローカルログを確実に管理する場面では、tracing-appender より完成されたファイル管理機能を持ちます。
slog と簡易ロガー群 ― 既存システムと開発用途
slog はRustで構造化ログを早くから実現していたライブラリです。key-valueによる構造化ログ、Drainを組み合わせる構成、slog-async、JSONやsyslogを含む豊富な周辺クレートを備えます。
本体に、非同期Drainの slog-async を添えます。
cargo add slog slog-async成熟しているものの、TokioやOTelを中心に据えるマルチスレッド・サーバーサービス等を扱う新規プロジェクトでは通常 tracing が優先されます。既存システムが slog で安定しているなら、性能だけを理由に全面移行する必然性はありません。
簡易ロガー群は、開発環境やCLI向けの軽量な選択肢です。最も手軽な env_logger であれば、facadeと合わせて二つで済みます。
cargo add log env_loggerenv_logger はCLIや開発環境、簡単なサーバー向けで、log4rs はJavaのlog4jに近い設定とappender構成を持ちます。fern はコードによる柔軟なdispatch、simple_logger は最小構成、pretty_env_logger は開発者向けの読みやすい端末表示という住み分けです。いずれも最高性能やOTel統合の第一候補ではなく、env_logger は特に簡単な反面、高頻度ログの本番ファイル出力を任せる設計ではありません。
CLIのログが相手にするのは、分散トレースではなく利用者への診断情報です。通常の結果をstdout、警告や診断ログをstderrへ出し、--verbose や RUST_LOG で詳細度を変えられることの方が効いてきます。
fn main() {
env_logger::init();
log::debug!(target: "scanner", "scanning input");
log::info!("completed");
}env_logger は標準でstderrへ書き、RUST_LOG=tool=debug のようにモジュール単位でフィルタできます。設定が簡単で、アプリケーションのコードを特定の出力実装へ強く結び付けません。
CLI内部に多数の段階や並列タスクがあり、処理区間を追跡したい場合は tracing-subscriber も有効です。ただしSpanを使わず数行のメッセージを出すだけなら、tracing を入れても得られる利点は小さくなります。構造化ログ基盤を先回りして載せるより、終了コード、stderr、エラーチェーン、--quiet と --verbose の整合を先に決める方が実際的です。
カーネル提供マクロ ― pr_info! 系とログバッファ
OSカーネル内では、一般的なRustアプリケーションと前提が異なります。標準エラーも通常のファイルI/Oもなく、メモリ確保やスリープを許せない文脈があります。LinuxカーネルのRustコードは、pr_info!、pr_warn!、pr_err!、dev_info! などカーネルが提供するログ機構を使い、出力はカーネルのログレベル、デバイス情報、レート制御、ログバッファという既存の運用へ統合されます。
独自OSやカーネルをRustで書く場合も、汎用ロガーの比較より先に、起動初期のシリアル出力、CPUごとのリングバッファ、割り込みとNMIの安全性、ロック再入、panic時のflushが問題になります。通常時に高速なloggerでも、スケジューラや割り当て器が壊れた後に動かなければ、最も必要な障害ログが残りません。
カーネルでの速さは、割り込み禁止時間の短さ、ロック競合の少なさ、再帰的な障害を招かないこと、障害後にバッファを回収できることで測られます。
defmt ― 組み込み向けの計装と転送
ベアメタルのマイコンでは、整形済み文字列を毎回UARTへ送るとCPU、バイナリサイズ、通信帯域を消費します。defmt はフォーマット情報をホスト側へ寄せ、デバイスからコンパクトなバイナリフレームを送る方式です。小容量・低帯域の環境で強い理由は実装速度ではなく、対象機器側で行う仕事を減らす設計にあります。
cargo add defmt defmt-rtt代表的な転送経路はRTTです。defmt-rtt はデバッグプローブ経由のRTTへ接続しますが、blockingモードではログを失いにくい代わりに、ホスト側が切断されてバッファが埋まると実行が停止し得ます。non-blockingや競合時dropを選べば制御処理を守れる一方、ログ欠損を受け入れることになります。
組み込みで確認すべき点は次のようになります。
- 割り込みハンドラから呼べるか
- logger内部で割り当てや長いクリティカルセクションが発生しないか
- デバッガ切断時に停止するか、ログを捨てるか
- 一つのフレームがリングバッファへ収まるか
- panicやHardFault後にどこまでflushできるか
- リリースビルドでDebugとTraceをコンパイルから除去できるか
RTTはデバッグ時の経路で、製品の保守記録には別の仕組みを併せます。固定長のRAMまたはフラッシュのリングバッファ、再起動後も残るクラッシュレコード、ホストへ転送する専用タスクがその中身です。この構成では容量を超えた分の扱いが問題になります。Memfaultのように、RAMバッファが埋まると古いログから上書きし、読み出し時に欠落件数を併せて出す実装もあります。リアルタイム制御では、文字列ログより固定IDと時刻と短い引数だけを記録するイベントトレースが適する場合もあります。
クレート比較
| クレート | 層 | 特徴 | OTel統合 | 主用途 |
|---|---|---|---|---|
| log | 計装facade | 無効化時のコストが極小 | bridge経由 | ライブラリの計装 |
| tracing | 計装API + フレームワーク | Event/Span、Layer構成 | tracing-opentelemetry ほか | サーバー全般、Tokio系 |
| tracing-subscriber | 構造化・レベル判定・Span管理 | Layerの重ね合わせ、fmt / JSON出力 | Layerとして連携 | tracingの購読側の中核 |
| tracing-appender | 出力 | 専用スレッドへの非同期書き出し | ― | ファイル出力 |
| fastrace | 計装API(トレース特化) | ホットパス最小、サンプリング前提 | fastrace-opentelemetry | 高頻度区間の分散トレース |
| fast_log | log backend | channel + バッチ書き込み | 別系統になる | ローカルファイルの高速ログ |
| spdlog-rs | 独立ロガー | コンパイル時フィルタ、非同期sink | 統合度は低い | 高速ローカルログ |
| flexi_logger | log backend | ローテーション・圧縮・実行中レベル変更 | ネイティブではない | ファイル運用重視 |
| slog | 計装 + 構造化 | key-value、Drain構成 | ― | 既存slogシステムの維持 |
| env_logger | log backend | 環境変数でフィルタ、最小構成 | ― | CLI、開発環境 |
| log4rs | log backend | log4j風の設定ファイルとappender構成 | ― | 設定driven なファイル出力 |
| fern | log backend | コードによる柔軟なdispatch | ― | 出力先を細かく分岐 |
| simple_logger / pretty_env_logger | log backend | 最小構成 / 読みやすい端末表示 | ― | 開発環境 |
| defmt | 計装API + 転送(組み込み) | 整形をホスト側へ寄せる、RTT転送 | ― | ベアメタル組み込み |
| カーネル提供マクロ | カーネル内の計装 | pr_info! 系、ログバッファへ統合 | ― | Linuxカーネルモジュール |
要件ごとの組み合わせ例
サーバーアプリケーションでの第一候補は、現時点では tracing 系です。計装オーバーヘッドの最小化を優先するなら fastrace、ローカルファイルへの書き込みスループットを優先するなら fast_log、既存の log エコシステムを維持するなら flexi_logger が別の候補として残ります。実行環境がカーネルや組み込みへ移ると、選択肢そのものが入れ替わります。
| 要件 | 推奨構成 | 選ぶ理由 |
|---|---|---|
| メジャー・長期運用・OTel対応 | tracing + tracing-subscriber + OpenTelemetry連携 | 非同期コンテキストとLayerが強い |
| ログ・トレース・メトリクスを統合 | tracing + tracing-opentelemetry + opentelemetry-appender-tracing | Trace IDを越境させ外部基盤へ送れる |
| 極低オーバーヘッドの分散トレース | fastrace + fastrace-opentelemetry | 高頻度Span記録に特化 |
| 高速なローカルファイルログ | fast_log | 非同期・バッチ構成を選べる |
log API互換、ローテーション重視 | flexi_logger | ファイルの世代管理機能が充実 |
C++ spdlog に近い高速ロガー | spdlog-rs | 同期・非同期loggerと複数sink |
| 小規模CLI | log + env_logger | 軽量でstderrと RUST_LOG を扱いやすい |
| 複雑なCLI、並列ジョブ | tracing + tracing-subscriber | 処理区間と構造化フィールドを追える |
| 公開ライブラリ | log または tracing | 出力先を利用側に委ねられる |
| 高度な構造化ログ | slog。ただし新規採用は tracing を優先 | key-valueとDrainが成熟している |
| Linuxカーネルモジュール | カーネルの pr_* / dev_* | カーネルのログ機構と制約に従える |
| ベアメタル開発時 | defmt + RTTなど | 転送量と対象側の整形コストを抑えられる |
| 製品組み込みの障害記録 | 固定長リングバッファなどの専用実装 | 再起動、切断、リアルタイム制約に合わせられる |
サーバー側で同じ計装モデルを揃える場合は、二段構成が扱いやすくなります。標準構成を tracing に置き、超高頻度の区間だけ fastrace と独自メトリクスとサンプリングされたイベントへ切り替える形です。全面的に高速ロガーへ置き換えるより、通常の診断情報とホットパス用テレメトリを分ける方が安全です。
OTelには二系統の構成
OpenTelemetryが前提になるのは、複数プロセスや複数サービスをまたぐTrace IDを保持し、ログとトレースとメトリクスをCollectorや観測基盤へ送る場面です。単一のCLIやオフラインのデスクトップアプリ、カーネル、マイコンの本番稼働で、OTel対応が要件になることは通常ありません。要件として現れるのは、E2Eテストで機器側の処理をサービス側のトレースへ繋ぐ場合や、edgeコンピューティングのようにOTelを前提とした基盤へ組み込む場合です。
採用する場合も、アプリのホットパスから同期送信する構成は避け、バッチ、キュー上限、再送、Collector停止時のbackpressure、終了時のflush期限を先に決めます。OTel対応の有無より、外部基盤が落ちたときに本体がどう振る舞うかが問題になります。
OTelへの送信を前提に置くなら、計装の入口を二つに分ける構成が妥当です。アプリケーションとDBの計装から、通常の tracing のEventとSpanが一方へ流れ、高頻度区間の fastrace Spanがもう一方へ流れます。前者はローカルのfmt/JSON出力と、OTel LogsおよびTracesのbridgeへ分岐し、後者はサンプリングとバッチ処理を経ます。bridgeとバッチの出口は、組み込みCollectorまたは外部Collectorで合流します。
アプリケーション / DB の計装
├─ tracing の Event・Span
│ ├─ tracing-subscriber::fmt ──→ ローカル診断ログ(fmt / JSON)
│ ├─ tracing-opentelemetry ───→ OTel Trace
│ └─ opentelemetry-appender-tracing ─→ OTel Logs
└─ fastrace の Span(超高頻度区間)
└─ サンプリング・バッチ処理
│
▼
opentelemetry-otlp ──→ Collector(組み込み or 外部)クレートの割り当ては次のようになります。公開APIと通常のイベントを tracing で発生させ、ローカル診断ログを tracing-subscriber::fmt、非同期のファイル出力を tracing-appender::non_blocking が受けます。OTel Traceは tracing-opentelemetry、OTel Logsは opentelemetry-appender-tracing が変換し、送信は opentelemetry-otlp が担います。超高頻度のSpanは必要な場所だけ fastrace に切り替え、メトリクスは opentelemetry かPrometheus向けの専用経路を通します。
性能測定の試験項目
ロガーの性能比較では、単純な「1秒間に何件書けたか」だけでは不十分です。性能を比較するには、少なくとも次の項目を別々に測る必要があります。
| 試験 | 測るもの | 測り方 |
|---|---|---|
| 無効レベル | フィルタされた呼び出しのコスト | Criterion / Divan |
| バイナリサイズ・静的データ量 | リンク後の増分 | cargo bloat、cargo size |
| 初期化時間 | 初回起動までのコスト | hyperfine |
| 同期出力 | 呼び出し元がI/O完了を待つコスト | Criterion(tempfileへ出力) |
| 非同期・blocking | キュー満杯時の遅延 | 負荷生成 + p99の記録 |
| 非同期・lossy | 欠損数を含む実効性能 | error_counter().dropped_lines() |
| JSON整形 | serializationのCPU・メモリコスト | Criterion、dhat |
| Span生成 | contextと属性の管理コスト | Criterion(Span有無で比較) |
| 割り込み禁止時間・ロック競合 | 本体処理を止める時間 | DWTサイクルカウンタ |
| UART・RTTの実効帯域 | 転送経路の上限 | ホスト側の受信バイト数/秒 |
| ローテーションとディスク上限 | 長時間稼働時の消費 | 長時間実行 + ディスク使用量の記録 |
| OTLP batch | SDKとexporterを含むコスト | Collectorをモックした負荷試験 |
| Collector停止 | backpressureとメモリ増加 | Collectorを停止してRSSを観測 |
| shutdown | flush時間と欠損件数 | 終了までの経過時間と出力行数 |
| panic・異常終了時 | ログの回収率 | 意図的なpanic後の出力行数 |
Criterionで呼び出し側のコストを測る
計装APIの層はRustコミュニティでデファクトのベンチマークハーネスである Criterionで測ります。ウォームアップと統計処理を持ち、実行間の差を回帰として検出します。より単純なAPIと割り当て計測を備えた divan も選択肢になります。
cargo add --dev criterion --features html_reports無効レベルのコストは、subscriberを入れずに呼び出す形で測ります。black_box を挟まないと、呼び出しごと最適化で消える点に注意が要ります。
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn disabled_level(c: &mut Criterion) {
c.bench_function("info! (filtered out)", |b| {
b.iter(|| tracing::info!(value = black_box(42), "message"));
});
}
criterion_group!(benches, disabled_level);
criterion_main!(benches);Span生成のコストは、同じ処理をSpanの有無で比較すると差分が出ます。JSON整形は fmt().json() と fmt() を別のベンチとして並べ、整形だけの差を見ます。
欠損と遅延はカウンタで押さえる
スループットの数字だけでは、lossyモードが何行捨てたかが見えません。tracing-appender は捨てた行数を返すカウンタを持つので、ベンチと併せて記録します。
let (writer, _guard) = tracing_appender::non_blocking::NonBlockingBuilder::default()
.lossy(true)
.finish(std::io::sink());
let counter = writer.error_counter();
// 負荷をかけたあとに欠損数を読む
println!("dropped: {}", counter.dropped_lines());dropped_lines() はlossyモードでない場合は常に0を返すため、blockingとlossyを同じ負荷で走らせれば、遅延と欠損のどちらを払っているかが数字で並びます。shutdown時のflush時間は、WorkerGuard をdropしてからプロセス終了までの経過時間と、実際に書けた行数で測ります。
組み込みでは、ホスト側の時計が使えないためDWTのサイクルカウンタで計装前後のサイクル数を引きます。cortex_m::peripheral::DWT::cycle_count() を前後で読み、割り込み禁止区間の長さをサイクル単位で確認します。バイナリサイズの増分は cargo bloat --release が用途別に分解します。
非同期lossyロガーと同期ロガーのスループットをそのまま比較すると、前者はログを捨てている分だけ速く見えます。実運用で効いてくるのは平均値ではなく、p99/p999の遅延、drop数、キュー使用量、メモリ消費量、shutdown時のflush時間などです。同じように、defmtの転送量とJSONログの表現力、カーネルリングバッファとOTLP exporterのスループットを一つの順位表へ並べても、比較としては成立しません。
まとめ
tracing は非同期サーバーと構造化された処理追跡に強く、OpenTelemetryはその情報を複数サービスの外へ運ぶ経路になります。CLIでは log と env_logger の単純さ、単体アプリではファイルの寿命管理、カーネルでは実行文脈と障害時の安全性、組み込みでは帯域とバイナリサイズとblockingの有無が判断を分けます。
選定を進める順序としては、先に目標値を決めておくと比較が収束します。サーバーなら「ログ呼び出しのp99を何μs以内に収めるか」「lossyモードで許容する欠損を何行/秒までとするか」「Collector停止中にRSSを何MBまで許すか」、組み込みなら「割り込み禁止区間を何サイクル以内にするか」「ログ機構のバイナリ増分を何KBまでとするか」が具体的な形になります。目標値がないまま候補を並べると、速い遅いの印象だけが残ります。
測定は目標値と同じ単位で取ります。呼び出し側のコストはCriterion、欠損は dropped_lines()、バイナリ増分は cargo bloat、組み込みの区間長はDWTのサイクルカウンタというように、項目ごとに手段が決まります。サーバー側では tracing を基準実装に置き、fastrace、fast_log、spdlog-rs を同じ出力条件・同じ欠損保証で並べます。数字は、無効レベル・同期・非同期lossy・Span生成・OTLPのどれを測ったものかを分けて読みます。
ロギングの構成は、計装API・構造化・出力の三層それぞれに選択肢があり、実行環境がそのどれを使えるかを決めます。クレート名から入るとこの対応関係が見えにくくなるため、記録したい対象と送り先を先に置き、層ごとに候補を当てはめる順序が扱いやすくなります。目標値と測定手段まで決めておけば、その構成が要求を満たしているかを数字で確認できます。
参考リンク
- tracing-appender — NonBlocking writer
- OpenTelemetry — Rust Getting Started
- fastrace — リポジトリ
- minitrace — fastrace への移行案内
- fast_log — リポジトリ
- spdlog-rs — 公式ドキュメント
- log — 公式ドキュメント
- env_logger — 公式ドキュメント
- tracing — 公式ドキュメント
- flexi_logger — WriteMode
- defmt — 公式ドキュメント
- defmt-rtt — 公式ドキュメント
- Memfault — MCU Logging
- Bencher — How to benchmark Rust code with Criterion
- Divan — Fast and Simple Benchmarking for Rust
- Memfault Interrupt — Profiling Firmware on Cortex-M
- Linux kernel — Rust coding guidelines(pr_info! ほか)