Version 0.4.0
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
Changes: - put ObservabilityMiddleware before ExceptionHandlerMiddleware to avoid repetative code - add application startup and last metrics update metrics along with CPU usage metric and threads count - move host and port to new uvicorn section at config along with new reload and forwarded_allow_ips - add request_id and remove trace_id/span_id generation if tracing is disabled - move logging logic from utils to observability - pass trace_id/span_id in HEX form
This commit is contained in:
@@ -3,13 +3,17 @@
|
||||
import itertools
|
||||
import traceback
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.metrics import Counter
|
||||
from opentelemetry.semconv.attributes import exception_attributes, http_attributes, url_attributes
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
from {{project_slug}}.dependencies import logger_dep
|
||||
from {{project_slug}}.exceptions.mapper import ExceptionMapper
|
||||
|
||||
from .observability import ObservableException
|
||||
from {{project_slug}}.observability.utils import URLsMapper
|
||||
|
||||
|
||||
class ExceptionHandlerMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few-public-methods
|
||||
@@ -22,31 +26,67 @@ class ExceptionHandlerMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few
|
||||
def __init__(
|
||||
self,
|
||||
app: FastAPI,
|
||||
debug: list[bool],
|
||||
*,
|
||||
debug: bool,
|
||||
exception_mapper: ExceptionMapper,
|
||||
):
|
||||
urls_mapper: URLsMapper,
|
||||
errors_metric: Counter,
|
||||
): # pylint: disable=too-many-arguments
|
||||
"""Passing debug as a list with single element is a hack to be able to change the value on the fly."""
|
||||
super().__init__(app)
|
||||
self._debug = debug
|
||||
self._mapper = exception_mapper
|
||||
self._exception_mapper = exception_mapper
|
||||
self._urls_mapper = urls_mapper
|
||||
self._metric = errors_metric
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
try:
|
||||
return await call_next(request)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
additional_headers: dict[str, str] | None = None
|
||||
if isinstance(exc, ObservableException):
|
||||
additional_headers = {"X-Trace-Id": exc.trace_id, "X-Span-Id": str(exc.span_id)}
|
||||
exc = exc.__cause__
|
||||
status_code = 500
|
||||
detail = "exception occured"
|
||||
|
||||
if isinstance(exc, HandlerNotFoundError):
|
||||
exc = exc.__cause__
|
||||
|
||||
cause = exc
|
||||
if isinstance(exc, HTTPException):
|
||||
status_code = exc.status_code # pylint: disable=no-member
|
||||
detail = exc.detail # pylint: disable=no-member
|
||||
if exc.__cause__ is not None:
|
||||
cause = exc.__cause__
|
||||
|
||||
if self._debug[0]:
|
||||
if (res := self._mapper.apply_if_known(exc)) is not None:
|
||||
self._metric.add(
|
||||
1,
|
||||
{
|
||||
http_attributes.HTTP_REQUEST_METHOD: request.method,
|
||||
url_attributes.URL_PATH: self._urls_mapper.map(request.method, request.url.path),
|
||||
"error_type": type(cause).__qualname__,
|
||||
http_attributes.HTTP_RESPONSE_STATUS_CODE: status_code,
|
||||
},
|
||||
)
|
||||
|
||||
span = trace.get_current_span()
|
||||
logger = logger_dep.from_request(request)
|
||||
|
||||
is_known = self._exception_mapper.is_known(cause)
|
||||
span.record_exception(cause, {"is_known": is_known})
|
||||
if is_known:
|
||||
log_func = logger.aerror
|
||||
else:
|
||||
log_func = logger.aexception
|
||||
await log_func("failed to handle request", error_type=type(cause).__name__)
|
||||
span.set_status(trace.StatusCode.ERROR)
|
||||
span.set_attributes(
|
||||
{
|
||||
exception_attributes.EXCEPTION_TYPE: type(cause).__name__,
|
||||
exception_attributes.EXCEPTION_MESSAGE: repr(cause),
|
||||
http_attributes.HTTP_RESPONSE_STATUS_CODE: status_code,
|
||||
}
|
||||
)
|
||||
|
||||
if self._debug:
|
||||
if (res := self._exception_mapper.apply_if_known(exc)) is not None:
|
||||
response = res
|
||||
else:
|
||||
response = JSONResponse(
|
||||
@@ -56,18 +96,23 @@ class ExceptionHandlerMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few
|
||||
"error": str(exc),
|
||||
"error_type": type(exc).__name__,
|
||||
"path": request.url.path,
|
||||
"params": request.url.query,
|
||||
"query_params": request.url.query,
|
||||
"tracebacks": _get_tracebacks(exc),
|
||||
},
|
||||
status_code=status_code,
|
||||
)
|
||||
else:
|
||||
response = self._mapper.apply(exc)
|
||||
if additional_headers is not None:
|
||||
response.headers.update(additional_headers)
|
||||
response = self._exception_mapper.apply(exc)
|
||||
return response
|
||||
|
||||
|
||||
class HandlerNotFoundError(Exception):
|
||||
"""Exception to raise on FastAPI 404 handler (only for situation when no handler was found for request).
|
||||
|
||||
Guranteed to have `.__cause__` as its parent exception.
|
||||
"""
|
||||
|
||||
|
||||
def _get_tracebacks(exc: Exception) -> list[list[str]]:
|
||||
tracebacks: list[list[str]] = []
|
||||
while exc is not None:
|
||||
|
||||
Reference in New Issue
Block a user