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

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

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

Урок 6 из 25 ~18 мин +50 XP

JSON или console: выбор энкодера

zap JSON encoder против console-энкодера — главный разворот логгера после уровней и полей: первый пишет машиночитаемый JSON, второй — колонки для человека. Разберём, как выглядит одна и та же запись в обоих форматах, что крутит EncoderConfig и почему прод живёт на JSON, а ноутбук разработчика — на консоли.

Содержание урока · 11 разделов

Одно и то же событие — order paid — на машине разработчика печатается колонками, а в проде уезжает строкой JSON. Настройки кода не менялись, вызов Info тот же, поля те же — поменялся энкодер: компонент, который превращает запись из полей в байты. В zap энкодеров встроено два, и выбор между ними — это выбор адресата: JSON-логи для машины и систем сбора, console-вывод для глаз. Ошибка выбора стоит дорого в обе стороны: JSON на ноутбуке изматывает глаза, консоль в проде ломает парсеры системы сбора — и половина полей превращается в невидимые. Хорошая новость: формат не вшит в приложение намертво. Энкодер — настройка, которую меняют одной строкой, и держат два сразу: каждый приёмник получает свой формат, о чём в конце урока. Песочница zap не исполняет сторонние пакеты, поэтому сравнивайте выводы локально — разница видна за один запуск.

Энкодер: кто собирает запись#

Запись лога в zap проходит два этапа: сначала формируется entry — время, уровень, сообщение, место вызова — и к нему прикладываются ваши поля; потом энкодер сериализует всё это в байты. Именно энкодер решает, будет ли время дробными секундами от эпохи, окажется ли уровень строчными или заглавными и куда денутся поля. Встроенных энкодера два: json — однострочный JSON-объект на запись, и console — колонки через табуляцию для терминала. Оба настраиваются через zapcore.EncoderConfig — набор ключей и правил сериализации, который мы ниже только покажем, а полностью разберём в уроке про конфиг и отдельно в zapcore, где энкодеры собираются руками. Важно понимать: пресеты NewProduction и NewDevelopment — это не два разных логгера, а два готовых набора настроек: уровень, энкодер и его конфиг. Поменяли энкодер — поменялся формат вывода при неизменных полях, уровнях и приёмниках.

Одна запись в двух форматах

GO
package 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 в файл

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

Проверка: уровень и энкодер

В каком выводе уровень печатается заглавными буквами?

Проверка: поля в 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-сборки: энкодер, приёмник и склейка двух ядер.

encCfg := zap.NewProductionEncoderConfig() consoleCore := zapcore.NewCore( zapcore.NewConsoleEncoder(encCfg), zapcore.AddSync(os.Stderr), zapcore.InfoLevel, ) jsonCore := zapcore.NewCore( zapcore.NewEncoder(encCfg), zapcore.AddSync(os.), zapcore.InfoLevel, ) logger := zap.New(zapcore.New(consoleCore, jsonCore))

Соберите настройку двух энкодеров

Расставьте строки сборки: конфиг, два ядра, склейка и запись.

Строки

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

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

Карточки: энкодеры zap

Термин

JSON-энкодер

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

Пишет одну строку JSON-объекта на запись: время эпохой, уровень строчными, поля в общем объекте. Формат для систем сбора и продакшена.

1 / 5

Что дальше по курсу#

Формат выбран — следующий вопрос, куда физически уезжают записи: приёмники, файлы и мультиплексирование — тема восьмого урока. А конфигурация целиком — уровень, энкодер, приёмники, стартовые поля в одной структуре — разбирается в уроке про 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 и посчитайте их количество.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · JSON или console: выбор энкодера)

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

Можно ли писать JSON и локально?

Можно, и иногда нужно: если stdout контейнера сразу уходит в сборщик, локальный вывод должен совпадать с продовым. Для чистого чтения из терминала удобнее console, а JSON можно прогонять через jq.

Чем console-энкодер лучше plain-текста из fmt.Println?

Запись всё равно собирается из полей: время, уровень и caller печатаются колонками автоматически, а поля не конкатенируются, а сериализуются. От структурного логирования консоль отличается только форматом, не содержимым.

Что будет, если собрать JSON-энкодер с development-конфигом?

Получится валидный JSON с ключами T, L, M вместо ts, level, msg и временем ISO-строкой — development-конфиг рассчитан на колонки, а ключи там короткие. Выглядит сюрпризом, поэтому при смене Encoding проверяйте EncoderConfig.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси