trallnag / prometheus-fastapi-instrumentator

Instrument your FastAPI with Prometheus metrics.
ISC License
948 stars 84 forks source link

FastAPI ^0.110 and on_event #292

Closed alegtk closed 6 months ago

alegtk commented 7 months ago

In the absence of a better place to collaborate (got a 404 on the contributing.md), I'm placing some findings here. We've a system that was using FastAPI v0.85. In the event of a maintenance it was natural to evolve dependencies and got v0.110.0. It looks like FastAPI changed the way it works with middleware since we started getting the message bellow.

Cannot add middleware after an application has started

Doing some research one can see that on_event is deprecated in favour of lifespan. on_event was the place we chose to init the instrumentator. I'm not sure if there is a way to keep this paradigm. The finding is that we can still successfully start the instrumentator outside lifespan as is the first suggested method in the manuals.

Maybe it's worth mentioning this at the readme.

NiclasLindqvist commented 6 months ago

I did it like this, seems to work fine

# Make a lifespan function that can do the .expose() that was previously done in the @app.on_event("startup")
@asynccontextmanager
async def lifespan(app: FastAPI):
    instrumentator.expose(app)
    yield

app = FastAPI(
    ...
    lifespan=lifespan,  # include the lifespan func in the FastAPI init call
)
instrumentator = Instrumentator().instrument(app)  # Initialize the instrumentator
alegtk commented 6 months ago

@NiclasLindqvist I confirm your assertion! Thank you for clearing my mistake. Now I see what was the difference from our previous code to that on the manuals that is equivalent to your sample. One should instantiate the Instrumentator outside the lifespan coroutine. We were doing it all at once, instantianting and pipelining the expose method. Maybe there is a drawback for some because the instrumentator object must be on the same scope as the coroutine. Perhaps one can solve this using FastAPI inherited state attribute as in

app = FastAPI(lifespan=...)
app.state.instr = Instrumentator(
        excluded_handlers=["/readiness", "/liveness", "/metrics"],
    ).instrument(app)
exhuma commented 1 month ago

I came across the same issue today. I had to use a different solution because i am using an app-factory and came up with the following, which keeps the global namespace clean:

from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

def create_lifespan_handler(instrumentator: Instrumentator):
    """
    Create a FastAPI lifespan handler for the application

    @param instrumentator: A prometheus instrumentator used to expose metrics
    """
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        instrumentator.expose(app)
        yield

    return lifespan

def create_app():
    instrumentator = Instrumentator()
    app = FastAPI(
        ...,
        lifespan=create_lifespan_handler(instrumentator),
    )
    instrumentator.expose(app)
    return app