diff --git a/app/core/config.py b/app/config.py similarity index 93% rename from app/core/config.py rename to app/config.py index 681d4c3..c370680 100644 --- a/app/core/config.py +++ b/app/config.py @@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): - """Application settings following 12-factor app principles.""" + env: str = Field(default='DEV', alias="ENV") # Server settings host: str = Field(default="0.0.0.0", alias="HOST") diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/core/database.py b/app/database.py similarity index 97% rename from app/core/database.py rename to app/database.py index e192901..465171e 100644 --- a/app/core/database.py +++ b/app/database.py @@ -5,7 +5,7 @@ from sqlalchemy import MetaData, create_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker -from app.core.config import settings +from app.config import settings class Base(DeclarativeBase): diff --git a/app/logging.py b/app/logging.py new file mode 100644 index 0000000..c54770d --- /dev/null +++ b/app/logging.py @@ -0,0 +1,42 @@ +"""Logging configuration with structured logging.""" +import logging +import sys + +import structlog + +from app.config import settings + + +def configure_logging() -> None: + """Configure structured logging.""" + + structlog.configure( + processors=[ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.processors.JSONRenderer( + ) if not settings.debug else structlog.dev.ConsoleRenderer(), + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + # Configure standard logging + logging.basicConfig( + format="%(message)s", + stream=sys.stdout, + level=getattr(logging, settings.log_level.upper()), + ) + + +def get_logger(name: str) -> structlog.BoundLogger: + """Get a configured logger instance.""" + return structlog.get_logger(name) # type:ignore[no-any-return] diff --git a/app/middleware.py b/app/middleware.py new file mode 100644 index 0000000..0ab17e2 --- /dev/null +++ b/app/middleware.py @@ -0,0 +1,36 @@ +from typing import Callable + +from fastapi import Request, Response +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded +from slowapi.middleware import SlowAPIMiddleware +from slowapi.util import get_remote_address + +from app.config import settings +from app.logging import get_logger + +logger = get_logger(__name__) + +# Rate limiter +limiter = Limiter(key_func=get_remote_address) + + +async def logging_middleware(request: Request, call_next: Callable) -> Response: + """Log all requests and responses.""" + logger.debug( + "Request received", + method=request.method, + url=str(request.url), + client_ip=get_remote_address(request) + ) + + response = await call_next(request) + + logger.debug( + "Response sent", + status_code=response.status_code, + method=request.method, + url=str(request.url) + ) + + return response diff --git a/app/resources/health.py b/app/resources/health.py index 39f7062..1fdf6c8 100644 --- a/app/resources/health.py +++ b/app/resources/health.py @@ -1,7 +1,7 @@ """Health check endpoints.""" from fastapi import APIRouter -from app.core.database import DatabaseService +from app.database import DatabaseService router = APIRouter() diff --git a/main.py b/main.py index 43a13ce..cba66d7 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,38 @@ """ Main entry point for the Loan Operations API. """ -import uvicorn -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware +import sentry_sdk +from ddtrace import patch_all +from fastapi import APIRouter, FastAPI +from sentry_sdk.integrations.fastapi import FastApiIntegration -from app.core.config import settings +from app.config import settings +from app.logging import configure_logging, get_logger +from app.middleware import logging_middleware from app.resources import health def create_app() -> FastAPI: """Create and configure the FastAPI application.""" + # Configure monitoring + if settings.sentry_dsn: + sentry_sdk.init( + dsn=settings.sentry_dsn, + integrations=[FastApiIntegration()], + traces_sample_rate=1.0, + environment=settings.dd_env, + ) + + if settings.dd_service: + # Configure Datadog tracing + patch_all() + + # Configure logging + configure_logging() + logger = get_logger(__name__) + + # Create FastAPI app app = FastAPI( title="Loan Operations API", description="SBA Loan Operations API", @@ -28,7 +49,9 @@ def create_app() -> FastAPI: def main() -> None: """Run the application.""" - + + import uvicorn + uvicorn.run( app, host=settings.host,