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

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

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

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

Сквозные поля: request ID через слои

Тридцать записей одного HTTP-вызова собираются одним фильтром — за это отвечают request id логи go: идентификатор рождается в middleware, живёт в контексте и приклеивается к каждой записи через With. Разберём оба источника идентификатора, прокидку через слои, фоновые задачи, которые теряют поля, и работу поддержки с кодом обращения.

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

Один вызов API оставляет тридцать записей: обработчик, сервис, репозиторий, кэш, внешние вызовы, финальная строка с длительностью. Без общего поля эти записи размазаны по потоку вперемешку с соседними вызовами, и разбор инцидента превращается в сопоставление по времени — занятие, которое при нагрузке в тысячу запросов в секунду никуда не годится. Механику логгера на запрос — With и контекст — показал восьмой урок; этот урок собирает полную картину: откуда идентификатор приходит, в каком формате его генерировать без зависимостей, как слои достают логгер из контекста, что происходит с полями в фоновых задачах и как поддержка находит запрос по коду обращения, который увидел пользователь. Отдельный сюжет — связь с трейсингом: request_id закрывает вопросы внутри сервиса, trace_id связывает сервисы между собой, и между ними есть ясная граница. К концу урока middleware, слои и фоновые задачи складываются в одну цепочку, где поле добавляется один раз и работает трижды: в логе, в метриках и в разговоре с пользователем.

Request id логи go: откуда берётся идентификатор#

У идентификатора два источника, и разбор request id логи go начинается с первого: заголовок X-Request-ID, который уже проставил балансир или шлюз. Его переиспользуют: инфраструктура до вашего сервиса уже собрала историю под этим номером, и переиспользование склеивает её со строками приложения — одна сквозная цепочка от входа в кластер до строки в репозитории. Заголовок подделываем, и это нормально: он не авторизация, а корреляция, доверять ему решениями не нужно. Если заголовок пуст — идентификатор генерирует middleware: восемь-шестнадцать случайных байт из crypto/rand, закодированные в hex или собранные в UUID-подобный формат без внешних зависимостей. Третье место, где идентификатор живёт, — ответ: middleware отдаёт его в заголовке ответа и в тексте ошибок, и пользователь с кодом обращения в руках даёт поддержке прямой ключ к своей истории.

Middleware: источник, контекст, ответ

GO
type loggerKey struct{}

func withRequestID(base *slog.Logger, next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                id := r.Header.Get("X-Request-ID")
                if id == "" {
                        id = newRequestID()
                }

                logger := base.With("request_id", id, "method", r.Method)
                ctx := context.WithValue(r.Context(), loggerKey{}, logger)
                w.Header().Set("X-Request-ID", id)

                start := time.Now()
                next.ServeHTTP(w, r.WithContext(ctx))
                logger.Info("запрос завершён", "duration", time.Since(start))
        })
}

func newRequestID() string {
        b := make([]byte, 8)
        if _, err := rand.Read(b); err != nil {
                return strconv.FormatInt(time.Now().UnixNano(), 36)
        }
        return hex.EncodeToString(b)
}

Заголовок балансира переиспользуется, пустой — рождается свой. Логгер с request_id и method кладётся в контекст под типом-ключом, идентификатор отдаётся клиенту в ответе до первого WriteHeader, а финальная запись с длительностью печатается от того же потомка. rand здесь — crypto/rand: ошибка генерации уводит на метку времени, чтобы middleware не встал.

Логирование http go: слои после middleware#

Дальше начинается дисциплина разбор логирование http go: поле request_id добавляет ровно один слой — middleware, и повторного With с этим ключом в коде нет, иначе в записи появятся дубли из ловушки восьмого урока. Слои достают логгер из контекста: slog.FromContext(ctx) возвращает потомка с предзаданными полями, а если middleware не было — дефолтный логгер, и приложение продолжает работать с меньшим контекстом. Контекст уже передаётся параметром через обработчик, сервис и репозиторий, поэтому логгер едет внутри без нового аргумента; там, где контекста нет, логгер передают полем структуры — правило из восьмого урока остаётся в силе. Аутентификационный middleware встраивается в цепочку после request-id: он берёт логгер из контекста, добавляет своим With поле user_id и кладёт обновлённого потомка обратно — к обработчику едет уже логгер с двумя сквозными полями, и записи знают и запрос, и пользователя. Такая цепочка собирается и для других сквозных полей — язык, регион, версия клиента: каждый middleware добавляет своё поле и передаёт дальше, а вызов записи в глубине слоёв получает всё сразу.

