From b6ae09732c4fdeb40e7efe3a7380414051730263 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 14:01:33 +0200
Subject: [PATCH 01/18] Logging but request id context is not bound properly
---
src/database/datasets.py | 2 ++
src/main.py | 42 +++++++++++++++++++++++++++++++++++++++-
2 files changed, 43 insertions(+), 1 deletion(-)
diff --git a/src/database/datasets.py b/src/database/datasets.py
index 26eb33d..27f1f4e 100644
--- a/src/database/datasets.py
+++ b/src/database/datasets.py
@@ -3,6 +3,7 @@
import datetime
from collections import defaultdict
+from loguru import logger
from sqlalchemy import text
from sqlalchemy.engine import Row
from sqlalchemy.ext.asyncio import AsyncConnection
@@ -11,6 +12,7 @@
async def get(id_: int, connection: AsyncConnection) -> Row | None:
+ logger.info("Fetching dataset from database", dataset_id=id_)
row = await connection.execute(
text(
"""
diff --git a/src/main.py b/src/main.py
index 8ffecd0..1c2c341 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,10 +1,14 @@
import argparse
import logging
-from collections.abc import AsyncGenerator
+import uuid
+from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
+from loguru import logger
+from starlette.requests import Request
+from starlette.responses import Response
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
@@ -55,9 +59,45 @@ def _parse_args() -> argparse.Namespace:
return parser.parse_args()
+async def add_request_context_to_log(
+ request: Request,
+ call_next: Callable[[Request], Awaitable[Response]],
+) -> Response:
+ identifier = uuid.uuid4().hex
+ host = request.client.host if request.client else "unknown host"
+ with logger.contextualize(request_id=identifier, client_ip=host):
+ return await call_next(request)
+
+
+async def request_response_logger(
+ request: Request,
+ call_next: Callable[[Request], Awaitable[Response]],
+) -> Response:
+ logger.info(
+ "request",
+ url=request.url,
+ headers=request.headers,
+ cookies=request.cookies,
+ path_params=request.path_params,
+ query_params=request.query_params,
+ body=await request.body(),
+ )
+ response: Response = await call_next(request)
+ logger.info(
+ "response",
+ status_code=response.status_code,
+ headers=response.headers,
+ media_type=response.media_type,
+ )
+ return response
+
+
def create_api() -> FastAPI:
+ logger.add("log.log", serialize=True)
fastapi_kwargs = load_configuration()["fastapi"]
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
+ app.middleware("http")(add_request_context_to_log)
+ app.middleware("http")(request_response_logger)
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
From 346c97fed53e59e44cfac2598de628236949a556 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 14:11:36 +0200
Subject: [PATCH 02/18] Fix middleware order
---
src/main.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/main.py b/src/main.py
index 1c2c341..5d3999a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -96,8 +96,11 @@ def create_api() -> FastAPI:
logger.add("log.log", serialize=True)
fastapi_kwargs = load_configuration()["fastapi"]
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
- app.middleware("http")(add_request_context_to_log)
+
+ # Order matters! Each added middleware wraps the previous, creating a stack.
+ # See also: https://fastapi.tiangolo.com/tutorial/middleware/#multiple-middleware-execution-order
app.middleware("http")(request_response_logger)
+ app.middleware("http")(add_request_context_to_log)
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
From 186d196385443a132ab1a10512c2e36dc2b6d89d Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 16:26:22 +0200
Subject: [PATCH 03/18] Fully transition to Loguru and make it configurable
---
pyproject.toml | 1 +
src/config.py | 14 ++++++++------
src/config.toml | 15 +++++++++++++++
src/database/datasets.py | 2 --
src/main.py | 15 ++++++++++++---
5 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 54cab3c..596d5b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,6 +14,7 @@ classifiers = [
]
dependencies = [
"fastapi",
+ "loguru",
"cryptography",
"pydantic",
"uvicorn",
diff --git a/src/config.py b/src/config.py
index 4a234a5..0ff7a91 100644
--- a/src/config.py
+++ b/src/config.py
@@ -1,13 +1,11 @@
import functools
-import logging
import os
import tomllib
import typing
from pathlib import Path
from dotenv import load_dotenv
-
-logger = logging.getLogger(__name__)
+from loguru import logger
TomlTable = dict[str, typing.Any]
@@ -27,9 +25,13 @@
_dotenv_file = Path(os.getenv(DOTENV_FILE_ENV, _config_directory / ".env"))
_dotenv_file = _dotenv_file.expanduser().absolute()
-logger.info("Configuration directory is '%s'", _config_directory)
-logger.info("Loading configuration file from '%s'", _config_file)
-logger.info("Loading environment variables from '%s'", _dotenv_file)
+
+logger.info(
+ "Determined configuration sources.",
+ configuration_directory=_config_directory,
+ configuration_file=_config_file,
+ dotenv_file=_dotenv_file,
+)
load_dotenv(dotenv_path=_dotenv_file)
diff --git a/src/config.toml b/src/config.toml
index f819ec9..384067d 100644
--- a/src/config.toml
+++ b/src/config.toml
@@ -4,6 +4,21 @@ minio_base_url="https://openml1.win.tue.nl"
[development]
allow_test_api_keys=true
+# Any number of logging.NAME configurations can be added.
+# NAME is for reference only, it has no meaning otherwise.
+# You can add any arguments to `loguru.logger.add`,
+# the `sink` variable will be used as first positional argument.
+# https://loguru.readthedocs.io/en/stable/api/logger.html
+[logging.develop]
+sink="develop.log"
+# One of loguru levels: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR
+level="DEBUG"
+# Automatically create a new file by date or file size
+rotation="50 MB"
+# Retention specifies the timespan after which automatic cleanup occurs.
+retention="1 day"
+compression="gz"
+
[fastapi]
root_path=""
diff --git a/src/database/datasets.py b/src/database/datasets.py
index 27f1f4e..26eb33d 100644
--- a/src/database/datasets.py
+++ b/src/database/datasets.py
@@ -3,7 +3,6 @@
import datetime
from collections import defaultdict
-from loguru import logger
from sqlalchemy import text
from sqlalchemy.engine import Row
from sqlalchemy.ext.asyncio import AsyncConnection
@@ -12,7 +11,6 @@
async def get(id_: int, connection: AsyncConnection) -> Row | None:
- logger.info("Fetching dataset from database", dataset_id=id_)
row = await connection.execute(
text(
"""
diff --git a/src/main.py b/src/main.py
index 5d3999a..dad7d02 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,5 +1,5 @@
import argparse
-import logging
+import sys
import uuid
from collections.abc import AsyncGenerator, Awaitable, Callable
from contextlib import asynccontextmanager
@@ -12,6 +12,7 @@
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
+from core.logging import setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
@@ -93,10 +94,15 @@ async def request_response_logger(
def create_api() -> FastAPI:
- logger.add("log.log", serialize=True)
+ # Default logging configuration so we have logs during setup
+ setup_sink = logger.add(sys.stderr, serialize=True)
+ setup_log_sinks()
+
fastapi_kwargs = load_configuration()["fastapi"]
+ logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
+ logger.info("Setting up middleware and exception handlers.")
# Order matters! Each added middleware wraps the previous, creating a stack.
# See also: https://fastapi.tiangolo.com/tutorial/middleware/#multiple-middleware-execution-order
app.middleware("http")(request_response_logger)
@@ -104,6 +110,7 @@ def create_api() -> FastAPI:
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
+ logger.info("Adding routers to app")
app.include_router(datasets_router)
app.include_router(qualities_router)
app.include_router(mldcat_ap_router)
@@ -115,11 +122,13 @@ def create_api() -> FastAPI:
app.include_router(study_router)
app.include_router(setup_router)
app.include_router(run_router)
+
+ logger.info("App setup completed.")
+ logger.remove(setup_sink)
return app
def main() -> None:
- logging.basicConfig(level=logging.INFO)
args = _parse_args()
uvicorn.run(
app="main:create_api",
From ee38e330c98fb6bb8f086f52c7f2b3ca0c4c5d8a Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 16:27:51 +0200
Subject: [PATCH 04/18] ignore log files
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index a306cbd..5bcdce7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
docker/mysql/data
+*.log
+logs/
.DS_Store
# Byte-compiled / optimized / DLL files
From 964e55c8db33b514f6656c2e8a259e971ceeb9ff Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 16:32:58 +0200
Subject: [PATCH 05/18] Add function to setup loguru sinks based on
configuration
---
src/core/logging.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
create mode 100644 src/core/logging.py
diff --git a/src/core/logging.py b/src/core/logging.py
new file mode 100644
index 0000000..54d70ae
--- /dev/null
+++ b/src/core/logging.py
@@ -0,0 +1,14 @@
+"""Utility functions for logging."""
+
+from loguru import logger
+
+from config import load_configuration
+
+
+def setup_log_sinks() -> None:
+ """Configure loguru based on app configuration."""
+ configuration = load_configuration()
+ for nickname, sink_configuration in configuration.get("logging", {}).items():
+ logger.info("Configuring sink", nickname=nickname, **sink_configuration)
+ sink = sink_configuration.pop("sink")
+ logger.add(sink, serialize=True, **sink_configuration)
From 48a73efc690f50707faaffccb09fc04041664edd Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 16:37:58 +0200
Subject: [PATCH 06/18] Move logging middleware to logging module
---
src/core/logging.py | 40 ++++++++++++++++++++++++++++++++++++++++
src/main.py | 40 ++--------------------------------------
2 files changed, 42 insertions(+), 38 deletions(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index 54d70ae..c575d79 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -1,6 +1,11 @@
"""Utility functions for logging."""
+import uuid
+from collections.abc import Awaitable, Callable
+
from loguru import logger
+from starlette.requests import Request
+from starlette.responses import Response
from config import load_configuration
@@ -12,3 +17,38 @@ def setup_log_sinks() -> None:
logger.info("Configuring sink", nickname=nickname, **sink_configuration)
sink = sink_configuration.pop("sink")
logger.add(sink, serialize=True, **sink_configuration)
+
+
+async def add_request_context_to_log(
+ request: Request,
+ call_next: Callable[[Request], Awaitable[Response]],
+) -> Response:
+ """Add a unique request id and client ip address to each log call."""
+ identifier = uuid.uuid4().hex
+ host = request.client.host if request.client else "unknown host"
+ with logger.contextualize(request_id=identifier, client_ip=host):
+ return await call_next(request)
+
+
+async def request_response_logger(
+ request: Request,
+ call_next: Callable[[Request], Awaitable[Response]],
+) -> Response:
+ """Log the incoming request and outgoing response."""
+ logger.info(
+ "request",
+ url=request.url,
+ headers=request.headers,
+ cookies=request.cookies,
+ path_params=request.path_params,
+ query_params=request.query_params,
+ body=await request.body(),
+ )
+ response: Response = await call_next(request)
+ logger.info(
+ "response",
+ status_code=response.status_code,
+ headers=response.headers,
+ media_type=response.media_type,
+ )
+ return response
diff --git a/src/main.py b/src/main.py
index dad7d02..fe09177 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,18 +1,15 @@
import argparse
import sys
-import uuid
-from collections.abc import AsyncGenerator, Awaitable, Callable
+from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
from loguru import logger
-from starlette.requests import Request
-from starlette.responses import Response
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
-from core.logging import setup_log_sinks
+from core.logging import add_request_context_to_log, request_response_logger, setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
@@ -60,39 +57,6 @@ def _parse_args() -> argparse.Namespace:
return parser.parse_args()
-async def add_request_context_to_log(
- request: Request,
- call_next: Callable[[Request], Awaitable[Response]],
-) -> Response:
- identifier = uuid.uuid4().hex
- host = request.client.host if request.client else "unknown host"
- with logger.contextualize(request_id=identifier, client_ip=host):
- return await call_next(request)
-
-
-async def request_response_logger(
- request: Request,
- call_next: Callable[[Request], Awaitable[Response]],
-) -> Response:
- logger.info(
- "request",
- url=request.url,
- headers=request.headers,
- cookies=request.cookies,
- path_params=request.path_params,
- query_params=request.query_params,
- body=await request.body(),
- )
- response: Response = await call_next(request)
- logger.info(
- "response",
- status_code=response.status_code,
- headers=response.headers,
- media_type=response.media_type,
- )
- return response
-
-
def create_api() -> FastAPI:
# Default logging configuration so we have logs during setup
setup_sink = logger.add(sys.stderr, serialize=True)
From dd806b1050b1f2dcd0e2c262797ef2274766cfeb Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 16:47:21 +0200
Subject: [PATCH 07/18] Remove automated logging of client ip address
---
src/core/logging.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index c575d79..5d1d093 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -23,10 +23,9 @@ async def add_request_context_to_log(
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
- """Add a unique request id and client ip address to each log call."""
+ """Add a unique request id to each log call."""
identifier = uuid.uuid4().hex
- host = request.client.host if request.client else "unknown host"
- with logger.contextualize(request_id=identifier, client_ip=host):
+ with logger.contextualize(request_id=identifier):
return await call_next(request)
From 37e974a62bd12cf95da235e25882d17a9b9df691 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Wed, 1 Apr 2026 17:02:27 +0200
Subject: [PATCH 08/18] Disable logging to file with pytest
---
tests/conftest.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/conftest.py b/tests/conftest.py
index 89a7bcb..2b8ac7a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,6 @@
import contextlib
import json
+import sys
from collections.abc import AsyncIterator, Iterable, Iterator
from pathlib import Path
from typing import Any, NamedTuple
@@ -9,6 +10,7 @@
import pytest
from _pytest.config import Config
from _pytest.nodes import Item
+from loguru import logger
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
@@ -74,6 +76,8 @@ async def py_api(
expdb_test: AsyncConnection, user_test: AsyncConnection
) -> AsyncIterator[httpx.AsyncClient]:
app = create_api()
+ logger.remove()
+ logger.add(sys.stderr, serialize=True)
# We use async generator functions because fixtures may not be called directly.
# The async generator returns the test connections for FastAPI to handle properly
From c62218c77697430401c7971eb2653d22da02458e Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 12:35:09 +0200
Subject: [PATCH 09/18] Use a different configuration for the tests
---
src/config.py | 3 ++-
src/core/logging.py | 8 ++++++--
src/main.py | 7 ++++---
tests/config.test.toml | 28 ++++++++++++++++++++++++++++
tests/conftest.py | 6 +-----
5 files changed, 41 insertions(+), 11 deletions(-)
create mode 100644 tests/config.test.toml
diff --git a/src/config.py b/src/config.py
index 0ff7a91..ffc4fb8 100644
--- a/src/config.py
+++ b/src/config.py
@@ -79,5 +79,6 @@ def load_database_configuration(file: Path = _config_file) -> TomlTable:
return database_configuration
-def load_configuration(file: Path = _config_file) -> TomlTable:
+def load_configuration(file: Path | None = None) -> TomlTable:
+ file = file or _config_file
return tomllib.loads(file.read_text())
diff --git a/src/core/logging.py b/src/core/logging.py
index 5d1d093..0878b8e 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -1,7 +1,9 @@
"""Utility functions for logging."""
+import sys
import uuid
from collections.abc import Awaitable, Callable
+from pathlib import Path
from loguru import logger
from starlette.requests import Request
@@ -10,12 +12,14 @@
from config import load_configuration
-def setup_log_sinks() -> None:
+def setup_log_sinks(configuration_file: Path | None = None) -> None:
"""Configure loguru based on app configuration."""
- configuration = load_configuration()
+ configuration = load_configuration(configuration_file)
for nickname, sink_configuration in configuration.get("logging", {}).items():
logger.info("Configuring sink", nickname=nickname, **sink_configuration)
sink = sink_configuration.pop("sink")
+ if sink == "sys.stderr":
+ sink = sys.stderr
logger.add(sink, serialize=True, **sink_configuration)
diff --git a/src/main.py b/src/main.py
index fe09177..93acd5d 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,6 +2,7 @@
import sys
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
+from pathlib import Path
import uvicorn
from fastapi import FastAPI
@@ -57,12 +58,12 @@ def _parse_args() -> argparse.Namespace:
return parser.parse_args()
-def create_api() -> FastAPI:
+def create_api(configuration_file: Path | None = None) -> FastAPI:
# Default logging configuration so we have logs during setup
setup_sink = logger.add(sys.stderr, serialize=True)
- setup_log_sinks()
+ setup_log_sinks(configuration_file)
- fastapi_kwargs = load_configuration()["fastapi"]
+ fastapi_kwargs = load_configuration(configuration_file)["fastapi"]
logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
diff --git a/tests/config.test.toml b/tests/config.test.toml
new file mode 100644
index 0000000..6942c90
--- /dev/null
+++ b/tests/config.test.toml
@@ -0,0 +1,28 @@
+arff_base_url="https://test.openml.org"
+minio_base_url="https://openml1.win.tue.nl"
+
+[development]
+allow_test_api_keys=true
+
+[logging.develop]
+sink="sys.stderr"
+level="DEBUG"
+
+[fastapi]
+root_path=""
+
+[databases.defaults]
+host="database"
+port="3306"
+# SQLAlchemy `dialect` and `driver`: https://docs.sqlalchemy.org/en/20/dialects/index.html
+drivername="mysql+aiomysql"
+
+[databases.expdb]
+database="openml_expdb"
+
+[databases.openml]
+database="openml"
+
+[routing]
+minio_url="http://minio:9000/"
+server_url="http://php-api:80/"
diff --git a/tests/conftest.py b/tests/conftest.py
index 2b8ac7a..163e4e7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,5 @@
import contextlib
import json
-import sys
from collections.abc import AsyncIterator, Iterable, Iterator
from pathlib import Path
from typing import Any, NamedTuple
@@ -10,7 +9,6 @@
import pytest
from _pytest.config import Config
from _pytest.nodes import Item
-from loguru import logger
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
@@ -75,9 +73,7 @@ async def php_api() -> AsyncIterator[httpx.AsyncClient]:
async def py_api(
expdb_test: AsyncConnection, user_test: AsyncConnection
) -> AsyncIterator[httpx.AsyncClient]:
- app = create_api()
- logger.remove()
- logger.add(sys.stderr, serialize=True)
+ app = create_api(Path(__file__).parent / "config.test.toml")
# We use async generator functions because fixtures may not be called directly.
# The async generator returns the test connections for FastAPI to handle properly
From 26592193090c5a9c4808dd88715fd8b03f39279d Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:02:25 +0200
Subject: [PATCH 10/18] Do not log the request body
---
src/core/logging.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index 0878b8e..d36df1e 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -45,7 +45,6 @@ async def request_response_logger(
cookies=request.cookies,
path_params=request.path_params,
query_params=request.query_params,
- body=await request.body(),
)
response: Response = await call_next(request)
logger.info(
From f7c6ff68ceb5b126b52d30c07bf6819b2932dae8 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:25:29 +0200
Subject: [PATCH 11/18] Disable loguru
---
src/config.py | 14 +++++++-------
src/core/logging.py | 7 ++++++-
src/main.py | 20 ++++++++++----------
3 files changed, 23 insertions(+), 18 deletions(-)
diff --git a/src/config.py b/src/config.py
index ffc4fb8..894e755 100644
--- a/src/config.py
+++ b/src/config.py
@@ -5,7 +5,7 @@
from pathlib import Path
from dotenv import load_dotenv
-from loguru import logger
+# from loguru import logger
TomlTable = dict[str, typing.Any]
@@ -26,12 +26,12 @@
_dotenv_file = _dotenv_file.expanduser().absolute()
-logger.info(
- "Determined configuration sources.",
- configuration_directory=_config_directory,
- configuration_file=_config_file,
- dotenv_file=_dotenv_file,
-)
+# logger.info(
+# "Determined configuration sources.",
+# configuration_directory=_config_directory,
+# configuration_file=_config_file,
+# dotenv_file=_dotenv_file,
+# )
load_dotenv(dotenv_path=_dotenv_file)
diff --git a/src/core/logging.py b/src/core/logging.py
index d36df1e..698e787 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -5,7 +5,6 @@
from collections.abc import Awaitable, Callable
from pathlib import Path
-from loguru import logger
from starlette.requests import Request
from starlette.responses import Response
@@ -14,6 +13,8 @@
def setup_log_sinks(configuration_file: Path | None = None) -> None:
"""Configure loguru based on app configuration."""
+ from loguru import logger
+
configuration = load_configuration(configuration_file)
for nickname, sink_configuration in configuration.get("logging", {}).items():
logger.info("Configuring sink", nickname=nickname, **sink_configuration)
@@ -28,6 +29,8 @@ async def add_request_context_to_log(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Add a unique request id to each log call."""
+ from loguru import logger
+
identifier = uuid.uuid4().hex
with logger.contextualize(request_id=identifier):
return await call_next(request)
@@ -38,6 +41,8 @@ async def request_response_logger(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Log the incoming request and outgoing response."""
+ from loguru import logger
+
logger.info(
"request",
url=request.url,
diff --git a/src/main.py b/src/main.py
index 93acd5d..dee397e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -6,7 +6,7 @@
import uvicorn
from fastapi import FastAPI
-from loguru import logger
+# from loguru import logger
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
@@ -60,22 +60,22 @@ def _parse_args() -> argparse.Namespace:
def create_api(configuration_file: Path | None = None) -> FastAPI:
# Default logging configuration so we have logs during setup
- setup_sink = logger.add(sys.stderr, serialize=True)
- setup_log_sinks(configuration_file)
+ # setup_sink = logger.add(sys.stderr, serialize=True)
+ # setup_log_sinks(configuration_file)
fastapi_kwargs = load_configuration(configuration_file)["fastapi"]
- logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
+ # logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
- logger.info("Setting up middleware and exception handlers.")
+ # logger.info("Setting up middleware and exception handlers.")
# Order matters! Each added middleware wraps the previous, creating a stack.
# See also: https://fastapi.tiangolo.com/tutorial/middleware/#multiple-middleware-execution-order
- app.middleware("http")(request_response_logger)
- app.middleware("http")(add_request_context_to_log)
+ # app.middleware("http")(request_response_#logger)
+ # app.middleware("http")(add_request_context_to_log)
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
- logger.info("Adding routers to app")
+ # logger.info("Adding routers to app")
app.include_router(datasets_router)
app.include_router(qualities_router)
app.include_router(mldcat_ap_router)
@@ -88,8 +88,8 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
app.include_router(setup_router)
app.include_router(run_router)
- logger.info("App setup completed.")
- logger.remove(setup_sink)
+ # logger.info("App setup completed.")
+ # logger.remove(setup_sink)
return app
From 38d1d8dc4a017961857a1adf3cf79012abc2c043 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 2 Apr 2026 14:25:52 +0000
Subject: [PATCH 12/18] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/config.py | 1 +
src/main.py | 4 +---
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/config.py b/src/config.py
index 894e755..2e1872a 100644
--- a/src/config.py
+++ b/src/config.py
@@ -5,6 +5,7 @@
from pathlib import Path
from dotenv import load_dotenv
+
# from loguru import logger
TomlTable = dict[str, typing.Any]
diff --git a/src/main.py b/src/main.py
index dee397e..3618e88 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,16 +1,14 @@
import argparse
-import sys
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path
import uvicorn
from fastapi import FastAPI
-# from loguru import logger
+# from loguru import logger
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
-from core.logging import add_request_context_to_log, request_response_logger, setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
From 1f1fa5581c9520f96de967eda2aabe95c8b9b643 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:36:12 +0200
Subject: [PATCH 13/18] Add back import statements
---
src/config.py | 2 +-
src/core/logging.py | 4 +---
src/main.py | 3 ++-
3 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/config.py b/src/config.py
index 2e1872a..dabc5b2 100644
--- a/src/config.py
+++ b/src/config.py
@@ -6,7 +6,7 @@
from dotenv import load_dotenv
-# from loguru import logger
+from loguru import logger
TomlTable = dict[str, typing.Any]
diff --git a/src/core/logging.py b/src/core/logging.py
index 698e787..0d463d7 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -7,13 +7,13 @@
from starlette.requests import Request
from starlette.responses import Response
+from loguru import logger
from config import load_configuration
def setup_log_sinks(configuration_file: Path | None = None) -> None:
"""Configure loguru based on app configuration."""
- from loguru import logger
configuration = load_configuration(configuration_file)
for nickname, sink_configuration in configuration.get("logging", {}).items():
@@ -29,7 +29,6 @@ async def add_request_context_to_log(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Add a unique request id to each log call."""
- from loguru import logger
identifier = uuid.uuid4().hex
with logger.contextualize(request_id=identifier):
@@ -41,7 +40,6 @@ async def request_response_logger(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Log the incoming request and outgoing response."""
- from loguru import logger
logger.info(
"request",
diff --git a/src/main.py b/src/main.py
index 3618e88..7fbb190 100644
--- a/src/main.py
+++ b/src/main.py
@@ -5,10 +5,11 @@
import uvicorn
from fastapi import FastAPI
+from loguru import logger
-# from loguru import logger
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
+from core.logging import add_request_context_to_log, request_response_logger, setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
From 9c81a9af7785434ff07ada38cc8e28a69831a0a1 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:40:53 +0200
Subject: [PATCH 14/18] Startup logging
---
src/config.py | 12 ++++++------
src/main.py | 12 ++++++------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/config.py b/src/config.py
index dabc5b2..2c5beeb 100644
--- a/src/config.py
+++ b/src/config.py
@@ -27,12 +27,12 @@
_dotenv_file = _dotenv_file.expanduser().absolute()
-# logger.info(
-# "Determined configuration sources.",
-# configuration_directory=_config_directory,
-# configuration_file=_config_file,
-# dotenv_file=_dotenv_file,
-# )
+logger.info(
+ "Determined configuration sources.",
+ configuration_directory=_config_directory,
+ configuration_file=_config_file,
+ dotenv_file=_dotenv_file,
+)
load_dotenv(dotenv_path=_dotenv_file)
diff --git a/src/main.py b/src/main.py
index 7fbb190..cee152a 100644
--- a/src/main.py
+++ b/src/main.py
@@ -2,6 +2,7 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path
+import sys
import uvicorn
from fastapi import FastAPI
@@ -9,7 +10,6 @@
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
-from core.logging import add_request_context_to_log, request_response_logger, setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
@@ -59,11 +59,11 @@ def _parse_args() -> argparse.Namespace:
def create_api(configuration_file: Path | None = None) -> FastAPI:
# Default logging configuration so we have logs during setup
- # setup_sink = logger.add(sys.stderr, serialize=True)
+ setup_sink = logger.add(sys.stderr, serialize=True)
# setup_log_sinks(configuration_file)
fastapi_kwargs = load_configuration(configuration_file)["fastapi"]
- # logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
+ logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
# logger.info("Setting up middleware and exception handlers.")
@@ -74,7 +74,7 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
- # logger.info("Adding routers to app")
+ logger.info("Adding routers to app")
app.include_router(datasets_router)
app.include_router(qualities_router)
app.include_router(mldcat_ap_router)
@@ -87,8 +87,8 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
app.include_router(setup_router)
app.include_router(run_router)
- # logger.info("App setup completed.")
- # logger.remove(setup_sink)
+ logger.info("App setup completed.")
+ logger.remove(setup_sink)
return app
From 6b276ded992b9fa19136b0b2a21e306893befa56 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:46:11 +0200
Subject: [PATCH 15/18] Add back the context middleware
---
src/config.py | 1 -
src/main.py | 7 ++++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/config.py b/src/config.py
index 2c5beeb..ffc4fb8 100644
--- a/src/config.py
+++ b/src/config.py
@@ -5,7 +5,6 @@
from pathlib import Path
from dotenv import load_dotenv
-
from loguru import logger
TomlTable = dict[str, typing.Any]
diff --git a/src/main.py b/src/main.py
index cee152a..a8aa6f9 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,11 +1,12 @@
import argparse
+import sys
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path
-import sys
import uvicorn
from fastapi import FastAPI
+from core.logging import add_request_context_to_log
from loguru import logger
from config import load_configuration
@@ -66,11 +67,11 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
app = FastAPI(**fastapi_kwargs, lifespan=lifespan)
- # logger.info("Setting up middleware and exception handlers.")
+ logger.info("Setting up middleware and exception handlers.")
# Order matters! Each added middleware wraps the previous, creating a stack.
# See also: https://fastapi.tiangolo.com/tutorial/middleware/#multiple-middleware-execution-order
# app.middleware("http")(request_response_#logger)
- # app.middleware("http")(add_request_context_to_log)
+ app.middleware("http")(add_request_context_to_log)
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
From 9f5862341dcac560718feb133565562d031a14f9 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 2 Apr 2026 14:48:15 +0000
Subject: [PATCH 16/18] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/core/logging.py | 5 +----
src/main.py | 2 +-
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index 0d463d7..d36df1e 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -5,16 +5,15 @@
from collections.abc import Awaitable, Callable
from pathlib import Path
+from loguru import logger
from starlette.requests import Request
from starlette.responses import Response
-from loguru import logger
from config import load_configuration
def setup_log_sinks(configuration_file: Path | None = None) -> None:
"""Configure loguru based on app configuration."""
-
configuration = load_configuration(configuration_file)
for nickname, sink_configuration in configuration.get("logging", {}).items():
logger.info("Configuring sink", nickname=nickname, **sink_configuration)
@@ -29,7 +28,6 @@ async def add_request_context_to_log(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Add a unique request id to each log call."""
-
identifier = uuid.uuid4().hex
with logger.contextualize(request_id=identifier):
return await call_next(request)
@@ -40,7 +38,6 @@ async def request_response_logger(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Log the incoming request and outgoing response."""
-
logger.info(
"request",
url=request.url,
diff --git a/src/main.py b/src/main.py
index a8aa6f9..4825547 100644
--- a/src/main.py
+++ b/src/main.py
@@ -6,11 +6,11 @@
import uvicorn
from fastapi import FastAPI
-from core.logging import add_request_context_to_log
from loguru import logger
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
+from core.logging import add_request_context_to_log
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
From 8e5c56125fc0601eba372d8445b226eb69656d58 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 16:57:29 +0200
Subject: [PATCH 17/18] Add back in logging, but with placeholder messages
---
src/core/logging.py | 30 ++++++++++++++++--------------
src/main.py | 7 ++++---
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index d36df1e..37b0b4a 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -38,19 +38,21 @@ async def request_response_logger(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Log the incoming request and outgoing response."""
- logger.info(
- "request",
- url=request.url,
- headers=request.headers,
- cookies=request.cookies,
- path_params=request.path_params,
- query_params=request.query_params,
- )
+ # logger.info(
+ # "request",
+ # url=request.url,
+ # headers=request.headers,
+ # cookies=request.cookies,
+ # path_params=request.path_params,
+ # query_params=request.query_params,
+ # )
+ logger.info("before request")
response: Response = await call_next(request)
- logger.info(
- "response",
- status_code=response.status_code,
- headers=response.headers,
- media_type=response.media_type,
- )
+ logger.info("after request")
+ # logger.info(
+ # "response",
+ # status_code=response.status_code,
+ # headers=response.headers,
+ # media_type=response.media_type,
+ # )
return response
diff --git a/src/main.py b/src/main.py
index 4825547..696c09b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -10,7 +10,7 @@
from config import load_configuration
from core.errors import ProblemDetailError, problem_detail_exception_handler
-from core.logging import add_request_context_to_log
+from core.logging import add_request_context_to_log, request_response_logger, setup_log_sinks
from database.setup import close_databases
from routers.mldcat_ap.dataset import router as mldcat_ap_router
from routers.openml.datasets import router as datasets_router
@@ -60,8 +60,9 @@ def _parse_args() -> argparse.Namespace:
def create_api(configuration_file: Path | None = None) -> FastAPI:
# Default logging configuration so we have logs during setup
+ logger.remove()
setup_sink = logger.add(sys.stderr, serialize=True)
- # setup_log_sinks(configuration_file)
+ setup_log_sinks(configuration_file)
fastapi_kwargs = load_configuration(configuration_file)["fastapi"]
logger.info("Creating FastAPI App", lifespan=lifespan, **fastapi_kwargs)
@@ -70,7 +71,7 @@ def create_api(configuration_file: Path | None = None) -> FastAPI:
logger.info("Setting up middleware and exception handlers.")
# Order matters! Each added middleware wraps the previous, creating a stack.
# See also: https://fastapi.tiangolo.com/tutorial/middleware/#multiple-middleware-execution-order
- # app.middleware("http")(request_response_#logger)
+ app.middleware("http")(request_response_logger)
app.middleware("http")(add_request_context_to_log)
app.add_exception_handler(ProblemDetailError, problem_detail_exception_handler) # type: ignore[arg-type]
From 4e5d3da21b5cac917b21b7bdd93834cce6e25188 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Apr 2026 20:04:27 +0200
Subject: [PATCH 18/18] Add back in full logging of requests
---
src/core/logging.py | 30 ++++++++++++++----------------
1 file changed, 14 insertions(+), 16 deletions(-)
diff --git a/src/core/logging.py b/src/core/logging.py
index 37b0b4a..d36df1e 100644
--- a/src/core/logging.py
+++ b/src/core/logging.py
@@ -38,21 +38,19 @@ async def request_response_logger(
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
"""Log the incoming request and outgoing response."""
- # logger.info(
- # "request",
- # url=request.url,
- # headers=request.headers,
- # cookies=request.cookies,
- # path_params=request.path_params,
- # query_params=request.query_params,
- # )
- logger.info("before request")
+ logger.info(
+ "request",
+ url=request.url,
+ headers=request.headers,
+ cookies=request.cookies,
+ path_params=request.path_params,
+ query_params=request.query_params,
+ )
response: Response = await call_next(request)
- logger.info("after request")
- # logger.info(
- # "response",
- # status_code=response.status_code,
- # headers=response.headers,
- # media_type=response.media_type,
- # )
+ logger.info(
+ "response",
+ status_code=response.status_code,
+ headers=response.headers,
+ media_type=response.media_type,
+ )
return response