Содержание урока · 11 разделов
- Энкодер: кто собирает запись
- Как выглядит console encoder: колонки и табуляция
- json против console по строкам записи
- JSON-логирование: что получаем в проде
- Экранирование и многострочность: где форматы расходятся
- Читаемость против парсабельности: моё правило
- EncoderConfig на превью: что крутится
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Одно и то же событие — order paid — на машине разработчика печатается колонками, а в проде уезжает строкой JSON. Настройки кода не менялись, вызов Info тот же, поля те же — поменялся энкодер: компонент, который превращает запись из полей в байты. В zap энкодеров встроено два, и выбор между ними — это выбор адресата: JSON-логи для машины и систем сбора, console-вывод для глаз. Ошибка выбора стоит дорого в обе стороны: JSON на ноутбуке изматывает глаза, консоль в проде ломает парсеры системы сбора — и половина полей превращается в невидимые. Хорошая новость: формат не вшит в приложение намертво. Энкодер — настройка, которую меняют одной строкой, и держат два сразу: каждый приёмник получает свой формат, о чём в конце урока. Песочница zap не исполняет сторонние пакеты, поэтому сравнивайте выводы локально — разница видна за один запуск.
Энкодер: кто собирает запись#
Запись лога в zap проходит два этапа: сначала формируется entry — время, уровень, сообщение, место вызова — и к нему прикладываются ваши поля; потом энкодер сериализует всё это в байты. Именно энкодер решает, будет ли время дробными секундами от эпохи, окажется ли уровень строчными или заглавными и куда денутся поля. Встроенных энкодера два: json — однострочный JSON-объект на запись, и console — колонки через табуляцию для терминала. Оба настраиваются через zapcore.EncoderConfig — набор ключей и правил сериализации, который мы ниже только покажем, а полностью разберём в уроке про конфиг и отдельно в zapcore, где энкодеры собираются руками. Важно понимать: пресеты NewProduction и NewDevelopment — это не два разных логгера, а два готовых набора настроек: уровень, энкодер и его конфиг. Поменяли энкодер — поменялся формат вывода при неизменных полях, уровнях и приёмниках.
Одна запись в двух форматах
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
prod, _ := zap.NewProduction()
defer prod.Sync()
dev, _ := zap.NewDevelopment()
defer dev.Sync()
orderID := "A-1024"
prod.Info("order paid", zap.String("order_id", orderID))
dev.Info("order paid", zap.String("order_id", orderID))
}Один и тот же вызов Info у двух логгеров с разными пресетами. Прод напечатает {"level":"info","ts":1712345678.12,"caller":"main/main.go:15","msg":"order paid","order_id":"A-1024"} — одна строка, время числом, уровень строчными. Development напечатает 2024-04-06T12:00:00.123+0300 INFO main/main.go:16 order paid {"order_id": "A-1024"} — колонки через табуляцию, время в ISO-формате, уровень заглавными. Поля одинаковы — различие даёт только энкодер.
Как выглядит console encoder: колонки и табуляция#
console-вывод устроен просто: время, уровень, имя логгера если есть, место в коде, сообщение — и JSON-объект с полями в хвосте, всё через табуляцию. Ключи из EncoderConfig — timeKey, levelKey и прочие — на колонки не влияют: console-энкодер печатает части записи по позициям, а ключи потратил бы только JSON. Поэтому в development-пресете ключи названы коротко — T, L, M — и это не влияет на вывод: колонки те же. Если часть записи не нужна, ей просто оставляют пустой ключ — колонка исчезает. Стек-трейс печатается отдельным блоком после строки: консольные записи остаются построчными, а трейс уезжает ниже — удобно в терминале, терпимо в файле. Разделитель меняется опцией ConsoleSeparator: таб по умолчанию, но можно поставить « | » для тех, кто читает логи в редакторе с узкой колонкой. Если логгер получил имя через Named, оно встаёт отдельной колонкой между уровнем и caller-ом — в JSON то же самое лежит под ключом logger. Мелочь, а глаз экономит.
json против console по строкам записи#
| Аспект | JSON-энкодер | console-энкодер |
|---|---|---|
| Формат записи | одна строка JSON-объекта | колонки через табуляцию, поля JSON-объектом в хвосте |
| Время | дробные секунды от эпохи (Epoch) | ISO-8601 строкой (ISO8601) |
| Уровень | строчными: info, error | заглавными: INFO, ERROR |
| Кто читает | системы сбора, jq, скрипты | человек в терминале |
| Парсинг | любой JSON-парсер без регулярок | только по позиции колонок |
| Где живёт | production, файлы, централизованный сбор | локальная разработка, демо |
JSON-логирование: что получаем в проде#
JSON-логирование — это контракт: каждая запись самодостаточна и парсится без знания о сервисе, который её написал. Система сбора разворачивает объект в поля, и любое поле становится фильтром, графиком или алертом — ровно то, о чём был первый урок про структурные логи. jq читает поток как таблицу: select по уровню, group по коду ошибки, счётчики за минуту — всё без регулярок. Числа остаются числами: duration 0.045 сортируется и агрегируется, а не превращается в строку «45ms». Кавычки и переводы строк внутри значений экранируются — запись не развалится на две строки посреди поля, что для построчных парсеров вопроса жизни и смерти. Цена тоже есть: JSON многословнее консоли процентов на двадцать-тридцать на той же записи, и человеком читается хуже. Но прод — не место для чтения глазами: там логи фильтруются запросами, и многословность окупается за неделю. Последнее преимущество скромное, но приятное: JSON-логи версионируются схемой — добавили поле, обновили схему в сборщике, и старые записи продолжают читаться по-старому без единой правки парсеров.
Экранирование и многострочность: где форматы расходятся#
Разница между форматами обнажается на неудобных значениях. Возьмите стек-трейс из ошибки: это сотни строк одним куском. JSON-энкодер экранирует управляющие символы — перевод строки становится \n внутри одной строки, и запись остаётся однострочной: парсер извлечёт трейс и покажет его с переносами в интерфейсе. console-энкодер трейс печатает отдельным блоком после колонок: в терминале это красиво, в файле записи перестают быть построчными, и наивный парсер склеит пятьдесят строк трейса в чужую запись. То же с кавычками и табуляцией внутри значений: JSON гарантирует целостность, колонки — нет. Отсюда практическое правило для смешанных систем: в файл, который будет парситься, — только JSON; console живёт либо в терминале, либо в файле, который читают глазами. И проверьте это на своей инфраструктуре: скормите сборщику обе записи из урока, одну — с трейсом, и посмотрите, что он сделал с каждой. Десять минут теста экономят неделю разбора, почему половина записей помечена битой.
Читаемость против парсабельности: моё правило#
Правило, которое у меня прижилось: console — везде, где логи читает человек в момент написания кода; JSON — везде, где логи читает машина или другой человек позже. Локальная машина — консоль. Тесты — console, чтобы падение прогона показывало читаемый контекст. Прод — JSON, без вариантов. Спорные места: staging, на котором бывает и то и другое — демо для продукта требует колонок, а сбор логов уже настроен; здесь выручает мультиплекс из следующего раздела — один логгер, два приёмника, каждый со своим форматом. Контейнеры — отдельный вопрос: docker logs отдаёт сырой stdout, и на локальном контейнере консольный вывод приятнее; но если stdout контейнера уезжает в сборщик, внутри должен быть JSON, иначе сборщик получает колонки и страдает. Спор о том, что удобнее, я закрываю вопросом: кто будет читать эти логи через месяц — вы сейчас или запрос к системе сбора? Для первого — консоль, для второго — JSON, и это почти всегда решает спор без обид. Отдельный кейс — миграция со старого текстового формата: включите JSON у нового логгера заранее, на тестовом стенде, и дайте сборщику месяц на параллельный приём. Формат — самая дешёвая часть переезда на zap: поля переносятся один в один, а старые парсеры текста просто выключаются.
Смешиваем энкодеры: console на экран, JSON в файл
GOpackage main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
encCfg := zap.NewProductionEncoderConfig()
consoleCore := zapcore.NewCore(
zapcore.NewConsoleEncoder(encCfg),
zapcore.AddSync(os.Stderr),
zapcore.InfoLevel,
)
jsonCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encCfg),
zapcore.AddSync(os.Stdout),
zapcore.InfoLevel,
)
logger := zap.New(zapcore.NewTee(consoleCore, jsonCore))
defer logger.Sync()
logger.Info("order paid", zap.String("order_id", "A-1024"))
}Два ядра — по одному на приёмник: console-энкодер пишет в stderr, JSON — в stdout; zapcore.NewTee склеивает их в один логгер, и каждый вызов Info уходит в оба. На экране — колонки, в файле или пайпе — JSON. Уровень задаётся в NewCore у каждого ядра, так что их можно развести и по серьёзности. Это уже сборка логгера руками: полный разбор ядра, энкодера и приёмника — в уроке про zapcore, здесь важна идея — формат решается на приёмнике, а не на логгере.
EncoderConfig на превью: что крутится#
zapcore.EncoderConfig — набор настроек, из которых энкодер собирает запись. Ключи решают, какие имена получат части записи: messageKey — ключ сообщения в JSON (в проде msg), timeKey (ts), levelKey (level), callerKey (caller), stacktraceKey (stacktrace). Правила сериализации решают, как эти части выглядят: encodeTime — время эпохой или ISO-строкой (EpochTimeEncoder против ISO8601TimeEncoder), encodeLevel — строчными или заглавными (LowercaseLevelEncoder против CapitalLevelEncoder), encodeDuration — длительность числом секунд или строкой «45ms», encodeCaller — полный путь или короткий файл:строка. Пустой ключ — часть записи пропускается: не хотите caller в JSON — оставьте callerKey пустым. Полный обход полей EncoderConfig с примерами — в уроке про zap.Config; пресеты, кстати, это просто предзаполненные EncoderConfig плюс уровень и приёмник. Меняя эти настройки, вы меняете словарь логов — а он, как мы обсуждали в уроке про поля, контракт на годы, поэтому правки конфига не делаются между делом.
Пока логи JSON, чтение из терминала решается одной командой: jq '.msg' app.log — только сообщения; jq 'select(.level=="error")' — одни ошибки; jq -c 'select(.order_id=="A-1024")' — события одного заказа. Это не замена системе сбора, а ежедневный инструмент разработчика до неё.
Консольная запись рассчитана на колонки фиксированной позиции, а многострочные сообщения и поля с переносами её разваливают: парсер системы сбора склеит или потеряет хвост. JSON самодостаточен: экранирование гарантирует одну строку на запись. Поэтому правило «JSON в проде» — не вкус, а требование трубопровода.
Проверка: уровень и энкодер
В каком выводе уровень печатается заглавными буквами?
Проверка: поля в console-выводе
Куда деваются поля записи в console-энкодере?
Что в первой колонке у development-пресета?
Прочитайте код и выберите, что он выведет:
dev, _ := zap.NewDevelopment()
dev.Info("service started")Сколько строк напечатает tee-логгер?
Прочитайте код и выберите, что он выведет:
logger := zap.New(zapcore.NewTee(consoleCore, jsonCore))
logger.Info("order paid", zap.String("order_id", "A-1024"))Соберите второе ядро
Допишите JSON-ядро tee-сборки: энкодер, приёмник и склейка двух ядер.
Соберите настройку двух энкодеров
Расставьте строки сборки: конфиг, два ядра, склейка и запись.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: энкодеры zap
Что дальше по курсу#
Формат выбран — следующий вопрос, куда физически уезжают записи: приёмники, файлы и мультиплексирование — тема восьмого урока. А конфигурация целиком — уровень, энкодер, приёмники, стартовые поля в одной структуре — разбирается в уроке про zap.Config. Карта курса — на его странице: после энкодеров начинается инженерия вывода — файлы, ротация и сэмплирование.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Энкодер сериализует запись в байты: json — однострочный объект, console — колонки для терминала.
- console печатает ISO-время и уровень заглавными; json — эпоху и строчные; решает это EncoderConfig, а не метод.
- Поля в console-выводе не теряются — они печатаются JSON-объектом в конце строки.
- JSON-логирование даёт парсинг без регулярок, фильтры по любому полю и числа вместо строк.
- Мой дефолт: console там, где читает человек сейчас, JSON там, где читает машина или человек потом.
- zapcore.NewTee + два ядра: console на экран, JSON в файл — один логгер, два формата.
Домашнее задание
Соберите программу из урока с двумя пресетами и одной записью с тремя полями — сравните строки глазами. Затем соберите tee-вариант: console в stderr, JSON в stdout, и запустите его с перенаправлением 2>dev.log 1>prod.log — убедитесь, что файлы содержат разные форматы одной записи. В конце примените jq к prod.log: найдите все записи с уровнем info и посчитайте их количество.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли писать JSON и локально?
Можно, и иногда нужно: если stdout контейнера сразу уходит в сборщик, локальный вывод должен совпадать с продовым. Для чистого чтения из терминала удобнее console, а JSON можно прогонять через jq.
Чем console-энкодер лучше plain-текста из fmt.Println?
Запись всё равно собирается из полей: время, уровень и caller печатаются колонками автоматически, а поля не конкатенируются, а сериализуются. От структурного логирования консоль отличается только форматом, не содержимым.
Что будет, если собрать JSON-энкодер с development-конфигом?
Получится валидный JSON с ключами T, L, M вместо ts, level, msg и временем ISO-строкой — development-конфиг рассчитан на колонки, а ключи там короткие. Выглядит сюрпризом, поэтому при смене Encoding проверяйте EncoderConfig.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
- zap.Config: полная настройка логгера
- Куда пишем: stdout, файлы и мультиплексирование
- Ротация логов: lumberjack
- zapcore: сердце библиотеки
- SugaredLogger: простой интерфейс
- Структурированные поля: zap.String и друзья
- Уровни логов: Debug, Info, Warn, Error
- Первый логгер: NewProduction и NewDevelopment
- Все курсы по Go
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси