Внедрение correlation ID через ASGI middleware необходимо для сквозного отслеживания запросов в распределенных системах. Это уникальный идентификатор, который связывает все события (логи, метрики, трейсы) одного пользовательского действия от момента поступления HTTP-запроса до завершения обработки и записи логов. Без него анализ ошибок в микросервисной архитектуре или при работе с асинхронными задачами становится невозможным, так как невозможно определить, к какому именно клиентскому запросу относится конкретная запись в логе.

Принцип работы и жизненный цикл идентификатора

Жизненный цикл correlation ID начинается на граничном роутере или балансировщике нагрузки (например, Nginx). Если заголовок X-Correlation-ID отсутствует в входящем запросе, система генерирует новый уникальный ID. Если он присутствует, его значение принимается как есть, чтобы продолжить цепочку отслеживания из внешнего источника (например, из мобильного приложения или другого микросервиса).

Этот ID сохраняется в контексте асинхронной задачи. В Python это часто реализуется через объект `Scope` в ASGI или глобальный словарь/контекстный менеджер. Все последующие вызовы функций (обработчики, сервисы, базы данных) должны иметь доступ к этому ID.

При формировании ответа сервер добавляет заголовок X-Correlation-ID с тем же значением, что и при поступлении запроса. Это позволяет клиенту или логированию на стороне клиента связать ответ с началом процесса.

Генерация уникального идентификатора

Для генерации используется стандартная библиотека `uuid` из модуля стандартной библиотеки Python. Рекомендуется использовать версию UUID v4, так как она обеспечивает высокую степень случайности и вероятность коллизий ничтожно мала.

Пример кода для генерации:

«`python
import uuid

def generate_id():
return str(uuid.uuid4())
«`

Строка должна быть преобразована в обычный текст (без дефисов или с ними, в зависимости от требований системы), но обычно используется формат с дефисами для удобства чтения и копирования.

Реализация middleware на ASGI

В отличие от WSGI, где часто используют `BaseHTTPMiddleware`, в чистом ASGI подход может быть более гибким. Однако для совместимости и простоты часто создают класс, наследуемый от `BaseHTTPMiddleware` (если используется Starlette или FastAPI) или пишут обработчик событий напрямую.

Ниже приведен пример реализации класса middleware на Python с использованием библиотеки `starlette`, который перехватывает запрос, генерирует ID при отсутствии заголовка и добавляет его в ответ.

«`python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import uuid

class CorrelationIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Проверяем наличие заголовка в запросе
correlation_id = request.headers.get('X-Correlation-ID')

# Если заголовка нет, генерируем новый
if not correlation_id:
correlation_id = str(uuid.uuid4())
# Добавляем заголовок к запросу (если нужно передать дальше в логику приложения)
request.headers['X-Correlation-ID'] = correlation_id

# Обрабатываем запрос
response = await call_next(request)

# Добавляем заголовок в ответ, если его там нет
if 'X-Correlation-ID' not in response.headers:
response.headers['X-Correlation-ID'] = correlation_id

return response
«`

В этом примере мы не меняем логику обработки запроса (`call_next`), а только манипулируем заголовками. Это минимальное вмешательство, которое гарантирует сквозную видимость.

Интеграция с логированием и метриками

После внедрения middleware необходимо убедиться, что все компоненты системы используют этот ID в логах. В Python это делается через параметр `extra` в вызове `logger.info` или `logger.error`.

«`python
import logging
logger = logging.getLogger(__name__)

def process_request(request):
correlation_id = request.headers.get('X-Correlation-ID')
logger.info(f"Processing request for user {request.user_id}, correlation ID: {correlation_id}")
«`

При работе с асинхронными задачами (например, через `asyncio.create_task` или очереди Celery/RQ), важно передавать этот ID в контекст задачи. Иначе при ошибке в фоновом процессе вы не сможете связать ошибку с исходным запросом пользователя.

«`python
import asyncio

class CorrelationContext:
def __init__(self, correlation_id):
self.correlation_id = correlation_id

async def background_task(request):
ctx = CorrelationContext(request.headers.get('X-Correlation-ID'))
# Логирование внутри задачи с использованием ID
logger.info(f"Background task started for {ctx.correlation_id}")
«`

Оценка результата и проверка корректности

Чтобы убедиться, что внедрение прошло успешно, выполните следующие проверки:

1. Отправьте запрос без заголовка X-Correlation-ID. В ответе должен появиться новый ID.

2. Отправьте второй запрос с тем же ID (например, через curl или Postman). В ответе он должен сохраниться.

3. Проверьте логи сервера и базы данных: все записи для одного запроса должны содержать один и тот же ID.

4. Убедитесь, что ID не теряется при передаче между микросервисами (если используется шлюз или API Gateway).

Ограничения и ошибки

Не используйте correlation ID как ключ для сессии пользователя. Это только идентификатор запроса, а не токен авторизации.

Избегайте хранения correlation ID в cookie клиента без необходимости, так как это увеличивает размер ответа и может нарушить политики безопасности (если ID содержит чувствительные данные, хотя UUID v4 обычно безопасен).

Если система обрабатывает очень высокий трафик, генерация UUID может создавать нагрузку на CPU. В таких случаях можно использовать простые инкрементальные счетчики или хеши времени + случайного числа, но это снижает вероятность коллизий.

Не передавайте correlation ID в открытом виде в теле ответа (JSON), если он не требуется клиенту для отладки. Обычно достаточно заголовка.

Практические примеры использования

Сценарий 1: Отслеживание ошибки в микросервисе. Клиент делает запрос, сервер вызывает сервис оплаты. Сервис оплаты падает с ошибкой. В логе сервиса оплаты есть correlation ID. По этому ID вы находите соответствующую запись в логах основного приложения и видите полную цепочку событий.

Сценарий 2: Анализ производительности. Вы измеряете время обработки запроса в разных узлах. С помощью correlation ID можно сгруппировать метрики по одному запросу и увидеть, где именно возникла задержка.

Сценарий 3: Отладка распределенных транзакций. Если одна часть транзакции завершилась успешно, а другая нет, correlation ID помогает найти причину сбоя в другой части системы.

Варианты решений для разных стеков

Если вы используете FastAPI, можно добавить middleware прямо в приложение:

«`python
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
import uuid

app = FastAPI()

class CorrelationIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
correlation_id = request.headers.get('X-Correlation-ID') or str(uuid.uuid4())
request.headers['X-Correlation-ID'] = correlation_id
response = await call_next(request)
if 'X-Correlation-ID' not in response.headers:
response.headers['X-Correlation-ID'] = correlation_id
return response

app.add_middleware(CorrelationIdMiddleware)
«`

Для Django (через ASGI) подход аналогичен, но нужно настроить middleware в `settings.py` или создать кастомный класс.

Если используется Nginx перед приложением, можно генерировать ID на уровне прокси-сервера с помощью модуля `lua-resty-uuid` или Lua-скриптов, чтобы не нагружать само приложение.

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

Можно ли использовать correlation ID вместо session ID?

Нет. Correlation ID — это идентификатор запроса, а не сессии пользователя. Session ID хранит данные о пользователе (авторизация, настройки), тогда как correlation ID используется только для отладки и отслеживания потока событий.

Что делать, если заголовок X-Correlation-ID уже есть в запросе?

Его следует использовать без изменения. Это позволяет продолжить цепочку отслеживания из внешнего источника (например, из мобильного приложения или другого микросервиса). Не генерируйте новый ID, если он уже передан.

Нужно ли добавлять correlation ID в тело ответа JSON?

Обычно нет. Достаточно заголовка X-Correlation-ID. Добавление в тело ответа увеличивает размер ответа и может нарушить соглашения о структуре данных API, если это не требуется специфическим клиентом.

Как избежать утечки correlation ID при логировании?

Не включайте correlation ID в логи, которые отправляются третьим лицам или публикуются. Используйте его только внутри системы для связывания событий. Если нужно передать клиенту, делайте это через заголовок ответа, а не через тело сообщения.

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import uuid

class CorrelationIdMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        correlation_id = request.headers.get('X-Correlation-ID')
        if not correlation_id:
            correlation_id = str(uuid.uuid4())
            request.headers['X-Correlation-ID'] = correlation_id
        response = await call_next(request)
        if 'X-Correlation-ID' not in response.headers:
            response.headers['X-Correlation-ID'] = correlation_id
        return response