Coverage for src / mysingle / core / app_factory.py: 0%
141 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1"""FastAPI application factory with simplified ServiceConfig (v2)."""
3from collections.abc import AsyncGenerator, Callable
4from contextlib import asynccontextmanager
5from typing import TYPE_CHECKING
7from beanie import Document
8from fastapi import FastAPI
9from fastapi.middleware.cors import CORSMiddleware
10from fastapi.routing import APIRoute
12from ..auth.exception_handlers import register_auth_exception_handlers
13from ..auth.init_data import create_first_super_admin, create_test_users
14from .config import settings
15from .db import init_mongo
16from .health import create_health_router
17from .logging import get_structured_logger, setup_logging
18from .service_types import ServiceConfig, ServiceType
20if TYPE_CHECKING:
21 pass
23setup_logging()
24logger = get_structured_logger(__name__)
27def custom_generate_unique_id(route: APIRoute) -> str:
28 """Generate unique ID for each route based on its tags and name."""
29 tag = route.tags[0] if route.tags else "default"
30 return f"{tag}-{route.name}"
33def create_lifespan(
34 service_config: ServiceConfig,
35 document_models: list[type[Document]] | None = None,
36 is_development: bool = False,
37) -> Callable:
38 """Create lifespan context manager for the application."""
40 @asynccontextmanager
41 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
42 # Startup
43 startup_tasks = []
45 # Initialize database if enabled
46 if service_config.enable_database:
47 # Prepare models list (copy provided list or start empty)
48 models_to_init: list[type[Document]] = []
49 if document_models:
50 models_to_init.extend(document_models)
52 # Ensure AuditLog is included when audit logging is enabled
53 if service_config.enable_audit_logging:
54 from .audit import AuditLog
56 if AuditLog not in models_to_init:
57 models_to_init.append(AuditLog)
59 # Ensure auth models are included ONLY for IAM service
60 # NON_IAM services use Kong Gateway auth and don't need User/OAuthAccount collections
61 if (
62 service_config.enable_auth
63 and service_config.service_type == ServiceType.IAM_SERVICE
64 ):
65 from ..auth.models import OAuthAccount, User
67 auth_models = [User, OAuthAccount]
68 for model in auth_models:
69 if model not in models_to_init:
70 models_to_init.append(model)
71 logger.info(
72 f"📦 IAM Service: Added User and OAuthAccount models for {service_config.service_name}"
73 )
74 elif service_config.enable_auth:
75 logger.info(
76 f"⏭️ Non-IAM Service: Skipping User/OAuthAccount models for {service_config.service_name}"
77 )
79 if models_to_init:
80 try:
81 client = await init_mongo(
82 models_to_init,
83 (
84 service_config.database_name
85 if service_config.database_name
86 else service_config.service_name
87 ),
88 )
89 startup_tasks.append(("mongodb_client", client))
90 logger.info(
91 f"✅ Connected to MongoDB for {service_config.database_name or service_config.service_name}"
92 )
94 # Create first super admin and test users (IAM service only)
95 if service_config.service_type == ServiceType.IAM_SERVICE:
96 logger.info(
97 f"� IAM Service: Creating super admin and test users for {service_config.service_name}"
98 )
99 await create_first_super_admin()
101 # Test users only in development/local environments
102 if is_development:
103 await create_test_users()
104 logger.info("👥 Test users created (development mode)")
105 else:
106 logger.info(
107 "⏭️ Skipping test user creation (production mode)"
108 )
109 else:
110 logger.info(
111 f"⏭️ Non-IAM Service: Skipping user creation for {service_config.service_name}"
112 )
114 except Exception as e:
115 logger.error(f"❌ Failed to connect to MongoDB: {e}")
116 if not settings.MOCK_DATABASE:
117 raise
118 logger.warning("🔄 Running with mock database")
119 else:
120 logger.info(
121 f"ℹ️ No document models configured; skipping Mongo initialization for {service_config.service_name}"
122 )
124 # Store startup tasks in app state
125 app.state.startup_tasks = startup_tasks
127 # Run custom lifespan if provided
128 if service_config.lifespan:
129 async with service_config.lifespan(app):
130 yield
131 else:
132 yield
134 # Shutdown
135 logger.info("🛑 Starting application shutdown...")
137 # MongoDB 연결 정리
138 for task_name, task_obj in startup_tasks:
139 if task_name == "mongodb_client":
140 try:
141 task_obj.close()
142 logger.info("✅ Disconnected from MongoDB")
143 except Exception as e:
144 logger.error(f"⚠️ Error disconnecting from MongoDB: {e}")
146 logger.info("👋 Application shutdown completed")
148 return lifespan
151def create_fastapi_app(
152 service_config: ServiceConfig,
153 document_models: list[type[Document]] | None = None,
154) -> FastAPI:
155 """
156 Create a standardized FastAPI application with simplified ServiceConfig.
157 """
158 # Application metadata
159 app_title = (
160 f"{settings.PROJECT_NAME} - "
161 f"{(service_config.service_name).replace('_', ' ').title()} "
162 f"[{(settings.ENVIRONMENT).capitalize()}]"
163 )
164 app_description = (
165 service_config.description
166 or f"{service_config.service_name} for Quant Platform"
167 )
169 # Check if we're in development
170 is_development = settings.ENVIRONMENT in ["development", "local"]
172 # Create lifespan
173 lifespan_func = create_lifespan(service_config, document_models, is_development)
175 # Create FastAPI app
176 app = FastAPI(
177 title=app_title,
178 description=app_description,
179 version=service_config.service_version,
180 generate_unique_id_function=custom_generate_unique_id,
181 lifespan=lifespan_func,
182 docs_url="/docs" if is_development else None,
183 redoc_url="/redoc" if is_development else None,
184 openapi_url="/openapi.json" if is_development else None,
185 )
187 # Add CORS middleware
188 final_cors_origins = service_config.cors_origins or settings.all_cors_origins
189 if final_cors_origins:
190 app.add_middleware(
191 CORSMiddleware,
192 allow_origins=final_cors_origins,
193 allow_credentials=True,
194 allow_methods=["*"],
195 allow_headers=["*"],
196 )
198 # Add authentication middleware (개선된 조건부 적용)
199 if service_config.enable_auth:
200 try:
201 from ..auth.middleware import AuthMiddleware
203 app.add_middleware(AuthMiddleware, service_config=service_config)
205 auth_status = "enabled"
206 if is_development:
207 auth_status += " (development mode - fallback authentication available)"
209 logger.info(
210 f"🔐 Authentication middleware {auth_status} for {service_config.service_name}"
211 )
213 # Register auth exception handlers for ALL services with auth enabled
214 # Both IAM and Non-IAM services need proper 401/403 error handling
215 register_auth_exception_handlers(app)
216 logger.info(
217 f"🔐 Auth exception handlers registered for {service_config.service_name}"
218 )
220 except ImportError as e:
221 logger.warning(f"⚠️ Authentication middleware not available: {e}")
222 except Exception as e:
223 logger.error(f"❌ Failed to add authentication middleware: {e}")
224 if not is_development:
225 raise # 프로덕션에서는 인증 실패 시 앱 시작 중단
226 else:
227 logger.info(f"🔓 Authentication disabled for {service_config.service_name}")
229 # Add metrics middleware with graceful fallback
230 if service_config.enable_metrics:
231 try:
232 from .metrics import (
233 MetricsConfig,
234 MetricsMiddleware,
235 create_metrics_middleware,
236 create_metrics_router,
237 get_metrics_collector,
238 )
240 # 메트릭 설정 생성 (개선된 기본값)
241 metrics_config = MetricsConfig(
242 max_duration_samples=1000,
243 enable_percentiles=True,
244 enable_histogram=True,
245 retention_period_seconds=3600, # 1시간
246 cleanup_interval_seconds=300, # 5분
247 )
249 # 제외할 경로 설정 (성능 최적화)
250 exclude_paths = {
251 "/health",
252 "/metrics",
253 "/docs",
254 "/redoc",
255 "/openapi.json",
256 "/favicon.ico",
257 "/robots.txt",
258 }
260 # Initialize metrics collector first
261 create_metrics_middleware(
262 service_config.service_name,
263 config=metrics_config,
264 exclude_paths=exclude_paths,
265 )
267 # Add middleware with collector
268 collector = get_metrics_collector()
269 app.add_middleware(
270 MetricsMiddleware,
271 collector=collector,
272 exclude_paths=exclude_paths,
273 include_response_headers=is_development, # 개발 환경에서만 헤더 추가
274 track_user_agents=False, # 성능을 위해 기본적으로 비활성화
275 )
277 # Add metrics router
278 metrics_router = create_metrics_router()
279 app.include_router(metrics_router)
281 logger.info(
282 f"📊 Enhanced metrics middleware and endpoints enabled for {service_config.service_name}"
283 )
284 except ImportError:
285 logger.warning(
286 f"⚠️ Metrics middleware not available for {service_config.service_name}"
287 )
288 except Exception as e:
289 logger.warning(
290 f"⚠️ Failed to add metrics middleware for {service_config.service_name}: {e}"
291 )
293 # Add health check endpoints
294 if service_config.enable_health_check:
295 health_router = create_health_router(
296 service_config.service_name, service_config.service_version
297 )
298 app.include_router(health_router)
299 logger.info(f"❤️ Health check endpoints added for {service_config.service_name}")
301 # Add audit logging middleware (shared)
302 if service_config.enable_audit_logging:
303 try:
304 from .audit.middleware import AuditLoggingMiddleware
306 enabled_flag = getattr(settings, "AUDIT_LOGGING_ENABLED", True)
307 app.add_middleware(
308 AuditLoggingMiddleware,
309 service_name=service_config.service_name,
310 enabled=bool(enabled_flag),
311 )
312 logger.info(
313 f"📝 Audit logging middleware enabled for {service_config.service_name}"
314 )
315 except Exception as e:
316 logger.warning(
317 f"⚠️ Failed to add audit logging middleware for {service_config.service_name}: {e}"
318 )
320 # Include auth routers only for IAM service
321 if service_config.service_type == ServiceType.IAM_SERVICE:
322 from ..auth.router import auth_router, user_router
324 app.include_router(auth_router, prefix="/api/v1/auth", tags=["Auth"])
325 app.include_router(user_router, prefix="/api/v1/users", tags=["User"])
326 logger.info(f"🔐 Auth routes added for {service_config.service_name}")
328 # Include OAuth2 routers if enabled (IAM only)
329 if service_config.enable_oauth:
330 try:
331 from ..auth.router import oauth2_router
333 app.include_router(
334 oauth2_router,
335 prefix="/api/v1",
336 )
337 logger.info(f"🔐 OAuth2 routes added for {service_config.service_name}")
338 except Exception as e:
339 logger.error(f"⚠️ Failed to include OAuth2 router: {e}")
341 logger.info(
342 f"🔐 Auth Public Paths for {service_config.service_name}: {settings.AUTH_PUBLIC_PATHS}"
343 )
345 return app