Содержание урока · 12 разделов
- zap.Config: все поля одной структуры
- Поля zap.Config и их роль
- Build(): от конфига к логгеру
- OutputPaths и ErrorOutputPaths: два разных потока
- Пресеты как конфиг: NewProductionConfig
- Ловушки правки пресетов вслепую
- YAML-конфиг: настройка как часть приложения
- InitialFields: стартовые поля каждой записи
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
До сих пор мы выбирали из двух готовых состояний: NewDevelopment для терминала и NewProduction для продакшена. Рано или поздно этого перестаёт хватать — по вполне рабочим причинам: staging пишет одновременно в stdout и файл, уровень включается на короткое окно расследования, в каждую запись с первого дня нужно поле app и version, а конфигурация живёт рядом с остальными настройками сервиса, а не в коде. Для всего этого в zap есть zap.Config — структура, в которой собрана вся настройка логгера: от уровня и формата до списка приёмников. Пресеты — это два предзаполненных экземпляра этой структуры, не более; поняв её поля, вы поймёте и то, что именно настраивали за вас. Разберём поля по одному, соберём логгер методом Build, посмотрим на YAML-файл, из которого конфиг читается, и разберём ловушки правки пресетов — их у этой структуры прилично.
zap.Config: все поля одной структуры#
Поля zap.Config разбиты на четыре группы. Поведение: Level — минимальный включённый уровень, хранится как AtomicLevel, а значит, может меняться на живом процессе; Development — флаг режима разработки, который меняет поведение DPanic и щедрость стек-трейсов; DisableCaller и DisableStacktrace выключают соответствующие части записи; Sampling — политика сэмплирования, nil отключает. Формат: Encoding — строка «json» или «console» (плюс сторонние зарегистрированные), EncoderConfig — тот самый набор ключей и правил сериализации из урока про энкодеры. Вывод: OutputPaths — список путей или URL-схем, куда уходят записи; ErrorOutputPaths — куда zap пишет собственные ошибки. Контекст: InitialFields — карта полей, которая попадает в каждую запись от корневого логгера. Обратите внимание на типы: Level не число, а AtomicLevel, OutputPaths не строка, а срез — обёртка настроена на несколько приёмников с первого дня, и это не ошибка дизайна, а его суть.
Поля zap.Config и их роль#
| Поле | Тип | Что делает |
|---|---|---|
| Level | AtomicLevel | минимальный включённый уровень; SetLevel меняет его на живом процессе |
| Development | bool | режим разработки: DPanic паникует, стеки с Warn |
| DisableCaller / DisableStacktrace | bool | убирают caller или автостеки из записей |
| Sampling | *SamplingConfig | политика сэмплирования; nil — сэмплирования нет |
| Encoding | string | "json" или "console" |
| EncoderConfig | zapcore.EncoderConfig | ключи и правила сериализации записи |
| OutputPaths | []string | куда пишутся записи: stderr, stdout, файлы |
| ErrorOutputPaths | []string | куда zap пишет свои внутренние ошибки |
| InitialFields | map[string]interface{} | поля, попадающие в каждую запись |
Полный конфиг и Build()
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
Development: false,
Encoding: "json",
EncoderConfig: zap.NewProductionEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
InitialFields: map[string]interface{}{
"app": "shop",
"version": "1.4.2",
},
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Info("built from config", zap.Int("port", 8080))
}Конфиг собран с нуля: уровень Debug, JSON-энкодер с production-набором настроек, вывод в stderr и два стартовых поля. cfg.Build() проверяет конфиг, открывает приёмники и возвращает (*zap.Logger, error) — ошибку проверяем: кривой путь в OutputPaths обнаружится именно здесь. В каждой записи появятся app и version: {"level":"info","app":"shop","version":"1.4.2",...}. Запустите локально — песочница zap не исполняет.
Build(): от конфига к логгеру#
Build — это фабрика: он разбирает Encoding, строит энкодер из EncoderConfig, открывает приёмники из OutputPaths через ту же механику, что и zap.Open, собирает ядро и оборачивает его в логгер с опциями — Development и сэмплирование прикладываются здесь же. Ошибка возвращается для всего, что нельзя распарсить: неизвестная схема пути, недопустимое Encoding, недоступный файл. Отсюда важное следствие: NewProduction() — это NewProductionConfig().Build(), и ничего больше. Пресеты не содержат магии, скрытой от вас: прод — это уровень Info, JSON, сэмплирование и стеки с Error; development — уровень Debug, консольный энкодер, стеки с Warn. Когда понадобится что-то между — конфиг пресета модифицируется по полям, как обычная структура. И раз пресеты — конфиги, их можно сравнивать: откройте NewProductionConfig и NewDevelopmentConfig в документации и увидите все различия списком — хороший способ убедиться, что никакой магии нет. Заметьте и цену: Build открывает файлы при старте и держит их открытыми — логгер, собранный в двух местах программы, откроет файл дважды; стройте один раз, как и всякий другой ресурс с состоянием.
OutputPaths и ErrorOutputPaths: два разных потока#
Пути в конфиге — это URL или файловые пути: «stderr», «stdout», «logs/app.log» — схема по умолчанию файловая, зарегистрировать можно и свои через RegisterSink. OutputPaths — приёмники записей; ErrorOutputPaths — то, куда zap пишет собственные проблемы: не открылся основной файл, упала запись, кончился диск. Потоки принципиально разные, и смешивать их не стоит: если основной приёмник и error-приёмник — один и тот же файл, сбой этого файла съест и записи, и сообщения о том, почему их нет. Дефолт для внутренних ошибок — stderr, и это разумный минимум; в серьёзной конфигурации error-поток направляют туда, за которым присматривает мониторинг. Полная механика приёмников — файлы, мультиплекс, буферизация — следующий урок; здесь держите в голове разделение ролей: записи — это данные, ошибки логгера — служебный канал, и пути у них свои. Проверка путей при этом ранняя: Build вернёт ошибку на неизвестной схеме и недоступном файле ещё при старте, а не в момент первой записи — ловите её и падайте с понятным сообщением, а не обнаруживайте пустые логи в три ночи.
Соберёте zap.Config{Encoding: "json"} с нуля и забудете EncoderConfig — получите записи вида {"L":"INFO"} или вовсе пустые объекты: у энкодера нет ключей, куда класть время, сообщение и поля. Ключи из EncoderConfig обязаны быть заполнены. Берите zap.NewProductionEncoderConfig() или zap.NewDevelopmentEncoderConfig() как базу — и правьте отдельные поля, а не начинайте с пустого объекта.
Пресеты как конфиг: NewProductionConfig#
Самый частый сценарий настройки — не конфиг с нуля, а модификация пресета: cfg := zap.NewProductionConfig(), дальше правятся нужные поля, и вызывается Build. Такой подход безопаснее ручного заполнения: EncoderConfig уже на месте, сэмплирование и стеки — как в проде, и вы меняете ровно то, что хотите. Типовые правки: cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel) — включить отладку; cfg.OutputPaths = append(cfg.OutputPaths, "logs/app.log") — добавить файл к stdout; cfg.InitialFields = map[string]interface{}{...} — стартовые поля. Отдельная тонкость — флаг Development: он не про вывод, а про поведение — в true DPanic паникует и стеки идут с Warn. Меняя его, вы меняете семантику уровней, о которых договорились в третьем уроке.
Модифицированный пресет
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
cfg := zap.NewProductionConfig()
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
cfg.OutputPaths = []string{"stderr", "logs/app.log"}
cfg.InitialFields = map[string]interface{}{
"app": "shop",
"version": "1.4.2",
}
logger, err := cfg.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
logger.Debug("debug enabled", zap.String("env", "staging"))
logger.Info("config applied")
}База — production-пресет: JSON-энкодер и его EncoderConfig остались нетронутыми. Уровень поднят до Debug — обе записи дойдут и до stderr, и до logs/app.log (папка должна существовать: zap не создаёт каталоги, и ошибка открытия уйдёт в ErrorOutputPaths, не в панику). Стартовые поля появятся в каждой записи. Такой конфиг — типичный staging: подробный, с локальной копией логов и меткой приложения.
Ловушки правки пресетов вслепую#
- сменили Encoding у development-конфига на "json" — ключи станут T, L, M: короткие имена рассчитаны на колонки, а не на парсеры; проверяйте EncoderConfig после смены формата
- cfg.Level = zap.DebugLevel не скомпилируется — Level это AtomicLevel, нужен NewAtomicLevelAt или SetLevel
- Development = false у копии development-пресета молча отключает панику DPanic — тестовая ловушка перестаёт ловить
- OutputPaths = []string{"logs/app.log"} затёрло stdout: записи уехали только в файл, сборщик с stdout остался голодным
- папка для файла не создана — zap не mkdir-ит; ошибка открытия уйдёт в ErrorOutputPaths, а записи — в никуда
- скопировали пресет копированием структуры и забыли про Sampling: в одном логгере сэмплирование есть, в другом нет — и объёмы разошлись
YAML-конфиг: настройка как часть приложения#
У всех полей zap.Config есть теги json и yaml, поэтому конфиг логгера — обычная структура для маршалинга: её можно хранить в YAML-файле рядом с остальной настройкой сервиса и загружать при старте. Даже сложные правила сериализации описываются строками: encodeTime: "iso8601" или "epoch", encodeLevel: "lowercase" или "capital" — у этих типов есть UnmarshalText, и yaml-библиотека их разворачивает. Логгер при этом становится конфигурируемым без пересборки: поменяли файл — перезапустили сервис, формат и уровень новые. Это же дверь к строгой проверке: Decoder с KnownFields поймает опечатку в ключе yaml вместо молчаливого нулевого значения. А в реальных приложениях файлы конфигов читает Viper — он умеет YAML, TOML и env-переменные поверх; в курсе про Cobra этому посвящён целый урок, здесь достаточно идеи: конфиг логов — часть общей конфигурации, и живёт она не в исходниках.
Конфиг из YAML-файла
GO// zap.yaml
level: debug
development: false
encoding: json
encoderConfig:
messageKey: msg
levelKey: level
timeKey: ts
encodeTime: iso8601
encodeLevel: lowercase
outputPaths:
- stderr
- logs/app.log
errorOutputPaths:
- stderr
initialFields:
app: shop
// main.go
func loadLogger(path string) (*zap.Logger, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg zap.Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return cfg.Build()
}YAML повторяет структуру конфига по тегам: encoderConfig вложен, outputPaths — список. encodeTime: iso8601 и encodeLevel: lowercase разворачиваются в энкодеры через UnmarshalText, поэтому строки тут не метафора, а рабочий механизм. loadLogger читает файл, разворачивает в zap.Config и строит логгер; ошибка на любом этапе возвращается наверх — в main её обрабатывают с fallback-логгером. Для строгости используйте yaml.Decoder с KnownFields(true): опечатка в ключе станет ошибкой чтения, а не молчаливым дефолтом.
InitialFields: стартовые поля каждой записи#
InitialFields — карта any: эти поля попадают в каждую запись от построенного логгера и от всех его потомков через With. Типичное содержимое — app, version, env: метки, по которым в централизованном хранилище логи сортируются по сервисам и релизам ещё до фильтра по содержимому. Цена та же, что у zap.Any из урока про поля: значения едут как interface{}, и сериализация идёт через рефлексию — для полей, которые пишутся один раз на запись и известны при старте, это приемлемо, но горячие поля лучше добавлять типизированным With на построенном логгере. Отдельная история — альтернатива: logger.With("app", "shop") после Build даёт тот же эффект без карты any и прозрачнее в коде; InitialFields выигрывает там, где конфиг приходит из YAML — карту удобнее описывать рядом с остальной настройкой, а не дописывать в main.
Проверка: пустой EncoderConfig
Что напечатает логгер, собранный из zap.Config{Encoding: "json"} без EncoderConfig?
Проверка: ErrorOutputPaths
Что именно пишет zap в ErrorOutputPaths?
Сколько записей дойдёт до stderr?
Прочитайте код и выберите, что он выведет:
cfg := zap.NewProductionConfig()
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
cfg.OutputPaths = []string{"stderr", "logs/app.log"}
logger, _ := cfg.Build()
logger.Info("one")
logger.Debug("two")Соберите конфиг staging
Восстановите модификацию пресета: уровень, список приёмников и сборка логгера.
Соберите модификацию пресета
Расставьте строки: от базового пресета до первой записи.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: конфигурация zap
Что дальше по курсу#
Конфиг управляет приёмниками списком путей — но что физически происходит при открытии файла, как дублировать записи в stdout и файл и зачем нужна буферизация, разбирается в уроке про приёмники вывода. Смена уровня на живом процессе — тема урока про AtomicLevel: поле Level из этого урока там станет рулем без перезапуска. Карта курса — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- zap.Config — вся настройка в одной структуре; пресеты NewProduction/NewDevelopment — два готовых конфига.
- cfg.Build() строит логгер: энкодер из EncoderConfig, приёмники из OutputPaths, ошибку возвращает явно.
- Encoding без EncoderConfig даёт записи без ключей — берите NewProductionEncoderConfig как базу.
- OutputPaths — поток записей, ErrorOutputPaths — служебный канал внутренних ошибок zap; их не смешивают.
- Конфиг читается из YAML: у полей есть yaml-теги, encodeTime/encodeLevel разворачиваются из строк.
- InitialFields — стартовые метки каждой записи; альтернатива — logger.With после Build.
- Модификация пресета по полям безопаснее конфига с нуля: EncoderConfig и сэмплирование уже на месте.
Домашнее задание
Соберите staging-конфиг модификацией пресета: уровень Debug, приёмники stderr и logs/staging.log, InitialFields с app и env. Запустите и проверьте файл глазами. Затем уберите EncoderConfig из полноручного конфига и посмотрите на урезанные записи — впишите вывод в заметки как маркер ошибки. В конце вынесите конфиг в zap.yaml и загрузите его через yaml.Unmarshal, добавив KnownFields(true) и поймав опечатку в ключе.
Личные заметкиПусто
Вопросы и ответы по уроку
zap.Config или два пресета — когда что?
Пока приложение пишет в stdout и разница между dev и прод исчерпывается уровнем и форматом — хватит пресетов и выбора по APP_ENV. Файлы, стартовые поля, YAML-конфигурация и нестандартные уровни — сигнал переходить на zap.Config.
Почему Build возвращает ошибку, а пресеты — нет?
Возвращают, просто в примерах её отбрасывают через _: оба пресета — это Config.Build под капотом. Ошибка означает проблему конфигурации — кривой путь или Encoding; в боевом коде её обрабатывают с запасным логгером.
Можно ли менять zap.Config после Build?
Логгер уже построен — правка структуры конфига на него не влияет. Level — исключение: это AtomicLevel, и SetLevel меняет порог на живом процессе. Для остального стройте новый конфиг и новый логгер.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси