Вход FastAPI в маршруты в функциях Azure

Я заметил, что любой logging.info('stuff happened in a route') на маршруте не попадал в мой журнал Application Insights. Экземпляры logging.info('stuff happened in entrypoint') были. Я нашел этот небольшой совет, но решение немного расплывчатое и, похоже, специфичное для запуска uvicorn, и я не знаю, как применить его к Azure.

Для справки вот фрагменты моего __init__.py

import logging
import azure.functions as func
from fastapi import FastAPI
from .routes import route1

app = FastAPI()
app.include_router(route1.router)

@app.get("/api/test"):
def test():
    logging.info("entry test")
    return "test"

async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return await func.AsgiMiddleware(app).handle_async(req, context)

Эти записи попадают в мои журналы

Тогда на маршруте 1 у меня есть

from fastapi import APIRouter
import logging

router=APIRouter(prefix = "/api/route1")

@router.get("/test")
def test():
    logging.info("resp route test")
    return "route test"

Сам маршрут работает, то есть, если я перейду к «api/route1/test», я получаю «тест маршрута ответа» в своем браузере, но затем, когда я проверяю журналы, у меня нет записи «тест маршрута».

Я попробовал добавить logging.config.dictConfig({"disable_existing_loggers": False, "version": 1}) в начало файла Route1.py, но это не помогло.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
104
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я внес несколько изменений в ваш код, чтобы получить журнал, а также вывод браузера.

__init__.py

import logging
import azure.functions as func
from fastapi import FastAPI
from .route import route1

app = FastAPI()
app.include_router(route1.router)

@app.get("/api/test")
async def test():
    logging.info('stuff happened in entrypoint')
    logging.info('entry test')
    return "test"

async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    logging.info('Received request from main')
    return await func.AsgiMiddleware(app).handle_async(req, context)

route1.py

from fastapi import APIRouter
import logging

router=APIRouter(prefix = "/api/route1")

@router.get("/test")
async def test():
    logging.info('stuff happened in a route')
    logging.info('resp route test')
    return "route test"

Я могу получить ожидаемый ответ при переходе к /api/route1/test на местном уровне.

Затем я опубликовал его в функциональном приложении.

Ниже приведены результаты в приложении-функции и в аналитике приложения.

Единственное отличие, которое я вижу, — это событие регистрации под def main у вас. В этом и есть разница или я что-то еще упускаю?

Dean MacGregor 28.06.2024 13:13

Добавьте async в def test() как для Route1.py, так и для init.py.

Ikhtesam Afrin 28.06.2024 13:23
Ответ принят как подходящий

Я только что решил эту проблему и обнаружил несколько особенностей входа в приложения Azure Function Apps/Application Insights.

  • Насколько я могу судить, единственный регистратор, который работает в Azure, — это корневой регистратор. Это logging.info() или logger = logging.getLogger(). print() утверждения не работают. Созданные логгеры (customer_logger = logging.getLogger(__name__)) не работают.
  • Я добавил специальную конфигурацию в корневой регистратор, и это сломало его в Azure. (Я не уверен, какая именно часть сломала его... я думаю, это какая-то конфигурация обработки потока. Кроме того, logging.config.dictConfig(logging_config) выдал исключение при локальном запуске с использованием func host start.)
  • По умолчанию логи созданных тредов не работают.
    • @Дин, это объясняет поведение, которое ты видел, где logging.info('stuff happened in entrypoint') сработало, но logging.info('stuff happened in route').
    • Общие сведения о том, как FastAPI обрабатывает запросы: асинхронные конечные точки (async def function_name) запускать в основном потоке в цикле событий. Синхронные конечные точки (def function_name) получают каждый свой собственный нить. Таким образом, если ваш API получает 5 запросов одновременно, они не блокируют друг друга.
    • @Дин, твои синхронные конечные точки не регистрировались, потому что они выполнялись из созданных потоков. Ведение журнала с асинхронных конечных точек должно работать по умолчанию (согласно ответу @Ikhtesam выше), но будьте осторожны, потому что, если ваши асинхронные конечные точки содержат, например. синхронные вызовы базы данных, они заблокируют основной поток, и вы потеряете все преимущества параллельной работы, которые FastAPI предоставляет «из коробки».
    • Приложения-функции должны каким-то образом использовать модуль журналирования, поскольку для регистрации из созданного потока вам необходимо связать этот поток обратно с родительским потоком invocation_id (концепция Azure). Существует объект, который Azure передает с каждым запросом, который называется «контекст». «Контекст» содержит атрибут thread_local_storage. thread_local_storage имеет атрибут invocation_id, и вам нужно установить его из созданного потока. Каким-то образом, как только это установлено, журналы работают.
    • Здесь есть общий пример , который не выглядит слишком знакомым, если вы используете FastAPI...

Чтобы исправить это, вам нужно сначала передать 1) объект Azure контекста и 2) родительский поток invocation_id до вашей конечной точки, а затем из созданного потока/конечной точки вам нужно установить thread_local_storage.invocation_id в родительский invocation_id .

1. Подготовьте конечную точку для получения контекста

Для каждой конечной точки существует неявный параметр, называемый request. Если вы просто определите его как параметр, вы можете использовать его в своей конечной точке и проверять в отладчике.

from fastapi import Request

@app.get("/api/test")
def hello_world(request: Request):
    logging.info("hello world!") # remember print() doesn't work

Внутри этого объекта request есть словарь scope.

2а. AsgiFunctionApp / AsgiMiddleware

Если вы используете azure.functions.AsgiFunctionApp или azure.function.AsgiMiddleware (определенной версии, не совсем точно, но, я думаю, более поздней, чем 1.8), контекст thread_local_storage и родительский поток invocation_id фактически включены в область действия для вас.

В этом случае вы должны сделать следующее в своей синхронной конечной точке (помните, что нет необходимости делать это в конечной точке async):

from fastapi import Request

@app.get("/api/test")
def hello_world(request: Request):
    thread_local_storage = request.scope.get("azure_functions.thread_local_storage")
    if thread_local_storage is not None:
        parent_invocation_id = request.scope.get("azure_functions.invocation_id")
        thread_local_storage.invocation_id = parent_invocation_id
    logging.info("hello world!") # remember print() doesn't work

2б. Пользовательское промежуточное программное обеспечение

Если вы используете собственное промежуточное программное обеспечение, как в этом примере здесь, вам нужно будет добавить родительский элемент invocation_id и контекст (или объект thread_local_storage) в область видимости.

Что-то вроде этого:

Затем вы должны установить его в своей конечной точке следующим образом:

from fastapi import Request

@app.get("/api/test")
def hello_world(request: Request):
    context_obj = request.scope.get("azure_context", {}).get("context_obj")
    if context_obj and hasattr(context_obj, "thread_local_storage"):
        invocation_id = request.scope.get("azure_context", {}).get("invocation_id")
        if invocation_id:
            context_obj.thread_local_storage.invocation_id = request.scope.get("azure_context", {}
            ).get("invocation_id")
    logging.info("hello world!") # remember print() doesn't work

3. (Необязательно) Функция оболочки

Раздражает включать этот код в начало всех ваших функций, поэтому вы можете создать для него функцию-обертку (х/т Евгений Николаев)

from fastapi import Request
from functools import wraps

def logging_context_wrapper(func):
    """Passes invocation_id to the thread local context to enable proper Azure Functions logging.

    Can be applied to a sync handler which has request: Request parameter - to get the context from.
    """

    request_param = next(
        (param for (param, annotation) in func.__annotations__.items() if annotation is Request),
        None,
    )
    if not request_param:
        raise Exception("Function must have a request parameter of type Request.")

    @wraps(func)
    def wrapper(*args, **kwargs):
        request = kwargs[request_param]
        # if you were 2b from above, sub whatever code you used
        thread_local_storage = request.scope.get("azure_functions.thread_local_storage")
        if thread_local_storage is not None:
            thread_local_storage.invocation_id = request.scope.get("azure_functions.invocation_id")
        return func(*args, **kwargs)

    return wrapper



@app.get("/api/test")
@logging_context_wrapper
def hello_world(request: Request):
    logging.info("hello world!") # remember print() doesn't work

Краткое содержание

  • Не используйте print(), просто logging.info(), logging.warning() и т. д.
  • Не добавляйте конфигурацию в logging, по крайней мере, пока она не заработает.
  • Ведение журнала с синхронных конечных точек (def foo, а не async def foo) по умолчанию не работает, поскольку синхронные конечные точки выполняются в отдельных потоках.
  • Для входа в систему с синхронных конечных точек вам необходимо установить контекст thread_local_storage.invocation_id в созданном потоке/конечной точке на контекст invocation_id родительского потока.

Вещи, которые я хотел бы, чтобы кто-то объяснил

  • Как работает ведение журнала Azure и почему не работают операторы print() или созданные средства ведения журнала?
  • Как управляется пул потоков в приложениях-функциях? Я могу установить переменную среды под названием PYTHON_THREADPOOL_THREAD_COUNT. Определяет ли это, сколько потоков FastAPI ему доступно?

Другие вопросы по теме

Похожие вопросы

Невозможно найти версию dotnet при развертывании приложения функции гибкого потребления с помощью действий GitHub
Исключение: AttributeError: Anonymous при запуске приложения с базовой функцией
Если контейнер хранилища BLOB-объектов является частным, нужно ли функциям Azure использовать токены sas для доступа?
Формат строки инициализации не соответствует спецификации, начиная с индекса 0 в приложении «Функции Azure»
Как создать zip-архив из нескольких файлов BLOB-объектов Azure?
Устойчивые функции Azure v4. Объект запроса триггера HTTP, отличный от версии 3
Некоторые журналы функций Azure отсутствуют в Application Insights
Функции Azure не принимают значение в кодировке URL-адреса в качестве параметра запроса?
Azure SignalR: ни один из транспортных средств, поддерживаемых клиентом, не поддерживается сервером
Функции Azure + .NET8 (изолированный режим) + OIDC — как проверить токен доступа, полученный в заголовке каждого запроса к моей функции Azure?