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

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

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

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

ConsoleWriter в zerolog: вывод для людей

ConsoleWriter в zerolog превращает JSON-события в цветные колонки для терминала: время, уровень INF или ERR, сообщение и поля key=value. JSON остаётся форматом записи, а ConsoleWriter — линзой для человека: разберём дефолты, PartsOrder, собственные форматтеры и цену, которую читаемый вывод берёт с вашего CPU.

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

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

GO
package 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=93

ConsoleWriter — обычный 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.

Кастомные форматтеры

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

Dev против prod: две конфигурации одного логгера#

Аспектdev: ConsoleWriterprod: чистый JSON
Читательчеловек в терминалесистема сбора: Loki, ELK
Разбор записиjson.Unmarshal каждой строкинулевые издержки — байты из буфера
Относительная ценав десятки раз дорожебазовая
ВремяTimeFormat, дефолт — KitchenRFC3339 строкой в ключе 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, формат времени, конструктор логгера и уровень события.

package main import ( "os" "time" "github.com/rs/zerolog" ) func main() { output := zerolog.ConsoleWriter{Out: os., TimeFormat: time.} logger := zerolog.(output).With().Timestamp().Logger() logger.().Str("user", "ann").Msg("signed in") }

Программа с ConsoleWriter

Расставьте строки в правильном порядке: импорты, настройка writer-а, логгер и запись.

Строки

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

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

Карточки: ConsoleWriter

Термин

ConsoleWriter

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

io.Writer-обёртка: парсит JSON-строки zerolog и печатает цветные колонки для терминала. Инструмент просмотра, не транспорт.

1 / 6

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

Следующий урок — 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 в zerolog: вывод для людей)

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

Как включить красивые логи в 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 — удобно, чтобы ошибки бросались в глаза.

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

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

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

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

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила