---
title: "ConsoleWriter в zerolog: вывод для людей"
description: "ConsoleWriter в zerolog: красивые логи в терминале, PartsOrder и форматтеры полей, TimeFormat против Kitchen-времени, цвета и NoColor, разделение dev и prod."
course: "Zerolog в Go: быстрое JSON-логирование"
url: "https://golangmaster.ru/courses/zerolog/lessons/05-log-console/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Zerolog в Go: быстрое JSON-логирование», урок 5 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/zerolog/lessons/05-log-console/*

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

### 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-логи](/courses/zerolog/lessons/11-log-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 не для каждого потока: в файл он пишет раскрашенные строки, которые система сбора не разберёт, так что это строго инструмент просмотра, а не транспорт — цена такого вывода разбирается в [уроке про производительность](/courses/zerolog/lessons/13-log-proizvoditelnost/). В тестах цена уходит сама собой: ConsoleWriter с NoColor поверх буфера — удобный способ показать падение теста человеческим выводом, потому что печать случается один раз, а не на каждом событии горячей нагрузки.

> **ConsoleWriter в проде — CPU на ветер.** Если оставить 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}); логгер собирается один раз, остальной код не знает, какой рендер работает. Правильную механику выбора — с дефолтами и валидацией — разберёт [урок про конфигурацию](/courses/zerolog/lessons/16-konfiguraciya-okrug/), а кто писал на zap, узнает здесь знакомую схему NewDevelopment и NewProduction: у zerolog пресетов нет, но выбор writer-а при старте решает ту же задачу, только дешевле. Полезная рамка для памяти: JSON и Console — не два формата логов, а две линзы на одни события; формат записи у вас один, линз столько, сколько нужно читателям. И не оставляйте переключатель флагом «на всякий случай» в проде: случайно включённый ConsoleWriter виден не сразу — записи-то продолжают приходить, — а p99 подрастает молча. Если нужна красота на проде, она живёт на стороне читателя: jq, ленты в Loki или отдельная утилита перерисовки, но не в пути каждого события.

### Проверка: уровень в колонке

**Вопрос:** Как ConsoleWriter печатает событие уровня info?

- ✅ INF — трёхбуквенное сокращение с цветом
- INFO — полностью, заглавными
- info — как в JSON, без изменений
- Цифрой 1 — числом уровня

ConsoleWriter прогоняет уровень через таблицу FormattedLevels: TRC, DBG, INF, WRN, ERR, FTL, PNC. Свой вид уровня задаёт FormatLevel — тогда стандартные сокращения заменяются вашей функцией.

### Проверка: время по умолчанию

**Вопрос:** В каком формате ConsoleWriter печатает время, если TimeFormat не задан?

- RFC3339 — полная дата с зоной
- ✅ Kitchen — часы и минуты вроде 7:18PM
- Секунды от эпохи
- Время не печатается вовсе

Внутренний дефолт ConsoleWriter — time.Kitchen: 7:18PM без даты. Для любых логов старше часа это неудобно, поэтому TimeFormat: time.RFC3339 стал фактическим стандартом настройки.

### Какая строка выйдет в терминал?

```go
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}

logger := zerolog.New(output).With().Timestamp().Logger()

logger.Info().Str("user", "ann").Msg("signed in")
```

**Что выведет программа?**

- ✅ 2024-04-06T12:00:00+03:00 INF signed in user=ann
- {"level":"info","time":"2024-04-06T12:00:00+03:00","user":"ann","message":"signed in"}
- 2024-04-06T12:00:00+03:00 INFO signed in user=ann
- 12:00PM INF signed in user=ann

ConsoleWriter печатает колонками, а не JSON: время в заданном RFC3339, уровень сокращён до INF, поле — хвостом key=value. JSON исчезает из терминала, хотя под капотом именно его zerolog отдал writer-у.

### Что напечатает Error-событие?

```go
output := zerolog.ConsoleWriter{Out: os.Stdout}

logger := zerolog.New(output).With().Timestamp().Logger()

logger.Error().Msg("boom")
```

**Что выведет программа?**

- ✅ 7:18PM ERR boom
- 7:18PM ERROR boom
- {"level":"error","message":"boom"}
- 7:18PM INF boom

Error в ConsoleWriter — это ERR, покрашенное красным; время без TimeFormat выходит в Kitchen-формате 7:18PM. JSON не покажется: writer перерисовывает каждую строку — это его работа.

### Соберите ConsoleWriter

Восстановите настройку: writer, формат времени, конструктор логгера и уровень события.

```go
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")
}
```

Подсказка: Вывод — os.Stdout; формат времени — time.RFC3339; поверх writer-а строит логгер zerolog.New; штатное событие начинает Info.

Правильные ответы в пропусках: `Stdout`, `RFC3339`, `New`, `Info`

### Программа с ConsoleWriter

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

Правильный порядок строк:

1. `package main`
2. `import (`
3. `  "os"`
4. `  "time"`
5. `  "github.com/rs/zerolog"`
6. `)`
7. `func main() {`
8. `  output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}`
9. `  logger := zerolog.New(output).With().Timestamp().Logger()`
10. `  logger.Info().Str("user", "ann").Msg("signed in")`
11. `}`

### Карточки: ConsoleWriter

- **ConsoleWriter** — io.Writer-обёртка: парсит JSON-строки zerolog и печатает цветные колонки для терминала. Инструмент просмотра, не транспорт.
- **PartsOrder** — Список ключей, вынесенных в колонки; по умолчанию time, level, caller, message. Свои ключи — например, request_id — становятся колонками тоже.
- **FormatLevel / FormatFieldName** — Функции func(i interface{}) string, задающие вид уровня, имени и значения поля. Стандартные INF и key=value заменяются вашими.
- **NoColor** — Опция ConsoleWriter: отключает ANSI-цвета для файлов, CI и пайплайнов, где краска становится мусором.
- **Kitchen-время** — Дефолтный TimeFormat ConsoleWriter: 7:18PM без даты. Поэтому почти всегда задают TimeFormat: time.RFC3339.
- **Хвост полей** — Поля вне PartsOrder печатаются key=value по алфавиту; порядок хвоста настраивает FieldsOrder, лишние прячут FieldsExclude.

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

Следующий урок — Logger.With: контекстные поля, которые есть у каждого события, от имени модуля до request_id. Дальше — условные поля и запись в файл с ротацией. Если тема форматов затянула: разбор ключей JSON и подготовка ленты под Loki ждут в [уроке про структуру JSON](/courses/zerolog/lessons/19-log-formaty-json/), а цена колонок на горячем пути — в уроке про производительность, где те же вызовы считаются в бенчмарках.

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

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