Рубрики
Без рубрики

Как отслеживать журналы запроса, используя идентификатор корреляции

Некоторое время назад я наткнулся на интересную проблему. В веб-приложении Python я хотел добавить … Помечено с Python, вентилятор, WebDev.

Некоторое время назад я наткнулся на интересную проблему. В веб-приложении Python я хотел добавить Correlation_id каждому журналу, создаваемому обработчиком запроса. Это позволило бы мне легко захватить все журналы, которые происходят в течение запроса, даже если запросы обрабатываются одновременно:

[480f4c31-3ceb-45be-afda-5676e59cc391 info] RegisterUserCommand handling started
[480f4c31-3ceb-45be-afda-5676e59cc391 info] Processing RegisterUserCommand command with params {'name': 'Alice', ...}
[249ae775-845d-456c-ab71-48776fcad938 info] RegisterUserCommand handling started
[249ae775-845d-456c-ab71-48776fcad938 info] Processing RegisterUserCommand command with params {'name': 'Bob', ...}
[249ae775-845d-456c-ab71-48776fcad938 info] RegisterUserCommand handling completed
[480f4c31-3ceb-45be-afda-5676e59cc391 warn] RegisterUserCommand handling failed

Как вы можете видеть, без Correlation_id Невозможно сказать из журналов, какую команду не удалось.

Так как мы можем иметь дело с этой проблемой? Это звучит легко в начале. Мы могли бы просто создать регистрацию регистратора, когда начинается обработка маршрута, легкий кресть:

import uuid

class Logger:
  """Not a real logger, but it is enough for the purpose of this post"""
  def __init__(self, correlation_id):
    self.correlation_id = correlation_id

  def info(self, *args, **kwargs):
    print(f'[{self.correlation_id} info]', *args, **kwargs)

@app.post("/register")
def register(payload):
    logger = Logger(uuid.uuid4())
    logger.info("Registering user", payload)
    ...
    return {"result": "OK"}

Первая проблема С вышеуказанным кодом вы должны добавить регистратор (uuid.uuid4 ()) каждому обработчику маршрута. Мы можем решить эту проблему, впрыскивая регистратор в функцию обработчика. Как вы это делаете, зависит от структуры, но давайте использовать функцию Create_Logger заводскую функцию.

def create_logger(): 
  logger = Logger(uuid.uuid4())

@app.post("/register")
def register(payload, logger = create_logger()):
    logger.info("Registering user", payload)
    return {"result": "OK"}

В результате с каждым запросом будет создан новый экземпляр регистратора (содержащий уникальный идентификатор). Давайте перейдем к дальше и добавим больше зависимостей ( Базызенгин , userservice ) на наш обработчик:

class DatabaseEngine:
  def __init__(self, logger:Logger):
    self.logger = logger   
    ...
  def connect(self):
    ...
  def execute(self, query):
    self.logger.info('Executing DB query...')
    ...

class UserService:
  def __init__(self, logger:Logger, db_engine:DatabaseEngine):
    self.logger = logger
    self.db = db_engine
  def register(self, payload):
    self.logger.info('RegisterUserCommand handling started for', payload)
    self.db.execute(...)
    time.sleep(1)
    self.logger.info('RegisterUserCommand handling completed for', payload)

def create_user_service():
  logger = Logger(uuid.uuid4())
  db_engine = DatabaseEngine(logger)
  db_engine.connect()
  user_service = UserService(logger=logger, db_engine=db_engine)
  return user_service

@app.post("/register")
def register(payload, service:UserService = create_user_service()):
    logger = service.logger # ugh!
    logger.info("Registering user", payload)
    result = service.register(payload)
    return {"result": result}

Вот Вторая проблема Отказ Поскольку регистратор является зависимостью для обоих Пользовательское обслуживание и Базызенгин Мы должны сначала создать регистратор. Более того, поскольку мы создали регистрацию регистратора с каждым запросом, каждый другой объект, который зависит от регистратора, должен быть создан также с каждым запросом. Так что имея глобальный db_engine Объект не является опцией, если мы не откажемся от регистратора за запрос в пользу глобального регистратора, которое мы не хотим делать. Расположенный целый график зависимости для приложения на любой запрос только из-за регистратора за запрос, кажется, похоже на излишки с точки зрения эффективности и управления ресурсами. Если вы думаете, что «Там должен быть лучший способ» ты прав. Давайте рассмотрим наши варианты.

Глобальный журнал

Почему нам все равно нужна регистратор за запрос? Официальный вариант поваренной книги Python говорит:

Хотя может быть заманчиво создать Логин Экземпляры на основе на связи, это не хорошая идея, потому что эти экземпляры не собраны мусором [источник]

