spec-first / connexion

Connexion is a modern Python web framework that makes spec-first and api-first development easy.
https://connexion.readthedocs.io/en/latest/
Apache License 2.0
4.47k stars 760 forks source link

How to add Prometheus client /metrics endpoint to Connexion 3.x AsyncApp? #1866

Open mehdihasan opened 7 months ago

mehdihasan commented 7 months ago

Description

While upgrading connexion from 2.x to 3.x, we found the previous ConnexionPrometheusMetrics from prometheus_flask_exporter is not working. I have not found any way to integrate prometheus_flask_exporter with the new AsyncApp. After some search, we found aioprometheus probably the library to use. Now, we want to integrate aioprometheus to our application.

Expected behaviour

Publishing application metrics from /metrics endpoint.

Actual behaviour

500 Internal Server Error

Steps to reproduce

We have tried to add aioprometheus MetricsMiddleware in our app as following,

middlewares = [middleware for middleware in ConnexionMiddleware.default_middlewares]
middlewares.insert(MetricsMiddleware)

However it is failing with the following message:

Traceback (most recent call last):
  File ".venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File ".venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
  File ".venv/lib/python3.10/site-packages/connexion/middleware/main.py", line 501, in __call__
    await self.app(scope, receive, send)
  File ".venv/lib/python3.10/site-packages/aioprometheus/asgi/middleware.py", line 175, in __call__
    path = self.get_full_or_template_path(scope)
  File ".venv/lib/python3.10/site-packages/aioprometheus/asgi/middleware.py", line 216, in get_full_or_template_path
    for route in self.starlette_app.routes:
AttributeError: 'ConnexionMiddleware' object has no attribute 'routes'
INFO:     127.0.0.1:40838 - "GET /v1/health HTTP/1.1" 500 Internal Server Error

Additional info:

Output of the commands:

RobbeSneyders commented 7 months ago

aioprometheus seems to assume the app that is stored in the state is a starlette app, while this is not the case in Connexion.

CC: @claws

I think you can work around this by setting use_template_urls to False though.

app = Connexion.App(__name__)
app.add_middleware(MetricsMiddleware, use_template_urls=False)
mehdihasan commented 7 months ago

Thanks @RobbeSneyders for your reply. Your suggestion worked - the app is running but metrics is not working yet. I am getting some other errors regarding app/object state.

BTW, what metrics solution do you recommened to integrate with Connexion AsyncApp?

chekolyn commented 6 months ago

@mehdihasan the app/object state error is coming from: https://github.com/claws/aioprometheus/blob/master/src/aioprometheus/asgi/starlette.py#L16

I got it working with something like this:

application = connexion.AsyncApp(__name__, specification_dir="../openapi/")

from starlette.requests import Request
from starlette.responses import Response

from aioprometheus import REGISTRY, render

async def metrics(request: Request) -> Response:
    """Render metrics into format specified by 'accept' header.

    This function first attempts to retrieve the metrics Registry from
    ``request.app.state`` in case the app is using a custom registry instead
    of the default registry. If this fails then the default registry is used.
    """
    registry = (
        request.app.state.registry
        if hasattr(request, "app") and hasattr(request.app, "state") and hasattr(request.app.state, "registry")
        else REGISTRY
    )
    content, http_headers = render(registry, request.headers.getlist("Accept"))
    return Response(content=content, media_type=http_headers["Content-Type"])

from aioprometheus import Counter, MetricsMiddleware
application.add_middleware(MetricsMiddleware, use_template_urls=False, exclude_paths=())
application.add_url_rule("/metrics", "metrics", metrics)