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

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

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

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

Логирование

Сервис отвечает и возвращает честные статусы — теперь ему нужны глаза. В go-kit логирование — не printf, а структурные пары ключ-значение из пакета kitlog: один интерфейс Logger, слоёные обёртки log.With, Valuer'ы для времени и идентификаторов запросов, уровни отдельным пакетом level. Ты соберёшь базовый логгер main, напишешь endpoint-middleware логирования — обёртку func(Endpoint) Endpoint из урока 02, которая пишет method, err и время каждого вызова, — и встретишь ServerFinalizer, честное место для лога HTTP-запроса. Ключи «go kit логирование» и «kitlog» станут рабочим инструментом, а не терминами.

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

Зачем go-kit начал с логгера#

Из всех сквозных функций логирование появилось в курсе первым не случайно: пока у сервиса нет метрик и трейсинга, лог — единственный свидетель о жизни. Но пакет kitlog решает более глубокую задачу, чем вывод строк: он делает событие структурой. Вызов logger.Log("method", "uppercase", "err", err) — это не строка, а набор пар ключ-значение, который форматтер рендерит на выходе как угодно: logfmt для консоли разработчика, JSON для сборщика логов платформы. Причина проста: в микросервисах логи читает не человек, а конвейер — grep по ключу, группировка, алерт; строка «failed to process» для него мусор, а пара "method", "uppercase" — индексируемое поле. Поэтому go kit логирование начинается с вопроса «какие ключи будут у события», а не «что напечатать». Ты уже видел kitlog в уроке 04: kitlog.NewJSONLogger(os.Stdout) и logger.Log("msg", "listening", "addr", ":8080") — сегодня разберём каждую букву этого заклинания и соберём из него настоящий наблюдательный слой.

Интерфейс Logger: одна строка на весь контракт#

Откроем исходники пакета github.com/go-kit/log — весь интерфейс умещается в одну сигнатуру: type Logger interface { Log(keyvals ...interface{}) error }. Это осознанный минимализм в духе endpoint: никаких уровней, имён полей, форматтеров в контракте — только пары. Три следствия, которые надо понять сразу. Первое: пары идут чётно — ключ, значение, ключ, значение; если передать нечётное количество, логгер не упадёт, а подставит в хвост значение ErrMissingValue — константу с текстом (MISSING), и в логе появится громкий маркер ошибки программиста. Второе: Log возвращает error, и по общему соглашению курса его игнорируют: в defer-обёртках пишут _ = logger.Log(...). Падать из-за невозможности записать лог — путь к каскадной аварии. Третье: реализация обязана быть безопасной для конкурентного доступа и не менять переданные keyvals — контракт записан в доке пакета, и на него опирается вся композиция. Один интерфейс, а в результате любой компонент — middleware, транспорт, клиент — принимает log.Logger и не знает, куда именно едет вывод: stdout, файл, syslog или сетевой коллектор.

Базовый логгер: JSON, logfmt, Nop

GO
import (
        "log"
        "net/http"
        "os"

        "github.com/go-kit/log"
        "github.com/go-kit/log/level"
)

func main() {
        // Форматтер №1: JSON для прод-коллектора.
        var logger log.Logger
        logger = log.NewJSONLogger(os.Stdout)
        // Форматтер №2: logfmt для локальной разработки.
        // logger = log.NewLogfmtLogger(os.Stderr)

        logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)

        logger.Log("msg", "listening", "addr", ":8080")
        log.Fatal(http.ListenAndServe(":8080", mux))
}

Два штатных форматтера kitlog: NewJSONLogger пишет каждое событие объектом, NewLogfmtLogger — пары ключ=значение через пробел, читаемые глазами. Оба НЕ безопасны для конкурентного использования без обёртки log.NewSyncLogger — оберни их, если логгер разделён между горутинами. log.With добавляет ко всем событиям пары ts и caller: DefaultTimestampUTC — это Valuer, который на каждый вызов Log вычисляет свежий RFC3339Nano в UTC, а DefaultCaller — файл и строку вызова.

log.With и Valuer: слоёные значения, вычисляемые на каждом событии#

Главный инструмент kitlog — функция log.With(logger, keyvals...), возвращающая новый Logger с добавленными парами. Слои складываются: базовый With в main задаёт ts и caller, middleware поверх добавляет request_id, и каждая строка лога собирает их все — без передачи логгера по аргументам через весь стек. Пара слов о направлении: With добавляет пары в конец (ближе к твоим данным), WithPrefix — в начало (ближе к ts), и именно WithPrefix используют хелперы пакета level. Значения в With могут быть не константами, а Valuer — функциями func() interface{}; они перевычисляются на каждый вызов Log. Из коробки есть log.Timestamp(t func() time.Time), готовые DefaultTimestamp и DefaultTimestampUTC (обе — RFC3339Nano, вторая в UTC) и DefaultCaller. Свой Valuer пишется за минуту — например, генератор request id, если тот не пришёл в заголовке. Важная граница: слой With — место для контекста вызова (какой сервис, какой компонент пишет), а не для данных конкретного запроса; тело запроса в слой With не кладут никогда, для него есть поля события.

Endpoint-middleware логирования

GO
func loggingMiddleware(logger log.Logger) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (response interface{}, err error) {
                        defer func(begin time.Time) {
                                _ = logger.Log(
                                        "method", "uppercase",
                                        "err", err,
                                        "took", time.Since(begin),
                                )
                        }(time.Now())

                        response, err = next(ctx, request)
                        return response, err
                }
        }
}

// Сборка в main: middleware надевается один раз.
upper := makeUppercaseEndpoint(svc)
upper = loggingMiddleware(logger)(upper)

Канон go-kit логирования: лог — это обёртка endpoint'а, а не строка в хендлере. Замыкание помнит logger и next; именованные результаты response и err видны defer'у, который фиксирует время от входа до выхода и пишет событие с err и took. Проверка type assertion на response даст доступ к Err-полю Failer, если хочешь логировать бизнес-ошибки отдельно от err. Middleware надевается в main один раз — на горячем пути запроса только вызов готовой функции.

Почему логи живут в middleware, а не в сервисе#

Вопрос, который тебе зададут на ревью: почему не написать logger.Log прямо в методе сервиса? Ответ — тот же принцип, что и весь курс. Сервис из урока 03 не знает ни о транспорте, ни о форматтерах: суй ему logger — и появится зависимость от инфраструктуры в самом чистом слое, а тесты сервиса потащат моки логгера. Endpoint-middleware — слой между транспортом и endpoint'ом: он видит и request, и response, и err, но остаётся транспорт-агностичным. Повесь один и тот же loggingMiddleware на HTTP-endpoint сегодня и на gRPC-endpoint из урока 13 завтра — логи поедут в обоих случаях, сервис не узнает об этом. Сравни уровни, куда можно вставить лог: декодер/энкодер знают только транспорт, endpoint-middleware знает операцию целиком, service middleware из урока 15 — даже бизнес-контекст, но требует интерфейса сервиса. Правило выбора: лог операции с временем и err — endpoint-middleware; лог бизнес-события («заказ создан») — service middleware; лог транспортных деталей — ServerFinalizer, о котором ниже.

X-Request-ID и ServerFinalizer: лог HTTP-запроса#

В уроке 04 мы обещали: идентификатор запроса появится в уроке про логи. Схема из двух частей. Часть первая — ServerBefore: RequestFunc читает заголовок X-Request-ID (или генерирует новый) и кладёт его в контекст с приватным ключом; теперь значение едет через endpoint в сервис и доступно любому слою. Часть вторая — ServerFinalizer: опция NewServer принимает ServerFinalizerFunc func(ctx context.Context, code int, r *http.Request) и вызывается после того, как ответ уже записан клиенту; это официальное место для лога запроса: method, uri, статус код, длительность, размер ответа. Именно поэтому finalizer'а есть доступ к response headers и размеру: когда опция установлена, Server оборачивает ResponseWriter в interceptingWriter и складывает их в контекст под ключами ContextKeyResponseHeaders и ContextKeyResponseSize. Корреляция собирается в цепочку: клиент шлёт X-Request-ID → ServerBefore кладёт в ctx → endpoint-middleware логирует операцию → finalizer логирует запрос; в платформе логов по одному request_id разворачивается весь путь вызова через все сервисы.

ServerBefore и ServerFinalizer рядом

GO
type requestIDKey struct{}

func toContext() kithttp.RequestFunc {
        return func(ctx context.Context, r *http.Request) context.Context {
                id := r.Header.Get("X-Request-ID")
                if id == "" {
                        id = fmt.Sprint(rand.Int63()) // учебный генератор
                }
                return context.WithValue(ctx, requestIDKey{}, id)
        }
}

mux.Handle("/uppercase", kithttp.NewServer(
        upper,
        decodeUppercaseRequest,
        encodeResponse,
        kithttp.ServerBefore(toContext()),
        kithttp.ServerFinalizer(func(ctx context.Context, code int, r *http.Request) {
                id, _ := ctx.Value(requestIDKey{}).(string)
                _ = logger.Log("method", r.Method, "uri", r.RequestURI,
                        "status", code, "request_id", id)
        }),
))