Если мы сделаем Correlation_id Глобальная переменная, то регистратор может быть глобальным объектом (или Singleton, если вы предпочитаете), ссылаясь на эту глобальную переменную. В результате оба db_engine и Служем Может использовать глобальный регистратор, что означает, что весь график зависимости может быть сконструирован сразу, на запуск приложения.

correlation_id = None

class Logger:
  def info(self, *args, **kwargs):
    print(f'[{correlation_id} info]', *args, **kwargs)

# application startup
logger = Logger()
db_engine = DatabaseEngine(logger)
db_engine.connect()

def create_user_service():
  user_service = UserService(logger=logger, db_engine=db_engine)
  return user_service

@app.post("/register")
def register(payload, service:UserService = create_user_service()):
    global correlation_id
    correlation_id = uuid.uuid4() # we need to generate new uuid for each request
    logger.info("Registering user", payload)
    result = service.register(payload)
    return {"result": result}

Этот подход наивен, и он будет работать, только если запросы обрабатываются последовательно, один за другим. Нет многопоточье, нет параллелизма здесь Отказ Если тема-2 изменения Correlation_id , в то время как Thread-1 все еще обрабатывает свой собственный запрос, журналы будут повреждены. Давайте моделируем это поведение:

threads = [
           threading.Thread(target=register, args=(payload,)) 
           for payload in ['Alice', 'Bob']
          ]

for thread in threads:
  thread.start()

for thread in threads:
  thread.join()

Как и ожидалось, журналы полностью запутаны:

[e2931dc9-ca83-4d79-9cfc-23aba03d264a info] Registering user Alice
[e2931dc9-ca83-4d79-9cfc-23aba03d264a info] RegisterUserCommand handling started for Alice
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] Registering user Bob
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] RegisterUserCommand handling started for Bob
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] Executing DB query...
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] Executing DB query...
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] RegisterUserCommand handling completed for Bob
[605e6790-387a-4fbe-ba08-874b57f8e0d6 info] RegisterUserCommand handling completed for Alice

Контекст резьбы на спасение

Если мы будем обработать входящие запросы с использованием потоков, мы могли бы использовать Python’s Threading.local () хранить информацию, специфичной для резьбы информации, Correlation_id в нашем случае.

class ContextLogger:
  """
  This time we have a logger which is using threading.local()
  """
  def __init__(self, thread_context):
    self.thread_context = thread_context

  def info(self, *args, **kwargs):
    print(f'[{self.thread_context.correlation_id} info]', *args, **kwargs)


request_context = threading.local()
logger = ContextLogger(request_context)

...

@app.post("/register")
def register(payload, service:UserService = create_user_service()):
    request_context.correlation_id = uuid.uuid4() # we store new uuid per thread
    ...

Переменные контекста

Мы можем использовать концептуально похожий подход при работе с COROUTINES. Если наша структура поддерживает параллельные запросы, работающие в одном потоке через async / ждать Звонки, нам нужно использовать Contextvars вместо Threading.local Отказ Это связано с контуром события, работающего в одном потоке. Вот обновленная версия регистратора:

class ConcurrentContextLogger:
  def __init__(self, correlation_id: ContextVar):
    self.correlation_id = correlation_id

  def info(self, *args, **kwargs):
    print(f'[{self.correlation_id.get()} info]', *args, **kwargs)

correlation_id: ContextVar[uuid.UUID] = ContextVar('correlation_id', default=uuid.UUID('00000000-0000-0000-0000-000000000000'))
logger = ConcurrentContextLogger(correlation_id)

...

@app.post("/register")
async def register(payload, service:UserService = create_user_service()):
    correlation_id.set(uuid.uuid4()) # we store new uuid per coroutine
    ...

Бонус

До сих пор мы использовали фиктивный регистратор для имитирования функциональности реального регистратора. Это то, как мы можем добавить корреляцию_id к экземпляру .Logger .Logger:

class ContextFilter(logging.Filter):
    def filter(self, record):
        record.correlation_id = correlation_id.get()
        return True  


logging.basicConfig(
  level=logging.DEBUG,
  format='%(asctime)-15s %(name)-5s %(levelname)-8s %(correlation_id)s %(message)s'
)

context_filter = ContextFilter()
logger = logging.getLogger('app')
logger.addFilter(context_filter)

logger.info('Hello World!')

Демонстрация

Если вы хотите поиграть с кодом, я подготовил rem для вас: https://replit.com/@pgorecki/request-logger?v=1

Получайте веселую регистрацию!

Эта статья была впервые опубликована в DDD в Python .

Оригинал: “https://dev.to/pgorecki/how-trace-request-logs-using-correlation-id-280h”