Почему observability — это суперсила для Rust-разработчика
Представьте: ваш сервис на Rust работает в production, но внезапно latency взлетает до небес. Логи молчат, метрики показывают 200 OK, а пользователи жалуются. Знакомо? Это — классическая «чёрная коробка». Без observability в Rust вы слепы.
Observability (наблюдаемость) — это не просто «логи + метрики». Это способность задавать системе любые вопросы без необходимости писать новый код. В Rust, с его акцентом на производительность и надежность, observability становится обязательным ингредиентом для production-grade приложений.
В этой статье мы разберем три столпа observability в Rust: трейсинг, метрики и логи. Вы получите готовые примеры кода с tracing, OpenTelemetry и Prometheus, которые можно сразу внедрить в свой проект.
Трейсинг в Rust: путешествие запроса через систему
Трейсинг (tracing) — это история жизни одного запроса. Вместо разрозненных логов вы получаете связанный путь: от входа в API до запроса в базу данных и ответа клиенту. Для Rust лучший инструмент — крейт tracing от сообщества Tokio.
Настройка tracing в Rust-приложении
Начнем с базовой настройки. Добавьте в Cargo.toml:
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-opentelemetry = "0.23"
opentelemetry = "0.22"
opentelemetry-otlp = "0.15"
Теперь инициализируем трейсер в main.rs:
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::WithExportConfig;
fn init_tracer() -> Result<(), Box<dyn std::error::Error>> {
global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
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_resource(opentelemetry::sdk::Resource::new(vec![
KeyValue::new("service.name", "rust-observability-demo")
]))
)
.install_simple()?;
// Связываем tracing с OpenTelemetry
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = fmt::Subscriber::builder()
.with_env_filter(EnvFilter::from_default_env())
.finish()
.with(telemetry);
tracing::subscriber::set_global_default(subscriber)?;
Ok(())
}
Создание спанов и трейсов
Теперь добавим трейсинг в бизнес-логику:
use tracing::{span, Span, Level, instrument};
#[instrument]
async fn handle_request(user_id: u64) -> Result<String, Box<dyn std::error::Error>> {
let user_data = get_user_data(user_id).await?;
let processed = process_data(&user_data).await?;
Ok(processed)
}
#[instrument(skip(db_pool))]
async fn get_user_data(user_id: u64) -> Result<User, Box<dyn std::error::Error>> {
// Имитация запроса к БД
let span = Span::current();
span.record("db.query", "SELECT * FROM users WHERE id = $1");
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(User { id: user_id, name: "Alice".into() })
}
#[instrument]
async fn process_data(user: &User) -> Result<String, Box<dyn std::error::Error>> {
let result = format!("Processed user: {}", user.name);
tracing::info!(user_id = user.id, "Data processed successfully");
Ok(result)
}
Атрибут #[instrument] автоматически создает span для функции и записывает все аргументы. В Jaeger или Grafana Tempo вы увидите красивую вложенную структуру: handle_request → get_user_data → process_data.
Метрики в Rust: цифры, которые говорят правду
Метрики — это агрегированные числовые показатели: количество запросов, время ответа, использование памяти. Для метрик Rust стандарт де-факто — prometheus и metrics крейты.
Интеграция Prometheus с актами
[dependencies]
metrics = "0.22"
metrics-exporter-prometheus = "0.14"
actix-web = "4"
Настраиваем экспорт метрик:
use actix_web::{web, App, HttpServer, HttpResponse, middleware::from_fn};
use metrics::{counter, histogram, gauge};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::sync::Arc;
async fn metrics_handler() -> HttpResponse {
let metrics_data = PROMETHEUS_HANDLER
.lock()
.unwrap()
.render()
.to_string();
HttpResponse::Ok()
.content_type("text/plain; charset=utf-8")
.body(metrics_data)
}
lazy_static::lazy_static! {
static ref PROMETHEUS_HANDLER: Arc<std::sync::Mutex<metrics_exporter_prometheus::PrometheusHandle>> = {
let builder = PrometheusBuilder::new();
let handle = builder
.install_recorder()
.expect("Failed to install Prometheus recorder");
Arc::new(std::sync::Mutex::new(handle))
};
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/metrics", web::get().to(metrics_handler))
.route("/api/data", web::post().to(api_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn api_handler(body: web::Json<serde_json::Value>) -> HttpResponse {
let start = std::time::Instant::now();
counter!("api_requests_total", "endpoint" => "/api/data", "method" => "POST");
// Бизнес-логика
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let duration = start.elapsed().as_secs_f64();
histogram!("api_request_duration_seconds", duration, "endpoint" => "/api/data");
gauge!("active_requests", 1.0);
HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
}
После запуска перейдите на http://localhost:8080/metrics — вы увидите:
api_requests_total— счетчик запросовapi_request_duration_seconds— гистограмма времени ответаactive_requests— текущее количество активных запросов
Логирование в Rust: структурированный подход
Логирование в Rust с помощью tracing — это не просто println!. Ст