はじめに
こんにちは、Product TeamのManagerのよしだです。弊社ではRustおよびNestJSを利用してマイクロサービスアーキテクチャのプロダクト開発を実施しております。 これまでの記事で、NestJSでのログ出力 、Rustでのログ出力の紹介をしておりますが、マイクロサービスアーキテクチャでは単体の情報収集だけではなくプロダクトの全体像、マイクロサービス間の振る舞いを観測する必要があります。今回は、解決策でもある分散トレーシングをRust、OpenTelemetry、New Relicで実現する方法を紹介したいと思います。
この記事はエモーションテック Advent Calendar 2022の11日目の記事です。
なぜ分散トレーシングなのか
マイクロサービスアーキテクチャは運用フェーズになると意識しなければならない機能、サーバー、その他リソースが多くなってきます。万が一問題が発生した場合、問題解決が難しくなってきます。その解決方法として、最初に実践できる方法がログの出力と収集です。各マイクロサービスが、トランザクションを識別するような一意のIDをログ出力することでトラブルの原因を見つけやすくなります。 しかしながら複数のマイクロサービスを横断した処理の場合、各サービスのパフォーマンス等も確認したくなることが多くなってきます。このような分散アーキテクチャの問題を解決する考えとして「分散トレーシング」(Distributed tracing)があります。 分散トレーシンングツールというとJaeger が有名ですが、 弊社ではこちらの記事でも紹介しているNew Relicを利用しております。そこで、New Relicを用いて分散トレーシングの実現をしてみました。
RustとNew RelicそしてOpenTelemetry
弊社ではRustを用いたマイクロサービスがあるのですが、残念ながらNew RelicのAPMがRustをサポートしていません(New Relicさん今後期待しています)。しかしながら、New RelicではOpenTelemetryのトレースも可能になっているため、RustでOpenTelemetryを導入し、New Relicにトレース情報を送信することを目指します。
RustにOpenTelemetryを導入してみる
RustにOpenTelemetryを導入するため、OpenTelemetry Rustを利用します。
[dependencies] opentelemetry = "0.18.0"
まずは、ライブラリのドキュメントを参考に動かしてみます。
use opentelemetry::{global, KeyValue}; use opentelemetry::sdk::export::trace::stdout; use opentelemetry::trace::{TraceContextExt, Tracer}; fn main() { let tracer = stdout::new_pipeline().install_simple(); tracer.in_span("doing_work", |cx| { let span = cx.span(); span.add_event( "tracer".to_string(), vec![KeyValue::new("Starting tracer", true)], ); }); global::shutdown_tracer_provider(); }
実行すると標準出力に以下のようなログが表示されます。
SpanData { span_context: SpanContext { trace_id: b02e4662307aa65d93a7b4604a7e9c14, span_id: f8b86556f1d0e088, trace_flags: TraceFlags(1), is_remote: false, trace_state: TraceState(None) }, parent_span_id: 0000000000000000, span_kind: Internal, name: "doing_work", start_time: SystemTime { tv_sec: 1670548094, tv_nsec: 689502000 }, end_time: SystemTime { tv_sec: 1670548094, tv_nsec: 689549000 }, attributes: EvictedHashMap { map: {}, evict_list: [], max_len: 128, dropped_count: 0 }, events: EvictedQueue { queue: Some([Event { name: "tracer", timestamp: SystemTime { tv_sec: 1670548094, tv_nsec: 689526000 }, attributes: [KeyValue { key: Static("Starting tracer"), value: Bool(true) }], dropped_attributes_count: 0 }]), max_len: 128, dropped_count: 0 }, links: EvictedQueue { queue: None, max_len: 128, dropped_count: 0 }, status: Unset, resource: Resource { attrs: {Static("service.name"): String(Owned("unknown_service"))}, schema_url: None }, instrumentation_lib: InstrumentationLibrary { name: "opentelemetry", version: Some("0.18.0"), schema_url: None } }
RustのプロジェクトにOpenTelemetryを導入できました。
OpenTelemeryの情報をNew Relicをイベントを送信してみる
次にNew Relicにこの情報を送信したいと思います。OpenTelemetryを用いてNew Relicにデータを送信(OpenTelemetryの表現だとエクスポート)するためには、2つの方法があります。
- アプリから直接データを送信する
- OpenTelemetry コレクター経由で送信する
今回はアプリ(Rust)から直接データを送信をしてみます。送信するためにはOpenTelemetry Rustに加えて他のcrateも導入します。
[dependencies] opentelemetry = { version = "0.18.0", default-features = false, features = ["trace", "rt-tokio-current-thread"] } opentelemetry-otlp = { version = "0.11.0", features = ["reqwest-client", "reqwest-rustls", "http-proto"] } opentelemetry-semantic-conventions = "0.10.0" tokio = { version = "1.21.2", features = ["full"] }
opentelemetry-otlpは、OpenTelemetry形式のデータを送信するためのcrateです。またopentelemetry-semantic-conventionsを導入し、OpenTelemetryの標準的な属性を扱いやすくします。 サンプルコードは以下です。
use opentelemetry::sdk::{trace, Resource}; use opentelemetry::trace::{TraceContextExt, Tracer}; use opentelemetry::{global, KeyValue}; use opentelemetry_otlp::WithExportConfig; use std::collections::HashMap; #[tokio::main] pub async fn main() -> std::io::Result<()> { let new_relic_app_name = "<New Relicで表示させるEntity名>"; let new_relic_license_key = "<New Relic API Key>"; let otel_exporter_otlp_traces_endpoint = "https://otlp.nr-data.net:4317/v1/traces"; let exporter = opentelemetry_otlp::new_exporter() .http() .with_endpoint(otel_exporter_otlp_traces_endpoint) .with_headers(HashMap::from([( "api-key".into(), new_relic_license_key.to_string(), )])); let tracer = opentelemetry_otlp::new_pipeline() .tracing() .with_exporter(exporter) .with_trace_config( trace::config().with_resource(Resource::new(vec![KeyValue::new( opentelemetry_semantic_conventions::resource::SERVICE_NAME, new_relic_app_name.to_string(), )])), ) .install_batch(opentelemetry::runtime::TokioCurrentThread) .expect("Error - Failed to create tracer."); tracer.in_span("doing_work", |cx| { let span = cx.span(); span.add_event( "tracer".to_string(), vec![KeyValue::new("Starting tracer", true)], ); }); global::shutdown_tracer_provider(); Ok(()) }
先ほどのコードとの違いは、トレース情報をNew Relic向けに送信するためのexporterを作成する箇所です。Exporterで設定する送信先のエンドポイントやヘッダーに関する情報は、New Relicのドキュメントに紹介されているので参考にするとよいです。ヘッダーに入れるNew RelicのAPI Keyの種類は間違いやすいので確認しましょう。
プログラムを実行するとNew Relicの画面だと以下のように表示されます。
Actix WebとOpenTelemetryを導入しNew Relicでトレースする
RustでOpenTelemetryを使って、New Relicに情報を送信できたので、最後は分散トレーシングを試すためにActix WebでのOpenTelemetryの導入方法を紹介します。方法としては、Rustでのログ出力で紹介されているtracingを組み合わせることで導入できます。 記事を参考に必要なcrateを追加していきます。
[dependencies] actix-web = "4" opentelemetry = { version = "0.18.0", default-features = false, features = ["trace", "rt-tokio-current-thread"] } opentelemetry-otlp = { version = "0.11.0", features = ["reqwest-client", "reqwest-rustls", "http-proto"] } opentelemetry-semantic-conventions = "0.10.0" tokio = { version = "1.21.2", features = ["full"] } tracing = "0.1.37" tracing-actix-web = { version = "0.6.2", features = ["opentelemetry_0_18"] } tracing-opentelemetry = "0.18.0" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "registry"] }
Actix WebのサンプルコードにOpenTelemetryとtracingを導入してみます。
use actix_web::{get, post, App, HttpResponse, HttpServer, Responder}; use opentelemetry::sdk::propagation::TraceContextPropagator; use opentelemetry::sdk::{trace, Resource}; use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig; use std::collections::HashMap; use tracing_actix_web::TracingLogger; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::Registry; #[get("/")] async fn hello() -> impl Responder { HttpResponse::Ok().body("Hello world!") } #[post("/echo")] async fn echo(req_body: String) -> impl Responder { HttpResponse::Ok().body(req_body) } #[actix_web::main] async fn main() -> std::io::Result<()> { let new_relic_app_name = "<New Relicで表示させるEntity名>"; let new_relic_license_key = "<New Relic API Key>"; opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); let otel_exporter_otlp_traces_endpoint = "https://otlp.nr-data.net:4317/v1/traces"; let exporter = opentelemetry_otlp::new_exporter() .http() .with_endpoint(otel_exporter_otlp_traces_endpoint) .with_headers(HashMap::from([( "api-key".into(), new_relic_license_key.to_string(), )])); let tracer = opentelemetry_otlp::new_pipeline() .tracing() .with_exporter(exporter) .with_trace_config( trace::config().with_resource(Resource::new(vec![KeyValue::new( opentelemetry_semantic_conventions::resource::SERVICE_NAME, new_relic_app_name.to_string(), )])), ) .install_batch(opentelemetry::runtime::TokioCurrentThread) .expect("Error - Failed to create tracer."); Registry::default() .with(OpenTelemetryLayer::new(tracer)) .init(); HttpServer::new(|| { App::new() .service(hello) .service(echo) .wrap(TracingLogger::default()) }) .bind(("0.0.0.0", 8081))? .run() .await?; opentelemetry::global::shutdown_tracer_provider(); Ok(()) }
プログラムを実行するとNew Relicの画面だと以下のように表示されます。
New Relicで分散トレーシングをやってみる
RustおよびActix WebにOpenTelemetryを導入し、New Relicにイベント情報を送信する方法を紹介しました。最後にOpenResty + OpenTelemety => Rust(Actix Web) + OpenTelemetry => NestJS + NewRelic agenetを用いた通信をしてみます(図だけの紹介です)。New Relicの画面では各機能がどの経路で通信が行われ、各処理にどの程度の時間がかかっているかがわかります。
おわりに
マイクロサービスの運用を改善する手段の一つである分散トレーシングをRust、OpenTelemetry、New Relicを使って実現をしてみました。弊社ではまだ導入段階なのでこれから本格的に利用していきたいと思います。また、OpenTelemetryおよびOpenTelemetryAPIをサポートするサービスが増えてきておりますので、OpenTelemetryの導入をすることで比較的容易に分散トレーシングができるようになりました。 みなさんもぜひRustでの開発時にOpenTelemetryを導入することもご検討ください。
エモーションテックでは顧客体験、従業員体験の改善をサポートし、世の中の体験を変えるプロダクトを開発しています。プロダクトに興味のある方、Rustを使ったアプリケーション開発をしたい方、ぜひ採用ページからご応募をお願いいたします。