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

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

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

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

Первый логгер: NewProduction и NewDevelopment

Пять строк кода отделяют вас от первого JSON-лога: в этом уроке мы создаём логгер вызовом zap.NewProduction — и заодно смотрим zap.NewDevelopment, второй готовый пресет. Установим пакет, напишем первую программу с Info и Error, разберём каждое поле в выводе и научимся выбирать пресет по окружению.

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

Установка: go mod init и go get#

Начните с пустого модуля: mkdir demo, cd demo, затем go mod init demo. Дальше — go get go.uber.org/zap: команда подтянет последний релиз ветки v1 и пропишет его в go.mod. На этом установка закончена — zap не требует инициализации, конфигов и внешних сервисов, логгер создаётся одним вызовом конструктора. Одна оговорка о формате курса: песочница сайта исполняет только чистый Go без сторонних модулей, поэтому zap-код здесь не запускается — держите терминал под рукой. Понимание проверяют quiz, predict, fill и order-задачи, а запускать примеры вы будете локально, где zap честно печатает свои строки в stdout. Это, кстати, лучший способ учиться: разница между двумя пресетами, о которой мы будем говорить, видна глазами за секунду.

Первая программа с логгером

GO
package main

import (
        "errors"

        "go.uber.org/zap"
)

func main() {
        logger, _ := zap.NewProduction()
        defer logger.Sync()

        logger.Info("service started",
                zap.Int("port", 8080),
        )

        err := errors.New("connection refused")
        logger.Error("dial failed",
                zap.Error(err),
        )
}

zap.NewProduction() возвращает два значения: логгер и ошибку конфигурации — в учебном коде её отбрасывают через _, в боевом обрабатывают с fallback-логгером. defer logger.Sync() сбрасывает буферы перед выходом. Info и Error — методы записи: первым аргументом сообщение, дальше любое число полей. Запуск напечатает две строки: {"level":"info","ts":1712345678.12,"caller":"main/main.go:12","msg":"service started","port":8080} и {"level":"error","ts":...,"caller":"main/main.go:16","msg":"dial failed","error":"connection refused"} — у Error ещё появится стек-трейс, мы его в выводе сократили.

zap.NewProduction: читаем JSON по ключам#

Каждая запись production-пресета — однострочный JSON. level — серьёзность строчными: info, error. ts — время в секундах от эпохи, дробное число: так его дешевле сериализовать и хранить, а человеко-читаемый формат отдаётся интерфейсу, который показывает логи. caller — файл и строка, откуда произошёл вызов: без него «кто это залогировал» превращается в детектив. msg — ваше сообщение, короткая фраза-событие. Дальше идут ваши поля в том порядке, как вы их передали: port, order_id, error. На уровне Error и выше пресет добавит ключ stacktrace со стеком горутины — сразу видно, где именно случилось. Ещё в production-пресете спрятано сэмплирование: после ста одинаковых записей в секунду zap начинает прореживать поток, чтобы сервис при шторме ошибок не утонул в собственных логах — этой механике посвящён отдельный урок.

Ключи записи по умолчанию в production#

КлючЧто содержитКто его кладёт
levelсерьёзность строчными: info, errorzap — по вызванному методу
tsвремя: дробные секунды от эпохиэнкодер, автоматически
callerфайл:строка вызоваzap — вызовы включены по умолчанию
msgсообщение-событиепервый аргумент Info и Error
ваши поляport, order_id, error и прочиевы — через zap.String и компанию
stacktraceстек горутиныzap — для Error и выше

zap.NewDevelopment: тот же код, другой вывод

GO
package main

import (
        "go.uber.org/zap"
)

func main() {
        logger, _ := zap.NewDevelopment()
        defer logger.Sync()

        logger.Debug("cache lookup",
                zap.String("key", "user:42"),
                zap.Bool("hit", false),
        )
}

Разница с прошлым примером — один конструктор: zap.NewDevelopment. Запуск покажет не JSON, а табличную строку: 2024-04-06T12:00:00.123+0300, табуляция, DEBUG, main/main.go:10, cache lookup и объект {"key": "user:42", "hit": false} в хвосте. Дата в человеко-читаемом формате, уровень заглавными, поля — колонкой в конце. Главное: Debug здесь виден — development-пресет стартует с уровня Debug, а не Info, поэтому отладочная запись доходит до терминала.

NewDevelopment: формат для человека#

NewDevelopment — пресет для терминала разработчика. Энкодер console пишет запись колонками: время в ISO-формате, уровень заглавными, место в коде, сообщение и объект с полями — глаз цепляет это мгновенно, без парсера в голове. Уровень по умолчанию — Debug: вы видите всё, включая отладочные записи, которые в production были бы отсечены. Стек-трейсы прилагаются раньше — с Warn, а не с Error: на машине разработчика лишний стек помогает поймать источник, а в проде он шумел бы в каждой второй записи. Режим development меняет и поведение DPanic — уровня, который паникует только в этом режиме; его разбор — в уроке про уровни логов. Итого: разработка — NewDevelopment, прод — NewProduction, и это не два конкурирующих вкуса, а два режима одного и того же логгера. Обратная конфигурация — production-пресет на ноутбуке — тоже встречается и заканчивается всегда одинаково: Debug-записи молчат, и половина отладки не работает; вместо колонок — однострочный JSON, который человек парсит глазами; стек-трейс на Warn не приходит, хотя именно его хотелось увидеть. Со стороны выглядит как «логгер сломался», на деле — просто не тот пресет.

Какой пресет выбрать: окружение решает#

Пресет — не вкусовщина, а свойство окружения. Локальная машина и тестовый стенд живут на NewDevelopment: человеко-читаемый вывод, Debug включён, стеки с Warn. Production — NewProduction: JSON для систем сбора, Info по умолчанию, сэмплирование. Правильный сервис выбирает пресет при старте, а не правкой кода между деплоями. Классика — переменная окружения: APP_ENV=production go run . — и логгер соберётся нужным. Если пресетов нужно больше — staging со своим уровнем, запись в файл вместо stdout, — это уже не выбор между двумя функциями, а zap.Config: полная конфигурация, которую можно описать в YAML и менять без пересборки; ей посвящён отдельный урок, а пока двух готовых пресетов хватает с головой. Законное исключение из правила — утилиты командной строки. Для CLI пользователь — человек в терминале, систем сбора нет, и JSON-простыня в stdout ломает пайплайн: demo | jq перестаёт работать, если между данными въелись логи. Для таких программ NewDevelopment остаётся даже в «проде»: релиз утилиты — тот же бинарник на машине пользователя, только без терминала разработчика. Что будет, если оставить в CLI production-пресет: каждая ошибка печатается JSON-строкой со стеком на десяток строк, пользователь видит {"level":"error"...} вместо человеческого объяснения, а в issues заводится вопрос «что это значит». Пресет — это интерфейс, и для терминала этим интерфейсом остаётся console-формат; к разделению потоков — данные в stdout, диагностика в stderr — вернёмся в уроке про приёмники.

Выбор пресета по APP_ENV

GO
package main

import (
        "os"

        "go.uber.org/zap"
)

func main() {
        var logger *zap.Logger

        if os.Getenv("APP_ENV") == "production" {
                logger, _ = zap.NewProduction()
        } else {
                logger, _ = zap.NewDevelopment()
        }
        defer logger.Sync()

        logger.Info("up and running",
                zap.String("env", os.Getenv("APP_ENV")),
        )
}

Обратите внимание на var logger zap.Logger до if: внутри веток используется присваивание =, а не :=, потому что переменная объявлена снаружи — иначе внутри if создалась бы своя, затенённая версия. Тип zap.Logger — именно его возвращают оба конструктора. Запустите с APP_ENV=production и без него и сравните две формы одной и той же записи.

Логгер — иммутабельная структура#

Тип *zap.Logger — иммутабельная структура: логгер хранит ядро (куда и как писать), уровень и набор опций вроде включённых вызовов. Иммутабельность даёт удобное свойство: логгер можно передавать по значению, держать в нескольких структурах и не бояться, что кто-то изменит его уровень под вами — With и Named не мутируют приёмник, а возвращают новый логгер с прицепленными полями или именем. Из этого следуют две практики. Первая: конструируйте логгер один раз в main и прокидывайте вниз — копирование дешёвое, пересоздание бессмысленное. Вторая: nil-логгер недопустим — если по пути зависимостей вместо настоящего логгера приехал nil, первая же запись упадёт с паникой; в тестах вместо nil подставляйте zap.NewNop() — заглушку, которая глотает записи. Кстати, именно иммутабельность делает безопасным выбор пресета по окружению из прошлого раздела: другой пресет — это другой логгер, а не перенастроенный старый.

Три ошибки первой недели#

Первая — потерянный Sync: программа пишет в файл, хвост записей пропадает при выходе, и виноватым назначают диск. Вторая — затенение логгера: logger, _ := zap.NewProduction() внутри ветки if создаёт локальную переменную, внешняя остаётся nil, и программа падает с паникой при первом же вызове — компилятор такое не ловит, а рантайм ловит редко и громко. Третья — логгер как глобальная переменная, присваиваемая из каждого пакета: настройки живут в трёх местах, тесты перезаписывают их друг у друга. Лечится просто: логгер рождается в main один раз и уезжает вниз через зависимости — структуру сервиса или параметр конструктора. Как безопасно пользоваться глобальным логгером, если без него никак, покажем в уроке про ReplaceGlobals — там же объясним, почему библиотечному коду он всё равно не нужен.

Сообщение и поля: что куда класть#

msg — короткая фраза-событие: что произошло, без данных внутри. «order created» — хорошо; «order A-1024 for user 42 created in 45ms» — плохо: данные в сообщении недоступны фильтрам, вы только что откатились в grep-мир из первого урока. Правило: факт — в msg, детали — в поля. «payment declined» плюс zap.String с причиной и zap.Int с суммой — запись и ищется, и считается, а не только читается. Из этого следует и дисциплина формулировок: одни и те же события во всём сервисе пишутся одинаково, иначе поиск по msg превращается в перечисление синонимов. Данные, которые нельзя утекать — пароли, номера карт, токены, — в лог не пишут вовсе: логи читает больше людей, чем кажется, и живут они дольше, чем хочется. zap честно напечатает всё, что вы ему отдали, — маскируйте чувствительное до вызова, а не после.

Проверка: пресеты

Что возвращает zap.NewProduction()?

Проверка: Sync

Зачем defer logger.Sync() в конце main?

Сколько строк напечатает программа?

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

logger, _ := zap.NewProduction()
logger.Debug("cache hit", zap.String("key", "user:42"))
logger.Info("served", zap.Int("status", 200))

Заполните пропуски в каркасе логгера

Соберите каркас: production-пресет, синхронизация и два метода записи — для обычного события и для провала.

package main import ( "errors" "go.uber.org/zap" ) func main() { logger, _ := zap.() defer logger.() logger.("service started", zap.Int("port", 8080)) err := errors.New("timeout") logger.("retry failed", zap.Error(err)) }

Соберите программу с Error-записью

Расставьте строки в правильном порядке: импорты, логгер, Sync и запись ошибки.

Строки

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

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

Карточки: пресеты и Sync

Термин

zap.NewProduction

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

Готовый пресет: JSON-энкодер, уровень Info, время — секунды от эпохи, сэмплирование, стек-трейсы с Error. Возвращает (*zap.Logger, error).

1 / 5

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

Следующий урок — уровни логов: что значит каждый из семи, почему Fatal — это os.Exit(1) и когда Warn, а когда Error. Потом — поля: zap.String, zap.Duration и отчего zap.Any дорогой. Если многословность конструкторов давит, загляните в урок про SugaredLogger — упрощённый интерфейс с Infow и парами ключ-значение. Карта курса — на его странице, а пример zap logger-а из этого урока — лучший стартовый шаблон для ваших программ.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • go get go.uber.org/zap, затем logger, _ := zap.NewProduction() — логгер готов к работе.
  • Production-запись: level, ts (секунды от эпохи), caller, msg, ваши поля и stacktrace с Error.
  • NewDevelopment — консольный вывод колонками, уровень Debug, стеки с Warn.
  • Выбор пресета — свойство окружения: APP_ENV при старте, а не правка кода между деплоями.
  • defer logger.Sync() сразу после создания: буфер сбрасывается, хвост записей не теряется.
  • Один логгер на приложение; контекст добавляют With и Named, а не новыми логгерами.

Домашнее задание

Соберите программу урока с APP_ENV: при значении production — NewProduction, иначе NewDevelopment. Запустите оба варианта и посчитайте разницу ключей в выводе. Потом перенаправьте вывод в файл командой >, уберите defer logger.Sync() и проверьте, теряется ли хвост записей при завершении — объясните результат в одном предложении.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Первый логгер: NewProduction и NewDevelopment)

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

Чем zap.NewProduction отличается от zap.NewDevelopment?

Production пишет JSON с уровнем Info и сэмплированием — для систем сбора логов. Development пишет консольные колонки с уровня Debug и стек-трейсами с Warn — для терминала разработчика.

Обязательно ли вызывать logger.Sync()?

Да, ставьте defer сразу после создания. Sync сбрасывает буферы: без него хвост записей может не долететь до файла, если программа завершится раньше.

Что делать со второй возвращаемой ошибкой конструктора?

NewProduction и NewDevelopment возвращают (*zap.Logger, error). В примерах ошибку отбрасывают, в боевом коде — обрабатывают: при провале берут запасной логгер и сообщают об этом другим способом.

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

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

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

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

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

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

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

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

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