Ответ
В распределенных системах (микросервисах) сквозной trace ID (идентификатор трассировки) используется для отслеживания полного пути запроса через несколько сервисов. Его добавление в логи происходит в три этапа: генерация, проброс и инъекция.
1. Генерация и получение
trace ID генерируется на входе в систему (например, в API Gateway или первом сервисе), если он отсутствует во входящем запросе. Если запрос уже содержит trace ID (например, в HTTP-заголовке X-Request-ID или traceparent), он извлекается оттуда.
2. Проброс через контекст
После получения trace ID его необходимо сделать доступным на протяжении всего жизненного цикла обработки запроса внутри сервиса. Для этого используются контекстно-локальные хранилища:
contextvarsв Python: Идеально подходит для асинхронных приложений (asyncio), так как корректно работает с конкурентными задачами.threading.local: Используется в многопоточных синхронных приложениях.
Пример с contextvars и Middleware в FastAPI:
import logging
from contextvars import ContextVar
from uuid import uuid4
from fastapi import Request
# Создаем контекстную переменную
trace_id_var: ContextVar[str] = ContextVar('trace_id', default=None)
async def add_trace_id_middleware(request: Request, call_next):
# Получаем или генерируем trace_id
trace_id = request.headers.get("X-Request-ID", str(uuid4()))
# Устанавливаем значение в контекст
token = trace_id_var.set(trace_id)
response = await call_next(request)
# Сбрасываем контекст после завершения запроса
trace_id_var.reset(token)
return response
3. Инъекция в логи
На этапе логирования trace ID извлекается из контекста и добавляется в каждую запись лога. Это делается с помощью кастомных фильтров или форматтеров.
Пример фильтра для logging:
class TraceIdFilter(logging.Filter):
def filter(self, record):
# Извлекаем trace_id из contextvars и добавляем в запись лога
record.trace_id = trace_id_var.get()
return True
# Настройка логгера
logger = logging.getLogger(__name__)
logger.addFilter(TraceIdFilter())
# Настройка форматтера для вывода trace_id
formatter = logging.Formatter("%(asctime)s - [%(levelname)s] - [trace_id=%(trace_id)s] - %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# При вызове logger.info() trace_id будет добавлен автоматически
logger.warning("User authentication failed")
Результат в логах:
2023-10-27 15:30:00,123 - [WARNING] - [trace_id=a1b2c3d4-e5f6-7890-1234-567890abcdef] - User authentication failed
Инструменты для observability, такие как OpenTelemetry, автоматизируют все эти шаги, включая передачу trace ID в дочерние HTTP-запросы.
Ответ 18+ 🔞
Да ты посмотри, какая хуйня творится в этих ваших распределённых системах! Микросервисы, блядь, разбежались как тараканы, и теперь надо понять, где запрос сдох. А для этого нужен сквозной trace ID — это как бирочка на ухе у скотины, только для запроса.
Вот как эта магия работает, на три простых, блядь, этапа: родить, протащить и впихнуть в логи.
1. Родить или найти
Когда запрос вваливается в систему, первым делом смотрим — а не принёс ли он уже свою бирочку? Ищем в заголовках, типа X-Request-ID. Нету? Ну тогда, ёпта, сами штампуем, новый уникальный, чтоб ни с чем не спутать. Как паспорт, блядь, для запроса.
2. Протащить через контекст, чтоб не потерять
Вот родили мы этот trace_id, а теперь его надо так пришпилить к запросу, чтобы он никуда не делся, пока тот по сервису шастает. Для этого есть специальные штуки — контекстно-локальные хранилища.
contextvarsв Python — это для наших асинхронных красавчиков на asyncio. Работает чётко, даже когда всё летит в разных тасках.threading.local— для старых добрых многопоточных приложений, где каждый поток — свой мир.
Смотри, как в FastAPI через middleware это делается, красиво же:
import logging
from contextvars import ContextVar
from uuid import uuid4
from fastapi import Request
# Вот эта переменная — наш карман, куда мы кладём trace_id для этого конкретного запроса
trace_id_var: ContextVar[str] = ContextVar('trace_id', default=None)
async def add_trace_id_middleware(request: Request, call_next):
# Ищем чужую бирочку или свою делаем
trace_id = request.headers.get("X-Request-ID", str(uuid4()))
# Кладём её в карман (контекст)
token = trace_id_var.set(trace_id)
# Пускаем запрос дальше по трубам
response = await call_next(request)
# После всего вытряхиваем карман, чтоб мусор не копился
trace_id_var.reset(token)
return response
3. Впихнуть в логи, чтобы потом не искать
А теперь самое вкусное — чтобы в каждом сообщении в логах этот trace_id красовался. Для этого делаем фильтр для логгера, который будет как штампик — каждую запись помечать.
class TraceIdFilter(logging.Filter):
def filter(self, record):
# Достаём из кармана наш trace_id и лепим к записи лога
record.trace_id = trace_id_var.get()
return True
# Настраиваем нашего логгера
logger = logging.getLogger(__name__)
logger.addFilter(TraceIdFilter())
# Говорим, как именно писать логи, чтобы trace_id был виден
formatter = logging.Formatter("%(asctime)s - [%(levelname)s] - [trace_id=%(trace_id)s] - %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# И теперь любое сообщение будет с бирочкой!
logger.warning("User authentication failed")
И что в итоге в логах? Красота, блядь!
2023-10-27 15:30:00,123 - [WARNING] - [trace_id=a1b2c3d4-e5f6-7890-1234-567890abcdef] - User authentication failed
Теперь хоть тысяча сервисов — по этому a1b2c3d4... весь путь запроса как на ладони. А вообще, есть же целые инструменты вроде OpenTelemetry, которые эту всю хуйню автоматизируют — и проброс между сервисами, и логирование. Но чтобы понять, как они работают, надо сначала вот это вот всё осознать, а то будешь как Герасим — «Муму» говорить, а нихуя не понимать.