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

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

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

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

Итоговый проект: логгер для HTTP-сервиса

Итоговый slog проект собирает курс в один модуль: логгер для сервиса go с конфигом уровня, middleware с полями запроса, честными уровнями, масками секретов и приёмкой тестами. Ниже — карта решений с отсылками к урокам, ключевой код конструктора и middleware, разбор спорных мест и чек-лист приёмки. Это тот каркас, который можно перенести в сервис за вечер и потом не переделывать.

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

Проект — это не «большой пример», а набор решений, каждое из которых курс разбирал по отдельности. Уровень из окружения с безопасным дефолтом, JSON-записи в stdout, request ID и поля запроса в middleware, err в записях ошибок, маскирование секретов, тесты логов и сбор платформой — по отдельности это уроки, вместе — каркас сервиса. В этом уроке пройдём проект как ревью: карта модуля, конструктор логгера, middleware с уровнями по статусу, конвенция записей по слоям, приёмка тестами и честное спорное — обёртка над slog или логгер напрямую. Код приводится фрагментами, которые связывают решения; полный модуль собирается в репозитории проекта и прогоняется локально, потому что HTTP-сервер песочнице не по силам.

Slog проект: карта модуля и решений#

Модуль логирования в сервисе состоит из трёх файлов и не растёт дальше без причины. Конструктор NewLogger собирает уровень из окружения, обработчик и дефолт — единственное место, где появляется slog.NewJSONHandler. Middleware ставит поля запроса и пишет итоговую запись. Вспомогательные функции — parseLevel, levelByStatus, newRequestID — чистые и потому тестируются без HTTP. Всё остальное — конвенции, а не код: какой уровень у бизнес-отказа, где живут err и kind, что попадает в лейблы метрик, а что в записи. Карта решений ниже связывает каждое из них с уроком курса: при разборе инцидента или онбординге нового разработчика ссылка «почему так» важнее строк кода. Из работ с логами в проекте сознательно нет файла: логи едут в stdout, и это решение шестнадцатого урока, — ротация и место на диске остаются платформе.

Конструктор: уровень, обработчик, дефолт

GO
func NewLogger() (*slog.Logger, *slog.LevelVar, error) {
        var level slog.LevelVar
        if err := level.UnmarshalText([]byte(os.Getenv("LOG_LEVEL"))); err != nil {
                level.Set(slog.LevelInfo)
        }

        logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
                Level: &level,
        }))
        slog.SetDefault(logger)
        return logger, &level, nil
}

Один конструктор на сервис: UnmarshalText разбирает LOG_LEVEL, пустая или битая переменная оставляет безопасный дефолт Info — механика восемнадцатого урока. LevelVar возвращается наружу: это точка динамической смены уровня на инциденте. SetDefault делает видимыми пакетные вызовы и мост легаси-пакета log из десятого урока. Если в сервисе есть Viper, уровень становится ключом конфига с тем же UnmarshalText — приоритеты источников разбирает курс по Viper.

Поля запроса: почему именно эти#

Набор полей middleware — предмет споров в каждом втором проекте, поэтому короткая расшифровка. request_id — ключ склейки записей запроса, из пятнадцатого урока; при подключении трейсера рядом появляется trace_id из двадцать второго, middleware не меняется — добавляется один With. route — путь шаблона, а не фактический URL: иначе значения параметров раздувают кардинальность поля, о чём говорили семнадцатый и двадцать первый уроки. status и duration_ms — пара, по которой строится и поиск медленных запросов, и график; уровень итоговой записи выбирает дежурный, и levelByStatus — его семантика в коде. Логгер кладётся в контекст через withLogger: глубинные функции пишут записи с полями запроса, не зная про middleware. Конструктор при этом не паникует на битой переменной уровня — Warn-запись и дефолт: старт сервиса важнее, а молчать об опечатке нельзя. Честная граница: паника в обработчике гасит итоговую запись, поэтому recovery ставится выше middleware логов — об этом ниже.

Middleware: поля запроса и уровень по статусу

GO
func Logging(base *slog.Logger) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                        start := time.Now()
                        logger := base.With("request_id", newRequestID(r), "route", r.URL.Path)

                        sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
                        next.ServeHTTP(sw, r.WithContext(withLogger(r.Context(), logger)))

                        logger.Log(r.Context(), levelByStatus(sw.status),
                                "запрос обработан",
                                "status", sw.status,
                                "duration_ms", time.Since(start).Milliseconds(),
                        )
                })
        }
}

