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

1"""FastAPI application factory with simplified ServiceConfig (v2).""" 

2 

3from collections.abc import AsyncGenerator, Callable 

4from contextlib import asynccontextmanager 

5from typing import TYPE_CHECKING 

6 

7from beanie import Document 

8from fastapi import FastAPI 

9from fastapi.middleware.cors import CORSMiddleware 

10from fastapi.routing import APIRoute 

11 

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 

19 

20if TYPE_CHECKING: 

21 pass 

22 

23setup_logging() 

24logger = get_structured_logger(__name__) 

25 

26 

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}" 

31 

32 

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.""" 

39 

40 @asynccontextmanager 

41 async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 

42 # Startup 

43 startup_tasks = [] 

44 

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) 

51 

52 # Ensure AuditLog is included when audit logging is enabled 

53 if service_config.enable_audit_logging: 

54 from .audit import AuditLog 

55 

56 if AuditLog not in models_to_init: 

57 models_to_init.append(AuditLog) 

58 

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 

66 

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 ) 

78 

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 ) 

93 

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() 

100 

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 ) 

113 

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 ) 

123 

124 # Store startup tasks in app state 

125 app.state.startup_tasks = startup_tasks 

126 

127 # Run custom lifespan if provided 

128 if service_config.lifespan: 

129 async with service_config.lifespan(app): 

130 yield 

131 else: 

132 yield 

133 

134 # Shutdown 

135 logger.info("🛑 Starting application shutdown...") 

136 

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}") 

145 

146 logger.info("👋 Application shutdown completed") 

147 

148 return lifespan 

149 

150 

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 ) 

168 

169 # Check if we're in development 

170 is_development = settings.ENVIRONMENT in ["development", "local"] 

171 

172 # Create lifespan 

173 lifespan_func = create_lifespan(service_config, document_models, is_development) 

174 

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 ) 

186 

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 ) 

197 

198 # Add authentication middleware (개선된 조건부 적용) 

199 if service_config.enable_auth: 

200 try: 

201 from ..auth.middleware import AuthMiddleware 

202 

203 app.add_middleware(AuthMiddleware, service_config=service_config) 

204 

205 auth_status = "enabled" 

206 if is_development: 

207 auth_status += " (development mode - fallback authentication available)" 

208 

209 logger.info( 

210 f"🔐 Authentication middleware {auth_status} for {service_config.service_name}" 

211 ) 

212 

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 ) 

219 

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}") 

228 

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 ) 

239 

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 ) 

248 

249 # 제외할 경로 설정 (성능 최적화) 

250 exclude_paths = { 

251 "/health", 

252 "/metrics", 

253 "/docs", 

254 "/redoc", 

255 "/openapi.json", 

256 "/favicon.ico", 

257 "/robots.txt", 

258 } 

259 

260 # Initialize metrics collector first 

261 create_metrics_middleware( 

262 service_config.service_name, 

263 config=metrics_config, 

264 exclude_paths=exclude_paths, 

265 ) 

266 

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 ) 

276 

277 # Add metrics router 

278 metrics_router = create_metrics_router() 

279 app.include_router(metrics_router) 

280 

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 ) 

292 

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}") 

300 

301 # Add audit logging middleware (shared) 

302 if service_config.enable_audit_logging: 

303 try: 

304 from .audit.middleware import AuditLoggingMiddleware 

305 

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 ) 

319 

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 

323 

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}") 

327 

328 # Include OAuth2 routers if enabled (IAM only) 

329 if service_config.enable_oauth: 

330 try: 

331 from ..auth.router import oauth2_router 

332 

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}") 

340 

341 logger.info( 

342 f"🔐 Auth Public Paths for {service_config.service_name}: {settings.AUTH_PUBLIC_PATHS}" 

343 ) 

344 

345 return app