Логирование в Rust приложениях: лучшие практики на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Логирование в 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. Управляйте уровнем логирования динамически

Блоги

Рекомендаций по книгам