actually use fastapi lifetime manager to setup logging
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
Some checks failed
Run testing on the backend code / Build (pull_request) Has been cancelled
Run linting on the backend code / Build (pull_request) Has been cancelled
Build and deploy the backend to staging / Deploy to staging (pull_request) Has been cancelled
Build and deploy the backend to staging / Build and push image (pull_request) Has been cancelled
This commit is contained in:
parent
bc63b57154
commit
4e07c10969
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@ -4,17 +4,21 @@
|
|||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
// backend - python app that launches a uvicorn server
|
// backend - python using fastapi
|
||||||
{
|
{
|
||||||
"name": "Backend - debug",
|
"name": "Backend - debug",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "launcher.py",
|
|
||||||
"env": {
|
"env": {
|
||||||
"DEBUG": "true"
|
"DEBUG": "true"
|
||||||
},
|
},
|
||||||
"jinja": true,
|
"jinja": true,
|
||||||
"cwd": "${workspaceFolder}/backend"
|
"cwd": "${workspaceFolder}/backend",
|
||||||
|
"module": "fastapi",
|
||||||
|
"args": [
|
||||||
|
"dev",
|
||||||
|
"src/main.py"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Backend - tester",
|
"name": "Backend - tester",
|
||||||
|
@ -7,7 +7,6 @@ RUN pip install pipenv
|
|||||||
RUN pipenv install --deploy --system
|
RUN pipenv install --deploy --system
|
||||||
|
|
||||||
COPY src src
|
COPY src src
|
||||||
COPY launcher.py .
|
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
@ -17,4 +16,4 @@ ENV OSM_CACHE_DIR=/cache
|
|||||||
ENV MEMCACHED_HOST_PATH=none
|
ENV MEMCACHED_HOST_PATH=none
|
||||||
ENV LOKI_URL=none
|
ENV LOKI_URL=none
|
||||||
|
|
||||||
CMD ["python", "launcher.py"]
|
CMD ["fastapi", "src/main.py", "--port", "8000", "--workers", "$NUM_WORKERS"]
|
||||||
|
@ -1,82 +0,0 @@
|
|||||||
"""Launcher for the FastAPI application. Fundametally this replicates the functionality of the uvicorn and fastapi CLI interfaces, but we need this to setup the logging correctly (and most importantly globally)"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import uvicorn
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
is_debug = os.getenv('DEBUG', "false") == "true"
|
|
||||||
is_kubernetes = os.getenv('KUBERNETES_SERVICE_HOST', None) is not None
|
|
||||||
|
|
||||||
def logger_setup():
|
|
||||||
"""
|
|
||||||
Setup the global logging configuration
|
|
||||||
"""
|
|
||||||
logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
|
|
||||||
# Make uvicorn conform to the global logging configuration
|
|
||||||
uvicorn_logger = logging.getLogger('uvicorn')
|
|
||||||
uvicorn_logger.propagate = True
|
|
||||||
uvicorn_logger.handlers = [] # Remove default handlers to avoid duplicate logs
|
|
||||||
|
|
||||||
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')
|
|
||||||
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=}")
|
|
||||||
if is_debug:
|
|
||||||
logging.basicConfig(
|
|
||||||
format = logging_format,
|
|
||||||
level = logging.DEBUG,
|
|
||||||
handlers = [loki_handler, logging.StreamHandler()]
|
|
||||||
)
|
|
||||||
# we need to silence the debug logs made by the loki handler
|
|
||||||
logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)
|
|
||||||
else:
|
|
||||||
logging.basicConfig(
|
|
||||||
format = logging_format,
|
|
||||||
level = logging.INFO,
|
|
||||||
handlers = [loki_handler, logging.StreamHandler()]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# if we are in a debug (local) session, set verbose and rich logging
|
|
||||||
from rich.logging import RichHandler
|
|
||||||
logging.basicConfig(
|
|
||||||
format = logging_format,
|
|
||||||
level = logging.DEBUG,
|
|
||||||
handlers = [RichHandler()]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def uvicorn_run():
|
|
||||||
"""
|
|
||||||
Run the FastAPI application using uvicorn
|
|
||||||
"""
|
|
||||||
num_workers = int(os.getenv('NUM_WORKERS', 1))
|
|
||||||
logger.info(f"Starting FastAPI+uvicorn with {num_workers=}, {is_debug=}, {is_kubernetes=}")
|
|
||||||
uvicorn.run(
|
|
||||||
# we could in theory directly import the app and pass it as an object
|
|
||||||
# this 'import string' is required for hot reloading and scaling
|
|
||||||
'src.main:app',
|
|
||||||
host = '0.0.0.0',
|
|
||||||
port = 8000,
|
|
||||||
log_config = None,
|
|
||||||
access_log = True,
|
|
||||||
# Disable uvicorn's logging configuration so that it inherits the global one
|
|
||||||
workers = num_workers,
|
|
||||||
# Hot reload breaks logging, so we leave it disabled
|
|
||||||
# reload = is_debug
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
logger_setup()
|
|
||||||
uvicorn_run()
|
|
@ -1,29 +0,0 @@
|
|||||||
version: 1
|
|
||||||
disable_existing_loggers: False
|
|
||||||
formatters:
|
|
||||||
standard:
|
|
||||||
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
handlers:
|
|
||||||
console:
|
|
||||||
class: logging.StreamHandler
|
|
||||||
formatter: standard
|
|
||||||
level: DEBUG
|
|
||||||
rich:
|
|
||||||
class: rich.logging.RichHandler
|
|
||||||
level: DEBUG
|
|
||||||
loki:
|
|
||||||
class: loki_logger_handler.loki_logger_handler.LokiLoggerHandler
|
|
||||||
level: DEBUG
|
|
||||||
formatter: standard
|
|
||||||
url: ${LOKI_URL}
|
|
||||||
labels:
|
|
||||||
app: anyway
|
|
||||||
environment: ${ENVIRONMENT}
|
|
||||||
loggers:
|
|
||||||
uvicorn:
|
|
||||||
handlers: [console, rich, loki]
|
|
||||||
level: DEBUG
|
|
||||||
propagate: False
|
|
||||||
root:
|
|
||||||
handlers: [console, rich, loki]
|
|
||||||
level: DEBUG
|
|
56
backend/src/logging_config.py
Normal file
56
backend/src/logging_config.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""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 not 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
|
||||||
|
# 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
|
||||||
|
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from fastapi import FastAPI, HTTPException, Query
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from .logging_config import configure_logging
|
||||||
from .structs.landmark import Landmark, Toilets
|
from .structs.landmark import Landmark, Toilets
|
||||||
from .structs.preferences import Preferences
|
from .structs.preferences import Preferences
|
||||||
from .structs.linked_landmarks import LinkedLandmarks
|
from .structs.linked_landmarks import LinkedLandmarks
|
||||||
@ -13,15 +15,26 @@ from .utils.optimizer import Optimizer
|
|||||||
from .utils.refiner import Refiner
|
from .utils.refiner import Refiner
|
||||||
from .cache import client as cache_client
|
from .cache import client as cache_client
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
manager = LandmarkManager()
|
manager = LandmarkManager()
|
||||||
optimizer = Optimizer()
|
optimizer = Optimizer()
|
||||||
refiner = Refiner(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")
|
@app.post("/trip/new")
|
||||||
def new_trip(preferences: Preferences,
|
def new_trip(preferences: Preferences,
|
||||||
start: tuple[float, float],
|
start: tuple[float, float],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Definition of the Landmark class to handle visitable objects across the world."""
|
"""Definition of the Landmark class to handle visitable objects across the world."""
|
||||||
|
|
||||||
from typing import Optional, Literal
|
from typing import Optional, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4, UUID
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@ -29,12 +29,12 @@ class Landmark(BaseModel) :
|
|||||||
description (Optional[str]): A text description of the landmark.
|
description (Optional[str]): A text description of the landmark.
|
||||||
duration (Optional[int]): The estimated time to visit the landmark (in minutes).
|
duration (Optional[int]): The estimated time to visit the landmark (in minutes).
|
||||||
name_en (Optional[str]): The English name of the landmark.
|
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_do (Optional[bool]): Whether the landmark is a "must-do" attraction.
|
||||||
must_avoid (Optional[bool]): Whether the landmark should be avoided.
|
must_avoid (Optional[bool]): Whether the landmark should be avoided.
|
||||||
is_secondary (Optional[bool]): Whether the landmark is secondary or less important.
|
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.
|
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
|
# Properties of the landmark
|
||||||
@ -52,7 +52,7 @@ class Landmark(BaseModel) :
|
|||||||
name_en : Optional[str] = None
|
name_en : Optional[str] = None
|
||||||
|
|
||||||
# Unique ID of a given landmark
|
# Unique ID of a given landmark
|
||||||
uuid: str = Field(default_factory=uuid4)
|
uuid: UUID = Field(default_factory=uuid4)
|
||||||
|
|
||||||
# Additional properties depending on specific tour
|
# Additional properties depending on specific tour
|
||||||
must_do : Optional[bool] = False
|
must_do : Optional[bool] = False
|
||||||
@ -60,7 +60,7 @@ class Landmark(BaseModel) :
|
|||||||
is_secondary : Optional[bool] = False
|
is_secondary : Optional[bool] = False
|
||||||
|
|
||||||
time_to_reach_next : Optional[int] = 0
|
time_to_reach_next : Optional[int] = 0
|
||||||
next_uuid : Optional[str] = None
|
next_uuid : Optional[UUID] = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Definition of the Trip class."""
|
"""Definition of the Trip class."""
|
||||||
|
|
||||||
import uuid
|
from uuid import uuid4, UUID
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pymemcache.client.base import Client
|
from pymemcache.client.base import Client
|
||||||
|
|
||||||
@ -19,9 +19,9 @@ class Trip(BaseModel):
|
|||||||
Methods:
|
Methods:
|
||||||
from_linked_landmarks: create a Trip from LinkedLandmarks object.
|
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
|
total_time: int
|
||||||
first_landmark_uuid: str
|
first_landmark_uuid: UUID
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -31,7 +31,7 @@ class Trip(BaseModel):
|
|||||||
"""
|
"""
|
||||||
trip = Trip(
|
trip = Trip(
|
||||||
total_time = landmarks.total_time,
|
total_time = landmarks.total_time,
|
||||||
first_landmark_uuid = str(landmarks[0].uuid)
|
first_landmark_uuid = landmarks[0].uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store the trip in the cache
|
# Store the trip in the cache
|
||||||
|
Loading…
x
Reference in New Issue
Block a user