diff --git a/.gitea/workflows/backend_run_lint.yaml b/.gitea/workflows/backend_run_lint.yaml index e3fd418..5db2228 100644 --- a/.gitea/workflows/backend_run_lint.yaml +++ b/.gitea/workflows/backend_run_lint.yaml @@ -25,8 +25,6 @@ jobs: ls -la # only install dev-packages pipenv install --categories=dev-packages - pipenv run pip freeze - working-directory: backend - name: Run linter diff --git a/.gitea/workflows/backend_run_test.yaml b/.gitea/workflows/backend_run_test.yaml index 36b5ee8..82ad499 100644 --- a/.gitea/workflows/backend_run_test.yaml +++ b/.gitea/workflows/backend_run_test.yaml @@ -25,7 +25,6 @@ jobs: ls -la # install all packages, including dev-packages pipenv install --dev - pipenv run pip freeze working-directory: backend - name: Run Tests diff --git a/.vscode/launch.json b/.vscode/launch.json index 48d1c86..6f1ce86 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,18 +9,16 @@ "name": "Backend - debug", "type": "debugpy", "request": "launch", - "module": "uvicorn", "env": { "DEBUG": "true" }, - "args": [ - // "--app-dir", - // "src", - "src.main:app", - "--reload", - ], "jinja": true, - "cwd": "${workspaceFolder}/backend" + "cwd": "${workspaceFolder}/backend", + "module": "fastapi", + "args": [ + "dev", + "src/main.py" + ] }, { "name": "Backend - tester", diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 9ddf6b2..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cmake.ignoreCMakeListsMissing": true -} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 25a5e31..b363917 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,5 +14,7 @@ EXPOSE 8000 ENV NUM_WORKERS=1 ENV OSM_CACHE_DIR=/cache ENV MEMCACHED_HOST_PATH=none +ENV LOKI_URL=none +# explicitly use a string instead of an argument list to force a shell and variable expansion CMD fastapi run src/main.py --port 8000 --workers $NUM_WORKERS diff --git a/backend/Pipfile b/backend/Pipfile index 4d06a94..1f9a632 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -25,3 +25,4 @@ pymemcache = "*" fastapi-cli = "*" scikit-learn = "*" pyqt6 = "*" +loki-logger-handler = "*" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 880b9a6..b13bd13 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb22b4e28c7aa199c94b688ad93d3ab0ccf1089a172131f4aec03b78e7bd7f1c" + "sha256": "6edd6644586e8814a0b4526adb3352dfc17828ca129de7a68c1d5929efe94daa" }, "pipfile-spec": 6, "requires": {}, @@ -507,6 +507,15 @@ "markers": "python_version >= '3.8'", "version": "==1.4.7" }, + "loki-logger-handler": { + "hashes": [ + "sha256:aa1a9c933282c134a1e4271aba3cbaa2a3660eab6ea415bad7a072444ab98aa8", + "sha256:f6114727a9e5e6f3f2058b9b5324d1cab6d1a04e802079f7b57a8aeb7bd0a112" + ], + "index": "pypi", + "markers": "python_version >= '2.7'", + "version": "==1.0.2" + }, "lxml": { "hashes": [ "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e", diff --git a/backend/deployment b/backend/deployment index 718df09..904f16b 160000 --- a/backend/deployment +++ b/backend/deployment @@ -1 +1 @@ -Subproject commit 718df09e88b63c9524c882ccbb8247ca1448d3ff +Subproject commit 904f16bfc0624b6ab8569e0a70050aaa3bd64b3f diff --git a/backend/report.html b/backend/report.html deleted file mode 100644 index d46b72d..0000000 --- a/backend/report.html +++ /dev/null @@ -1,1094 +0,0 @@ - - - - - Backend Testing Report - - - - -

Backend Testing Report

-

Report generated on 13-Dec-2024 at 08:58:37 by pytest-html - v4.1.1

-
-

Environment

-
-
- - - - - -
-
-

Summary

-
-
-

12 tests took 865 ms.

-

(Un)check the boxes to filter the results.

