From 4aaa28245b966c43ccbee2babfbd7d8d54fa86ad Mon Sep 17 00:00:00 2001 From: Tim Weber Date: Fri, 5 May 2023 08:14:54 +0000 Subject: [PATCH] =?UTF-8?q?Update=20FastAPI=200.87.0=20=E2=86=92=200.95.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the process, replace prometheus_fastapi_instrumentator with starlette_exporter, mainly because of . --- dearmep/main.py | 19 +++++++++------ poetry.lock | 54 +++++++++++++++++++++---------------------- pyproject.toml | 4 ++-- tests/test_metrics.py | 36 ++++++++--------------------- 4 files changed, 51 insertions(+), 62 deletions(-) diff --git a/dearmep/main.py b/dearmep/main.py index 4e12f9d9..9b0f3a77 100644 --- a/dearmep/main.py +++ b/dearmep/main.py @@ -1,8 +1,11 @@ import logging from typing import Optional + from fastapi import FastAPI -from prometheus_fastapi_instrumentator import Instrumentator from pydantic import ValidationError +from starlette_exporter import PrometheusMiddleware, handle_metrics +from starlette_exporter.optional_metrics import request_body_size, \ + response_body_size from yaml.parser import ParserError from . import __version__, static_files @@ -49,13 +52,15 @@ def create_app(config_dict: Optional[dict] = None) -> FastAPI: version=__version__, ) - @app.on_event("startup") - def prometheus_instrument(): - Instrumentator( - should_group_status_codes=False, - ).instrument(app).expose(app) - app.include_router(api_v1.router, prefix="/api/v1") static_files.mount_if_configured(app, "/static") + app.add_middleware( + PrometheusMiddleware, + app_name=APP_NAME, + group_paths=True, + optional_metrics=[request_body_size, response_body_size], + ) + app.add_route("/metrics", handle_metrics) + return app diff --git a/poetry.lock b/poetry.lock index ac292da1..43c4d591 100644 --- a/poetry.lock +++ b/poetry.lock @@ -201,25 +201,25 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.87.0" +version = "0.95.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.87.0-py3-none-any.whl", hash = "sha256:254453a2e22f64e2a1b4e1d8baf67d239e55b6c8165c079d25746a5220c81bb4"}, - {file = "fastapi-0.87.0.tar.gz", hash = "sha256:07032e53df9a57165047b4f38731c38bdcc3be5493220471015e2b4b51b486a4"}, + {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, + {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, ] [package.dependencies] pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.21.0" +starlette = ">=0.26.1,<0.27.0" [package.extras] all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.114)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.7.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "coverage[toml] (>=6.5.0,<7.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.114)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "flake8" @@ -715,22 +715,6 @@ files = [ [package.extras] twisted = ["twisted"] -[[package]] -name = "prometheus-fastapi-instrumentator" -version = "5.9.1" -description = "Instrument your FastAPI with Prometheus metrics" -category = "main" -optional = false -python-versions = ">=3.7.0,<4.0.0" -files = [ - {file = "prometheus-fastapi-instrumentator-5.9.1.tar.gz", hash = "sha256:3651a72f73359a28e8afb0d370ebe3774147323ee2285e21236b229ce79172fc"}, - {file = "prometheus_fastapi_instrumentator-5.9.1-py3-none-any.whl", hash = "sha256:b5206ea9aa6975a0b07f3bf7376932b8a1b2983164b5abb04878e75ba336d9ed"}, -] - -[package.dependencies] -fastapi = ">=0.38.1,<1.0.0" -prometheus-client = ">=0.8.0,<1.0.0" - [[package]] name = "py-mmdb-encoder" version = "1.0.4" @@ -1062,14 +1046,14 @@ files = [ [[package]] name = "starlette" -version = "0.21.0" +version = "0.26.1" description = "The little ASGI library that shines." category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "starlette-0.21.0-py3-none-any.whl", hash = "sha256:0efc058261bbcddeca93cad577efd36d0c8a317e44376bcfc0e097a2b3dc24a7"}, - {file = "starlette-0.21.0.tar.gz", hash = "sha256:b1b52305ee8f7cfc48cde383496f7c11ab897cd7112b33d998b1317dc8ef9027"}, + {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, + {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, ] [package.dependencies] @@ -1079,6 +1063,22 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "starlette-exporter" +version = "0.15.1" +description = "Prometheus metrics exporter for Starlette applications." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "starlette_exporter-0.15.1-py3-none-any.whl", hash = "sha256:24eeaef01f05ef973984704427f6e6a93d468f487b8b26ad77548d963affc9fe"}, + {file = "starlette_exporter-0.15.1.tar.gz", hash = "sha256:3bde5d863effb26684210fe016a1ebf2b383efedf21a4b2f28585ec5064f4033"}, +] + +[package.dependencies] +prometheus-client = ">=0.12" +starlette = "*" + [[package]] name = "tomli" version = "2.0.1" @@ -1444,4 +1444,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "9c3f3e2ab095fc2da9e1dc0d86be31719c5156465c686bde774e30fa962a939d" +content-hash = "0b7f3c1ef080eb14c6df49aa34cfb44eec1bc8cb8dd1d50bd3a47a6478471f8a" diff --git a/pyproject.toml b/pyproject.toml index 621cceb5..21588fa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,7 @@ dearmep = "dearmep.cli:run" [tool.poetry.dependencies] python = "^3.8" -fastapi = "^0.87" -prometheus-fastapi-instrumentator = "^5.9.1" +fastapi = "^0.95.1" PyYAML = "^6.0" limits = "^3.3.1" lzip = "^1.2.0" @@ -23,6 +22,7 @@ defusedxml = "^0.7.1" httpx = ">=0.23" uvicorn = {extras = ["standard"], version = "^0.21.1"} python-geoacumen = ">=2023" +starlette-exporter = "^0.15.1" [tool.poetry.group.dev.dependencies] flake8 = "^4.0.1" diff --git a/tests/test_metrics.py b/tests/test_metrics.py index daacfe4b..c0516831 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -4,36 +4,19 @@ from fastapi.testclient import TestClient import pytest -from conftest import fastapi_app_func, fastapi_factory_func +from dearmep.config import APP_NAME -# If we initialize the Prometheus instrumentator more than once, it will create -# duplicate timeseries, which is not allowed. Therefore, we only get to call -# this once. -@pytest.fixture(scope="session") -def prom_client(): - # We can't simply use the fastapi_app fixture because its scope is - # per-function, not per-session. - with fastapi_factory_func() as start: - with fastapi_app_func(start) as app: - client = TestClient(app) - - with client as client_with_events: - # The `with` causes `@app.on_event("startup")` code to run. - # - yield client_with_events - - -def metrics_lines_func(prom_client: TestClient) -> Iterable[str]: - res = prom_client.get("/metrics") +def metrics_lines_func(client: TestClient) -> Iterable[str]: + res = client.get("/metrics") assert res.status_code == status.HTTP_200_OK for line in res.iter_lines(): yield str(line).rstrip("\r\n") @pytest.fixture -def metrics_lines(prom_client: TestClient): - yield list(metrics_lines_func(prom_client)) +def metrics_lines(client: TestClient): + yield list(metrics_lines_func(client)) def test_python_info_in_metrics(metrics_lines: Iterable[str]): @@ -45,14 +28,15 @@ def test_python_info_in_metrics(metrics_lines: Iterable[str]): ] -def test_non_grouped_status_codes(prom_client: TestClient): +def test_non_grouped_status_codes(client: TestClient): # Do a throwaway request in order to have at least one request in the # metrics when doing the actual test. - assert prom_client.get("/metrics").status_code == status.HTTP_200_OK + assert client.get("/metrics").status_code == status.HTTP_200_OK - mark = 'http_requests_total{handler="/metrics",method="GET",status="200"} ' + mark = f'starlette_requests_total{{app_name="{APP_NAME}",method="GET",' \ + + 'path="/metrics",status_code="200"} ' assert [ line - for line in metrics_lines_func(prom_client) + for line in metrics_lines_func(client) if line.startswith(mark) ]