Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 1 из 25 ~16 мин +20 XP

Знакомство с slog: зачем структурированные логи

Golang slog — структурный логгер стандартной библиотеки Go, который с версии 1.21 закрыл десятилетний вопрос «какой логгер выбрать». Но чтобы почувствовать, зачем он нужен, начнём не с API, а с двух часов ночи: дежурный смотрит в консоль и пытается понять, что произошло с заказом 42. В этом уроке — честный разбор проблемы текстовых логов, устройство записи slog и путь лога от stdout до хранилища, где его читает человек.

Содержание урока · 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 и используют во всех сервисах команды.

Куда попадают логи на проде#

Второй вопрос, без которого непонятна ценность структуры: где логи живут на проде. Ответ неожиданно скучный: приложение почти не заботится о месте. Сервис пишет в стандартный вывод, и дальше подключается окружение. 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.

Меню: три уровня в одной программе

GO
package 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 написан отдельный десятый урок, а про уровни по окружению — восемнадцатый.

Проверка: структурная запись

Чем структурный лог отличается от текстового?

Проверка: путь лога на проде

Куда сервис должен писать логи, чтобы их собрал Docker?

Что выведет программа?

Прочитайте код и выберите, что он выведет:

slog.Info("заказ оформлен", "id", 42, "amount", 990)

// обработчик — дефолтный, без настроек

Соберите первую запись

Допишите минимальную программу с одной структурной записью уровня Warn.

package import "" func main() { slog.("оплата зависла", "", 42, "attempt", 3) }

Соберите минимальную программу по строкам

Расставьте строки в порядке следования: объявление пакета, импорт, точка входа, две записи по возрастанию важности, закрывающая скобка.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: знакомство с slog

Термин

Структурированные логи

нажмите, чтобы перевернуть
Определение

Формат, в котором запись — сообщение плюс именованные поля. Поля фильтруются и группируются в хранилище без парсинга строк.

1 / 6

Карта собрана: текстовые логи — строка без полей, структурные — запись с полями, 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 · Знакомство с slog: зачем структурированные логи)

Вопросы и ответы по уроку

Чем 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) и отдаёт коллектору, а файл внутри приложения означает ротацию, права и место на диске на стороне приложения.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

pgx25 уроков~12 чпрактика25 уроков уже доступны

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY