Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Логирование

В рамках изучения логирования, мы будем рассматривать библиотеку tracing. Эта библиотека представляет из себя целую экосистему для логирования, но основных крэйта в ней два:

  • tracing — предоставляет API для логирования (является фасадом)
  • tracing-subscriber — реализация логгера для tracing

В экосистеме Rust также присутствует другая популярная библиотека логирования — log (фасад) и её реализации: env_logger и log4rs. Но мы сосредоточимся только на tracing по двум причинам:

  • tracing изначально создана с прицелом на асинхронность, что важно для бекенд приложений
  • tracing также умеет работать с библиотеками, которые используют log для логирования

Первый взгляд на tracing

Для начала нам нужно подключить в Cargo.toml соответствующие зависимости:

[package]
name = "test_rust"
version = "0.1.0"
edition = "2024"

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time", "json"] }

Теперь мы готовы воспользоваться логгером:

fn main() {
    tracing_subscriber::fmt().init(); // Включаем tracing логирование
    tracing::info!("Hello"); // Пишем в лог информационное сообщение
}

Если мы запустим программу, то увидим в консоли такое лог-сообщение.

$ cargo run
2025-12-15T23:04:17.046455Z  INFO test_rust: Hello

Оно состоит из следующих компонентов:

  • 2025-12-15T23:04:17.046455Z — время создания лог-сообщения
  • INFO — уровень лог-сообщения
  • test_rust — имя программы
  • Hello — текст лог-сообщения

Внутреннее устройство

Давайте еще раз взглянем на наш пример:

fn main() {
    tracing_subscriber::fmt().init();
    tracing::info!("Hello");
}

Запись tracing_subscriber::fmt().init() является сокращённой и делает сразу несколько вещей. Для ясности давайте распишем этот пример подробнее:

use tracing_subscriber::FmtSubscriber;
use tracing_subscriber::fmt::SubscriberBuilder;
use tracing_subscriber::util::SubscriberInitExt;

fn main() {
    let builder: SubscriberBuilder = tracing_subscriber::fmt();
    let subscriber: FmtSubscriber = builder.finish();
    subscriber.init(); // регистрация сабскрайбера в глобальном диспатчере

    tracing::info!("Hello");
}

Здесь мы делаем следующее:

  1. tracing_subscriber::fmt() — создаём билдер SubscriberBuilder, который позволяет провести настройку сабскрайбера.
  2. builder.finish() — создаём объект сабскрайбера (Subscriber), который является непосредственным обработчиком лог-сообщений. (В примере тип сабскрайбера — FmtSubscriber, но это просто псевдоним для генерик структуры Subscriber).
  3. subscriber.init() — регистрируем объект сабскрайбера в глобальном диспетчере (dispatcher).
  4. tracing::info!("Hello") — отправляем лог-событие глобальному диспетчеру, который перенаправит его для обработки в сабскрайбер.

Упрощённо, работа механизма логирования выглядит так:

Для нас, как для пользователей библиотеки tracing, наиболее важным элементом является сам Subscriber.

pub struct Subscriber<
    N = format::DefaultFields, // перечень полей лог-сообщения
    E = format::Format<format::Full>, // формат лог-сообщения
    F = LevelFilter, // уровни логирования: TRACE, DEBUG, INFO, WARN, ERROR
    W = fn() -> io::Stdout, // то куда производится запись, по умолчанию - STDOUT
> {
    inner: layer::Layered<F, Formatter<N, E, W>>,
}

На данный момент эта структура должна быть совершенно непонятной, но к концу главы мы разберёмся со всеми её составляющими. А пока давайте приступим к практике логирования.

Note

Если решите покопаться во внутренностях библиотеки, имейте в виду, что есть трэйт Subscriber, объявленный в крэйте tracing, а есть одноимённая структура Subscriber из крэйта tracing-subscriber. В этой главе, мы в основном имеем дело со структурой Subscriber, но если вам понадобится реализовать какой-то очень специфический логгер, например, который пишет логи напрямую в базу данных, то вам, скорее всего, придётся реализовать именно трэйт Subscriber.

Формат лог-записей

Билдер SubscriberBuilder, с которым мы уже познакомились, позволяет настроить формат логирования для сабскрайбера. Билдер предоставляет следующие методы для настройки формата лог-записей:

  • with_ansi(bool) — указывает, будет ли логгер использовать управляющие ANSI коды для того, чтобы выделять разные части лог-записи разными цветами. При перенаправлении логов с консоли в файл этот флаг следует устанавливать в false. По умолчанию: true.
  • with_file(bool) — добавлять ли в лог-запись имя файла, из которого было произведено логирование. По умолчанию: false.
  • with_line_number(bool) — добавлять ли в лог-запись номер строки, из которой было произведено логирование. По умолчанию: false.
  • with_target(bool) — добавлять ли в лог-запись имя программы. По умолчанию: true.
  • with_thread_ids(bool) — добавлять ли в лог-запись ID потока, из которого было произведено логирование. По умолчанию: false.
  • with_thread_names(bool) — добавлять ли в лог-запись имя потока, из которого было произведено логирование. По умолчанию: false.
  • with_timer(Format) — определяет формат даты и времени в лог-записи. По умолчанию: UtcTime.
  • without_time() — выключает отображение даты и времени в лог-записи.
  • json() — лог-запись будет представлена в виде JSON объекта. Доступно только при включенной “json” фиче.

Для примера, настроим формат лог-сообщений так, чтобы убрать из них имя приложения, но добавить имя файла и номер строки. Плюс вместо астрономического времени будет отображаться, сколько времени прошло с момента запуска программы.

use tracing_subscriber::fmt::time::Uptime;

fn main() {
    tracing_subscriber::fmt()
        .with_target(false)
        .with_file(true)
        .with_line_number(true)
        .with_timer(Uptime::default())
        .init();

    tracing::info!("Hello");
}

Вывод программы:

$ cargo run
0.000186881s  INFO src/main.rs:11: Hello

Отдельно стоит отметить форматирование в виде JSON объекта:

fn main() {
    tracing_subscriber::fmt().json().init();
    tracing::info!("Hello");
}

Программа выведет:

$ cargo run
{"timestamp":"2025-12-17T14:28:29.197314Z","level":"INFO","fields":{"message":"Hello"},"target":"test_rust"}

Уровни логирования

tracing предлагает пять уровней логирования: TRACE, DEBUG, INFO, WARN, ERROR. По умолчанию логируются все лог-сообщения с уровнем INFO и выше.

То есть такая программа:

fn main() {
    tracing_subscriber::fmt().init();

    tracing::trace!("Hello");
    tracing::debug!("Hello");
    tracing::info!("Hello");
    tracing::warn!("Hello");
    tracing::error!("Hello");
}

выведет:

$ cargo run
2025-12-16T14:57:02.786497Z  INFO test_rust: Hello
2025-12-16T14:57:02.786556Z  WARN test_rust: Hello
2025-12-16T14:57:02.786567Z ERROR test_rust: Hello

Чтобы установить отображаемый уровень логирования, используется метод with_max_level из билдера SubscriberBuilder. Например, если мы хотим, чтобы выводились уровни DEBUG, INFO, WARN и ERROR, то надо настроить сабскрайбер так:

tracing_subscriber::fmt()
    .with_max_level(LevelFilter::DEBUG)
    .init();

Фильтрация сообщений

Если вы включите DEBUG или TRACE уровень логирования в приложении, в котором имеются зависимости (другие Rust библиотеки), то велика вероятность, что, кроме логов непосредственно из вашего кода, вы увидите целый шквал сообщений из кода зависимостей.

К счастью, tracing-subscriber позволяет фильтровать лог-сообщения по таким критериям как:

  • target — он же крэйт
  • модуль
  • уровень лога

Фильтр задаётся на билдере SubscriberBuilder при помощи метода with_env_filter, который принимает аргумент типа impl Into<EnvFilter>.

Объект EnvFilter можно сконструировать “руками”, но гораздо проще задать его строкой. Эта строка должна содержать разделённые запятой секции вида таргет::модуль=уровень, где:

  • таргет (опционален) — имя крэйта, из которого происходит логирование
  • модуль (опционален) — имя модуля в этом крэйте, из которого происходит логирование
  • уровень — уровень логирования

Например, строка "myapp::mod1=debug,myapp::mod2=trace,info" указывает, что для всего приложения должен использоваться уровень INFO, но для модуля mod1 из крэйта myapp должен использоваться уровень DEBUG, а для модуля mod1 из крэйта myapp — TRACE.

Важно заметить, что порядок секций в строке не важен: если мы поменяем строку на "info,myapp::mod1=debug,myapp::mod2=trace", то ничего не изменится.

В качестве примера рассмотрим программу с двумя модулями: для одного модуля будет установлен DEBUG уровень, для другого — WARN, а для всего приложения — INFO.

fn main() {
    tracing_subscriber::fmt()
        .with_env_filter("test_rust::mod_a=debug,test_rust::mod_b=warn,info")
        .init();

    mod_a::func();
    mod_b::func();
    tracing::info!("from root: test");
}

mod mod_a {
    pub fn func() {
        // выведется, так как для этого модуля уровень логирования - DEBUG
        tracing::debug!("from mod_a: test");
    }
}

mod mod_b {
    pub fn func() {
        // не выведется, так как для этого модуля уровень логирования - WARN
        tracing::info!("from mod_b: test");
    }
}

Запустим программу:

$ cargo run
2025-12-16T20:09:58.958779Z DEBUG test_rust::mod_a: from mod_a: test
2025-12-16T20:09:58.958855Z  INFO test_rust: from root: test

Альтернативно, вместо указания правил фильтрации в виде одной строки, мы можем создать объект EnvFilter вручную:

let filter = tracing_subscriber::EnvFilter::from_default_env()
    .add_directive("test_rust::mod_a=debug".parse().unwrap())
    .add_directive("test_rust::mod_b=warn".parse().unwrap())
    .add_directive("info".parse().unwrap());

tracing_subscriber::fmt()
    .with_env_filter(filter)
    .init();

но на практике так обычно не делают, потому что для программ на Rust принято задавать опции фильтрации логов при помощи переменной окружения RUST_LOG.

Для этого достаточно просто сконфигурировать фильтр логов так:

tracing_subscriber::fmt()
    .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
    .init();

и после этого можно задавать опции фильтрации через переменную окружения:

$ export RUST_LOG="test_rust::mod_a=debug,test_rust::mod_b=warn,info"
$ cargo run
2025-12-16T20:09:58.958779Z DEBUG test_rust::mod_a: from mod_a: test
2025-12-16T20:09:58.958855Z  INFO test_rust: from root: test

Note

На Windows:

set RUST_LOG="test_rust::mod_a=debug,test_rust::mod_b=warn,info"
cargo run
2025-12-16T20:09:58.958779Z DEBUG test_rust::mod_a: from mod_a: test
2025-12-16T20:09:58.958855Z  INFO test_rust: from root: test

Запись логов в файл

По умолчанию логи пишутся на стандартный вывод, но это можно изменить путём задания альтернативного приёмника при помощи метода with_writer у билдера SubscriberBuilder.

Этот метод принимает объект любого типа, реализующего трэйт MakeWriter, что включает любые типы, реализующие трэйт std::io::Write. А так как тип std::fs::File реализует std::io::Write, то примитивное логирование в файл можно реализовать так:

fn main() {
    let file = std::fs::File::create("app.log").unwrap();
    tracing_subscriber::fmt()
        .with_writer(file)
        .with_ansi(false) // консольные управляющие символы в файле не нужны
        .init();
    tracing::info!("Hello");
}

Запустив эту программу, вы увидите, что действительно был создан файл app.log.

Разумеется, в долгоживущем бекенд приложении нужна более гибкая запись логов в файл. Экосистема tracing предлагает библиотеку tracing-appender, которая умеет:

  • производить запись логов в файл в неблокирующем режиме
  • автоматически переключаться на новый лог файл каждый день/час/минуту/и т.д.
  • автоматически удалять устаревшие файлы с логами

Для начала подключим tracing-appender в Cargo.toml.

[package]
name = "test_rust"
version = "0.1.0"
edition = "2024"

[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] }
tracing-appender = "0.2"

Теперь давайте настроим логирование так, чтобы запись логов производилась в каталог logs, а файлы с логами имели префикс app.log, и новый лог файл создавался каждый час.

fn main() {
    let appender = tracing_appender::rolling::daily("logs", "app.log");

    tracing_subscriber::fmt()
        .with_writer(appender)
        .with_ansi(false)
        .init();

    tracing::info!("Hello");
}

После запуска программы в каталоге проекта должна появиться директория logs, в которой должен находиться файл с именем формата app.log.год-месяц-день.

Если нам потребуется более гибкая настройка, то мы должны воспользоваться билдером RollingFileAppender.

use tracing_appender::rolling::{RollingFileAppender, Rotation};

fn main() {
    let appender = RollingFileAppender::builder()
        .rotation(Rotation::DAILY) // ротация лог-файла каждый день
        .max_log_files(10) // хранить только 10 последних лог-файлов
        .filename_prefix("app") // префикс имени лог-файла до компонента даты
        .filename_suffix("log") // суффикс имени лог-файла после компонента даты
        .build("logs") // имя директории для лог-файлов
        .unwrap();

    tracing_subscriber::fmt()
        .with_writer(appender)
        .with_ansi(false)
        .init();

    tracing::info!("Hello");
}

После запуска в каталоге logs должен появиться лог-файл с именем формата app.год-месяц-день.log. Например, app.2025-12-17.log.

Если мы хотим писать логи одновременно и в файл, и на консоль, то для простоты можем воспользоваться комбинатором and из утилитарного трэйта MakeWriterExt, который позволяет скомбинировать два приёмника логов в один.

use tracing_subscriber::fmt::writer::MakeWriterExt;

fn main() {
    let file_appender = tracing_appender::rolling::daily("logs", "app.log")
        .with_max_level(tracing::Level::INFO);

    let stdout_appender = std::io::stdout
        .with_max_level(tracing::Level::DEBUG);

    tracing_subscriber::fmt()
        .with_writer(stdout_appender.and(file_appender))
        .with_ansi(false)
        .init();

    tracing::info!("Hello");
}

В случае если для каждого из приёмников логов требуется более тонкая настройка, придётся воспользоваться слоями, о которых мы поговорим дальше в этой главе.

Неблокирующее логирование

Как мы уже сказали в предыдущей секции, библиотека tracing-appender предоставляет не только функциональность для гибкой записи логов в файл, но также и функциональность для неблокирующей записи.

Для того чтобы сделать из синхронного писателя неблокирующий, нужно воспользоваться функцией non_blocking:

fn main() {
    let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout());

    tracing_subscriber::fmt()
        .with_writer(non_blocking)
        .init();

    tracing::info!("Hello");
}

Функция non_blocking запускает отдельный поток, который по каналу принимает лог-сообщения и производит запись. Функция non_blocking возвращает кортеж из двух объектов:

  1. Объект типа NonBlocking, который по сути является обёрткой над отправителем (Sender) лог-сообщений в канал.
  2. Охранный объект типа WorkerGuard, чей деструктор сначала убедится, что все лог-сообщения из канала записаны, а после завершит поток и уничтожит канал. Нужен для корректного завершения приложения.

Warning

Важно! Если новые лог-сообщения будут поступать быстрее, чем поток будет успевать их записывать, то канал переполнится и новые лог-сообщения будут просто отбрасываться. Размер канала по умолчанию рассчитан на 128000 лог-сообщений.

Слои

Layer (слой) — трэйт, задающий интерфейс для фильтрации, форматирования и записи логов. Совсем как сабскрайбер. Однако, в отличие от сабскрайберов, несколько слоёв можно объединить в один и использовать этот объёдинённый слой как сабскайбер. При этом каждый из слоёв может быть сконфигурирован так же гибко, как сабскрайбер.

Например, мы можем создать один слой для записи логов в стандартный вывод, другой слой — для записи в файла. Комбинация этих слоёв будет производить запись и в стандартный вывод, и в файл.

Для создания логгера из слоёв, мы должны использовать объект Registry, который создаётся функцией tracing_subscriber::registry(). Это легче понять на примере:

use tracing_subscriber::{fmt::layer, layer::SubscriberExt, util::SubscriberInitExt};

fn main() {
    // Слой для записи в файл
    let file_layer = layer()
        .with_writer(tracing_appender::rolling::daily("logs", "app.log"))
        .with_ansi(false);

    // Слой для записи на консоль
    let stdout_layer = layer()
        .with_writer(std::io::stdout);

    tracing_subscriber::registry() // registry() вместо fmt() 
        .with(stdout_layer)
        .with(file_layer)
        .init();

    tracing::info!("Hello");
}

Запустив программу, мы увидим, что она записала лог-сообщение и на консоль, и в файл.


И на этом моменте мы готовы еще раз взглянуть на определение структуры Subscriber, которое мы видели в самом начале главы:

pub struct Subscriber<
    N = format::DefaultFields, // перечень полей лог-сообщения
    E = format::Format<format::Full>, // формат лог-сообщения
    F = LevelFilter, // уровни логирования: TRACE, DEBUG, INFO, WARN, ERROR
    W = fn() -> io::Stdout, // то куда производится запись, по умолчанию - STDOUT
> {
    inner: layer::Layered<F, Formatter<N, E, W>>, // слои
}

Как видите, эта структура содержит поле inner, которое имеет тип Layered — хранилище слоёв. На самом деле, все наши сабскрайберы из предыдущих примеров, всегда состояли из слоёв. Просто у них был только один слой.

Теперь мы можем детализировать раньше приведённую диаграмму, указав внутреннее устройство стандартного сабскрайбера (структуры из библиотеки tracing-subscriber).

Т.е. когда мы конфигурируем логгер просто как:

tracing_subscriber::fmt().init()

то создаётся сабскрайбер с одним слоем, который печатает логи в STDOUT.

Span

Последнее, с чем нам осталось познакомиться — span (диапазон, охват). Это понятие сложно перевести на русский, поэтому мы будем называть его просто span.

Span представляет некий участок кода, к которому можно привязать контекст логирования с дополнительными атрибутами.

И это гораздо проще понять на примере:

use tracing::{Span, span::Entered};

fn main() {
    tracing_subscriber::fmt().init();

    // Для INFO логов создаём span с атрибутом attr1 и значение 5
    let my_span: Span = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    let _enter: Entered<'_> = my_span.enter();
    
    tracing::info!("Hello");
}

Такая программа напечатает:

2025-12-17T23:07:23.633043Z  INFO span1{attr1=5}: test_rust: Hello

Как видите, в строке лога добавилось span1{attr1=5}.

Как это работает?

  • Сначала мы создаём объект типа Span, при помощи которого говорим, что хотим, чтобы для логов с уровнем INFO добавлялся атрибут с именем “attr1” и значением 5.
  • Далее мы мы “активируем” span в текущей области путём вызова метода enter. Этот метод возвращает объект типа Entered, который представляет из себя активированный span.
  • В той области кода, в которой жив объект Entered, к логам будут добавляться атрибуты из span объекта.

Рассмотрим следующий пример, который наглядно демонстрирует, что атрибуты из span добавляются в лог-сообщение, только в той области, где существует объект Entered.

fn main() {
    tracing_subscriber::fmt().init();

    let my_span = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    {
        let mut _enter = my_span.enter();
        tracing::info!("Hello 1");
    }
    tracing::info!("Hello 2");
}

Вывод программы.

2025-12-17T23:35:55.528112Z  INFO span1{attr1=5}: test_rust: Hello 1
2025-12-17T23:35:55.528148Z  INFO test_rust: Hello 2

В большинстве ситуаций мы хотим “активировать” span сразу после его создания, но мы не можем написать так:

let my_span: Entered<'_> = tracing::span!(Level::INFO, "span1", attr1 = 5).enter();

Дело в том, что объект Entered хранит ссылку на объект span, от которого он был создан. А так как в коде выше, объект span создаётся и сразу же уничтожается, то ссылку становится хранить не на что.

Для решения этой проблемы тип Span предлагает другой метод — entered, который возвращает объект типа EnteredSpan. Он ведёт себя подобно Entered, но вместо того, чтобы держать ссылку на объект Span, он забирает объект Span себе во владение.

use tracing::{Level, span::EnteredSpan};

fn main() {
    tracing_subscriber::fmt().init();

    let my_span: EnteredSpan = tracing::span!(Level::INFO, "span1", attr1 = 5)
        .entered();
    tracing::info!("Hello 1");
    my_span.exit();
    tracing::info!("Hello 2");
}

Программа выводит:

2025-12-18T00:34:31.045841Z  INFO span1{attr1=5}: test_rust: Hello 1
2025-12-18T00:34:31.045887Z  INFO test_rust: Hello 2

В работе, подход с методом entered используется чаще, потому что он короче. Но в этой главе мы продолжим использовать метод enter, так как он более наглядный.

Пересечение span

Когда мы создаём объект span в области, где уже активирован другой span, то уже активированный span становится родителем свежесозданного. Это означает, что в лог-сообщение попадут атрибуты и из родительского, и из дочернего span-ов.

Рассмотрим это на примере:

fn main() {
    tracing_subscriber::fmt().init();

    let span1 = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    let _enter1 = span1.enter();

    let span2 = tracing::span!(tracing::Level::INFO, "span2", attr2 = 7);
    let _enter2 = span2.enter();
    
    tracing::info!("Hello");
}

Вывод программы:

2025-12-17T23:08:54.836930Z  INFO span1{attr1=5}:span2{attr2=7}: test_rust: Hello

Теперь давайте перепишем этот пример так, чтобы второй объект span создавался до активации первого span. В такой ситуации отношение родитель-потомок установлено не будет, и последний активированный span просто перекроет собой всё, что было активировано до него.

fn main() {
    tracing_subscriber::fmt().init();

    let span1 = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    let span2 = tracing::span!(tracing::Level::INFO, "span2", attr2 = 7);

    let _enter1 = span1.enter();
    let _enter2 = span2.enter(); // перекроет собой span1
    
    tracing::info!("Hello");
}

Вывод программы:

2025-12-17T23:09:36.704697Z  INFO span2{attr2=7}: test_rust: Hello

Также, надо заметить, что макрос span! позволяет при помощи параметра parent явно указать родительский span.

fn main() {
    tracing_subscriber::fmt().init();

    let span1 = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    let span2 = tracing::span!(parent: &span1, tracing::Level::INFO, "span2", attr2 = 7);

    let _enter1 = span1.enter();
    let _enter2 = span2.enter();
    
    tracing::info!("Hello");
}

Эта программа уже выводит атрибуты от обоих span:

2025-12-18T00:55:30.040449Z  INFO span1{attr1=5}:span2{attr2=7}: test_rust: Hello

instrument

Библиотека tracing предоставляет макрос instrument, который позволяет “инструментировать” определение функции, путём оборачивания её тела в span.

Рассмотрим пример:

fn main() {
    tracing_subscriber::fmt().init();
    let _ = func(1, "PARAM2");
}

#[tracing::instrument]
fn func(param1: i32, param2: &str) -> String {
    tracing::info!("Hello");
    String::from("RESULT")
}

Вывод программы

2025-12-18T15:27:16.330514Z  INFO func{param1=1 param2="PARAM2"}: test_rust: Hello

Как видите, инструментирование создаёт span с таким же именем, как у функции, и с атрибутами, совпадающими с аргументами функции.

Макрос instrument предлагает ряд параметров для настройки:

fn main() {
    tracing_subscriber::fmt()
        .without_time()
        .init();
    let _ = func(1, "PARAM2");
}

#[tracing::instrument(
    name = "span_func", // имя span
    level = "info",     // уровень логирования
    fields(attr1 = %param1, attr2="xxx"), // дополнительные атрибуты
    skip(param1), // не включать аргумент param1 в атрибуты
    ret // включить автоматическое логирование результата функции
)]
fn func(param1: i32, param2: &str) -> String {
    tracing::info!("Hello");
    String::from("RESULT")
}

Вывод программы:

INFO span_func{param2="PARAM2" attr1=1 attr2="xxx"}: test_rust: Hello
INFO span_func{param2="PARAM2" attr1=1 attr2="xxx"}: test_rust: return="RESULT"

Внутреннее устройство span

При активации объекта span, его данные помещаются в thread local, что позволяет сохранять доступ к объекту span даже во вложенных функциях.

Например:

fn main() {
    tracing_subscriber::fmt().init();

    let span = tracing::span!(tracing::Level::INFO, "span1", attr1 = 5);
    let _entered = span.enter();
    
    func();
}

fn func() {
    tracing::info!("Hello");
}

Вывод программы:

2025-12-18T00:58:56.030620Z  INFO span1{attr1=5}: test_rust: Hello

Как видите, атрибуты из span попали в лог вывод из функции.

Warning

При написании async кода, span может работать некорректно, если в области действия активного span объекта, вставить вызов await. Но об этом мы поговорим позже.