Содержание урока · 9 разделов
Второй час ночи, дежурному приходит алерт «процент ошибок вырос», и он открывает логи сервиса заказов. Перед ним тысячи строк вида «2024-03-14 02:17:33 ошибка при обработке заказа 42», и чтобы понять, что случилось, нужно глазами вычитать похожие строки, найти их соседей и вручную склеить историю заказа из разрозненных кусков. Так выглядит разбор текстовых логов: машина хранит всё, а помогает только человеку, да и то при условии, что авторы строк писали аккуратно и одинаково. Структурированное логирование переворачивает расклад: запись лога — не фраза, а набор полей со значениями, которые машина умеет фильтровать, группировать и считать без единой регулярки. В этом уроке разберём, чем плохи текстовые логи, какое место пакет log/slog занимает среди логгеров Go и куда вообще попадают логи на проде — от стандартного вывода контейнера до хранилища, в котором дежурный ищет ответы.
Проблема текстовых логов: парсинг глазами и регулярками#
Беда текстовых логов не в том, что их плохо пишут, а в том, что строка — плоский формат. Чтобы ответить на вопрос «сколько заказов упало на оплате за последний час», дежурный собирает конвейер из grep, cut и awk, а в худшем случае тянет регулярку, потому что часть значений слеплена пробелами посреди фразы. Каждый сервис в компании лепит строки по-своему: один начинает с даты, второй — с уровня, третий вставляет всё после двоеточия, и парсер, написанный для первого сервиса, на втором выдаёт мусор. Разбор ломается молча: поле переименовали — фильтр перестал находить записи, но узнает об этом человек только на следующем инциденте. А попробуйте сгруппировать ошибки по пользователю или посчитать длительность обработки, если она вшита в середину фразы «обработано за 137 мс». Всё это лечится одним изменением контракта: лог пишется не как фраза, а как запись с именованными полями, и машина сама делает то, что раньше делали глаза.
Лог читают двое: человек во время инцидента и программа, которая собирает миллионы записей в хранилище. Текстовый формат обслуживает первого и проваливает второго: программе нужен парсер под каждый сервис. Структурированный формат закрывает обоих — человек видит поля, программа их индексирует. Поэтому договорённость о формате логов в команде так же важна, как договорённость о схеме таблицы в базе.
Golang slog: структурный логгер стандартной библиотеки#
Ответом экосистемы стал пакет log/slog, вошедший в стандартную библиотеку в версии Go 1.21. До этого выбор делали между сторонними логгерами: zap от Uber, zerolog, logrus, — и каждая команда таскала свой набор обёрток поверх них. Slog договорился о вещах более важных, чем скорость: единый API уровней и атрибутов, заменяемый обработчик вывода и ноль внешних зависимостей. Вызов логирования выглядит как сообщение с парами «ключ — значение»: slog.Info("заказ оформлен", "id", 42). Уровень говорит, насколько важна запись, атрибуты несут поля, а Handler решает, во что превратить запись: строку, JSON или корпоративный формат. Логирование в go долгие годы было слабым местом стандартной библиотеки — теперь это слой, на котором строят и собственные обёртки, и миграцию со старых пакетов. Если раньше вопрос «какой у вас логгер go» означал выбор зависимости, то теперь у него есть ответ по умолчанию.
Два стиля одной записи
GO// старый стиль: фраза склеена руками, поля внутри строки
log.Printf("заказ %d оформлен на сумму %d, пользователь %s",
order.ID, order.Amount, order.User)
// slog: сообщение и поля идут отдельными значениями
slog.Info("заказ оформлен",
"id", order.ID,
"amount", order.Amount,
"user", order.User)Оба варианта пишут в один поток, но выдают разный материал. log.Printf отдаёт готовую фразу, из которой поля приходится выковыривать регуляркой — а текст в значениях легко ломает такой парсер. slog.Info передаёт сообщение и пары ключ-значение отдельно, и обработчик сам решает, как их нарисовать: по умолчанию получится строка вида «INFO заказ оформлен id=42 amount=990 user=anna», а после замены обработчика — строка JSON с теми же полями. Заметьте, что числа передаются числами: тип значения сохраняется, и сумма не превращается в строку на выходе. Полный разбор вызова и формата по умолчанию — во втором уроке.
Текстовый лог против структурного#
| Вопрос дежурного | Текстовые логи | Структурные логи |
|---|---|---|
| Сколько ошибок за последний час? | grep -c и надежда, что формат строк одинаковый | фильтр по полю level=ERROR и подсчёт |
| Что случилось с заказом 42? | ручное склеивание строк по подстроке | фильтр по полю order_id=42 |
| Какие пользователи чаще попадают в ошибки? | выписки из консоли и ручная таблица | группировка по полю user |
| Новая колонка в отчёте | новая регулярка и правка парсера | новое поле в записи, парсер не меняется |
| Чтение человеком | удобно сразу, если писал аккуратный автор | удобно при внятных именах полей |
Поля дают пользу ровно до тех пор, пока их имена договорены. Если один сервис пишет order_id, второй orderId, а третий вообще id заказа в сообщении, хранилище логов превратится в три несмешивающихся озера, и поиск по заказу потребует трёх запросов. Поэтому в командах появляется короткий словарь: имена полей в нижнем регистре через подчёркивание, обязательные поля для каждого запроса — идентификатор пользователя, идентификатор запроса, длительность, — и запрет класть значения внутрь фразы сообщения. У slog под эту договорённость есть механика предзаданных полей и групп, до которых курс доберётся в восьмом и седьмом уроках. Ещё один слой — типы значений: длительность хранится числом миллисекунд, а не строкой со словом «мс», иначе усреднение в хранилище невозможно. Правило, которое стоит вынести из урока: поле придумывают один раз, пишут в общий README и используют во всех сервисах команды.
Кейс: сервис печатал отладку через fmt.Println — «зашёл в обработчик», «вышел», «пользователь такой-то». Работало, пока не понадобилось убрать шум: у fmt нет уровней, нет включения и выключения, нет направления в отдельный поток — только stdout. Вычищать печать пришлось вручную, и отладка исчезла ровно в тот день, когда была нужна. Вывод: отладочные сообщения либо логируются с уровнем Debug, который отключается настройкой, либо не попадают в продакшен.
Куда попадают логи на проде#
Второй вопрос, без которого непонятна ценность структуры: где логи живут на проде. Ответ неожиданно скучный: приложение почти не заботится о месте. Сервис пишет в стандартный вывод, и дальше подключается окружение. Docker собирает stdout и stderr каждого контейнера в файл на хосте — драйвер по умолчанию называется json-file, а docker logs показывает содержимое. Для сервисов под systemd поток ловит journald, записи читаются через journalctl -u имя-сервиса. Дальше по конвейеру стоит коллектор — Promtail, Fluent Bit или Vector, — который отправляет записи в хранилище: Loki, Elasticsearch или что-то самописное. Именно там структурные поля превращаются в индекс, по которому за секунды ищут по идентификатору заказа и уровню. Запись в файлы из приложения, ротация и права на диск — тема двадцать четвёртого урока с командами Docker и journald. Сейчас важно запомнить принцип: stdout наружу, структуру внутрь, сбор — окружению.
Из чего состоит запись slog#
- Время: момент записи, проставляется обработчиком вывода.
- Уровень: Debug, Info, Warn или Error — метка важности.
- Сообщение: короткая фраза о событии, без вставленных в неё значений.
- Атрибуты: пары ключ-значение — поля, по которым ищут и группируют.
- Источник: файл и строка кода, если включить опцию AddSource.
- Обработчик: компонент, который превращает запись в строку или JSON.
Меню: три уровня в одной программе
GOpackage main
import (
"log/slog"
)
func main() {
slog.Info("заказ оформлен", "id", 42, "amount", 990)
slog.Warn("оплата зависла", "id", 42, "attempt", 3)
slog.Error("платёж не прошёл", "id", 42, "err", "timeout")
}Программа целиком, чтобы снять магию: импорт log/slog — и записи готовы, никакого конструктора логгера в минимальном случае не нужно. Каждая строка вывода по умолчанию выглядит так: дата и время, уровень INFO, WARN или ERROR, сообщение и пары ключ=значение в порядке вызова. Обработчик по умолчанию пишет в stderr через пакет log, поэтому в терминале записи видны сразу, а в конвейере их можно отделить от stdout. Уровни и их семантика — тема третьего урока, атрибуты и их типы — четвёртого.
Спорное мнение: не каждому сервису нужна структура#
Мнение, с которым можно спорить на кухне: структурированные логи нужны не всякому проекту. CLI-утилите, которая печатает прогресс, или скрипту на сто строк достаточно fmt и stderr: у них нет дежурных, инцидентов и миллиона записей в сутки, а структура лишь разбухает код. Граница переезда конкретная: сервис живёт дольше недели, его перезапускают в чужое время суток, и есть человек, который ночью отвечает на вопрос «что сломалось». Тогда стоимость настройки slog — один импорт и несколько строк — исчезает на фоне первой же ночи, когда разбор занял минуты вместо часов. Пользователям logrus и zap тоже не нужно ничего переписывать сегодня: обе библиотеки спокойно соседствуют с slog, и миграцию обычно делают по слоям, а не одним коммитом. Про переезд со старого пакета log написан отдельный десятый урок, а про уровни по окружению — восемнадцатый.
Пакета log/slog в песочнице сайта нет: там работают подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors, time. Механику структурной записи можно размять прямо в браузере: соберите из fmt.Println пару ключ=значение и почувствуйте, как растёт код с каждым новым полем, — ровно эту рутину убирает slog. Полные примеры курса запускайте локально: go mod init и go run main.go, сторонних пакетов не потребуется, log/slog уже внутри тулчейна начиная с Go 1.21.
Проверка: структурная запись
Чем структурный лог отличается от текстового?
Проверка: путь лога на проде
Куда сервис должен писать логи, чтобы их собрал Docker?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
slog.Info("заказ оформлен", "id", 42, "amount", 990)
// обработчик — дефолтный, без настроекСоберите первую запись
Допишите минимальную программу с одной структурной записью уровня Warn.
Соберите минимальную программу по строкам
Расставьте строки в порядке следования: объявление пакета, импорт, точка входа, две записи по возрастанию важности, закрывающая скобка.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: знакомство с slog
Карта собрана: текстовые логи — строка без полей, структурные — запись с полями, golang slog — способ писать вторые без зависимостей, а на проде логи собирают Docker и journald и отвозят в хранилище, где поля становятся индексом. Дальше — практика: во втором уроке вызовем slog.Info и разберём формат по умолчанию до символа, в третьем — уровни и их семантику, в четвёртом — атрибуты. Если интересен альтернативный путь с упором на скорость, посмотрите курс по zap — там тот же структурный подход с другим набором компромиссов. Общая страница курса slog держит план всех двадцати пяти уроков, домашнее задание ниже — разминка перед первым кодом.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Текстовый лог — строка: вопросы к нему решаются парсингом и регулярками, которые ломаются молча.
- Структурный лог — запись с полями: фильтрация и группировка идут по именованным значениям.
- log/slog — структурный логгер стандартной библиотеки с Go 1.21: уровни, атрибуты, сменный Handler.
- Имена полей — договорённость команды: один словарь на все сервисы, значения без текста внутри.
- На проде сервис пишет в stdout: сбором занимаются Docker и journald, дальше — коллектор и хранилище.
- fmt.Println в проде — отладка без выключателя: у печати нет уровней и направлений.
Домашнее задание
Найдите в своём проекте или любом опенсорс-сервисе три строки лога и письменно ответьте: какие значения в них спрятаны внутри фразы и как выглядела бы запись slog с этими значениями в виде пар ключ-значение. Затем запустите минимальную программу из урока локально и проверьте направление вывода: добавьте перенаправление 2>err.log и убедитесь, что stdout пуст, а записи ушли в файл ошибок.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем slog лучше zap и logrus?
Вопрос не в превосходстве, а в цене зависимости: slog — стандартная библиотека, значит ноль модулей, гарантии совместимости Go 1 и общий API для всей команды. zap быстрее на предельных нагрузках, logrus живёт в режиме поддержки; обе библиотеки соседствуют с slog, миграцию делают постепенно.
Нужен ли slog новый Go?
Пакет log/slog появился в Go 1.21. На старых версиях есть промежуточный путь — модуль golang.org/x/exp/slog с тем же API на время перехода, но обновление тулчейна обычно дешевле, чем содержание экспериментальной зависимости.
Можно ли писать логи сразу в файл?
Технически да — обработчику передают io.Writer от открытого файла. На проде чаще пишут в stdout: стандартные потоки забирает окружение (Docker, systemd) и отдаёт коллектору, а файл внутри приложения означает ротацию, права и место на диске на стороне приложения.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
PostgreSQL в Go: драйвер pgx с нуля до продакшена
Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.
- Подключаться к PostgreSQL через pgx и pgxpool
- Сканировать строки в структуры и справляться с NULL
- Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
- Проводить транзакции и массовые вставки через Batch и COPY