Слева — RequestFunc из урока 04: условия вызова, а не данные запроса, кладутся в ctx с приватным структурным ключом. Справа — finalizer: он выполнится после записи ответа, поэтому знает финальный статус код. Приватный ключ-структура вместо строки — канон работы с context.Value из урока 02: типобезопасно и без коллизий.

Уровни: пакет level поверх kitlog#

В интерфейсе Logger нет уровней — и это не упущение, а точка расширения. Уровень в go-kit логировании — просто ещё одна пара в событии, по умолчанию ключ level со значениями debug, info, warn, error. Пакет github.com/go-kit/log/level даёт удобные хелперы: level.Error(logger) возвращает логгер с префиксом level=error, и далее level.Info, level.Warn, level.Debug — все они построены на log.WithPrefix над твоим логгером. Зачем нужен ключ, если можно писать "level", "error" руками: чтобы работала вторая половина пакета — фильтр. level.NewFilter(logger, level.AllowInfo()) оборачивает базовый логгер и пропускает только события с уровнем info и выше: debug-записи пишутся в коде всегда, но молчат в проде, где фильтр AllowInfo, и шумят локально, где AllowDebug. Важные дефолты фильтра: без опций он давит ВСЕ leveled-события (собери фильтр с Allow... — иначе ничего не увидишь), а события без уровня проходят мимо фильтра, пока ты не включил опцию SquelchNoLevel. Уровень — не замена тексту, а измеримая размерность: алерт платформы смотрит на rate error-событий, а не парсит слова.

Уровни в сборке main

GO
var logger log.Logger
logger = log.NewLogfmtLogger(os.Stderr)
logger = level.NewFilter(logger, level.AllowInfo())
logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller)

// Ошибка инфраструктуры:
level.Error(logger).Log("msg", "decode failed", "err", err)
// Обычное событие:
level.Info(logger).Log("msg", "listening", "addr", ":8080")

// Debug-записи пишутся в коде сразу, но молчат под AllowInfo:
level.Debug(logger).Log("msg", "request decoded", "req", req)

Порядок слоёв важен: фильтр ставится над форматтером, а With — над фильтром, чтобы ts и caller не вычислялись зря на отфильтрованных событиях. level.Error добавляет pair level=error через WithPrefix, дальше событие идёт сквозь фильтр (error пропущен) и форматтер. Уровень придёт в JSON/logfmt полем — платформа группирует по нему без регулярок.

Какой лог — из какого места#

Тип событияГде пишетсяЧто содержитУровень
Жизненный цикл процессаmain до ListenAndServemsg, addr, версия сборкиinfo
HTTP-запрос целикомServerFinalizermethod, uri, status, request_idinfo / error по коду
Операция сервисаendpoint-middlewaremethod, err, tookwarn при err != nil
Бизнес-событиеservice middleware (урок 15)заказ создан, id, суммаinfo
Инфраструктурный сбойServerErrorHandler / кодerr, transporterror
Шум отладкиlevel.Debug где угодноreq, responsedebug — фильтруется

Чего в логи не класть#

Логи — данные, которые ты отправляешь неизвестно куда на годы вперёд, поэтому гигиена важнее многословия. Пароли, токены, заголовок Authorization — никогда, ни в debug, ни при ошибке; запрос с JWT в логе — это слитый доступ. Персональные данные — с осторожностью: имя и почта в логе делают его субъектом GDPR и удорожают хранение; часто достаточно хешированного идентификатора. Тела запросов целиком — только в debug и только на низовых окружениях: строка с телом в error-логе на проде быстро превращается в хранилище личных переписок. Про объём тоже думай: лог на каждый запрос — норма, лог на каждый внутренний цикл — инцидент бюджета; при всплесках помогает семплирование. И последнее — длина ключей: короткие имена (ts, err, took) — конвенция kitlog, они экономят и место, и глаза; главное — договориться об одном словаре ключей на все сервисы платформы, иначе фильтры коллектора будут враждовать с твоими названиями.

logger.Log("method", "uppercase", "err") — что окажется в логе?

Почему loggingMiddleware — это endpoint-middleware, а не строки в методе сервиса?

logger = level.NewFilter(log.NewLogfmtLogger(os.Stderr), level.AllowInfo()). Что попадёт в вывод?

Порядок пар в слоёном логгере

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

logger := log.NewJSONLogger(os.Stdout)
logger = log.With(logger, "a", 1)
logger = log.With(logger, "b", 2)
_ = logger.Log("c", 3)

Valuer: сколько раз вычислится ts?

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

logger := log.NewJSONLogger(os.Stdout)
logger = log.With(logger, "ts", log.DefaultTimestampUTC)

_ = logger.Log("msg", "first")
time.Sleep(time.Second)
_ = logger.Log("msg", "second")

Собери базовый логгер stringsvc

Восстанови сборку логгера в main. Пропуски — по порядку: форматтер kitlog, слоёная функция, Valuer времени UTC.

var logger log.Logger logger = log.(os.Stderr) logger = log.(logger, "ts", log., "caller", log.DefaultCaller) _ = logger.Log("msg", "listening", "addr", ":8080")

Путь события лога одного запроса

Расставь этапы генерации логов для POST /uppercase в правильном порядке: от прихода запроса до последней записи.

Строки

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

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

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

Термин

log.Logger

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

Интерфейс kitlog: Log(keyvals ...interface{}) error — пары ключ-значение; нечётная длина лечится ErrMissingValue (MISSING).

1 / 6

Что дальше#

Сервис обрёл голос: структура события вместо строк, слои With вместо передачи логгера по стеку, уровни через фильтр, request id через ServerBefore и финальный лог запроса через ServerFinalizer. Вернуть контекст обёрток можно в уроке 02-endpoint, к опциям транспорта — в уроке 04-http-transport, к кодеку ошибок — в уроке 05-codeki; полная программа — на странице курса. Логирование — первый наблюдательный слой; второй — урок 07-metriki: Counter, Gauge и Histogram пакета metrics, prometheus-реализации, латентность в гистограмме и instrumenting middleware рядом с тем loggingMiddleware, который ты только что написал. Третье измерение — трейсинг — ждёт в уроке 08: там запросы получат сквозные спаны, и request id из этого урока окажется их предком.

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

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

Вывод

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

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

  • kitlog — структурный лог: Logger.Log(keyvals ...interface{}) error; чётные пары, нечётные добираются ErrMissingValue.
  • Два штатных форматтера: NewJSONLogger для коллектора и NewLogfmtLogger для консоли; NewNopLogger — тихий заглушка.
  • log.With наслаивает пары, Valuer вычисляется на каждый Log: ts и caller — конвенция log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller).
  • Лог операции — endpoint-middleware: defer + time.Since пишет method/err/took, работает на любом транспорте, сервис не знает про логгер.
  • X-Request-ID кладёт ServerBefore в ctx, финальный лог запроса пишет ServerFinalizer со статусом и request_id.
  • Уровни — пара level=key из пакета level: level.Error(logger) для события, level.NewFilter(logger, level.AllowInfo()) для прод-фильтра.
  • В логи не едут секреты и персональные данные; словарь ключей — единый на платформу.

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

Возьми stringsvc из урока 05 и надень на него полный лог-контур. Первое: собери базовый логгер — LogfmtLogger для локали, фильтр level.AllowInfo, слой With с ts и caller, и убедись, что каждое событие несёт level, ts и caller. Второе: напиши loggingMiddleware(logger) endpoint.Middleware и повесь его на оба endpoint'а; в логе должны появиться method, err и took — проверь на успешном запросе и на ErrEmpty. Третье: добавь ServerBefore с X-Request-ID (чужой заголовок или генерация) и ServerFinalizer с method, uri, status, request_id; двумя curl-вызовами с одним заголовком добейся, чтобы обе записи — middleware и finalizer — срослись по одному id. Четвёртое: уровняй события — ошибки декодера как level.Error, слушание порта как level.Info, декодирование тела как level.Debug; переключи фильтр между AllowInfo и AllowDebug и опиши разницу одной фразой. Пятое: почисти вывод — ни одного секрета, ни одного тела запроса в error-логе; для этого перепиши место, где err выводится целиком, на err.Error().

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Логирование)

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

JSON или logfmt — что выбрать для продакшена?

Смотри на потребителя. Коллекторы вроде Elasticsearch и Loki любят JSON: поля парсятся без правил. logfmt удобнее человеку в консоли и дешевле на диске; платформы вроде Heroku проглотят и его. В kitlog смена формата — одна строка в main: NewJSONLogger вместо NewLogfmtLogger, весь остальной код не меняется.

Почему Log возвращает error, если его принято игнорировать?

Контракт честный: запись может не удаться — диск полон, коллектор недоступен. kitlog не решает за тебя, что делать с этим сбоем, и по умолчанию курс рекомендует _ = logger.Log(...): падать из-за невозможности записать лог хуже, чем остаться без записи. Если важность выше — оберни логгер своим middleware с политикой ретраев или метрикой потерь.

Логировать на уровне endpoint или сервиса?

Endpoint-middleware — для операций: время, err, request id; он транспорт-агностичен и не требует менять интерфейс сервиса. Service middleware (урок 15) — для бизнес-событий с доменным контекстом. Строки лога в самом сервисе — только когда без них никак; тогда логгер передаётся через конструктор вместе с зависимостями, а не создается на месте.

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

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

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

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации