diff --git a/README.md b/README.md index 12cf911f..02d97ad0 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,13 @@ pre-commit run --all-files # Database management -The deployed service uses a Postgres database on AWS RDS. -In order to generate migrations for the database locally, +The deployed service uses a Postgres database on AWS RDS (provisioned automatically by the +infrastructure stack). In order to generate migrations for the database locally, we use a Postgres docker container to generate migrations against. +At runtime the task receives database connection details through environment variables +(`DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`) sourced from AWS Secrets Manager. + After making any changes to the database models, run the `generate_migrations.py` script to create migrations: diff --git a/main.py b/main.py index 064fcb46..fc513e32 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import os from contextlib import asynccontextmanager from dotenv import dotenv_values @@ -50,6 +51,13 @@ async def lifespan(app: FastAPI): allow_headers=["*"], ) +metrics_enabled = os.getenv("ENABLE_PROMETHEUS_METRICS", "").lower() in {"1", "true", "yes"} + +if metrics_enabled: + from prometheus_fastapi_instrumentator import Instrumentator + + Instrumentator().instrument(app).expose(app, include_in_schema=False) + @app.get("/") def public_route(): diff --git a/pyproject.toml b/pyproject.toml index 64c03a26..8b70bd34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "authlib>=1.6.1", "sqladmin>=0.21.0", "itsdangerous>=2.2.0", + "prometheus-fastapi-instrumentator>=6.0.0", "apscheduler[redis,sqlalchemy]~=3.11", "loguru>=0.7.3", ] diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 00000000..5c043938 --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,36 @@ +import importlib + +from fastapi.testclient import TestClient + +import main as main_module +from auth.management import get_management_token +from config import get_settings +from galaxy.config import get_galaxy_settings + + +def test_metrics_endpoint_disabled(test_client): + response = test_client.get("/metrics") + assert response.status_code == 404 + + +def test_metrics_endpoint_enabled(monkeypatch, mock_settings, mock_galaxy_settings): + monkeypatch.setenv("ENABLE_PROMETHEUS_METRICS", "1") + instrumented_main = importlib.reload(main_module) + app = instrumented_main.app + + app.dependency_overrides[get_settings] = lambda: mock_settings + app.dependency_overrides[get_galaxy_settings] = lambda: mock_galaxy_settings + app.dependency_overrides[get_management_token] = lambda: "mock_token" + + monkeypatch.setattr("db.admin.DatabaseAdmin.setup", lambda *args, **kwargs: None) + + try: + with TestClient(app) as client: + response = client.get("/metrics") + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/plain") + assert "http_requests_total" in response.text + finally: + app.dependency_overrides.clear() + monkeypatch.delenv("ENABLE_PROMETHEUS_METRICS", raising=False) + importlib.reload(main_module) diff --git a/uv.lock b/uv.lock index 1a57c393..ab9cb96e 100644 --- a/uv.lock +++ b/uv.lock @@ -14,6 +14,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "httpx" }, { name = "itsdangerous" }, + { name = "prometheus-fastapi-instrumentator" }, { name = "loguru" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic-settings" }, @@ -52,6 +53,7 @@ requires-dist = [ { name = "moto", marker = "extra == 'dev'", specifier = ">=5.0.5" }, { name = "polyfactory", marker = "extra == 'dev'", specifier = ">=2.21.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7.0" }, + { name = "prometheus-fastapi-instrumentator", specifier = ">=6.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "pydantic-settings", specifier = ">=2.8.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, @@ -731,6 +733,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, +] + [[package]] name = "psycopg" version = "3.2.9"