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

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

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

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

Тестирование логов: writer в буфер

Лог — это вывод вашей программы, и ломается он как любой другой вывод: уровень не тот, поле потерялось, событие исчезло из ленты. Пока проверок нет, всё это узнаётся при разборе инцидента, когда запись уже была нужна вчера. Тесты логов zerolog не опираются на отдельную библиотеку: в поставке zerolog 1.34.0 нет ни zerolog/test, ни zerolog/assert — пакетов с готовыми утверждениями попросту не существует, я проверил это по исходникам. Из тестового в поставке только TestWriter из writer.go, который пересылает строки в t.Log, — удобная пересылка, но не проверки. Вся методика проверки логов на Go сводится к связке: writer в буфер и encoding/json. zerolog.New(&buf) направляет события в bytes.Buffer, каждая запись складывается туда JSON-строкой, дальше — разбор в map[string]any и проверки по ключам. В этом уроке: одиночная запись, очередь из буфера, честный факт о поле level, подмена глобального логгера с восстановлением через defer и хук, который считает события без разбора JSON.

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

Поломанный лог долго не выдаёт себя: тесты зелёные, дашборд мигает, а в ленте тем временем потерялся уровень, уехало поле или исчезла подпись модуля. Обнаруживается это ночью, на разборе, когда запись уже нужна была вчера. Выход тот же, что и для остального кода, — тест, и тесты логов zerolog строятся безо всякой магии: writer подменяется буфером, события складываются в него JSON-строками, дальше — стандартный encoding/json и проверки по ключам. Готовых утверждений в zerolog нет: ни zerolog/test, ни zerolog/assert не существуют, и это честный факт поставки 1.34.0, а не недоработка — JSON-лог достаточно проверять картой. Приём знаком вам из урока про сублоггеры: там буферный сублоггер проверял подпись module, здесь тот же приём разворачивается в полный набор — одиночная запись, очередь событий, уровень с честной историей про NoLevel, подмена глобального логгера с восстановлением и хук-счётчик без разбора JSON. К концу урока — чек-лист теста логов и три рабочих шаблона: карта, очередь, хук.

Буфер вместо writer-а: zerolog.New и bytes.Buffer#

Конструктор zerolog.New принимает io.Writer и не знает ничего про файлы и сеть — буфер подходит наравне с остальными. В тестах пишут logger := zerolog.New(&buf), и здесь важен именно указатель: метод Write объявлен на *bytes.Buffer и растит внутренний слайс, поэтому значением буфер в конструктор не подойдёт — компилятор напомнит сразу. Внутри New обычный writer заворачивается в LevelWriterAdapter, и логгер ставит себе порог TraceLevel: собственный порог никого не режет, а вот глобальный уровень по умолчанию равен DebugLevel — trace-записи без настройки до буфера не дойдут, но это уже раздел про глобальную подмену. Каждое событие уходит в буфер одной JSON-строкой с переводом строки на конце — так работают AppendEndMarker и AppendLineBreak в методе write события, и потому буфер после трёх вызовов Msg содержит три строки. Читается результат через buf.Bytes(); между подтестами буфер сбрасывается buf.Reset() — иначе в него доедут хвосты прошлых проверок, и очередь разбора собьётся. Нюанс конструктора: zerolog.New(nil) не паникует, а молча подставляет io.Discard — записи просто исчезнут. Если события пишут из горутин, буфер оборачивается в SyncWriter: сам bytes.Buffer под параллельными Write рвёт внутренний слайс, и это правило из урока про сублоггеры действует в тестах так же, как в проде.

Тест одной записи через map[string]any

GO
package main

import (
        "bytes"
        "encoding/json"
        "errors"
        "testing"

        "github.com/rs/zerolog"
)

func TestPaymentLog(t *testing.T) {
        var buf bytes.Buffer
        logger := zerolog.New(&buf)

        err := errors.New("quota exceeded")
        logger.Error().
                Err(err).
                AnErr("cause", err).
                Int("amount", 150).
                Msg("payment rejected")

        var m map[string]any
        if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
                t.Fatal(err)
        }
        if m["level"] != "error" {
                t.Fatalf("level: %v", m["level"])
        }
        if m["message"] != "payment rejected" {
                t.Fatalf("message: %v", m["message"])
        }
        if m["error"] != "quota exceeded" || m["cause"] != "quota exceeded" {
                t.Fatalf("error/cause: %v / %v", m["error"], m["cause"])
        }
        if m["amount"] != float64(150) {
                t.Fatalf("amount: %v", m["amount"])
        }
}

Логгер получает &buf — указатель, именно он реализует io.Writer. После Msg буфер содержит одну JSON-строку: json.Unmarshal превращает её в карту, и проверки идут по ключам level, message, error и cause из урока про ошибки. Числовое поле amount возвращается как float64, поэтому сравнение с float64(150), а не с int. Такой тест падает на первой же поломке поля, а не в проде.

Поля, уровень и сообщение: что проверять в карте#

Контракт ключей в записи фиксирован глобальными переменными zerolog: LevelFieldName равен level, MessageFieldName — message, ErrorFieldName — error. Поле level пишется у всех уровней, включая Trace: в newEvent уровня проверяется только условие level != NoLevel и непустое имя поля, поэтому trace-событие уезжает в буфер как level со значением trace — константа LevelTraceValue именно такова в globals.go. Не пишется уровень ровно в одном случае: события метода log.Log() создаются с уровнем NoLevel, и ключа level в их JSON нет вовсе. Событий Disabled не существует как записей: should() отсекает их до создания события — тест проверяет отсутствие строки, а не null-уровень. Сообщение — последний ключ строки: msg() дописывает его перед самым write, после всех полей цепочки. Порядок самих полей zerolog соблюдает в порядке добавления: сначала level, затем контекст из With, затем поля цепочки, в конце message. Но map[string]any этот порядок не сохраняет — раскладка по карте стирает след добавления, поэтому проверяйте по ключам, а не сравнением строки целиком; если порядок полей действительно нужен, читайте поток токенов json.Decoder.Token(). Разбор полей error и cause уже готов у вас из урока про ошибки — здесь он просто переехал в тест.

Очередь событий: несколько записей в одном буфере#

Один буфер за время теста накапливает столько строк, сколько событий прошло через пороги, и это меняет способ чтения. json.Unmarshal разбирает только первый объект и падает на втором с сообщением про недопустимый символ после верхнего значения — классическая ошибка при первом же переходе от одной записи к очереди. Для потока берётся json.NewDecoder(&buf): метод More() отвечает, есть ли следующий объект, Decode достаёт его в очередную карту, и цикл собирает слайс событий. Порядок событий при последовательном логировании детерминирован: каждый вызов Msg дописывает свою строку в буфер сразу, — сравнивайте очередь уровней или сообщений с ожидаемым списком. Альтернатива без json — bufio.Scanner: построчно читает буфер, а каждую строку вы отдаёте Unmarshal. Полезна и обратная проверка: после ожидаемого числа событий More() обязан вернуть false — так ловятся лишние события в конце ленты.

Проверка порядка уровней через json.Decoder

GO
package main

import (
        "bytes"
        "encoding/json"
        "testing"

        "github.com/rs/zerolog"
)

func TestEventsOrder(t *testing.T) {
        var buf bytes.Buffer
        logger := zerolog.New(&buf)

        logger.Info().Str("step", "checkout").Msg("start")
        logger.Warn().Str("step", "checkout").Msg("retry")
        logger.Error().Msg("gave up")

        dec := json.NewDecoder(&buf)
        want := []string{"info", "warn", "error"}
        for i, w := range want {
                if !dec.More() {
                        t.Fatalf("event %d missing", i)
                }
                var m map[string]any
                if err := dec.Decode(&m); err != nil {
                        t.Fatal(err)
                }
                if m["level"] != w {
                        t.Fatalf("event %d: %v, want %s", i, m["level"], w)
                }
        }
        if dec.More() {
                t.Fatal("extra events in buffer")
        }
}

Три вызова Msg дали три строки в буфере — по одной на событие. json.NewDecoder читает буфер как поток: More() говорит, есть ли следующий объект, Decode достаёт его в карту. Цикл сравнивает уровни с ожидаемой очередью info, warn, error — порядок записей в одной горутине детерминирован. Финальный dec.More() ловит лишние события, дописанные в цепочку после проверяемых.

Тест логов фиксирует контракт полей, а не правду о транспортном слое: ротация файла, цвета ConsoleWriter, диод и сеть за границей буфера не проверяются — для них существуют отдельные интеграционные прогоны. Золотые строки — сравнение буфера с сохранённой строкой целиком — работают, но хрупки: любое новое поле ломает все образцы разом. Практичнее свои хелперы-утверждения поверх карты: wantStr(t, m, "user_id", "u1") с t.Helper внутри, который достаёт ключ, печатает %v полученного и валит тест с внятным сообщением. Такие хелперы — ровно то, чего в zerolog нет, и пишутся они один раз на проект: три функции на строки, числа и наличие ключа закрывают девять десятых проверок. Отсутствующий ключ и ключ с нулевым значением здесь разные случаи: nil после чтения карты означает, что поле не писалось вовсе, — на этом строится проверка контрактных пропусков вроде молчаливой обработки nil-ошибки.

Что проверяем и чем#

Что проверяемИнструментПодводный камень
Поля событияключи в map[string]anyчисла приходят float64, ключ может отсутствовать
Уровень записиm["level"]у NoLevel-записей log.Log() ключа level нет
Сообщениеm["message"]message — последний ключ строки
Несколько событийjson.Decoder: More и Decodejson.Unmarshal читает только первый объект
Порядок событийсравнение очереди уровнейпорядок полей внутри записи в карту не доезжает
Записи из горутинобщий буфер плюс SyncWriterбез обёртки JSON ломается посередине

Чек-лист теста логов одним списком#

  • логгер в буфер: logger := zerolog.New(&buf); буфер сбрасывайте buf.Reset между подтестами
  • разбор: json.Unmarshal для одной записи, json.NewDecoder с More и Decode для очереди
  • проверяйте ключами level, message и поля события; карту не сравнивайте со строкой целиком
  • числа после Unmarshal — float64: приведение или сравнение с float64
  • уровень есть у всех записей, кроме log.Log(): у NoLevel ключа level нет
  • глобальные log.Logger и SetGlobalLevel — сохранить и вернуть defer-ом; дефолт глобального уровня — DebugLevel
  • хук zerolog.Hook собирает уровни и сообщения без разбора JSON: Run получает их параметрами
  • готовых ассертов в zerolog нет: буфер, encoding/json и свои хелперы — вся инфраструктура

Глобальный логгер и уровни: подмена с восстановлением#

Код под тестом часто зовёт log.Info() напрямую из пакета github.com/rs/zerolog/log — то есть пишет через глобальный log.Logger. Его подменяют как любую глобальную переменную: old := log.Logger, затем log.Logger = zerolog.New(&buf) и defer-восстановление в конце теста — без него следующий тест пакета унаследует буфер и начнёт видеть чужие записи. Вторая ручка — zerolog.SetGlobalLevel: она меняет атомарную переменную gLevel, чьё нулевое значение равно DebugLevel, и trace-записи по умолчанию до буфера не доходят. Тест, проверяющий поле level у трассировки, поднимает планку SetGlobalLevel(zerolog.TraceLevel) и возвращает её defer-ом — восстановленное значение и есть дефолт DebugLevel. Та же механика — основа тестов конфигурации из урока про окружение: собрали логгер по переменным окружения, прогнали событие, проверили уровень в карте. Когда нужно посчитать события по уровням без JSON, берётся хук: интерфейс zerolog.Hook с методом Run(e *Event, level Level, message string) и адаптер HookFunc для функции. Хук срабатывает в момент Msg до записи в writer — собирайте уровень и сообщение из параметров, а не из буфера события: JSON в этот момент ещё не дописан. Логгер с io.Discard и хуком-счётчиком — тихий детектор шумных веток: ни одной строки в stdout, зато точные числа по уровням.

Подмена глобального логгера и проверка Trace

GO
package main

import (
        "bytes"
        "encoding/json"
        "testing"

        "github.com/rs/zerolog"
        "github.com/rs/zerolog/log"
)

func TestTraceVisible(t *testing.T) {
        old := log.Logger
        defer func() { log.Logger = old }()
        defer zerolog.SetGlobalLevel(zerolog.DebugLevel)

        zerolog.SetGlobalLevel(zerolog.TraceLevel)

        var buf bytes.Buffer
        log.Logger = zerolog.New(&buf)

        log.Trace().Str("op", "probe").Msg("tick")

        var m map[string]any
        if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
                t.Fatal(err)
        }
        if m["level"] != "trace" {
                t.Fatalf("level: %v", m["level"])
        }
}

Глобальный log.Logger сохранён в old и возвращён defer-ом — тест не портит соседей по пакету. Второй defer возвращает глобальный уровень: gLevel в globals.go — новая нулевая int32, то есть DebugLevel по умолчанию. Trace-событие видно только при поднятой планке SetGlobalLevel(zerolog.TraceLevel); m["level"] сравнивается со строкой trace — без подъёма тест честно падает. Два defer сработают в обратном порядке: сначала уровень, затем логгер.

Проверка: числа в карте

Почему m["amount"] == 150 ложно, хотя в записи amount равен 150?

Проверка: что есть в zerolog для тестов

Какой тестовый помощник действительно есть в zerolog 1.34.0?

Что напечатает разбор NoLevel-записи?

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

var buf bytes.Buffer
logger := zerolog.New(&buf)

logger.Log().Str("op", "poll").Msg("tick")

var m map[string]any
_ = json.Unmarshal(buf.Bytes(), &m)

fmt.Println(m["level"], m["op"])

Соберите тест уровня

Восстановите цепочку: логгер в буфер, событие, разбор и проверка уровня.

func TestLevel(t *testing.T) { var buf bytes.Buffer logger := zerolog.(&buf) logger.().Str("op", "charge").Msg("ok") var m map[string]any if err := json.(buf.Bytes(), &m); err != nil { t.Fatal(err) } if m["level"] != "" { t.Fatalf("level: %v", m["level"]) } }

Тело теста очереди событий

Расставьте строки тела теста в правильном порядке: от буфера до проверки порядка уровней.

Строки

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

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

Карточки: тесты логов

Термин

Логгер в буфер

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

zerolog.New(&buf): *bytes.Buffer реализует io.Writer. Запись — одна JSON-строка с переводом строки. buf.Reset между подтестами.

1 / 6

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

Проверка логов в тестах — фундамент, на который дальше ложатся метрики и трейсы: урок про Prometheus добавит счётчик рядом с событием, урок про трейсы — корреляцию записей одного запроса. Справочное закрепление: возьмите функцию с двумя ветками — успех и отказ — и напишите тест, который в одной очереди проверяет оба события: поля, уровни, отсутствие лишних записей. Затем сломайте контракт — уберите поле или смените уровень — и убедитесь, что тест падает с внятным сообщением, а не молча проходит. Если сам go test для вас новый инструмент, вернитесь к основам в курсе по основам Go: один тест на буфер стоит дешевле одной ночной пересборки инцидента.

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

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

Вывод

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

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

  • Тест логов zerolog — writer в буфер и encoding/json: готовых ассертов в пакете нет.
  • zerolog.New(&buf): запись — одна JSON-строка; buf.Reset между подтестами, SyncWriter при горутинах.
  • map[string]any: ключи level, message и поля события; числа приходят float64.
  • Уровень пишется у всех записей, кроме log.Log(): у NoLevel ключа level нет, у trace — значение trace.
  • Несколько событий: json.Decoder с More и Decode; Unmarshal читает только первый объект.
  • Порядок событий в одной горутине детерминирован: сравнивайте очередь уровней, не строки.
  • Глобальные log.Logger и SetGlobalLevel — сохранить и вернуть defer-ом; дефолт — DebugLevel.
  • Хук zerolog.Hook получает уровень и сообщение параметрами до записи — счётчик без JSON.

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

Соберите тест из урока на свою функцию с двумя ветками: успех пишет Info с полями, отказ — Error с error и cause. Проверьте в одной очереди оба события через json.Decoder и добавьте финальную проверку dec.More() на отсутствие лишних записей. Добавьте trace-ветку и убедитесь, что без SetGlobalLevel(zerolog.TraceLevel) она не доходит до буфера, а с поднятой планкой — доходит и восстанавливается defer-ом. Напишите хелперы wantStr и wantFloat с t.Helper и перепишите на них прямые сравнения. Подмените глобальный log.Logger, забудьте восстановление и посмотрите, как упадёт следующий тест пакета, — затем верните defer. Напоследок проверьте NoLevel-запись: ключ level в карте должен отсутствовать.

Личные заметкиПусто
Хранятся только в вашем браузере (zerolog · Тестирование логов: writer в буфер)

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

Есть ли в zerolog готовые хелперы для тестов логов?

Пакетов zerolog/test и zerolog/assert не существует — проверка по составу поставки 1.34.0. Из тестового в writer.go живут TestWriter и NewTestWriter: они пересылают готовые строки в t.Log через интерфейс TestingLog, обрезая перевод строки, и годятся для отладочного показа, а не для проверок. Всё остальное — bytes.Buffer, encoding/json и собственные хелперы с t.Helper.

Почему тест не видит Trace-записи?

Глобальный уровень по умолчанию равен DebugLevel: переменная gLevel в globals.go инициализируется нулевым значением, а DebugLevel — ноль. Trace-события отсекаются в should() ещё до создания события. Поднимите планку zerolog.SetGlobalLevel(zerolog.TraceLevel) внутри теста и верните её defer-ом — восстановленное значение и есть дефолтный DebugLevel.

Можно ли проверять порядок полей внутри одной записи?

Zerolog пишет ключи в порядке добавления: level, поля контекста из With, поля цепочки, message последним. Но map[string]any порядок не сохраняет, поэтому обычные тесты проверяют по ключам. Если порядок действительно нужен — читайте поток токенов json.Decoder.Token() или сравнивайте префикс строки до разбора: карта после Unmarshal про порядок уже не помнит.

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

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

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

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

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

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

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