Логирование в Rust приложениях: лучшие практики на практике
Логирование — один из ключевых элементов любого production-приложения. Оно помогает диагностировать ошибки, отслеживать поведение системы и собирать метрики. В экосистеме Rust для этого существует несколько зрелых решений. В этой статье мы разберём лучшие практики логирования на реальных примерах кода.
1. Основы: crate log и фасадная архитектура
В Rust стандартом де-факто является crate log. Он предоставляет абстрактный фасад, не привязанный к конкретной реализации. Это означает, что вы пишете логи через макросы log, а какой именно backend будет их обрабатывать (выводить в консоль, в файл, отправлять в ELK) — решается на этапе подключения другого крейта.
Подключение базового логирования
// Cargo.toml
[dependencies]
log = "0.4"
env_logger = "0.11"
// main.rs
use log::{info, warn, error, debug};
fn main() {
// Инициализируем env_logger (читает переменную RUST_LOG)
env_logger::init();
info!("Приложение запущено");
warn!("Используется конфигурация по умолчанию");
error!("Критическая ошибка: {}", "файл не найден");
}
Запустите приложение с переменной окружения:
RUST_LOG=info cargo run
На экране вы увидите:
[2025-01-15T10:30:00Z INFO my_app] Приложение запущено
[2025-01-15T10:30:00Z WARN my_app] Используется конфигурация по умолчанию
[2025-01-15T10:30:00Z ERROR my_app] Критическая ошибка: файл не найден
2. Гибкая настройка уровней логирования
Переменная RUST_LOG позволяет гибко фильтровать сообщения по модулям и уровням. Это критично для больших проектов, где шум от отладочных логов может скрыть важные события.
Примеры фильтрации
# Только ошибки и предупреждения из всего приложения
RUST_LOG=warn
# Отладка только в модуле database, остальное — info
RUST_LOG=info,my_crate::database=debug
# Все уровни, включая trace
RUST_LOG=trace
На практике используйте такой подход:
use std::env;
fn setup_logging() {
if env::var("RUST_LOG").is_err() {
// Значение по умолчанию для development
env::set_var("RUST_LOG", "info,my_crate=debug");
}
env_logger::init();
}
3. Структурированное логирование с tracing
Для сложных систем (микросервисы, асинхронные приложения) лучше использовать крейт tracing. Он поддерживает спаны (spans) и структурированные поля, что позволяет связывать логи в цепочки запросов.
Сравнение log и tracing
- log — простой, лёгкий, подходит для небольших проектов.
- tracing — мощный, поддерживает спаны, события, структурированные поля; идеален для async-кода и систем с tracing-инструментацией.
Пример с tracing и tracing-subscriber
// Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
use tracing::{info, error, span, Level};
use tracing_subscriber::FmtSubscriber;
#[tokio::main]
async fn main() {
// Настройка subscriber для вывода в stdout
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::TRACE)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("Не удалось установить subscriber");
// Создаём спан для HTTP-запроса
let request_span = span!(Level::INFO, "http_request", method = "GET", path = "/api/users");
let _guard = request_span.enter();
info!("Обработка запроса начата");
// Вложенный спан для работы с БД
let db_span = span!(Level::DEBUG, "db_query", query = "SELECT * FROM users");
let _db_guard = db_span.enter();
// ... работа с БД
drop(_db_guard);
info!("Запрос успешно обработан");
// _guard выходит из области видимости — спан завершается
}
Вывод будет содержать временные метки, имена спанов и поля:
2025-01-15T10:35:00.123Z INFO http_request{method="GET" path="/api/users"}: Обработка запроса начата
2025-01-15T10:35:00.124Z DEBUG http_request{method="GET" path="/api/users"}:db_query{query="SELECT * FROM users"}: ...
4. Лучшие практики для production
4.1. Не логируйте чувствительные данные
Никогда не выводите пароли, токены, персональные данные (PII) в логи. Используйте пользовательский тип, реализующий трейт Display с маскировкой:
struct MaskedToken(String);
impl std::fmt::Display for MaskedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}***", &self.0[..3])
}
}
4.2. Используйте асинхронное логирование
Блокирующие операции в логировании могут замедлить приложение. Для высоконагруженных систем используйте tracing-appender или log4rs с асинхронным appender.
4.3. Структурируйте логи для машинной обработки
В production предпочтительнее выводить логи в JSON. Это упрощает интеграцию с ELK, Grafana Loki или Datadog.
use tracing_subscriber::fmt::format::Json;
let subscriber = FmtSubscriber::builder()
.json()
.with_max_level(Level::INFO)
.finish();
Вывод в JSON:
{"timestamp":"2025-01-15T10:40:00.123Z","level":"INFO","fields":{"message":"Приложение запущено"},"target":"my_app"}
4.4. Управляйте уровнем логирования динамически