Прямой ответ
Сквозной идентификатор запроса добавляется к каждой записи лога, поэтому все записи одного обращения находятся одним запросом независимо от числа сервисов.
| Шаг | Что выполняется |
|---|---|
| Генерация | Идентификатор создаётся при входе запроса |
| Передача | Идентификатор передаётся в заголовках вызовам |
| Добавление | Идентификатор включается в каждую запись лога |
| Поиск | Записи одного обращения выбираются по идентификатору |
from fastapi import Request
import uuid
@app.middleware("http")
async def correlation(request: Request, call_next):
cid = request.headers.get("X-Correlation-Id") or str(uuid.uuid4())
request.state.cid = cid
response = await call_next(request)
response.headers["X-Correlation-Id"] = cid
return response
Идентификатор, полученный из заголовка, сохраняется, поэтому идентификатор, созданный на границе, сохраняется по всему пути. О распространении контекста — в статье про контекст запроса.
Передача идентификатора между сервисами
Идентификатор передаётся в исходящих вызовах, поэтому цепочка сервисов сохраняет единый признак обращения.
Исходящий вызов. Клиент включает идентификатор в заголовок исходящего запроса, поэтому принимающий сервис продолжает тот же признак.
Сохранение входящего. Идентификатор, полученный в заголовке, сохраняется и используется дальше, поэтому новый признак не создаётся.
Создание при отсутствии. При отсутствии входящего идентификатора создаётся новый, поэтому обращение без признака не остаётся без него.
Практическое следствие: создание нового идентификатора при наличии входящего разрывает цепочку, поэтому часть пути не находится по исходному признаку.
Отдельный аспект — включение идентификатора в запись лога. Идентификатор добавляется к каждой записи, поэтому поиск по нему возвращает все записи обращения.
Три признака некорректной реализации.
Отсутствие передачи в исходящих вызовах. Идентификатор не доходит до следующего сервиса, поэтому цепочка разрывается.
Создание нового при наличии входящего. Цепочка разрывается, поэтому часть пути не находится.
Отсутствие идентификатора в записи лога. Поиск по признаку не возвращает записи обращения.
О структуре записей для поиска — в статье про структуру логов; о связи идентификатора с трейсом — в статье про основы трассировки.
Формат идентификатора и совместимость
Формат идентификатора определяет возможность сопоставления записей и перехода к дереву операций.
Единый формат. Идентификатор одинакового формата во всех сервисах обеспечивает сопоставление записей без приведения.
Совместимость с трейсом. Идентификатор трейса служит тем же признаком обращения, поэтому переход от записи к дереву выполняется по совпадающему значению.
Уникальность. Идентификатор уникален в пределах обращения, поэтому записи разных обращений не смешиваются.
Практическое следствие: разный формат идентификатора в сервисах требует приведения при сопоставлении, поэтому поиск усложняется.
Практика: настройка сквозного идентификатора
1. Генерируйте идентификатор при входе запроса. Признак создаётся на границе, поэтому все последующие записи относятся к нему.
2. Сохраняйте входящий идентификатор. Признак, полученный в заголовке, используется дальше, поэтому цепочка не разрывается.
3. Передавайте идентификатор в исходящих вызовах. Признак включается в заголовок, поэтому следующий сервис продолжает цепочку.
4. Добавляйте идентификатор к каждой записи лога. Поиск по признаку возвращает все записи обращения.
5. Возвращайте идентификатор в ответе. Пользователь получает признак, по которому обращение находится в логах.
6. Используйте единый формат идентификатора. Единый формат упрощает поиск и сопоставление записей.
7. Проверяйте полноту цепочки. Отсутствие идентификатора в части записей указывает на разрыв передачи.
Рекомендации:
- Не создавайте новый идентификатор при наличии входящего. Новый признак разрывает цепочку, поэтому часть пути не находится.
- Учитывайте передачу через промежуточные узлы. Прокси может не передавать заголовок, поэтому признак теряется.
- Сопоставляйте идентификатор с трейсом. Связь признака обращения с идентификатором трейса позволяет перейти от записи к дереву операций.
- Фиксируйте идентификатор в ошибке. Признак в записи ошибки позволяет найти остальные записи обращения.
- Пересматриваем передачу при добавлении сервисов. Новый сервис требует включения признака в заголовки вызовов.
Практический сценарий: записи обращения не собрались
Контекст: часть записей обращения находилась по признаку, а часть отсутствовала, поэтому полная картина не складывалась.
- Диагностика. Проверка исходящих вызовов показала, что идентификатор не включался в заголовок при обращении к одному из сервисов. Записи этого сервиса создавались без признака, поэтому поиск по идентификатору их не возвращал.
- Причина. Передача идентификатора выполнялась не во всех исходящих вызовах, поэтому часть пути оставалась без признака.
- Действия. (а) Добавили передачу идентификатора в заголовки всех исходящих вызовов; (б) проверили, что принимающий сервис сохраняет входящий признак, а не создаёт новый; (в) включили идентификатор в формат записи лога каждого сервиса; (г) проверили поиск по признаку и убедились, что записи всех сервисов возвращаются; (д) добавили возврат идентификатора в ответе, чтобы признак был доступен пользователю.
- Результат. Все записи обращения находятся одним запросом по идентификатору.
Вывод: поиск по идентификатору возвращает записи только тех сервисов, которые получили и сохранили признак. Полнота цепочки обеспечивается передачей идентификатора во всех исходящих вызовах и включением признака в формат записи каждого сервиса.
Практический сценарий: записи обращения не собрались
Контекст: часть записей обращения находилась по признаку, а часть отсутствовала, поэтому полная картина не складывалась.
- Диагностика. Проверка исходящих вызовов показала, что идентификатор не включался в заголовок при обращении к одному из сервисов. Записи этого сервиса создавались без признака, поэтому поиск по идентификатору их не возвращал.
- Причина. Передача идентификатора выполнялась не во всех исходящих вызовах, поэтому часть пути оставалась без признака.
- Действия. (а) Добавили передачу идентификатора в заголовки всех исходящих вызовов; (б) проверили, что принимающий сервис сохраняет входящий признак, а не создаёт новый; (в) включили идентификатор в формат записи лога каждого сервиса; (г) проверили поиск по признаку и убедились, что записи всех сервисов возвращаются; (д) добавили возврат идентификатора в ответе, чтобы признак был доступен пользователю.
- Результат. Все записи обращения находятся одним запросом по идентификатору.
Вывод: поиск по идентификатору возвращает записи только тех сервисов, которые получили и сохранили признак. Полнота цепочки обеспечивается передачей идентификатора во всех исходящих вызовах и включением признака в формат записи каждого сервиса.
FAQ
Зачем нужен сквозной идентификатор? Он связывает записи одного обращения в разных сервисах, поэтому поиск по признаку возвращает все записи независимо от числа сервисов.
Что делать при наличии входящего идентификатора? Сохранять и использовать дальше, поскольку создание нового разрывает цепочку. Новый признак создаётся только при отсутствии входящего.
Как идентификатор передаётся между сервисами? Через заголовок исходящего запроса. Принимающий сервис сохраняет признак и использует дальше.
Как идентификатор связан с трейсом? Идентификатор трейса служит тем же признаком обращения, поэтому связь признака с трейсом позволяет перейти от записи лога к дереву операций.
Источники: OpenTelemetry traces, FastAPI middleware, Python logging — ссылки в шапке.