Отслеживание: логи, метрики, трейсинг, профилировщик
Система наблюдаемости языка — постепенно наращиваемые модули поверх одной идеи (см. ИСТОРИЯ_РАЗРАБОТКИ.md, где подробно описан замысел): логи, метрики, трейсинг, профилировщик.
Логи (стандартная_библиотека/лог.код)
подключить лог;
функция главная(): целое
Логгер логгер = создать_логгер(уровень_инфо(), да, "приложение.log");
лог_инфо(логгер, "сервер запущен");
лог_предупреждение(логгер, "конфиг не найден, используются значения по умолчанию");
лог_ошибка(логгер, "не удалось подключиться к базе данных");
вернуть 0;
конец
создать_логгер(минимальный_уровень, в_консоль, путь_к_файлу):
минимальный_уровень— сообщения строже этого уровня отбрасываются
молча (обычная модель "что видно в проде" / "что видно при отладке");
задаётся одной из именованных функций уровень_отладка() /
уровень_инфо() / уровень_предупреждение() / уровень_ошибка() /
уровень_критическая(), а не магическим числом напрямую.
в_консоль— печатать ли черезвывести(...).путь_к_файлу— куда дописывать строки лога;""отключает запись
в файл (дописывается через дописать_в_файл_бинарно, то есть
безопасно для одновременной записи из нескольких запусков программы —
каждая запись это отдельный write() в конец файла).
Пять функций записи — лог_отладка/лог_инфо/лог_предупреждение/
лог_ошибка/лог_критическая(логгер, сообщение) — каждая строка
получает метку времени и уровень автоматически:
[2026-09-29 19:42:46] ОШИБКА: не удалось подключиться к базе данных
Метка времени — собственная реализация (без вызова C
strftime/localtime — тот же принцип "без внешней зависимости",
что и у хранилище.код): раскладывает unix-время на год/месяц/день/
час/минуту/секунду напрямую, алгоритмом Говарда Хиннанта
"civil_from_days" — тот же алгоритм, что лежит в основе <chrono> из
C++20. Функции время_из_unix(unix_секунды): ДатаВремя и
дата_время_в_текст(ДатаВремя): текст доступны и отдельно, не
только внутри логгера.
⚠️ Честно, узко (v1):
- Время всегда UTC, не местный часовой пояс — учёт часового пояса
и перехода на летнее/зимнее время конкретного региона отдельная, более сложная задача.
- Поддержаны только даты после 1970-01-01 (неотрицательные
unix-времена).
- Пока нет структурированных полей (
ключ=значение) сверх самого
текста сообщения, и нет вывода в JSON — только человекочитаемая строка. Запланировано следующим шагом.
- Как и у любой строки, построенной через
+/интерполяцию и не
сохранённой в переменную, промежуточные буферы форматирования одной записи лога честно утекают (тот же принятый во всём языке компромисс "утечка, не крах" — см. раздел про ARC выше) — при активном логировании это стоит иметь в виду до тех пор, пока не будет отдельного решения для этого класса утечек в масштабах всего языка.
Метрики (стандартная_библиотека/метрики.код)
Второй кирпич системы наблюдаемости — счётчики и измерители, та же модель, что в Prometheus/StatsD/большинстве промышленных систем:
- счётчик (counter) — только растёт (запросов обработано, ошибок
случилось). Сбрасывается в ноль только перезапуском программы.
- измеритель (gauge) — текущее значение, может расти и падать
(открытых соединений сейчас, занято памяти, температура датчика).
подключить метрики;
функция главная(): целое
Метрики м = создать_метрики();
счётчик_увеличить_на_единицу(м, "запросы_всего");
счётчик_увеличить(м, "ошибок_всего", 2);
измеритель_установить(м, "открытых_соединений", 5.0);
измеритель_увеличить(м, "открытых_соединений", 2.0);
измеритель_уменьшить(м, "открытых_соединений", 1.0);
метрики_напечатать(м);
вернуть 0;
конец
API:
создать_метрики(): Метрикисчётчик_увеличить(м, имя, на)/счётчик_увеличить_на_единицу(м, имя)
/ счётчик_значение(м, имя): целое — счётчик заводится с нуля при
первом обращении, дальше только прибавляет; не может убывать.
измеритель_установить(м, имя, значение)(устанавливает РОВНО в
значение) / измеритель_увеличить(м, имя, на) /
измеритель_уменьшить(м, имя, на) / измеритель_значение(м, имя): дробное
метрики_снимок(м): текст— снимок ВСЕХ метрик в текстовом формате
Prometheus exposition format
(# TYPE имя counter\nимя значение\n) — тот же формат, который отдаёт
любой настоящий /metrics-эндпоинт, значит его понимает реальный
Prometheus-сервер без переделки. Строки идут в порядке РЕГИСТРАЦИИ
метрики (первого обращения к её имени).
метрики_напечатать(м)— печатает снимок черезвывести(...).метрики_записать_файл(м, путь): результат из верного— ПЕРЕЗАПИСЫВАЕТ
файл целиком (не дописывает, в отличие от лог.код) — снимок метрик
это текущее состояние на момент вызова, а не история событий.
⚠️ Честно, узко (v1):
- Нет гистограмм (распределение величины — например, "сколько
запросов уложилось в 10мс, сколько в 100мс, сколько дольше" — нужно для процентилей задержки) — задумано отдельным следующим шагом.
- Нет меток (labels — Prometheus умеет
запросы{путь="/логин", код="200"}, здесь только плоское имя
метрики).
- ✅ Отдача снимка по HTTP — рабочий пример
examples/веб/наблюдаемость_http.код: /metrics отдаёт
метрики_снимок(...) с заголовком `Content-Type: text/plain;
version=0.0.4; charset=utf-8` — тем же, по которому настоящий
Prometheus-сервер узнаёт формат и может скрейпить эндпоинт без
дополнительной настройки. См. подробности (и честное упрощение —
обработка запросов строго последовательная, не параллельная, из-за
отсутствия потокобезопасности у Метрики/Трейсер) в разделе про
трейсинг ниже.
Трейсинг (стандартная_библиотека/трейсинг.код)
Третий и последний из первоначально задуманных кирпичей — "спаны" (span, именованные, размеченные по времени участки выполнения, вложенные друг в друга, как скобки), с человекочитаемым деревом длительностей на выходе. Та же модель, что в реальных системах трассировки (OpenTelemetry, Jaeger, Zipkin, "flame graph").
подключить трейсинг;
функция обработать_запрос(Трейсер т)
спан_начать(т, "обработать_запрос");
спан_начать(т, "прочитать_бд");
// ... работа с базой данных ...
спан_завершить(т);
спан_начать(т, "сформировать_ответ");
// ... формирование ответа ...
спан_завершить(т);
спан_завершить(т);
конец
функция главная(): целое
Трейсер т = создать_трейсер();
обработать_запрос(т);
трейс_напечатать(т);
вернуть 0;
конец
Вывод (отступ — вложенность, длительности посчитаны по монотонным
часам, монотонное_время_нс()):
прочитать_бд: 5.096 мс
сформировать_ответ: 4.949 мс
обработать_запрос: 10.064 мс
API:
создать_трейсер(): Трейсерспан_начать(т, имя)— отмечает вход в именованный участок кода.спан_завершить(т)— закрывает самый последний из открытых
спанов (не принимает имя — как закрывающая скобка не указывает, какой открывающей она соответствует, порядок сам это определяет).
трейс_снимок(т): текст— накопленный текст всех уже ЗАВЕРШЁННЫХ
спанов, деревом, в порядке закрытия. Спаны, которые ещё не дошли до
спан_завершить(), в снимок не попадают.
трейс_напечатать(т)— печатает снимок черезвывести(...).трейс_очистить(т)— сбрасывает накопленный журнал (например, между
запросами HTTP-сервера); не трогает ещё открытые спаны.
трейс_записать_файл(т, путь): результат из верного— перезаписывает
файл целиком накопленным журналом (та же причина, что у
метрики_записать_файл — см. выше).
⚠️ Честно, узко (v1):
- Спаны обязаны закрываться в строго обратном порядке открытия ("(а(б)в)"
законно, перекрёстное "(а[б)в]" — нет) — спан_завершить() не
проверяет, ЧТО именно закрывается, только "самый последний из
открытых". Настоящий span_id/родитель (как в OpenTelemetry, для
произвольного/асинхронного/межпоточного порядка) — не реализован.
- Один
Трейсер— один линейный синхронный поток выполнения; для
трассировки нескольких потоков параллельно нужен отдельный
Трейсер на поток (Трейсер, как Список/Словарь, не
потокобезопасен).
спан_завершить()без соответствующегоспан_начать()не крашит
программу — в журнал попадает явная строка-предупреждение
(⚠️ спан_завершить() без соответствующего спан_начать()).
- Нет отправки трейсов по сети (распределённая трассировка между
сервисами, протокол передачи контекста между процессами) — только локальный снимок одного процесса, хоть и уже доступный по HTTP (см. ниже).
Метрики и трейсинг по HTTP (examples/веб/наблюдаемость_http.код)
Рабочий пример, объединяющий сеть.код + метрики.код + трейсинг.код:
HTTP-сервер на порту 9090 с двумя эндпоинтами —
GET /metrics— снимокметрики_снимок(...)с заголовком
Content-Type: text/plain; version=0.0.4; charset=utf-8 (тот самый,
по которому настоящий Prometheus-сервер узнаёт формат — эндпоинт можно
напрямую указать в scrape_configs без дополнительной настройки).
GET /trace— снимоктрейс_снимок(...), дерево уже обработанных
запросов (сам HTTP-запрос к /trace — тоже спан, но попадёт в
снимок только со СЛЕДУЮЩЕГО обращения — снимок это история уже
случившегося на момент вызова, см. честные ограничения выше).
$ curl http://localhost:9090/metrics
# TYPE http_запросов_всего counter
http_запросов_всего 3
...
$ curl http://localhost:9090/trace
маршрутизация: 0.009 мс
http_запрос: 2.182 мс
⚠️ Честное, сознательное упрощение: в отличие от
examples/веб/http_сервер.код (каждое подключение — свой поток
параллельно), здесь подключения обрабатываются строго
ПОСЛЕДОВАТЕЛЬНО, одно за другим. Причина — прямое следствие уже
указанного выше ограничения: Метрики/Трейсер не потокобезопасны,
параллельная запись из нескольких потоков в общий экземпляр была бы
гонкой данных. Сделать это безопасным (например, один поток-владелец
метрик и канал для запросов на обновление, тем же приёмом, что уже
применён в http_сервер.код для передачи сокета клиента в поток) —
отдельный, более сложный следующий шаг, сознательно не взятый здесь
ради ясности примера.
Профилировщик (стандартная_библиотека/профилировщик.код)
В отличие от трейсинг.код (дерево ОДНОГО прохода выполнения — "что
происходило и в каком порядке"), профилировщик агрегирует: сколько
раз был вызван именованный участок кода и сколько суммарно/в среднем
времени он занял за ВЕСЬ прогон программы — вопрос "куда уходит
время", на который дерево одного запроса не отвечает. Ручной API — тот
же дух, что у начать_замер/закончить_замер, расставляемых вручную
вокруг измеряемого кода (а не автоматическая инструментация каждой
функции компилятором — тот путь требует поддержки в кодогенераторе и
накладных расходов на каждый вызов каждой функции, отдельный, гораздо
больший шаг на будущее).
подключить профилировщик;
функция главная()
Профилировщик п = создать_профилировщик();
начать_замер(п, "обработка_запроса");
// ... код ...
закончить_замер(п, "обработка_запроса");
профиль_напечатать(п);
// обработка_запроса: вызовов=1, всего=1.156 мс, среднее=1.156 мс
конец
создать_профилировщик(): Профилировщикначать_замер(п, имя)— отмечает начало измеряемого участка.закончить_замер(п, имя)— отмечает конец и прибавляет прошедшее
время к накопленной статистике этого имени (вызовов, всего, среднее считается из вызовов/всего).
замер_вызовов(п, имя): целое,замер_всего_нс(п, имя): целое—
текущая статистика одного имени напрямую (без отчёта целиком).
профиль_отчёт(п): текст/профиль_напечатать(п)— сводка по
всем измеренным участкам, в порядке их ПЕРВОГО измерения (сортировки по убыванию времени в языке пока нет — самый затратный участок приходится искать глазами по колонке "всего").
⚠️ Честно, узко (v1):
начать_замер/закончить_замердля ОДНОГО И ТОГО ЖЕ имени не
поддерживают вложенность/рекурсию — повторный начать_замер(п, "а")
без закончить_замер между вызовами молча перезаписывает время
начала, первый вызов "теряется" (засчитается только внешний
интервал). Для вложенных участков с РАЗНЫМИ именами всё работает
нормально. Если нужна именно вложенность как таковая — для этого
существует трейсинг.код (спаны); разумно использовать оба модуля
вместе: трейсинг — "что происходило и в каком порядке в этом
запуске", профилировщик — "куда суммарно ушло время за всю
программу".
закончить_замер()без предшествующегоначать_замер()того же
имени молча ничего не делает (не крашит программу, но и не пишет
предупреждение, в отличие от спан_завершить() в трейсинге — писать
было бы некуда без отдельного текстового журнала; для диагностики
таких ошибок используйте лог.код рядом).
- Та же причина, что и в
метрики.код/трейсинг.код:Словарьне
поддерживает перечисление ключей, поэтому имена измеряемых участков
хранятся дважды — в Словаре (быстрый доступ) и параллельно в
Списке (порядок перебора при отчёте).
- Не потокобезопасен — как и
Список/Словарь/Метрики/Трейсер,
один Профилировщик на один поток параллельно.