Skip to content

Commit 31cd550

Browse files
fynnosbigabig
authored andcommitted
unify logging of exceptions in API
1 parent d89cb9c commit 31cd550

31 files changed

+1024
-1103
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fastapi import Request
2+
from fastapi.responses import PlainTextResponse
3+
from loguru import logger
4+
5+
exception_handlers = []
6+
7+
8+
def exception_handler(http_status_code: int):
9+
def decorator(exception_class):
10+
def handle_exception(req: Request, exc: Exception):
11+
logger.exception(exc)
12+
return PlainTextResponse(str(exc), status_code=http_status_code)
13+
14+
exception_handlers.append((exception_class, handle_exception))
15+
return exception_class
16+
17+
return decorator

backend/src/core/auth/authz_user.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
from typing import NoReturn
22

3-
from fastapi import Depends, Request
3+
from fastapi import Depends, Request, status
44
from loguru import logger
55
from sqlalchemy.orm import Session
66

77
from common.crud_enum import Crud
88
from common.dependencies import get_current_user, get_db_session
9+
from common.exception_handler import exception_handler
910
from core.user.user_orm import UserORM
1011
from repos.db.crud_base import NoSuchElementError
1112
from repos.db.orm_base import ORMBase
1213

1314

15+
@exception_handler(status.HTTP_403_FORBIDDEN)
1416
class ForbiddenError(Exception):
1517
def __init__(self):
1618
super().__init__("User is not authorized")

backend/src/core/auth/validation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
from typing import Sequence
22

3-
from fastapi import Depends
3+
from fastapi import Depends, status
44
from sqlalchemy.orm import Session
55

66
from common.crud_enum import Crud
77
from common.dependencies import get_db_session
8+
from common.exception_handler import exception_handler
89
from repos.db.crud_base import NoSuchElementError
910
from repos.db.orm_base import ORMBase
1011

1112

13+
@exception_handler(status.HTTP_400_BAD_REQUEST)
1214
class InvalidError(Exception):
1315
def __init__(self, note=None):
1416
note_description = f": {note}" if note is not None else ""

backend/src/core/doc/source_document_crud.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from fastapi import status
12
from fastapi.encoders import jsonable_encoder
23
from sqlalchemy import func
34
from sqlalchemy.exc import SQLAlchemyError
45
from sqlalchemy.orm import Session
56

7+
from common.exception_handler import exception_handler
68
from common.sdoc_status_enum import SDocStatus
79
from core.annotation.annotation_document_orm import AnnotationDocumentORM
810
from core.doc.folder_crud import crud_folder
@@ -22,6 +24,7 @@
2224
from systems.event_system.events import source_document_deleted
2325

2426

27+
@exception_handler(status.HTTP_500_INTERNAL_SERVER_ERROR)
2528
class SourceDocumentPreprocessingUnfinishedError(Exception):
2629
def __init__(self, sdoc_id: int):
2730
super().__init__(f"SourceDocument {sdoc_id} is still getting preprocessed!")

backend/src/main.py

Lines changed: 17 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import os
55
from contextlib import asynccontextmanager
6-
from http import HTTPStatus
76

87
from fastapi import FastAPI
98
from fastapi.middleware.cors import CORSMiddleware
@@ -17,8 +16,7 @@
1716
from starlette.middleware.sessions import SessionMiddleware
1817
from uvicorn.main import run
1918

20-
from modules.crawler.crawler_exceptions import NoDataToCrawlError
21-
from repos.elastic.elastic_crud_base import NoSuchObjectInElasticSearchError
19+
from common.exception_handler import exception_handlers
2220
from repos.elastic.elastic_repo import ElasticSearchRepo
2321
from repos.llm_repo import LLMRepo
2422
from utils.import_utils import import_by_suffix
@@ -40,23 +38,10 @@
4038
startup(reset_data=RESET_DATA, sql_echo=False)
4139
os.environ["STARTUP_DONE"] = "1"
4240

41+
from rq.exceptions import NoSuchJobError
42+
4343
from config import conf
44-
from core.auth.authz_user import ForbiddenError
45-
from core.auth.validation import InvalidError
46-
from core.doc.source_document_crud import SourceDocumentPreprocessingUnfinishedError
47-
from modules.eximport.export_service import (
48-
ExportJobPreparationError,
49-
NoSuchExportJobError,
50-
)
51-
from modules.eximport.import_service import ImportJobPreparationError
52-
from modules.eximport.no_data_export_error import NoDataToExportError
53-
from repos.db.crud_base import NoSuchElementError
54-
from repos.filesystem_repo import (
55-
FileAlreadyExistsInFilesystemError,
56-
FileNotFoundInFilesystemError,
57-
FilesystemRepo,
58-
SourceDocumentNotFoundInFilesystemError,
59-
)
44+
from repos.filesystem_repo import FilesystemRepo
6045

6146
# import all jobs dynamically
6247
import_by_suffix("_job.py")
@@ -126,100 +111,32 @@ def custom_openapi():
126111
app.add_middleware(SessionMiddleware, secret_key=conf.api.auth.session.secret)
127112

128113

129-
# add custom exception handlers
130-
# TODO Flo: find a better place for this! (and Exceptions in general. move into own file)
131-
@app.exception_handler(NoSuchElementError)
132-
async def no_such_element_error_handler(_, exc: NoSuchElementError):
133-
return PlainTextResponse(str(exc), status_code=404)
134-
135-
136-
@app.exception_handler(NoDataToCrawlError)
137-
async def no_data_to_crawl_handler(_, exc: NoDataToCrawlError):
138-
return PlainTextResponse(str(exc), status_code=400)
139-
140-
141-
@app.exception_handler(NoDataToExportError)
142-
async def no_data_to_export_handler(_, exc: NoDataToExportError):
143-
return PlainTextResponse(str(exc), status_code=404)
144-
145-
146-
@app.exception_handler(NoSuchExportJobError)
147-
async def no_such_export_job_handler(_, exc: NoSuchExportJobError):
148-
return PlainTextResponse(str(exc), status_code=404)
149-
150-
151-
@app.exception_handler(ExportJobPreparationError)
152-
async def export_job_preparation_error_handler(_, exc: ExportJobPreparationError):
153-
return PlainTextResponse(str(exc), status_code=500)
154-
155-
156-
@app.exception_handler(ImportJobPreparationError)
157-
async def import_job_preparation_error_handler(_, exc: ImportJobPreparationError):
158-
return PlainTextResponse(str(exc), status_code=500)
159-
160-
161-
@app.exception_handler(NoSuchObjectInElasticSearchError)
162-
async def no_such_object_in_es_error_handler(_, exc: NoSuchObjectInElasticSearchError):
163-
return PlainTextResponse(str(exc), status_code=500)
164-
165-
166-
@app.exception_handler(SourceDocumentNotFoundInFilesystemError)
167-
async def source_document_not_found_in_filesystem_error_handler(
168-
_, exc: SourceDocumentNotFoundInFilesystemError
169-
):
170-
return PlainTextResponse(str(exc), status_code=500)
171-
172-
173-
@app.exception_handler(SourceDocumentPreprocessingUnfinishedError)
174-
async def source_document_preprocessing_unfinished_error_handler(
175-
_, exc: SourceDocumentPreprocessingUnfinishedError
176-
):
177-
return PlainTextResponse(str(exc), status_code=500)
178-
179-
180-
@app.exception_handler(FileNotFoundInFilesystemError)
181-
async def file_not_found_in_filesystem_error_handler(
182-
_, exc: FileNotFoundInFilesystemError
183-
):
184-
return PlainTextResponse(str(exc), status_code=500)
185-
186-
187-
@app.exception_handler(FileAlreadyExistsInFilesystemError)
188-
async def file_already_exists_in_filesystem_error_handler(
189-
_, exc: FileAlreadyExistsInFilesystemError
190-
):
191-
return PlainTextResponse(str(exc), status_code=406)
114+
# import & register all endpoints dynamically
115+
endpoint_modules = import_by_suffix("_endpoint.py")
116+
endpoint_modules.sort(key=lambda x: x.__name__.split(".")[-1])
117+
for em in endpoint_modules:
118+
app.include_router(em.router)
192119

193120

194121
@app.exception_handler(IntegrityError)
195122
async def integrity_error_handler(_, exc: IntegrityError):
123+
logger.exception(exc)
196124
if isinstance(exc.orig, UniqueViolation):
197125
msg = str(exc.orig.pgerror).split("\n")[1]
198126
return PlainTextResponse(msg, status_code=409)
199127
else:
200128
return PlainTextResponse(str(exc), status_code=500)
201129

202130

203-
@app.exception_handler(ForbiddenError)
204-
def forbidden_error_handler(_, exc: ForbiddenError):
205-
return PlainTextResponse(str(exc), status_code=403)
206-
207-
208-
@app.exception_handler(InvalidError)
209-
def invalid_error_handler(_, exc: InvalidError):
210-
return PlainTextResponse(str(exc), status_code=HTTPStatus.BAD_REQUEST)
211-
131+
@app.exception_handler(NoSuchJobError)
132+
async def no_such_job_error_handler(_, exc: NoSuchJobError):
133+
logger.exception(exc)
134+
return PlainTextResponse(str(exc), status_code=404)
212135

213-
# import & register all exception handlers dynamically
214-
exception_hanlders = import_by_suffix("_exception_handler.py")
215-
for eh in exception_hanlders:
216-
eh.register_exception_handlers(app)
217136

218-
# import & register all endpoints dynamically
219-
endpoint_modules = import_by_suffix("_endpoint.py")
220-
endpoint_modules.sort(key=lambda x: x.__name__.split(".")[-1])
221-
for em in endpoint_modules:
222-
app.include_router(em.router)
137+
# register all exception handlers in fastAPI
138+
for ex_class, handler_func in exception_handlers:
139+
app.add_exception_handler(ex_class, handler_func)
223140

224141

225142
def main() -> None:

backend/src/modules/crawler/crawler_exceptions.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

backend/src/modules/crawler/crawler_job.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,29 @@
22
import subprocess
33
from pathlib import Path
44

5+
from fastapi import status
56
from loguru import logger
67
from pydantic import Field
78

9+
from common.exception_handler import exception_handler
810
from common.job_type import JobType
911
from core.project.project_crud import crud_project
10-
from modules.crawler.crawler_exceptions import NoDataToCrawlError
1112
from modules.doc_processing.doc_processing_dto import ProcessingJobInput
1213
from repos.db.sql_repo import SQLRepo
1314
from repos.filesystem_repo import FilesystemRepo
14-
from systems.job_system.job_dto import (
15-
EndpointGeneration,
16-
Job,
17-
JobOutputBase,
18-
JobTiming,
19-
)
15+
from systems.job_system.job_dto import EndpointGeneration, Job, JobOutputBase, JobTiming
2016
from systems.job_system.job_register_decorator import register_job
2117

2218
sqlr = SQLRepo()
2319
fsr: FilesystemRepo = FilesystemRepo()
2420

2521

22+
@exception_handler(status.HTTP_400_BAD_REQUEST)
23+
class NoDataToCrawlError(Exception):
24+
def __init__(self, what_msg: str):
25+
super().__init__(what_msg)
26+
27+
2628
class CrawlerJobInput(ProcessingJobInput):
2729
project_id: int = Field(
2830
description="The ID of the Project to import the crawled data."

backend/src/modules/eximport/bbox_annotations/export_bbox_annotations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
BBoxAnnotationExportCollection,
1111
BBoxAnnotationExportSchema,
1212
)
13-
from modules.eximport.no_data_export_error import NoDataToExportError
13+
from modules.eximport.export_exceptions import NoDataToExportError
1414
from repos.filesystem_repo import FilesystemRepo
1515

1616

backend/src/modules/eximport/codes/export_codes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
CodeExportCollection,
1111
CodeExportSchema,
1212
)
13-
from modules.eximport.no_data_export_error import NoDataToExportError
13+
from modules.eximport.export_exceptions import NoDataToExportError
1414
from repos.filesystem_repo import FilesystemRepo
1515

1616

backend/src/modules/eximport/cota/export_cota.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,9 @@
66
from loguru import logger
77
from sqlalchemy.orm import Session
88

9-
from modules.concept_over_time_analysis.cota_crud import (
10-
crud_cota,
11-
)
12-
from modules.concept_over_time_analysis.cota_dto import (
13-
COTARead,
14-
)
15-
from modules.concept_over_time_analysis.cota_orm import (
16-
ConceptOverTimeAnalysisORM,
17-
)
9+
from modules.concept_over_time_analysis.cota_crud import crud_cota
10+
from modules.concept_over_time_analysis.cota_dto import COTARead
11+
from modules.concept_over_time_analysis.cota_orm import ConceptOverTimeAnalysisORM
1812
from modules.eximport.cota.cota_export_schema import (
1913
COTAExportCollection,
2014
COTAExportSchema,
@@ -24,7 +18,7 @@
2418
transform_timeline_settings_for_export,
2519
transform_training_settings_for_export,
2620
)
27-
from modules.eximport.no_data_export_error import NoDataToExportError
21+
from modules.eximport.export_exceptions import NoDataToExportError
2822
from repos.filesystem_repo import FilesystemRepo
2923

3024

0 commit comments

Comments
 (0)