-
- -
-
-
-
- - 2 Failed, - - 10 Passed, - - 0 Skipped, - - 0 Expected failures, - - 0 Unexpected passes, - - 0 Errors, - - 0 Reruns -
-
-  /  -
-
-
-
-
-
-
-
- - - - - - - - - - - - -
ResultTestDetailed tripTrip DurationTarget DurationExecution timeLinks
- - - \ No newline at end of file diff --git a/backend/src/persistence.py b/backend/src/cache.py similarity index 100% rename from backend/src/persistence.py rename to backend/src/cache.py diff --git a/backend/src/constants.py b/backend/src/constants.py index 21ed016..60a26c7 100644 --- a/backend/src/constants.py +++ b/backend/src/constants.py @@ -1,6 +1,5 @@ -"""Module allowing to access the parameters of route generation""" +"""Module setting global parameters for the application such as cache, route generation, etc.""" -import logging import os from pathlib import Path @@ -16,21 +15,6 @@ cache_dir_string = os.getenv('OSM_CACHE_DIR', './cache') OSM_CACHE_DIR = Path(cache_dir_string) -# if we are in a debug session, set verbose and rich logging -if os.getenv('DEBUG', "false") == "true": - from rich.logging import RichHandler - logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[RichHandler()] - ) -else: - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - ) - - MEMCACHED_HOST_PATH = os.getenv('MEMCACHED_HOST_PATH', None) if MEMCACHED_HOST_PATH == "none": MEMCACHED_HOST_PATH = None diff --git a/backend/src/logging_config.py b/backend/src/logging_config.py new file mode 100644 index 0000000..c43a246 --- /dev/null +++ b/backend/src/logging_config.py @@ -0,0 +1,58 @@ +"""Sets up global logging configuration for the application.""" + +import logging +import os + +logger = logging.getLogger(__name__) + + +def configure_logging(): + """ + Called at startup of a FastAPI application instance to setup logging. Depending on the environment, it will log to stdout or to Loki. + """ + + is_debug = os.getenv('DEBUG', "false") == "true" + is_kubernetes = os.getenv('KUBERNETES_SERVICE_HOST') is not None + + + if is_kubernetes: + # in that case we want to log to stdout and also to loki + from loki_logger_handler.loki_logger_handler import LokiLoggerHandler + loki_url = os.getenv('LOKI_URL') + loki_url = "http://localhost:3100/loki/api/v1/push" + if loki_url is None: + raise ValueError("LOKI_URL environment variable is not set") + + loki_handler = LokiLoggerHandler( + url = loki_url, + labels = {'app': 'anyway', 'environment': 'staging' if is_debug else 'production'} + ) + + logger.info(f"Logging to Loki at {loki_url} with {loki_handler.labels} and {is_debug=}") + logging_handlers = [loki_handler, logging.StreamHandler()] + logging_level = logging.DEBUG if is_debug else logging.INFO + # silence the chatty logs loki generates itself + logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) + # no need for time since it's added by loki or can be shown in kube logs + logging_format = '%(name)s - %(levelname)s - %(message)s' + + else: + # if we are in a debug (local) session, set verbose and rich logging + from rich.logging import RichHandler + logging_handlers = [RichHandler()] + logging_level = logging.DEBUG if is_debug else logging.INFO + logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + + + + logging.basicConfig( + level = logging_level, + format = logging_format, + handlers = logging_handlers + ) + + # also overwrite the uvicorn loggers + logging.getLogger('uvicorn').handlers = logging_handlers + logging.getLogger('uvicorn.access').handlers = logging_handlers + logging.getLogger('uvicorn.error').handlers = logging_handlers + diff --git a/backend/src/main.py b/backend/src/main.py index a2efbf1..c8c1f40 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,7 +2,9 @@ import logging from fastapi import FastAPI, HTTPException, Query +from contextlib import asynccontextmanager +from .logging_config import configure_logging from .structs.landmark import Landmark, Toilets from .structs.preferences import Preferences from .structs.linked_landmarks import LinkedLandmarks @@ -11,17 +13,28 @@ from .utils.landmarks_manager import LandmarkManager from .utils.toilets_manager import ToiletsManager from .utils.optimizer import Optimizer from .utils.refiner import Refiner -from .persistence import client as cache_client - +from .cache import client as cache_client logger = logging.getLogger(__name__) -app = FastAPI() manager = LandmarkManager() optimizer = Optimizer() refiner = Refiner(optimizer=optimizer) +@asynccontextmanager +async def lifespan(app: FastAPI): + """Function to run at the start of the app""" + logger.info("Setting up logging") + configure_logging() + yield + logger.info("Shutting down logging") + + +app = FastAPI(lifespan=lifespan) + + + @app.post("/trip/new") def new_trip(preferences: Preferences, start: tuple[float, float], diff --git a/backend/src/structs/landmark.py b/backend/src/structs/landmark.py index a494896..da0f122 100644 --- a/backend/src/structs/landmark.py +++ b/backend/src/structs/landmark.py @@ -1,7 +1,7 @@ """Definition of the Landmark class to handle visitable objects across the world.""" from typing import Optional, Literal -from uuid import uuid4 +from uuid import uuid4, UUID from pydantic import BaseModel, Field @@ -29,12 +29,12 @@ class Landmark(BaseModel) : description (Optional[str]): A text description of the landmark. duration (Optional[int]): The estimated time to visit the landmark (in minutes). name_en (Optional[str]): The English name of the landmark. - uuid (str): A unique identifier for the landmark, generated by default using uuid4. + uuid (UUID): A unique identifier for the landmark, generated by default using uuid4. must_do (Optional[bool]): Whether the landmark is a "must-do" attraction. must_avoid (Optional[bool]): Whether the landmark should be avoided. is_secondary (Optional[bool]): Whether the landmark is secondary or less important. time_to_reach_next (Optional[int]): Estimated time (in minutes) to reach the next landmark. - next_uuid (Optional[str]): UUID of the next landmark in sequence (if applicable). + next_uuid (Optional[UUID]): UUID of the next landmark in sequence (if applicable). """ # Properties of the landmark @@ -52,7 +52,7 @@ class Landmark(BaseModel) : name_en : Optional[str] = None # Unique ID of a given landmark - uuid: str = Field(default_factory=uuid4) + uuid: UUID = Field(default_factory=uuid4) # Additional properties depending on specific tour must_do : Optional[bool] = False @@ -60,7 +60,7 @@ class Landmark(BaseModel) : is_secondary : Optional[bool] = False time_to_reach_next : Optional[int] = 0 - next_uuid : Optional[str] = None + next_uuid : Optional[UUID] = None def __str__(self) -> str: """ @@ -139,4 +139,4 @@ class Toilets(BaseModel) : class Config: # This allows us to easily convert the model to and from dictionaries - orm_mode = True \ No newline at end of file + from_attributes = True \ No newline at end of file diff --git a/backend/src/structs/trip.py b/backend/src/structs/trip.py index c00f591..d9b19d7 100644 --- a/backend/src/structs/trip.py +++ b/backend/src/structs/trip.py @@ -1,6 +1,6 @@ """Definition of the Trip class.""" -import uuid +from uuid import uuid4, UUID from pydantic import BaseModel, Field from pymemcache.client.base import Client @@ -19,9 +19,9 @@ class Trip(BaseModel): Methods: from_linked_landmarks: create a Trip from LinkedLandmarks object. """ - uuid: str = Field(default_factory=uuid.uuid4) + uuid: UUID = Field(default_factory=uuid4) total_time: int - first_landmark_uuid: str + first_landmark_uuid: UUID @classmethod @@ -31,7 +31,7 @@ class Trip(BaseModel): """ trip = Trip( total_time = landmarks.total_time, - first_landmark_uuid = str(landmarks[0].uuid) + first_landmark_uuid = landmarks[0].uuid ) # Store the trip in the cache diff --git a/backend/src/tests/test_utils.py b/backend/src/tests/test_utils.py index d5b69ad..73f4ec7 100644 --- a/backend/src/tests/test_utils.py +++ b/backend/src/tests/test_utils.py @@ -4,7 +4,7 @@ from fastapi import HTTPException from pydantic import ValidationError from ..structs.landmark import Landmark -from ..persistence import client as cache_client +from ..cache import client as cache_client def landmarks_to_osmid(landmarks: list[Landmark]) -> list[int] :