Слои достают логгер из контекста

GO
func (s *Service) AcceptOrder(ctx context.Context, orderID string) error {
        logger := slog.FromContext(ctx)

        logger.Info("заказ принят", "order_id", orderID)
        if err := s.repo.Save(ctx, orderID); err != nil {
                logger.Error("сохранение не удалось", "err", err)
                return err
        }
        return nil
}

FromContext вернул потомка middleware: обе записи несут request_id и method без единого упоминания в сервисном коде. Если запрос пришёл без middleware, вернётся дефолтный логгер — записи потеряют request_id, но не сломаются.

Фоновые задачи из запроса: потерянные поля#

Контекст запроса отменяется при выходе из обработчика, и фоновая работа, уехавшая с ним, получает отмену вместо результата. Наивный обход — go func с context.Background() — решает проблему отмены и ломает логгер: поля запроса жили в контексте, Background их не содержит, и записи фоновой горутины печатаются без request_id. Правильный ход — context.WithoutCancel: он сохраняет все значения контекста, включая логгер, и снимает отмену; записи продолжают нести request_id, а их видимость не зависит от жизни запроса. Для задач с собственным жизненным циклом — очереди писем, ночные пересчёты, вебхуки — поверх сохранённого контекста собирают потомка с task_id: поля request_id и task_id вместе отвечают на вопросы «из какого запроса выросла задача» и «что сделала задача сама». Если трейс уже заведён, уходит и trace_id — цепочка переживает переезд из запроса в фоновую работу.

Склейка строк: фильтр и работа поддержки#

Смысл поля раскрывается у потребителя. Записи одного вызова склеивает фильтр: jq по request_id вытягивает полный маршрут — метод, шаги обработчика, ошибки, финальную длительность; то же делает дашборд с фильтром по полю. Длительность из финальной записи собирается в распределение latency, и медленные вызовы находятся до того, как их найдёт пользователь. Второй потребитель — поддержка: идентификатор возвращается в заголовке ответа и в тексте ошибки — «внутренняя ошибка, код обращения ab12c3d4» — и пользователь приносит этот код первой строкой обращения. Третий — безопасность: сам по себе идентификатор не персональные данные, но записи рядом с ним живут user_id и адреса, и правила девятнадцатого урока про секреты и ПДн на них распространяются целиком. Так одно короткое поле работает трижды: склеивает лог, кормит метрики и заменяет час расспросов пользователя. Побочный эффект склейки — дисциплина в самих записях: когда история вызова читается целиком, пустые и бесполезные строки видны сразу, и их убирают — связка с антипаттернами двадцать третьего урока прямая.

Путь request id по сервису#

ЭтапЧто происходитКто отвечает
входчтение X-Request-ID или генерация из crypto/randmiddleware
прокидкаbase.With(request_id) и context.WithValue под typed-ключомmiddleware
слоиslog.FromContext(ctx) и записи без упоминания поляобработчик, сервис, репозиторий
фоновые задачиcontext.WithoutCancel плюс потомок с task_idкод, запускающий задачу
ответзаголовок X-Request-ID и код обращения в ошибкеmiddleware и обработчик
разборфильтр по request_id склеивает историю вызовадежурный и поддержка

Граница с трейсингом заслуживает отдельного абзаца. Request_id — локальная склейка: он живёт внутри вашего сервиса и его коллектора, и этого хватает, пока сервис один или их два. Trace_id из трейсера глобальнее: он связывает десятки сервисов, спаны и очереди, и его прокидка через заголовки — работа инструментации, а не вашего middleware. Практический порядок такой: request_id ставят с первого дня, потому что он бесплатен; когда система растёт до связки сервисов, trace_id приходит из трейсера и по совместительству вытесняет request_id из части дашбордов — двадцать второй урок разбирает эту связку. Переезд делается без драм: оба поля могут сосуществовать в одной записи, каждое отвечает своему фильтру. Имена полей в любом случае фиксируются заранее: коллектор семнадцатого урока ждёт request_id и user_id ровно с такими именами, и переименование поля потом превращается в миграцию схем.

