Содержание урока · 10 разделов
- JSON для машины, ConsoleWriter для человека
- Дефолты ConsoleWriter: Kitchen-время, INF, key=value
- Дефолты ConsoleWriter одним списком
- PartsOrder: какие поля становятся колонками
- Цена читаемости: парсинг каждой строки
- Dev против prod: две конфигурации одного логгера
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
JSON для машины, ConsoleWriter для человека#
Zerolog пишет только JSON — другого формата записи у библиотеки нет, и это осознанное решение: система сбора получает один стабильный формат и в dev, и в prod. Человек в терминале — другой читатель, и для него в поставке есть ConsoleWriter: writer-обёртка, которая принимает JSON-строки zerolog, парсит каждую и печатает заново колонками — время, трёхбуквенный уровень вроде INF или WRN, сообщение и хвост полей в виде key=value. Ключи подсвечиваются цветом, уровень получает свою краску, и за секунду взгляд выхватывает ERR среди ленты INF. Такой же выбор делает любой CLI: --pretty для человека, JSON по умолчанию для скриптов. ConsoleWriter не второй логгер, а рендер поверх тех же событий: logger у вас один, события одни, меняется только то, кто смотрит — машина или вы. Все примеры урока запускаются локально, где цветной вывод виден по-настоящему; песочница сайта показывает тематическую программу на чистом Go, поэтому колонки ConsoleWriter стоит проверить в своём терминале. Доля правды в этом делении такая: логи пишутся один раз, а читаются тысячи раз, и чаще всего именно в разработке, где и живёт ConsoleWriter. Платить парсингом за каждую запись только потому, что разбор удобно читать раз в день, — та экономия на спичках, которая на горячем пути становится инцидентом.
Первый ConsoleWriter
GOpackage main
import (
"os"
"time"
"github.com/rs/zerolog"
)
func main() {
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
logger := zerolog.New(output).With().Timestamp().Logger()
logger.Info().Str("user", "ann").Msg("signed in")
logger.Warn().Int("percent", 93).Msg("disk almost full")
}
// Output:
// 2024-04-06T12:00:00+03:00 INF signed in user=ann
// 2024-04-06T12:00:00+03:00 WRN disk almost full percent=93ConsoleWriter — обычный io.Writer: его передают в zerolog.New вместо os.Stdout. Zerolog пишет в него JSON-строки, а writer парсит каждую и печатает заново: время в заданном TimeFormat, трёхбуквенный уровень INF или WRN, сообщение, поля key=value. Логгер строится как обычно — с With().Timestamp(), иначе ключа time не будет и колонка времени опустеет. Уровень WRN из Warn — работа внутренней таблицы сокращений, а не вашей функции: свою можно поставить через FormatLevel.
Дефолты ConsoleWriter: Kitchen-время, INF, key=value#
У ConsoleWriter несколько дефолтов, которые сразу становятся заметны в терминале. Время: если TimeFormat не задан, zerolog печатает время в формате time.Kitchen — что-то вроде 7:18PM, без даты и секунд; это настолько неожиданно для новичков, что TimeFormat задают почти всегда, чаще всего time.RFC3339. Уровни: вместо info и warn — трёхбуквенные INF, DBG, WRN, ERR, FTL, PNC; строка короткая и различима в потоке даже без цвета. Поля: всё, что не попало в колонки, печатается хвостом как key=value; строковые значения с пробелами берутся в кавычки, чтобы не рвать колонку. Цвета: уровень и ключи подсвечиваются ANSI-кодами; для логов в файле или CI-пайплайне краска становится мусором, и её отключает опция NoColor: true — тот же переключатель стоит в любой CLI-библиотеке, от тестов до вывода команд. Колонка caller, заявленная в PartsOrder, при этом остаётся пустой: у zerolog caller выключен по умолчанию, и чтобы он появился в JSON, а следом в терминале, нужен вызов .Caller() на событии или логгере. Пустая колонка — не ошибка, а следствие; спотыкаются здесь те, кто ждал файл:строку как в zap из коробки. Три буквы выбраны не случайно: колонка уровня одинаковой ширины для всех уровней, и глаз мгновенно выхватывает первую букву — E среди I. Поля печатаются как key=value: то, что в JSON было аккуратным объектом, здесь становится компактной строкой, которую глазами читать даже быстрее.
Как это устроено внутри — важно понимать, чтобы оценивать цену. ConsoleWriter.Write получает байты одной JSON-строки, прогоняет их через json.Unmarshal в map и заново собирает строку вывода. Отсюда следуют три факта. Первый: ConsoleWriter читает любой поток JSON-строк в формате zerolog — не обязательно свежие записи. Второй: разбор идёт рефлексией, поэтому человекочитаемый вывод на порядки дороже самой записи. Третий: порядок колонок не совпадает с порядком ключей JSON — колонки берутся из PartsOrder, а оставшиеся поля печатаются по алфавиту; в свежих версиях порядок хвоста настраивает FieldsOrder, а лишние поля прячут FieldsExclude и PartsExclude. Из этой механики — два практических следствия. FieldsExclude прячет поля из вывода: быстрый способ не светить token в общем терминале команды. FieldsOrder фиксирует порядок хвоста, когда алфавит мешает читать связанные поля рядом. А поскольку на вход идёт любой поток JSON-строк, один и тот же ConsoleWriter работает и в процессе сервиса, и в отдельной утилите, которая перечитывает вчерашний файл. Раз на входе обычный json-парсер, ему безразлично, кто записал строку: свежий zerolog, другая версия или вообще другая программа в том же формате — нужны лишь валидный JSON и знакомые ключи. Неизвестный ключ writer не уронит — напечатает его в хвосте, поэтому новые версии zerolog с новыми ключами не ломают просмотр.
Дефолты ConsoleWriter одним списком#
- время — time.Kitchen (7:18PM): задайте TimeFormat: time.RFC3339 почти всегда
- уровни — TRC, DBG, INF, WRN, ERR, FTL, PNC: короткая различимая колонка
- колонки — PartsOrder по умолчанию: time, level, caller, message
- хвост — остальные поля key=value по алфавиту; порядок настраивает FieldsOrder
- цвета — ANSI по умолчанию; для файлов и CI — NoColor: true
- форматтеры — FormatLevel, FormatFieldName, FormatFieldValue и другие, сигнатура func(interface{}) string
PartsOrder: какие поля становятся колонками#
PartsOrder — список ключей, которые ConsoleWriter вынесет в колонки в указанном порядке; по умолчанию это time, level, caller и message. Всё остальное — ваши поля — печатается хвостом. Колонку можно отдать любому ключу: PartsOrder с request_id превратит идентификатор запроса в отдельную колонку, и глаза дежурного перестанут искать его в хвосте — приём, который готовит почву для урока про HTTP-логи и трейс-идентификаторов, где request_id появляется в каждой записи. Правило подбора: в колонки отдают то, что читают глазами на каждой строке; редкие поля оставляют в хвосте, иначе ширина вывода разбухает. Ключа из PartsOrder нет в записи — колонка просто опустеет: так работает caller, который по умолчанию не пишется. Отсюда и приятная свобода: PartsOrder можно менять без оглядки на код логирования — версия для дежурного с вынесенным request_id не меняет ни одной строки в сервисе. И не переусердствуйте: каждая колонка — это ширина строки, которую придётся скроллить на узком терминале; три-четыре колонки сверх дефолта — потолок. PartsOrder не меняет сам JSON — он меняет только рендер, поэтому одну и ту же ленту можно смотреть с разным PartsOrder, не трогая logger.
Кастомные форматтеры
GOpackage main
import (
"fmt"
"os"
"strings"
"github.com/rs/zerolog"
)
func main() {
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true}
output.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%-6s|", i))
}
output.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
logger := zerolog.New(output).With().Timestamp().Logger()
logger.Info().Str("user", "ann").Msg("hello")
}
// Output:
// 7:18PM INFO | hello user=ANNФорматтер — функция func(i interface{}) string. FormatLevel получает строку уровня info и здесь возвращает INFO с разделителем; FormatFieldValue получает значение поля и приводит его к верхнему регистру. Для имени поля есть FormatFieldName, для сообщения — FormatMessage, а FormatPartValueByName форматирует значение конкретного поля по имени. NoColor отключает ANSI-цвета: в примере они не нужны. Форматтер получает interface{} и обязан вернуть строку — отсюда и Sprintf внутри: другим способом вытащить значение не выйдет.
Цена читаемости: парсинг каждой строки#
За колонки платят CPU. ConsoleWriter на каждую запись делает json.Unmarshal и пересобирает строку — это десятки раз дороже, чем простая запись JSON в stdout, и на горячем пути разница станет графике. Поэтому распределение такое: в dev — ConsoleWriter, человек смотрит в терминал и парсит взглядом; в prod — чистый JSON, а человекочитаемый рендер возникает там, где он нужен, и тем, кто его вызывает: jq на машине, линзы в Loki и Kibana, или маленькая Go-программа, которая читает файл логов и печатает через ConsoleWriter — разбор инцидента не должен страдать. ConsoleWriter не для каждого потока: в файл он пишет раскрашенные строки, которые система сбора не разберёт, так что это строго инструмент просмотра, а не транспорт — цена такого вывода разбирается в уроке про производительность. В тестах цена уходит сама собой: ConsoleWriter с NoColor поверх буфера — удобный способ показать падение теста человеческим выводом, потому что печать случается один раз, а не на каждом событии горячей нагрузки.
Если оставить ConsoleWriter в проде, каждый запрос потратит лишние микросекунды на Unmarshal и рендер, которые никто не увидит: терминала нет, а сборщик получит раскрашенный текст вместо JSON и сломается на первой же записи с кавычками. Что сломалось бы конкретно: парсер Loki откажется от ленты, а p99 подрастёт без единой строки бизнес-кода. Диагностика проста: если в Loki приехали цветные строки с INF — где-то остался dev-писатель, ищите его по отсутствию NoColor. Проверка при настройке сборщика стоит минуты: пришлите тестовую запись с кавычкой в значении — сломавшийся парсер найдётся сразу.
ConsoleWriter принимает любой поток JSON-строк: маленькая программа из десяти строк читает app.log bufio-сканером и копирует байты в ConsoleWriter — и вчерашний файл предстаёт цветными колонками. Для error-полей в свежих версиях есть красное подсвечивание: FormatErrFieldValue. Тот же приём работает и на тестах: падение с человекочитаемой лентой читается быстрее JSON-простыни.
Dev против prod: две конфигурации одного логгера#
| Аспект | dev: ConsoleWriter | prod: чистый JSON |
|---|---|---|
| Читатель | человек в терминале | система сбора: Loki, ELK |
| Разбор записи | json.Unmarshal каждой строки | нулевые издержки — байты из буфера |
| Относительная цена | в десятки раз дороже | базовая |
| Время | TimeFormat, дефолт — Kitchen | RFC3339 строкой в ключе time |
| Уровень | INF, WRN, ERR с цветом | info, warn, error строчными |
| Ваши поля | хвостом key=value | типизированными ключами JSON |
Разделение dev и prod делается при старте и сводится к выбору writer. Простейший вариант — переменная окружения: APP_ENV=production означает zerolog.New(os.Stdout), любой другой — zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}); логгер собирается один раз, остальной код не знает, какой рендер работает. Правильную механику выбора — с дефолтами и валидацией — разберёт урок про конфигурацию, а кто писал на zap, узнает здесь знакомую схему NewDevelopment и NewProduction: у zerolog пресетов нет, но выбор writer-а при старте решает ту же задачу, только дешевле. Полезная рамка для памяти: JSON и Console — не два формата логов, а две линзы на одни события; формат записи у вас один, линз столько, сколько нужно читателям. И не оставляйте переключатель флагом «на всякий случай» в проде: случайно включённый ConsoleWriter виден не сразу — записи-то продолжают приходить, — а p99 подрастает молча. Если нужна красота на проде, она живёт на стороне читателя: jq, ленты в Loki или отдельная утилита перерисовки, но не в пути каждого события.
Проверка: уровень в колонке
Как ConsoleWriter печатает событие уровня info?
Проверка: время по умолчанию
В каком формате ConsoleWriter печатает время, если TimeFormat не задан?
Какая строка выйдет в терминал?
Прочитайте код и выберите, что он выведет:
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
logger := zerolog.New(output).With().Timestamp().Logger()
logger.Info().Str("user", "ann").Msg("signed in")Что напечатает Error-событие?
Прочитайте код и выберите, что он выведет:
output := zerolog.ConsoleWriter{Out: os.Stdout}
logger := zerolog.New(output).With().Timestamp().Logger()
logger.Error().Msg("boom")Соберите ConsoleWriter
Восстановите настройку: writer, формат времени, конструктор логгера и уровень события.
Программа с ConsoleWriter
Расставьте строки в правильном порядке: импорты, настройка writer-а, логгер и запись.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: ConsoleWriter
Что дальше по курсу#
Следующий урок — Logger.With: контекстные поля, которые есть у каждого события, от имени модуля до request_id. Дальше — условные поля и запись в файл с ротацией. Если тема форматов затянула: разбор ключей JSON и подготовка ленты под Loki ждут в уроке про структуру JSON, а цена колонок на горячем пути — в уроке про производительность, где те же вызовы считаются в бенчмарках.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- ConsoleWriter — writer-обёртка: парсит JSON-строки zerolog и печатает цветные колонки для человека.
- Дефолты: Kitchen-время вроде 7:18PM, уровни INF/DBG/WRN/ERR, поля хвостом key=value.
- PartsOrder задаёт колонки — по умолчанию time, level, caller, message; свои ключи выносятся так же.
- Форматтеры FormatLevel, FormatFieldName, FormatFieldValue перекрашивают вывод функциями func(interface{}) string.
- Цена: json.Unmarshal каждой записи — десятки раз дороже JSON-вывода; ConsoleWriter живёт только в dev.
- JSON и Console — две линзы на одни события: формат записи один, рендеров сколько нужно.
- Caller в колонке пуст, пока не вызван .Caller(): zerolog не пишет место вызова по умолчанию.
- FieldsExclude и FieldsOrder управляют хвостом полей: спрятать token или задать порядок.
Домашнее задание
Соберите программу урока и запустите её в терминале, чтобы увидеть цвета. Потом уберите TimeFormat и сравните время — вы увидите Kitchen-формат. Добавьте в PartsOrder собственный ключ: PartsOrder: []string{"time", "level", "message", "user"} и логируйте .Str("user", ...), чтобы идентификатор стал колонкой. Наконец, напишите вторую маленькую программу, которая читает текстовый файл JSON-строк и копирует их в ConsoleWriter. Заодно сравните объём файла при записи JSON и ConsoleWriter — разница убедит не включать его в prod.
Личные заметкиПусто
Вопросы и ответы по уроку
Как включить красивые логи в zerolog?
Передайте ConsoleWriter в конструктор: logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger(). В терминале появятся цветные колонки: время, INF или ERR, сообщение и поля key=value. Тот же writer пригодится и для перечитывания старых файлов логов отдельной утилитой.
Можно ли использовать ConsoleWriter в проде?
Не стоит: на каждую запись идёт json.Unmarshal и пересборка строки — это на порядки дороже JSON-вывода, а сборщик логов получит раскрашенный текст вместо JSON. ConsoleWriter — инструмент dev-терминала и ручного просмотра файлов. Если в системе сбора появились цветные INF-строки — где-то остался dev-писатель, ищите по NoColor.
Как изменить формат уровня или полей в ConsoleWriter?
Назначьте свои функции: output.FormatLevel для уровня, output.FormatFieldName и output.FormatFieldValue для полей, output.FormatMessage для сообщения. Сигнатура одна — func(i interface{}) string, внутри можно вернуть любую строку. FormatErrFieldValue красит только значения поля error — удобно, чтобы ошибки бросались в глаза.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Валидация в Go: go-playground/validator
Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.
- Валидировать структуры тегами binding и validate
- Понимать ValidationErrors и писать понятные сообщения
- Работать с вложенностью, срезами и картами через dive
- Писать свои функции-валидаторы и StructLevel-правила