RustでもOTLPでJaegerにテレメトリを送りたい
2024年12月29日追記
RustのOpenTelemetryライブラリはまだ安定化されておらず、割とドラスティックにAPIが更新されています。
そのため、現時点の最新のライブラリを使おうとして下のコードをコピペしてもビルドが通らないと思います。
拙作のadvoidのtrace.rsは割と最近のバージョンへの追従を頑張っているので、良かったらそちらを参考にしてみてください。
はじめに
テキストベースのロギングは時代遅れ、時代はOpenTelemetryを使ったハイカラな計装!!ということでタイトル通りに試してみます。
とはいってもRust関係でまとまった記事を書いてくださってるのは以下のブログくらいみたいなので、とりあえずは以下の記事を参考に試してみます。
OpenTelemetry
OpenTelemetryはそれぞれの監視ツールベンダが提供してきたAPIを共通化し、アプリケーションコードから可能な限りベンダ固有のコードを除去することを目的としてる。と個人的に認識しています。
例えばNew RelicからAWS X-Rayに監視バックエンドを変更しようとした際、OpenTelemetryを使用していればアプリケーションのコネクタ部分だけ変えればすぐにメトリクスの送信先を変えられるといった感じらしいです。
今回はシングルバイナリでサクッと建てられるJaegerを使います。
setlocal
set BASE_DIR=%~dp0
set SPAN_STORAGE_TYPE=badger
set BADGER_EPHEMERAL=false
set BADGER_DIRECTORY_VALUE=C:\path\to\.jaeger\data
set BADGER_DIRECTORY_KEY=C:\path\to\.jaeger\key
start http://localhost:16686
call %BASE_DIR%jaeger-all-in-one.exe
endlocal
みたいなバッチを作っておくとサクッと立ち上げられるので便利です。
tracingの初期化
今回はトレーシングライブラリとしてtokio-rs/tracingを使用します。
tracingのレイヤーとしてOpenTelemetryのテレメトリを送信するControllerを差し込みます。
tracing自体はtokioには依存せず使用できますが、テレメトリの送信にgRPCを使用しており、gRPCがtonicを使用しているため自動的にtokioに依存することになります。 が、そこそこの規模のアプリケーションを開発する場合ほぼtokioを使うことになると思うので特に気にしなくても良いと思います。
use opentelemetry::sdk::metrics::controllers::BasicController;
use opentelemetry_otlp::WithExportConfig;
pub(crate) struct OtelInitGuard();
impl Drop for OtelInitGuard {
fn drop(&mut self) {
opentelemetry::global::shutdown_tracer_provider();
}
}
// https://github.com/open-telemetry/opentelemetry-rust/blob/d4b9befea04bcc7fc19319a6ebf5b5070131c486/examples/basic-otlp/src/main.rs#L35-L52
fn build_metrics_controller() -> BasicController {
use opentelemetry::sdk::export::metrics::aggregation::cumulative_temporality_selector;
use opentelemetry::sdk::metrics::selectors::simple::histogram;
opentelemetry_otlp::new_pipeline()
.metrics(
histogram(Vec::new()),
cumulative_temporality_selector(),
opentelemetry::runtime::Tokio,
)
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://localhost:4317"),
)
.build()
.expect("Failed to build metrics controller")
}
pub(crate) fn init_tracing(service: &'static str, version: &'static str) -> OtelInitGuard {
use opentelemetry::sdk::trace::{RandomIdGenerator, Sampler};
// Configure otel exporter.
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("http://localhost:4317"),
)
.with_trace_config(
opentelemetry::sdk::trace::config()
.with_sampler(Sampler::AlwaysOn)
.with_id_generator(RandomIdGenerator::default())
.with_resource(opentelemetry::sdk::Resource::new(vec![
opentelemetry::KeyValue::new("service.name", service),
opentelemetry::KeyValue::new("service.version", version),
])),
)
.install_batch(opentelemetry::runtime::Tokio)
// .install_simple()
.expect("Not running in tokio runtime");
// Compatible layer with tracing.
let otel_trace_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let otel_metrics_layer = tracing_opentelemetry::MetricsLayer::new(build_metrics_controller());
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
tracing_subscriber::Registry::default()
.with(tracing_subscriber::fmt::Layer::new())
.with(otel_trace_layer)
.with(otel_metrics_layer)
.with(tracing_subscriber::filter::LevelFilter::INFO)
.init();
OtelInitGuard()
}
アプリケーションコード
起動時にOpenTelemetryの初期化さえしてしまえば、あとは普通にtracingを使うだけです。
メソッドに#[instrument]
を貼れば自動的にSpanを作ってコンテキストを埋め込んでくれるので便利です。
mod otl;
use crate::otl::init_tracing;
use tracing::{error, info, instrument};
#[instrument]
async fn start(x: i32, y: i32) -> Option<i32> {
add(multiply(x, y).await, multiply(x, y).await).await
}
#[instrument]
async fn add(x: i32, y: i32) -> Option<i32> {
let ans = x + y;
if ans <= 10 {
info!(
ans = ans,
"特に出すべきログがないからとりあえず適当なメッセージを出しています"
);
Some(ans)
} else {
error!(ans = ans, "something went wrong");
None
}
}
#[instrument]
async fn multiply(x: i32, y: i32) -> i32 {
x * y
}
#[tokio::main]
async fn main() {
let service = env!("CARGO_PKG_NAME");
let version = env!("CARGO_PKG_VERSION");
let _guard = init_tracing(service, version);
let value = start(1, 2).await;
println!("{:?}", value);
let value = start(10, 22).await;
println!("{:?}", value);
}