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