Правила request id#

  • Заголовок X-Request-ID от балансира переиспользуют: история склеивается сквозь инфраструктуру.
  • Свой идентификатор — crypto/rand плюс hex: без внешних зависимостей и без метки времени в роли основы.
  • Поле request_id добавляет ровно middleware: повторный With даёт дубли в каждой записи.
  • Слои достают логгер через slog.FromContext: без middleware вернётся дефолтный, и это не поломка.
  • Фоновая работа уходит с context.WithoutCancel и своим task_id, а не с потерянным Background.
  • Идентификатор отдаётся в ответе и в тексте ошибки: код обращения экономит час расспросов.

Связи с другими уроками: восьмой урок про With показывает механику потомков и ловушку дублей, семнадцатый — схему полей для Loki и ELK, девятнадцатый — границы того, что пишут рядом с пользователем. Связку с трейсером разбирает двадцать второй урок, тесты middleware с захватом вывода — двадцатый. Со стороны фреймворков ту же задачу решают middleware gin, а механику контекста раскладывает урок про context курса по продвинутому Go. Карта курса — на странице курса.

Проверка: внешний заголовок

Балансир присылает заголовок X-Request-ID. Что с ним делает правильная middleware?

Проверка: фоновая горутина

Обработчик запустил go func с context.Background() внутри. Что окажется в записях этой горутины?

Какие поля в записи обработчика?

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

// middleware положил логгер с полем request_id в контекст
func handler(w http.ResponseWriter, r *http.Request) {
        logger := slog.FromContext(r.Context())
        logger.Info("заказ принят", "order_id", "o-17")
}

Соберите middleware и слой

Допишите: чтение заголовка, заполнение буфера, укладку логгера в контекст и достаёт-в-слое.

id := r..Get("X-Request-ID") if id == "" { b := make([]byte, 8) rand.(b) id = hex.EncodeToString(b) } logger := base.With("request_id", id) ctx := context.(r.Context(), loggerKey{}, logger) w.Header().Set("X-Request-ID", id) next.ServeHTTP(w, r.WithContext(ctx)) // в сервисном слое: l := slog.(ctx) l.Info("заказ принят", "order_id", orderID)

Middleware по строкам

Расставьте цепочку по порядку: чтение заголовка, генерация, логгер, контекст, ответ, передача дальше.

Строки

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

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

Карточки: request id

Термин

два источника ID

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

Заголовок X-Request-ID от балансира переиспользуют; без него — crypto/rand и hex без зависимостей.

1 / 6

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

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

Вывод

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

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

  • Идентификатор приходит в заголовке балансира или рождается из crypto/rand — и в обоих случаях склеивает историю.
  • Middleware добавляет request_id один раз: потомок с полями едет в контексте под типом-ключом.
  • Слои достают логгер slog.FromContext: записи несут запрос без единого упоминания поля в коде.
  • Фоновая работа уходит с context.WithoutCancel и task_id, иначе записи теряют request_id.
  • Код обращения в ответе и в ошибке даёт поддержке прямой ключ к истории вызова.
  • Request_id закрывает локальную склейку; между сервисами работает trace_id из трейсера.

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

Соберите локально middleware из урока и повесьте на два маршрута: проверьте, что записи одного вызова склеиваются фильтром по request_id, а заголовок ответа содержит идентификатор. Затем добавьте аутентификационную middleware с полем user_id поверх request-id и убедитесь, что обе записи несут оба поля без дублей.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Сквозные поля: request ID через слои)

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

Можно ли доверять заголовку X-Request-ID от клиента?

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

Чем request_id отличается от trace_id?

Request_id — локальный идентификатор вашего сервиса: он склеивает записи вызова в одном коллекторе. Trace_id приходит из трейсера и связывает вызов между десятками сервисов и спанами. Практика: request_id с первого дня, trace_id — когда сервисы связываются в цепочку.

Как не потерять request_id в фоновых задачах?

Уходите из запроса с context.WithoutCancel — значения контекста, включая логгер, сохраняются при снятой отмене. Задаче с собственным жизненным циклом добавляют task_id через With, и в логе видны и породивший запрос, и судьба задачи.

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

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

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

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