Содержание урока · 13 разделов
- echo логирование: два потока вместо одного
- e.Logger: интерфейс из log.go и gommon под капотом
- Уровень лога: как gommon решает, печатать ли
- Структурированные логи: slog из стандартной библиотеки
- Три пути присадки slog в Echo
- RequestLogger: RequestLoggerConfig по списку из исходников
- Корреляция по RequestID: три способа положить id в запись
- Куда что пишет: карта вывода приложения
- Куда увести строки metricsTimer
- Что дальше: ошибки и их формат
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
echo логирование: два потока вместо одного#
У любого веб-приложения два лог-потока, и у них разные заказчики. Первый — журнал запросов: одна строка (или запись) на каждый HTTP-запрос, его читают дежурные и метрики, в нём важны метод, путь, статус и латентность. Второй — события самого приложения: «не смогли открыть конфиг», «миграция прошла», «задача создана»; их читают разработчики, и в них важны атрибуты — id сущности, имя пользователя, длительность операции. echo логирование устроено ровно так: за первый поток отвечает middleware, за второй — e.Logger, и смешивать их не стоит.
В седьмом уроке вы уже разобрали штатный middleware.Logger: fasttemplate-формат с тегами ${...}, LoggerConfig, CustomTagFunc, скипер. Свежие версии v4 подвели черту: в v4.14 фабрики Logger и LoggerWithConfig получили комментарий Deprecated с прямой рекомендацией — «please use middleware.RequestLogger or middleware.RequestLoggerWithConfig instead». Сегодняшний урок рассказывает, что пришло на смену, и прихватывает второй вопрос, который всё время зависал рядом: уровень лога — почему одни записи печатаются, а другие молча съедаются.
e.Logger: интерфейс из log.go и gommon под капотом#
Поле Logger у структуры Echo объявлено как Logger Logger — интерфейс, который живёт в файле log.go корневого пакета. Контракт широкий: пары Output() io.Writer / SetOutput(w io.Writer), Prefix() / SetPrefix(p string), Level() log.Lvl / SetLevel(v log.Lvl) плюс SetHeader(h string); семейство Print и Printf без уровня; по пять семейств с уровнями — Debug, Info, Warn, Error, Fatal, Panic — в трёх вариантах каждое (обычный, с форматом f и JSON-вариант с суффиксом j). Реализация по умолчанию — не из Echo: конструктор присваивает Logger: log.New("echo"), где log — это github.com/labstack/gommon/log, зависимость из go.mod (v0.4.2). Префикс "echo" вы видите в каждой записи.
Внутри echo.New() происходит ещё два незаметных, но важных действия. Первое: e.Logger.SetLevel(log.ERROR) — сразу после создания уровень лога устанавливается в ERROR, и это объясняет половину жалоб «а почему Info не печатается». Второе: e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) — Echo создаёт стандартный *log.Logger с префиксом "echo: ", разделяющий тот же вывод, и отдаёт его http.Server-у как ErrorLog в configureServer. Наконец, глобальный DefaultHTTPErrorHandler тоже пишет через e.Logger: если отправить ответ не удалось (ошибка внутри c.JSON), он зовёт e.Logger.Error(err). То есть всё, что Echo сообщает о себе, течёт через один интерфейс — и весь этот поток перенастраивается тремя методами из следующего блока.
SetLevel, SetOutput, SetHeader: ручки e.Logger
GOimport (
"io"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log" // уровни и реализация по умолчанию
)
func main() {
e := echo.New()
// New() уже поставил log.ERROR — поднимаем порог до INFO,
// иначе e.Logger.Info будет молчать
e.Logger.SetLevel(log.INFO)
// заголовок записи — fasttemplate с тегами уровня gommon;
// заголовок в форме JSON-объекта валиден и удобен для парсеров
e.Logger.SetHeader(`{"time":"${time_rfc3339_nano}","level":"${level}","prefix":"${prefix}","file":"${short_file}","line":"${line}"}`)
// вывод: консоль + файл; в файл уйдёт плоский текст —
// gommon сам отключит цвет для не-терминала
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
e.Logger.Fatal(err) // печать + os.Exit(1)
}
e.Logger.SetOutput(io.MultiWriter(os.Stdout, f))
e.Logger.Infof("приложение стартует, режим %s", "dev")
e.Logger.Fatal(e.Start(":1323"))
}Три ручки — три абстракции: SetLevel решает «печатать ли вообще», SetHeader — как выглядит заголовок записи, SetOutput — куда падают байты. Теги заголовка у gommon свои: time_rfc3339, time_rfc3339_nano, level, prefix, long_file, short_file, line — это НЕ те теги, что у middleware.Logger из седьмого урока (${uri}, ${status}, ${latency_human}): два шаблона живут в разных пакетах и не перепутаны. Если заголовок заканчивается закрывающей скобкой объекта, gommon печатает запись как JSON — дописывает "," и поле "message" с вашим текстом, выходит одна валидная строка. SetOutput с файлом или пайпом автоматически выключает цвет: внутри стоит проверка *os.File плюс isatty.IsTerminal. И две детали про Fatal-семейство: оно всегда печатает независимо от уровня и завершает процесс os.Exit(1), а Panic-семейство паникует. Про песочницу — как и весь курс Echo, этот урок не исполняется браузерным движком сайта: runner-ов здесь нет, код запускается локально — go mod init, go get github.com/labstack/echo/v4, go run и curl.
Что напечатает свежий e := echo.New()
Прочитайте код и выберите, что он выведет:
func main() {
e := echo.New() // SetLevel(log.ERROR) уже внутри
e.Logger.Info("приложение запущено")
e.Logger.Warnf("медленный запрос: %s", "3.2s")
e.Logger.Errorf("не смогли открыть конфиг: %v", err)
e.Logger.Printf("слушаю порт %s", ":1323")
e.Logger.Fatal(e.Start(":1323"))
}Уровень лога: как gommon решает, печатать ли#
Тип уровня — log.Lvl (uint8) с константами DEBUG=1, INFO=2, WARN=3, ERROR=4, OFF=5; внутренние уровни Panic и Fatal стоят выше и не экспортируются. Вся логика решения — одна строка в методе log: запись печатается, если level >= l.Level() или level == 0. Отсюда три практических следствия. Первое: OFF глушит всё уровневое — живут только Print и Printf; при этом Fatal и Panic напечатаются даже при OFF (их уровни выше), потому что они обязаны успеть сообщить причину смерти: Fatal завершает процесс os.Exit(1), Panic паникует. Второе: SetLevel атомарен — внутри atomic.StoreUint32, чтение atomic.LoadUint32, — можно крутить уровень в рантайме без гонок: по сигналу, по env-флагу, по кнопке в админке. Третье: уровень тесно связан с debug-режимом — configureServer при e.Debug == true зовёт e.Logger.SetLevel(log.DEBUG), а заодно заезжает бонусом в другой файл: DefaultHTTPErrorHandler в Debug-режиме добавляет в JSON-ответ поле "error" с текстом ошибки, которого в обычном режиме нет.
Проверка: что переживает SetLevel(log.OFF)
Вы вызвали e.Logger.SetLevel(log.OFF). Какие из вызовов что-то напечатают?
Структурированные логи: slog из стандартной библиотеки#
Строка «user alice created todo 42» удобна человеку и неудобна машине: чтобы посчитать задачи по пользователям, её надо парсить. Структурированные логи переворачивают формат: событие — это сообщение плюс набор атрибутов ключ-значение, а сериализация (текст или JSON) — забота хендлера. Со времён Go 1.21 она решается стандартной библиотекой: пакет log/slog. Примитивы короткие: slog.New(h) оборачивает хендлер в *slog.Logger; slog.NewJSONHandler и slog.NewTextHandler — две штатные сериализации; опции — slog.HandlerOptions{Level, ReplaceAttr}: порог уровня и перерисовка атрибутов на выходе; атрибуты — slog.String, slog.Int, slog.Duration, slog.Int64, slog.Group; logger.With("service", "api") возвращает НОВОГО логгера с приклеенными атрибутами.
Отдельный метод — slog.SetDefault(l): он делает l логгером пакетных функций slog.Info/slog.Errorf и заодно перепривязывает пакет log: внутри вызываются log.SetOutput и log.SetFlags(0), после чего всё, что печатает пакетный log, течёт через ваш хендлер на уровне Info. Тонкость, на которой ловятся: перепривязывается только ДЕФОЛТНЫЙ пакетный log.Logger — чужие экземпляры *log.Logger, созданные через log.New, живут своей жизнью. К ним относится и e.StdLogger из Echo. Именно поэтому связка «slog echo» в рецептах собирается не одной строкой SetDefault, а тремя путями, которые разберём ниже.
slog echo: JSONHandler, With и SetDefault
GO// структурированный логгер приложения: JSON в stdout
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // порог ниже дефолтного Info
}))
// постоянные атрибуты: клеймо сервиса на каждой записи
logger = logger.With("service", "todos-api", "env", "dev")
// в обработчике (продолжение канона урока 08):
func createTodo(c echo.Context) error {
in, _ := c.Get(ctxTodoInputKey).(TodoInput)
t := Todo{ID: nextID, Title: in.Title, Priority: in.Priority}
nextID++
todos = append(todos, t)
logger.Info("todo created",
slog.Int("id", t.ID),
slog.String("title", t.Title),
slog.Duration("took", 421*time.Microsecond),
)
return c.JSON(http.StatusCreated, t)
}
// строка в stdout — одна запись, парсится без regexp:
// {"time":"2026-02-14T10:22:31.104+03:00","level":"INFO","msg":"todo created",
// "service":"todos-api","env":"dev","id":7,"title":"купить хлеб","took":421000}
// делаем логгером по умолчанию: пакетные slog.Info и пакет log
// тоже потекут через JSONHandler
slog.SetDefault(logger)Порядок полей в JSON-записи фиксирован: time, level, msg — служебные, затем With-атрибуты (service, env), затем атрибуты конкретного вызова. Duration сериализуется в наносекундах — 421000, это нормально: человекочитаемую латентность держите отдельным атрибутом со строкой. logger.With возвращает нового логгера — переприсваивайте переменную, атрибуты «на месте» не приклеиваются. И про пороги: HandlerOptions{Level: slog.LevelDebug} — зеркало SetLevel у gommon, только у slog уровни типизированы (slog.LevelDebug, LevelInfo, LevelWarn, LevelError), а не числа 1-5.
Проверка: slog.SetDefault и e.StdLogger
После slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) — куда попадут сообщения об ошибках http.Server, которые Echo отдаёт в e.StdLogger?
Три пути присадки slog в Echo#
Путь первый — грубая сила: e.Logger.SetOutput(адаптер), где адаптер превращает строки gommon в вызовы slog. Работает, но честно скажем, что это костыль: адаптер видит ГОТОВУЮ строку, атрибуты уже сериализованы, и вы либо парсите их обратно, либо пишете текстом. Спасает только JSON-режим заголовка из блока выше — тогда каждая строка gommon сама валидный JSON. Этот путь живёт для старых проектов, где весь код уже пишет через e.Logger.
Путь второй — установить slog.SetDefault и подключить middleware.RequestLogger() без конфига: его дефолтная LogValuesFunc пишет именно пакетными slog.LogAttrs, значит, попадает в ваш JSONHandler автоматически. Минимум кода, но формат записи фиксирован — REQUEST со стандартным набором полей, и приклеить атрибут из c.Get не выйдет.
Путь третий — явный и рекомендуемый: middleware.RequestLoggerWithConfig с собственной LogValuesFunc, в замыкании которой живёт ваш *slog.Logger. Полный контроль над уровнем, сообщением и атрибутами. Любопытно, что доккомментарий request_logger.go содержит готовые примеры для slog, fmt.Printf, zerolog, zap и logrus — Echo прямо документирует, что middleware не навязывает библиотеку: LogValuesFunc получает структуру значений и пишет хоть в бумажную газету.
RequestLogger: RequestLoggerConfig по списку из исходников#
RequestLoggerConfig — это Skipper (первое поле по канону каталога), два крючка и полтора десятка флагов. Крючки: BeforeNextFunc func(c echo.Context) — вызывается перед next(c), удобно поставить метку начала; LogValuesFunc func(c echo.Context, v RequestLoggerValues) error — обязательная, документ помечает её словом Mandatory. Флаги-булевы, каждый вытаскивает своё значение в v: LogLatency (длительность цепочки), LogProtocol, LogRemoteIP (через c.RealIP()), LogHost, LogMethod, LogURI (полный URI с query), LogURIPath (только путь), LogRoutePath (шаблон маршрута c.Path() — тот же, что тег ${route}), LogRequestID, LogReferer, LogUserAgent, LogStatus, LogError, LogContentLength, LogResponseSize. Списковые поля: LogHeaders []string, LogQueryParams []string, LogFormValues []string — называют имена, и в v.Headers/v.QueryParams/v.FormValues падают срезы значений.
RequestLoggerValues — зеркало с типами: StartTime, Latency time.Duration, Protocol, RemoteIP, Host, Method, URI, URIPath, RoutePath, RequestID, Referer, UserAgent string, Status int, Error error, ContentLength string, ResponseSize int64. Если LogValuesFunc не задана, ToMiddleware возвращает ошибку "missing LogValuesFunc callback function for request logger middleware", а фабрика RequestLoggerWithConfig на ней паникует — то же правило «ошибка конфигурации стреляет при подключении», которое вы видели у KeyAuth в восьмом уроке. Дефолтный RequestLogger() включает latency, remoteIP, host, method, uri, requestID, userAgent, status, error, contentLength, responseSize плюс HandleError: true и печатает REQUEST или REQUEST_ERROR.
RequestLoggerWithConfig и slog: корреляция и честный статус
GOfunc main() {
e := echo.New()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// RequestID подключаем — он ставит X-Request-Id в ответ
e.Use(middleware.RequestID())
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogLatency: true,
LogMethod: true,
LogURI: true,
LogStatus: true,
LogError: true,
LogRequestID: true,
HandleError: true, // ошибку оформляет глобальный обработчик ДО записи
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
level, msg := slog.LevelInfo, "request"
if v.Error != nil {
level, msg = slog.LevelError, "request_error"
}
user, _ := c.Get(ctxUserKey).(string) // bearerAuth из урока 08
logger.LogAttrs(context.Background(), level, msg,
slog.String("request_id", v.RequestID),
slog.String("method", v.Method),
slog.String("uri", v.URI),
slog.Int("status", v.Status),
slog.Duration("latency", v.Latency),
slog.String("user", user),
)
return nil
},
}))
api := e.Group("/api", bearerAuth(AuthConfig{Token: "dev-token-42", Username: "alice"}))
api.GET("/profile", profile)
e.Logger.Fatal(e.Start(":1323"))
}Читается так: запрос прошёл цепочку (или упал), значения сняты, LogValuesFunc превратила их в одну JSON-запись с id пользователя и запроса. Три момента, где свежие исходники отвечают заранее. HandleError: true — сразу после next(c) middleware зовёт c.Error(err): глобальный обработчик оформляет ответ, поэтому v.Status уже честный 401/400/500, а v.Error остаётся заполненным для ветки request_error. HandleError: false — ответ может быть не оформлен, и тогда LogStatus достаёт код из echo.HTTPError сам, через errors.As; иначе взял бы дефолтный 200 из response.go. Ошибка, возвращённая вашей LogValuesFunc, заменит исходную — return nil, если не хотите портить раскрутку цепочки. И ctxUserKey достаётся c.Get-ом: RequestLogger — обычное звено, весь контекст к нему доступен.
Проверка: HandleError и статус 401 в записи
bearerAuth вернул 401-ошибку. В RequestLoggerConfig стоят LogStatus: true, LogError: true, HandleError: true. Что увидят LogValuesFunc и клиент?
Конфиг без LogValuesFunc
Прочитайте код и выберите, что он выведет:
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogStatus: true,
LogURI: true,
// LogValuesFunc кто-то забыл
}))
// что произойдёт при старте?Корреляция по RequestID: три способа положить id в запись#
Механику middleware.RequestID вы разобрали в седьмом уроке: заголовок клиента переиспользуется, иначе генерируется randomString(32), заголовок X-Request-Id ставится в ответ до next(c). Здесь — только то, как идентификатор попадает в структурированную запись. Способ первый, встроенный: LogRequestID: true — RequestLogger сам читает константу echo.HeaderXRequestID из заголовков запроса, а если клиент её не прислал, берёт из заголовка ответа (там RequestID к моменту снятия значений уже стоит). Порядок относительно RequestID в цепочке не критичен: значения снимаются на раскрутке, после next(c), — но RequestID первым остаётся доброй привычкой.
Способ второй — RequestIDHandler func(c echo.Context, string) в RequestIDConfig: положите id в контекст своей константой c.Set(ctxRequestIDKey, id) и читайте в LogValuesFunc через c.Get вместе с пользователем — так id попадает и в собственные события приложения, не только в access-запись. Способ третий — протянуть заголовок дальше: если ваш сервис ходит в бэкенды, копируйте X-Request-Id в исходящие запросы, и строки логов разных программ склеятся по одному значению. И прибережём сантиметр честности: не создавайте slog.With(logger, ...) на каждый запрос — With аллоцирует нового логгера; в access-логе передавайте id атрибутом в LogAttrs, как в коде выше.
Куда что пишет: карта вывода приложения#
| Источник строк | Пишет по умолчанию | Как перенаправить |
|---|---|---|
| Собственные события приложения | пакетный slog: текст в stdout через пакет log | slog.SetDefault(JSONHandler) или свой *slog.Logger в замыкании |
| Access-запись на каждый запрос | middleware.Logger — Deprecated с v4.14, fasttemplate-строка | RequestLoggerWithConfig + LogValuesFunc со slog |
| e.Logger (Fatal, Info, Error) | gommon log: JSON-заголовок по умолчанию, stdout | e.Logger.SetOutput / SetHeader / SetLevel |
| Ошибки http.Server (ErrorLog) | e.StdLogger → e.Logger.Output() | унаследует e.Logger.SetOutput — writer общий |
| Ошибка отправки ответа в DefaultHTTPErrorHandler | e.Logger.Error(err) | тот же e.Logger: SetOutput влияет и сюда |
| Строки метрик metricsTimer (урок 08) | fmt.Printf в stdout | заменить на slog.Info/Warn с атрибутами |
Куда увести строки metricsTimer#
В восьмом уроке metricsTimer печатала fmt.Printf("%s: total=%d err=%d avg=%s last=%s") — работало, но строку нельзя было ни отфильтровать по уровню, ни распарсить без regexp. Переезд делается в три действия. Первое: в MetricsConfig добавляется поле Log *slog.Logger, nil-резолв в теле фабрики ставит slog.Default() — тот же паттерн «дефолты на первой ступени», что и для Prefix. Второе: fmt.Printf заменяется на logger.Info с атрибутами: slog.Int64("total", total), slog.Int64("errored", errored), slog.Duration("avg", elapsed/time.Duration(total)), slog.Duration("last", latency). Третье — уровни по смыслу: обычная строка Info, а если вы делали SlowThreshold из домашнего задания, медленные запросы заслуживают Warn — дежурный увидит их в алертах, не переписывая фильтры.
Помните и про отличие потоков: метрика — агрегат за жизнь процесса, лог — событие. Строка лога с total на каждый запрос — это деградация до Printf с лишними расходами; настоящие счётчики стоит отдавать в Prometheus-экспортёр, а в логе оставлять событие «случилось что-то, достойное внимания». И правило статуса никуда не делось: c.Error(err) до печати нужно и здесь — при HandleError: true у RequestLogger тот же контракт выполняет middleware сам.
Флаги RequestLoggerConfig тянут сырые значения: LogURI включает query целиком — uri="/reset?token=..." уедет в JSON как есть; LogHeaders и LogQueryParams дублируют то же в срезах. Пароль в теле POST не попадёт (тело не логируется ни одним флагом), а вот токен в query и заголовок Authorization — легко; про запрет логировать Authorization вы уже слышали в восьмом уроке. Лекарства два: для приватных API берите LogURIPath вместо LogURI (только путь, без query), а маскирование полей — ReplaceAttr в HandlerOptions: функция получает атрибут на выходе и может подрезать значение. Одноразовые ссылки и токены восстановления — первая проверка любого нового флага.
Соберите конфиг RequestLogger
Заполните пропуски: фабрика, флаг RequestID, обязательная функция, пакетный вызов записи и атрибут статуса.
Соберите main() со slog и access-логом
Расставьте строки в правильном порядке: логгер создаётся и становится дефолтным до подключения middleware, RequestID стоит раньше access-лога, запуск — последним.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: логирование
Что дальше: ошибки и их формат#
Мы разложили echo логирование по двум потокам и свели их на общем языке JSON: e.Logger управляется тремя ручками с уровнями DEBUG-OFF и атомарной сменой порога, slog даёт структурированные записи с With и SetDefault, а middleware.RequestLogger заменяет deprecated Logger из каталога и снимает честный статус через HandleError или errors.As. Идентификатор запроса из RequestID доезжает до записи тремя способами, строки metricsTimer переезжают в slog.Info/Warn с атрибутами, а ReplaceAttr прикрывает токены в query. order-задание — репетиция вашего main: логгер, SetDefault, RequestID, access-лог, маршруты, запуск.
Белым пятном осталась ошибка как объект: мы упоминали echo.HTTPError по имени, но не разбирали — поле Internal для исходной причины, Message, принимающий не только строки, собственный ErrorHandler с единым форматом всего API и его дружбу с HandleError: true из этого урока. Полный разбор — дело десятого урока. Пока проверьте себя карточками, соберите fill и order ещё раз и перенесите конфиг RequestLogger в учебный сервер восьмого урока: bearerAuth уже кладёт пользователя в контекст — одна запись с request_id, user, uri и статусом закрывает половину вопросов дежурного. Навигация по курсу — на странице курса Echo, фабрики звеньев и c.Set/c.Get — в уроке про кастомные middleware, каталог штатных слоёв и судьба старого Logger — в уроке про встроенные middleware.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- echo логирование — два потока: журнал запросов (middleware) и события приложения (e.Logger). Смешивать их — значит лишать оба фильтров по уровню и парсинга.
- e.Logger — интерфейс из log.go поверх labstack/gommon: SetLevel, SetOutput, SetHeader плюс семейства Debug/Info/Warn/Error/Fatal/Panic. New() ставит уровень ERROR — Info по умолчанию молчит.
- Уровень лога: gommon печатает при level >= текущего; Print (0) проходит всегда, OFF глушит уровневое, Fatal/Panic печатаются и завершают процесс. SetLevel атомарен — меняется в рантайме.
- slog: New + JSONHandler/TextHandler, HandlerOptions{Level, ReplaceAttr}, With для постоянных атрибутов, SetDefault перепривязывает пакет log (только пакетный logger — e.StdLogger живёт своей жизнью).
- middleware.Logger помечен Deprecated с v4.14; замена — RequestLogger()/RequestLoggerWithConfig с RequestLoggerConfig: флаги Log* вытаскивают значения в RequestLoggerValues, LogValuesFunc обязательна и паникуется при отсутствии на e.Use.
- HandleError: true — middleware сам зовёт c.Error(err): статус в записи честный (401/400/500); без HandleError код достаётся errors.As-ом из echo.HTTPError.
- Корреляция: LogRequestID читает X-Request-Id (запрос, фолбэк — ответ), RequestIDHandler кладёт id в контекст для c.Get, заголовок протягивается между сервисами.
Домашнее задание
Соберите всё в учебном сервере восьмого урока. Первое: добавьте в MetricsConfig поле Log *slog.Logger с nil-резолвом в slog.Default() и переведите печать на logger.Info/Warn с атрибутами total/errored/avg/last — медленные запросы из SlowThreshold пусть идут с уровнем Warn. Второе: включите в RequestIDConfig RequestIDHandler и положите id в контекст константой ctxRequestIDKey; в LogValuesFunc добавьте атрибуты request_id из v.RequestID и user из c.Get(ctxUserKey). Третье: включите LogURI и убедитесь curl-ом, что uri содержит query целиком, затем замените на LogURIPath и напишите ReplaceAttr, маскирующую значение атрибута uri до первой буквы пути — сравните две записи и объясните себе, какая из них утечёт токен из query в продакшен.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем e.Logger отличается от middleware.RequestLogger?
Это два разных потока. e.Logger — интерфейс логгера приложения (файл log.go поверх labstack/gommon): вы зовёте его методы сами, у него уровни DEBUG-OFF, SetOutput/SetHeader и префикс "echo". RequestLogger — middleware access-лога: одна запись на запрос, собирается из флагов Log* в структуру RequestLoggerValues, а печать делегирует вашей LogValuesFunc — хоть slog, хоть zap. Старый middleware.Logger из каталога с v4.14 помечен Deprecated именно в пользу RequestLogger.
Почему e.Logger.Info ничего не печатает?
Потому что echo.New() ставит уровень ERROR: e.Logger.SetLevel(log.ERROR). Правило gommon — печать при level >= текущего: Info (2) и Warn (3) ниже порога и глушатся, Error (4) проходит, Print/Printf проходят всегда. Лечение: e.Logger.SetLevel(log.INFO) сразу после создания или e.Debug = true — configureServer включит DEBUG. SetLevel атомарен, порог можно менять в рантайме.
Как подружить slog и стандартный пакет log?
slog.SetDefault(l): пакетные функции log.Printf/log.Fatal начнут писать через хендлер l на уровне Info — внутри это log.SetOutput + log.SetFlags(0). Но перепривязывается только пакетный логгер: экземпляры, созданные log.New (среди них e.StdLogger от Echo), продолжают писать туда, куда настроены при создании. Их перенаправляйте вручную — либо через общий writer e.Logger.SetOutput, либо собственным адаптером io.Writer на хендлере slog.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
GORM: ORM-библиотека для Go
Курс GORM: модели, CRUD, ассоциации, транзакции, хуки, пагинация, оптимизация запросов. 25 уроков с примерами и проектом слоя данных.
- Подключать БД и настраивать пул соединений
- Описывать модели и связи: 1:1, 1:N, M:N
- Писать CRUD и сложные выборки chainable-стилем
- Контролировать N+1 через Preload