-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b287fe8
commit 678c012
Showing
12 changed files
with
1,245 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
"""NIAPI Lib.""" | ||
from __future__ import annotations | ||
|
||
from app.lib import cors, exceptions, log, openapi, schema, serialization, settings, static_files, template | ||
|
||
__all__ = ["settings", "schema", "log", "template", "static_files", "openapi", "cors", "exceptions", "serialization"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""NIAPI CORS config.""" | ||
from litestar.config.cors import CORSConfig | ||
|
||
from app.lib import settings | ||
|
||
config = CORSConfig(allow_origins=settings.app.BACKEND_CORS_ORIGINS) | ||
"""Default CORS config.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
"""NIAPI exception types. | ||
Also, defines functions that translate service and repository exceptions | ||
into HTTP exceptions. | ||
""" | ||
from __future__ import annotations | ||
|
||
import sys | ||
from typing import TYPE_CHECKING | ||
|
||
from litestar.contrib.repository.exceptions import ConflictError, NotFoundError, RepositoryError | ||
from litestar.exceptions import ( | ||
HTTPException, | ||
InternalServerException, | ||
NotFoundException, | ||
PermissionDeniedException, | ||
) | ||
from litestar.middleware.exceptions._debug_response import create_debug_response | ||
from litestar.middleware.exceptions.middleware import create_exception_response | ||
from litestar.status_codes import HTTP_409_CONFLICT, HTTP_500_INTERNAL_SERVER_ERROR | ||
from structlog.contextvars import bind_contextvars | ||
|
||
if TYPE_CHECKING: | ||
from typing import Any | ||
|
||
from litestar.connection import Request | ||
from litestar.middleware.exceptions.middleware import ExceptionResponseContent | ||
from litestar.response import Response | ||
from litestar.types import Scope | ||
|
||
__all__ = ( | ||
"AuthorizationError", | ||
"HealthCheckConfigurationError", | ||
"MissingDependencyError", | ||
"ApplicationError", | ||
"after_exception_hook_handler", | ||
) | ||
|
||
|
||
class ApplicationError(Exception): | ||
"""Base exception type for the lib's custom exception types.""" | ||
|
||
|
||
class ApplicationClientError(ApplicationError): | ||
"""Base exception type for client errors.""" | ||
|
||
|
||
class AuthorizationError(ApplicationClientError): | ||
"""A user tried to do something they shouldn't have.""" | ||
|
||
|
||
class MissingDependencyError(ApplicationError, ValueError): | ||
"""A required dependency is not installed.""" | ||
|
||
def __init__(self, module: str, config: str | None = None) -> None: | ||
"""Missing Dependency Error. | ||
Args: | ||
module: name of the package that should be installed | ||
config: name of the extra to install the package. | ||
""" | ||
config = config if config else module | ||
super().__init__( | ||
f"You enabled {config} configuration but package {module!r} is not installed. " | ||
f'You may need to run: "poetry install niapi[{config}]"', | ||
) | ||
|
||
|
||
class HealthCheckConfigurationError(ApplicationError): | ||
"""An error occurred while registering a health check.""" | ||
|
||
|
||
class _HTTPConflictException(HTTPException): | ||
"""Request conflict with the current state of the target resource.""" | ||
|
||
status_code = HTTP_409_CONFLICT | ||
|
||
|
||
async def after_exception_hook_handler(exc: Exception, _scope: Scope) -> None: | ||
"""Binds ``exc_info`` key with exception instance as value to structlog | ||
context vars. | ||
This must be a coroutine so that it is not wrapped in a thread where we'll lose context. | ||
Args: | ||
exc: the exception that was raised. | ||
_scope: scope of the request | ||
""" | ||
if isinstance(exc, ApplicationError): | ||
return | ||
if isinstance(exc, HTTPException) and exc.status_code < HTTP_500_INTERNAL_SERVER_ERROR: | ||
return | ||
bind_contextvars(exc_info=sys.exc_info()) | ||
|
||
|
||
def exception_to_http_response( | ||
request: Request[Any, Any, Any], | ||
exc: ApplicationError | RepositoryError, | ||
) -> Response[ExceptionResponseContent]: | ||
"""Transform repository exceptions to HTTP exceptions. | ||
Args: | ||
request: The request that experienced the exception. | ||
exc: Exception raised during handling of the request. | ||
Returns: | ||
Exception response appropriate to the type of original exception. | ||
""" | ||
http_exc: type[HTTPException] | ||
if isinstance(exc, NotFoundError): | ||
http_exc = NotFoundException | ||
elif isinstance(exc, ConflictError | RepositoryError): | ||
http_exc = _HTTPConflictException | ||
elif isinstance(exc, AuthorizationError): | ||
http_exc = PermissionDeniedException | ||
else: | ||
http_exc = InternalServerException | ||
if request.app.debug: | ||
return create_debug_response(request, exc) | ||
return create_exception_response(http_exc(detail=str(exc.__cause__))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
"""NIAPI Logging Configuration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import logging | ||
import sys | ||
from typing import TYPE_CHECKING | ||
|
||
import structlog | ||
from litestar.logging.config import LoggingConfig | ||
|
||
from app.lib import settings | ||
|
||
from . import controller | ||
from .utils import EventFilter, msgspec_json_renderer | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Sequence | ||
from typing import Any | ||
|
||
from structlog import BoundLogger | ||
from structlog.types import Processor | ||
|
||
__all__ = ( | ||
"default_processors", | ||
"stdlib_processors", | ||
"config", | ||
"configure", | ||
"controller", | ||
"get_logger", | ||
) | ||
|
||
|
||
default_processors = [ | ||
structlog.contextvars.merge_contextvars, | ||
controller.drop_health_logs, | ||
structlog.processors.add_log_level, | ||
structlog.processors.TimeStamper(fmt="iso", utc=True), | ||
] | ||
"""Default processors to apply to all loggers. See :mod:`structlog.processors` for more information.""" | ||
|
||
stdlib_processors = [ | ||
structlog.processors.TimeStamper(fmt="iso", utc=True), | ||
structlog.stdlib.add_log_level, | ||
structlog.stdlib.ExtraAdder(), | ||
EventFilter(["color_message"]), | ||
structlog.stdlib.ProcessorFormatter.remove_processors_meta, | ||
] | ||
"""Processors to apply to the stdlib logger. See :mod:`structlog.stdlib` for more information.""" | ||
|
||
if sys.stderr.isatty() or "pytest" in sys.modules: | ||
LoggerFactory: Any = structlog.WriteLoggerFactory | ||
console_processor = structlog.dev.ConsoleRenderer( | ||
colors=True, | ||
exception_formatter=structlog.dev.plain_traceback, | ||
) | ||
default_processors.extend([console_processor]) | ||
stdlib_processors.append(console_processor) | ||
else: | ||
LoggerFactory = structlog.BytesLoggerFactory | ||
default_processors.extend([msgspec_json_renderer]) | ||
|
||
|
||
def configure(processors: Sequence[Processor]) -> None: | ||
"""Call to configure `structlog` on app startup. | ||
The calls to :func:`get_logger() <structlog.get_logger()>` in :mod:`controller.py <app.lib.log.controller>` | ||
to the logger that is eventually called after this configurator function has been called. Therefore, nothing | ||
should try to log via structlog before this is called. | ||
Args: | ||
processors: A list of processors to apply to all loggers | ||
Returns: | ||
None | ||
""" | ||
structlog.configure( | ||
cache_logger_on_first_use=True, | ||
logger_factory=LoggerFactory(), | ||
processors=processors, | ||
wrapper_class=structlog.make_filtering_bound_logger(settings.log.LEVEL), | ||
) | ||
|
||
|
||
config = LoggingConfig( | ||
root={"level": logging.getLevelName(settings.log.LEVEL), "handlers": ["queue_listener"]}, | ||
formatters={ | ||
"standard": {"()": structlog.stdlib.ProcessorFormatter, "processors": stdlib_processors}, | ||
}, | ||
loggers={ | ||
"uvicorn.access": { | ||
"propagate": False, | ||
"level": settings.log.UVICORN_ACCESS_LEVEL, | ||
"handlers": ["queue_listener"], | ||
}, | ||
"uvicorn.error": { | ||
"propagate": False, | ||
"level": settings.log.UVICORN_ERROR_LEVEL, | ||
"handlers": ["queue_listener"], | ||
}, | ||
}, | ||
) | ||
"""Pre-configured log config for application deps. | ||
While we use structlog for internal app logging, we still want to ensure | ||
that logs emitted by any of our dependencies are handled in a non- | ||
blocking manner. | ||
""" | ||
|
||
|
||
def get_logger(*args: Any, **kwargs: Any) -> BoundLogger: | ||
"""Return a configured logger for the given name. | ||
Args: | ||
*args: Positional arguments to pass to :func:`get_logger() <structlog.get_logger()>` | ||
**kwargs: Keyword arguments to pass to :func:`get_logger() <structlog.get_logger()>` | ||
Returns: | ||
Logger: A configured logger instance | ||
""" | ||
config.configure() | ||
configure(default_processors) # type: ignore[arg-type] | ||
return structlog.getLogger(*args, **kwargs) # type: ignore |
Oops, something went wrong.