---
title: "Знакомство с slog: зачем структурированные логи"
description: "Golang slog — структурное логирование в Go: почему текстовые логи ломают разбор инцидентов, что дают атрибуты и уровни и куда попадают логи на проде."
course: "Логирование в Go: структурные логи на slog"
url: "https://golangmaster.ru/courses/slog/lessons/01-znakomstvo-slog/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

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

*Курс «Логирование в Go: структурные логи на slog», урок 1 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/slog/lessons/01-znakomstvo-slog/*

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

Второй час ночи, дежурному приходит алерт «процент ошибок вырос», и он открывает логи сервиса заказов. Перед ним тысячи строк вида «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 с теми же полями. Заметьте, что числа передаются числами: тип значения сохраняется, и сумма не превращается в строку на выходе. Полный разбор вызова и формата по умолчанию — во [втором уроке](/courses/slog/lessons/02-pervye-stroki-loga/).

### Текстовый лог против структурного

| Вопрос дежурного | Текстовые логи | Структурные логи |
| --- | --- | --- |
| Сколько ошибок за последний час? | grep -c и надежда, что формат строк одинаковый | фильтр по полю level=ERROR и подсчёт |
| Что случилось с заказом 42? | ручное склеивание строк по подстроке | фильтр по полю order_id=42 |
| Какие пользователи чаще попадают в ошибки? | выписки из консоли и ручная таблица | группировка по полю user |
| Новая колонка в отчёте | новая регулярка и правка парсера | новое поле в записи, парсер не меняется |
| Чтение человеком | удобно сразу, если писал аккуратный автор | удобно при внятных именах полей |

Поля дают пользу ровно до тех пор, пока их имена договорены. Если один сервис пишет order_id, второй orderId, а третий вообще id заказа в сообщении, хранилище логов превратится в три несмешивающихся озера, и поиск по заказу потребует трёх запросов. Поэтому в командах появляется короткий словарь: имена полей в нижнем регистре через подчёркивание, обязательные поля для каждого запроса — идентификатор пользователя, идентификатор запроса, длительность, — и запрет класть значения внутрь фразы сообщения. У slog под эту договорённость есть механика предзаданных полей и групп, до которых курс доберётся в [восьмом](/courses/slog/lessons/08-with-predlog/) и [седьмом](/courses/slog/lessons/07-group/) уроках. Ещё один слой — типы значений: длительность хранится числом миллисекунд, а не строкой со словом «мс», иначе усреднение в хранилище невозможно. Правило, которое стоит вынести из урока: поле придумывают один раз, пишут в общий README и используют во всех сервисах команды.

> **fmt.Println на проде: отладка без выключателя.** Кейс: сервис печатал отладку через fmt.Println — «зашёл в обработчик», «вышел», «пользователь такой-то». Работало, пока не понадобилось убрать шум: у fmt нет уровней, нет включения и выключения, нет направления в отдельный поток — только stdout. Вычищать печать пришлось вручную, и отладка исчезла ровно в тот день, когда была нужна. Вывод: отладочные сообщения либо логируются с уровнем Debug, который отключается настройкой, либо не попадают в продакшен.

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

Второй вопрос, без которого непонятна ценность структуры: где логи живут на проде. Ответ неожиданно скучный: приложение почти не заботится о месте. Сервис пишет в стандартный вывод, и дальше подключается окружение. Docker собирает stdout и stderr каждого контейнера в файл на хосте — драйвер по умолчанию называется json-file, а docker logs показывает содержимое. Для сервисов под systemd поток ловит journald, записи читаются через journalctl -u имя-сервиса. Дальше по конвейеру стоит коллектор — Promtail, Fluent Bit или Vector, — который отправляет записи в хранилище: Loki, Elasticsearch или что-то самописное. Именно там структурные поля превращаются в индекс, по которому за секунды ищут по идентификатору заказа и уровню. Запись в файлы из приложения, ротация и права на диск — тема [двадцать четвёртого урока](/courses/slog/lessons/24-logi-na-prode/) с командами 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 написан отдельный [десятый урок](/courses/slog/lessons/10-migraciya-s-log/), а про уровни по окружению — [восемнадцатый](/courses/slog/lessons/18-urovni-po-env/).

> **Песочница и локальный запуск.** Пакета 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?

- ✅ В stdout и stderr — Docker перехватывает стандартные потоки сам
- В файл /var/log/app.log внутри контейнера
- В journald хоста напрямую из процесса
- В базу данных через SQL-драйвер

Контейнерный контракт прост: процесс пишет в стандартные потоки, Docker складывает их в файл на хосте, дальше подключается коллектор. Файл внутри контейнера остаётся без сборки — контейнер умрёт вместе с ним при пересоздании. Подробности и команды — в двадцать четвёртом уроке.

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

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

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

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

- ✅ Строку вида 2024/03/14 02:17:33 INFO заказ оформлен id=42 amount=990
- JSON вида {"level":"INFO","msg":"заказ оформлен","id":42,"amount":990}
- Строку INFO заказ оформлен с полями в скобках [id=42 amount=990]
- Ничего: без настроенного обработчика slog молчит

Обработчик по умолчанию печатает через пакет log: дата и время, уровень INFO, сообщение и пары ключ=значение в порядке вызова. JSON появится после явной замены обработчика на JSONHandler — это пятый урок. Молчать slog тоже не умеет: разумный вариант по умолчанию у него есть.

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

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

```go
package ___

import "___"

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

Подсказка: Имя пакета — main, путь импорта — log/slog, запись уровня Warn делает slog.Warn, а ключ перед атрибутом 42 — order.

Правильные ответы в пропусках: `main`, `log/slog`, `Warn`, `order`

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

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

Правильный порядок строк:

1. `package main`
2. `import "log/slog"`
3. `func main() {`
4. `slog.Info("заказ оформлен", "id", 42)`
5. `slog.Warn("оплата зависла", "id", 42)`
6. `}`

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

- **Структурированные логи** — Формат, в котором запись — сообщение плюс именованные поля. Поля фильтруются и группируются в хранилище без парсинга строк.
- **log/slog** — Пакет стандартной библиотеки Go с версии 1.21: уровни, атрибуты и заменяемый обработчик вывода без внешних зависимостей.
- **Атрибут** — Пара ключ-значение в записи: id=42, user=anna. Носитель полей, по которым ведут поиск и группировки.
- **Уровень** — Метка важности записи: Debug, Info, Warn, Error. По уровню записи отфильтровываются и настраиваются по окружению.
- **Handler** — Обработчик, который превращает запись slog в конкретный формат: строку, JSON или корпоративный вид. Меняется без правки вызовов.
- **json-file** — Драйвер Docker по умолчанию: собирает stdout и stderr контейнера в файл на хосте, доступный через docker logs.

Карта собрана: текстовые логи — строка без полей, структурные — запись с полями, golang slog — способ писать вторые без зависимостей, а на проде логи собирают Docker и journald и отвозят в хранилище, где поля становятся индексом. Дальше — практика: во [втором уроке](/courses/slog/lessons/02-pervye-stroki-loga/) вызовем slog.Info и разберём формат по умолчанию до символа, в [третьем](/courses/slog/lessons/03-urovni-logov/) — уровни и их семантику, в [четвёртом](/courses/slog/lessons/04-klyuchi-atributy/) — атрибуты. Если интересен альтернативный путь с упором на скорость, посмотрите [курс по zap](/courses/zap/) — там тот же структурный подход с другим набором компромиссов. Общая [страница курса slog](/courses/slog/) держит план всех двадцати пяти уроков, домашнее задание ниже — разминка перед первым кодом.

## Коротко: что запомнить

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