Поля ставятся один раз через With: request_id берётся из заголовка или генерируется, route — путь без значений параметров. statusWriter ловит код ответа, итоговая запись выходит через Log с уровнем по статусу: 5xx — Error, 4xx — Warn, остальное — Info. duration_ms — целые миллисекунды: с парным histogram это та же пара, что в двадцать первом уроке.

Записи сервиса: конвенция по слоям#

Middleware отвечает за периметр, а записи внутри сервиса держит конвенция, которую курс собирал по урокам. Ошибки хранилища и внешних вызовов пишутся на Error с err в поле — оборачивание через %w, детали из одиннадцатого урока. Бизнес-отказы — на Warn с kind из классификации: заказ не оплачен из-за нехватки средств — это не сбой сервиса, и Error тут был бы спамом уровней из двадцать третьего урока. События — на Info с полями, по которым ищут: order, user_id, kind. Отладочные детали — на Debug, и прод-уровень их отсекает. Маскирование подключается в конструкторе: ReplaceAttr с картой чувствительных ключей и тип Token с LogValue из девятнадцатого урока — тест на маску в проекте обязательный, потому что секреты уезжают в хранилище молча. Метрики ошибок инкрементятся рядом с записью — пара из двадцать первого урока, — и лейблы держат только малые наборы.

Приёмка: тесты и чек-лист#

Проект считается готовым, когда логи проверяются тестами из двадцатого урока. Первый тест — маскирование: фикстура с токеном проходит через логгер, в буфере маска, а не значение. Второй — поля запроса: после запроса в буфере есть request_id, route, status, duration_ms, и уровень итоговой записи соответствует статусу. Третий — отсутствие записей Error в штатном сценарии: успешный запрос не пишет ошибок, и регресс ловится в CI. Четвёртый — уровень: таблица статусов против levelByStatus. Сбор проверяется руками: сервис под journald или в контейнере, journalctl -u и docker logs показывают те же записи, что и буфер теста — это связка двадцать четвёртого урока. Приёмка завершается чек-листом ниже: он короткий, и каждый пункт из него когда-то был инцидентом в чужом проекте.

Спорное: slog напрямую или обёртка сервиса#

Мнение, которое стоит оспорить на старте проекта: логирование надо спрятать за своим интерфейсом, чтобы когда-нибудь сменить логгер. За: интерфейс Logger с методами Info и Error ограничивает поверхность и упрощает моки в тестах. Против: slog — стандартная библиотека, и смена на что-то другое — гипотетическая цена; обёртка теряет механику: Enabled до сборки полей из четырнадцатого урока, LogValuer, группами и With-копиями из восьмого. Практика обёрток тоже честно печальна: в каждом втором проекте своя обёртка, и каждая со своими особенностями. Компромисс проекта: slog.Logger живёт в коде напрямую, без посредников; обёртка уместна на одной границе — доменный фасад, который публикует бизнес-события, и под ней уже slog. Граница по тестам: тесты логов пишутся на Handler и буфер, а не на мок интерфейса — буфер проверяет то же, что увидит прод.

Реестр решений проекта#

РешениеВыборУрок
УровеньLOG_LEVEL и LevelVar, дефолт Infoвосемнадцатый
Формат и местоJSONHandler в stdout, без файловпятый и шестнадцатый
Сквозные поляrequest_id в middleware, trace_id при трейсерепятнадцатый и двадцать второй
Ошибкиerr в записи, kind в метрикеодиннадцатый и двадцать первый
СекретыReplaceAttr и LogValuer, тест на маскудевятнадцатый
Приёмкатесты логов и чек-листдвадцатый

Чек-лист приёмки логгера#

  • Без LOG_LEVEL сервис стартует на Info и пишет JSON в stdout.
  • Каждая запись запроса несёт request_id, route, status и duration_ms.
  • Уровень итоговой записи: 5xx — Error, 4xx — Warn, остальное — Info.
  • Тест на маску проходит: токен не доезжает до буфера и хранилища.
  • Штатный сценарий не пишет Error: тест в CI это проверяет.
  • Recovery стоит выше Logging: паника даёт 500 и запись Error со стеком.

Проверка: где собирать логгер

Где в HTTP-сервисе правильное место для slog.NewJSONHandler?

Проверка: уровень для 404

Какой уровень вернёт levelByStatus для запроса, завершившегося 404?

Что напечатает таблица уровней?

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

func levelByStatus(status int) slog.Level {
        if status >= 500 {
                return slog.LevelError
        }
        if status >= 400 {
                return slog.LevelWarn
        }
        return slog.LevelInfo
}

fmt.Println(levelByStatus(404), levelByStatus(500), levelByStatus(200))

Соберите конструктор

Допишите: держатель уровня, безопасный дефолт и тип обработчика.

func NewLogger() *slog.Logger { var level slog. level.(slog.LevelInfo) return slog.New(slog.NewHandler(os.Stdout, &slog.HandlerOptions{Level: &level})) }

Сборка сервиса по строкам

Расставьте по порядку: уровень, обработчик, дефолтный логгер, маршрутизатор, middleware на ручке, запуск сервера.

Строки

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

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

Карточки: итоговый проект

Термин

NewLogger

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

Единственный конструктор: уровень из LOG_LEVEL через UnmarshalText, JSONHandler в stdout, SetDefault. LevelVar возвращается наружу.

1 / 6

Проект собран, и каждое его решение ведёт в урок: уровень из окружения — в восемнадцатом, формат — в пятом, With-копии и поля запроса — в восьмом и пятнадцатом, ошибки — в одиннадцатом, маскирование — в девятнадцатом, тесты — в двадцатом, связка с метриками и трейсингом — в двадцать первом и двадцать втором, сбор — в двадцать четвёртом. Middleware в целом — в курсе chi и собственных middleware Fiber; уровень из конфига — в курсе Viper; метрики и алерты — в курсе Prometheus. Карта курса — на странице курса.

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

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

Вывод

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

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

  • Проект — набор решений с отсылками к урокам: уровень, формат, поля, ошибки, секреты, приёмка.
  • NewLogger — единственный конструктор: LOG_LEVEL, дефолт Info, JSONHandler в stdout, LevelVar наружу.
  • Middleware ставит поля один раз: request_id и route через With, statusWriter ловит статус, итоговая запись — с duration_ms.
  • levelByStatus: 5xx — Error, 4xx — Warn, остальное — Info; уровень выбирает дежурный, код лишь выражает семантику.
  • Recovery выше Logging: паника даёт 500 и запись Error со стеком, итоговая запись при панике не теряется молча.
  • Приёмка — тестами логов: маска, поля запроса, отсутствие Error в штатном сценарии, локальный docker logs.

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

Соберите модуль проекта в своём сервисе: NewLogger с LOG_LEVEL и дефолтом Info, middleware с request_id, route, status и duration_ms, levelByStatus. Прогоните приёмку: тест маскирования с токеном, тест полей запроса по буферу, тест отсутствия Error в штатном сценарии. Затем запустите сервис в контейнере с log-opts ротации, найдите свои записи через docker logs и сверьте поля с тем, что печатал буфер теста.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Итоговый проект: логгер для HTTP-сервиса)

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

Нужна ли своя обёртка над slog?

Для сервиса — обычно нет: slog — стандартная библиотека, и обёртка теряет Enabled до сборки полей, LogValuer и With-копии. Обёртка уместна на одной границе — доменный фасад бизнес-событий — и под ней всё равно slog. Смена логгера в Go-сервисе — гипотетический сценарий: контракт slog де-факто общий для библиотек.

Почему route — шаблон пути, а не фактический URL?

Фактический URL несёт значения параметров: /orders/a-42, /orders/b-77 — и поле разрастается до кардинальности заказов, ломая группировки и поиск. Шаблон /orders/{id} держит малое число значений — число маршрутов. Значения параметров при этом живут в полях события: order, user_id — там им место.

Где инициализировать логгер в тестах?

Тесты не используют конструктор сервиса: они создают свой логгер через slog.New с буфером из двадцатого урока. Конструктор NewLogger — это старт сервиса и окружение; тестовый логгер — контролируемый буфер с фиксированным уровнем. Общее между ними — HandlerOptions, чтобы тест проверял то, что увидит прод.

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

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

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

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

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY