EchoMiddleTechnical

Как прокидывать request id/correlation id через middleware?

Request ID генерируется middleware (встроенным или своим), кладётся в echo.Context через c.Set и в заголовок ответа X-Request-ID, затем прокидывается в context.Context для использования в сервисах и логере.

Request ID / Correlation ID в Echo

Request ID нужен для сквозной трассировки запроса через все слои: middleware, хендлер, сервис, репозиторий, внешние вызовы. Стандартная схема: middleware генерирует (или берёт из заголовка) ID, кладёт его в echo.Context и в context.Context, а логер достаёт его из контекста при каждой записи.

1. Встроенный middleware RequestID

package main

import (
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()

	e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{
		Generator: func() string {
			return "req-" + echo.MustGetRequestID()
		},
		RequestIDHandler: func(c echo.Context, id string) {
			// Дополнительная обработка после генерации
			c.Set("requestID", id)
		},
		TargetHeader: echo.HeaderXRequestID, // "X-Request-ID"
	}))

	e.Logger.Fatal(e.Start(":8080"))
}

2. Кастомный middleware с прокидыванием в context.Context

package middleware

import (
	"context"

	"github.com/google/uuid"
	"github.com/labstack/echo/v4"
)

type contextKey string

const RequestIDKey contextKey = "requestID"

func RequestID() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// Берём из входящего заголовка или генерируем новый
			id := c.Request().Header.Get(echo.HeaderXRequestID)
			if id == "" {
				id = uuid.New().String()
			}

			// Кладём в echo.Context для хендлеров
			c.Set(string(RequestIDKey), id)

			// Кладём в context.Context для сервисов
			req := c.Request().WithContext(
				context.WithValue(c.Request().Context(), RequestIDKey, id),
			)
			c.SetRequest(req)

			// Прокидываем в ответ
			c.Response().Header().Set(echo.HeaderXRequestID, id)

			return next(c)
		}
	}
}

// Хелпер для извлечения из context.Context
func GetRequestID(ctx context.Context) string {
	if id, ok := ctx.Value(RequestIDKey).(string); ok {
		return id
	}
	return ""
}

3. Интеграция со структурированным логером (zerolog)

package main

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"myapp/middleware"
)

func someService(ctx context.Context) {
	requestID := middleware.GetRequestID(ctx)
	log.Ctx(ctx).Info().
		Str("request_id", requestID).
		Msg("processing request")
}

// Или настроить zerolog так, чтобы request_id автоматически
// добавлялся из контекста через zerolog.Ctx(ctx)
func setupLogger(ctx context.Context, requestID string) context.Context {
	logger := zerolog.Ctx(ctx).With().
		Str("request_id", requestID).
		Logger()
	return logger.WithContext(ctx)
}

4. Прокидывание в исходящие HTTP-запросы

func callExternalAPI(ctx context.Context, url string) (*http.Response, error) {
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	req.Header.Set("X-Request-ID", middleware.GetRequestID(ctx))
	req.Header.Set("X-Correlation-ID", middleware.GetRequestID(ctx))
	return http.DefaultClient.Do(req)
}

Подводные камни

  • Брать ID только из входящего заголовка без генерации запасного — при его отсутствии строка окажется пустой во всех логах.
  • Доверять значению X-Request-ID от клиента без валидации — возможно внедрение произвольных строк в логи (log injection).
  • Класть ID только в echo.Context, но не в context.Context — сервисный слой, не знающий об Echo, не сможет его получить.
  • Не добавлять ID в заголовок ответа — клиент не сможет сопоставить ошибку с конкретным запросом в логах.
  • Использовать math/rand вместо crypto/rand или UUID — коллизии при высокой нагрузке.
  • Не прокидывать ID во внешние HTTP-вызовы — трассировка обрывается на границе сервиса.
  • Регистрировать middleware RequestID после Logger — логер запишет запрос без ID, если стоит раньше в цепочке.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics