""" FastAPI 应用主入口 集成 Webhook 处理、防抖、队列管理等服务 """ import asyncio from contextlib import asynccontextmanager from typing import Dict, Any import structlog from fastapi import FastAPI, Request, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, Response from redis import asyncio as aioredis from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST from app.config import get_settings from app.services.dedup_service import DeduplicationService from app.services.jenkins_service import JenkinsService from app.services.webhook_service import WebhookService from app.tasks.jenkins_tasks import get_celery_app # 路由导入将在运行时动态处理 # 配置结构化日志 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() ], context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) logger = structlog.get_logger() # 监控指标 WEBHOOK_REQUESTS_TOTAL = Counter( "webhook_requests_total", "Total number of webhook requests", ["status", "environment"] ) WEBHOOK_REQUEST_DURATION = Histogram( "webhook_request_duration_seconds", "Webhook request duration in seconds", ["environment"] ) QUEUE_SIZE = Gauge( "queue_size", "Current queue size", ["queue_type"] ) DEDUP_HITS = Counter( "dedup_hits_total", "Total number of deduplication hits" ) # 全局服务实例 dedup_service: DeduplicationService = None jenkins_service: JenkinsService = None webhook_service: WebhookService = None celery_app = None redis_client: aioredis.Redis = None @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" global dedup_service, jenkins_service, webhook_service, celery_app, redis_client # 启动时初始化 logger.info("Starting Gitea Webhook Ambassador") try: # 初始化 Redis 连接 settings = get_settings() redis_client = aioredis.from_url( settings.redis.url, password=settings.redis.password, db=settings.redis.db, encoding="utf-8", decode_responses=True ) # 测试 Redis 连接 await redis_client.ping() logger.info("Redis connection established") # 初始化 Celery celery_app = get_celery_app() # 初始化服务 dedup_service = DeduplicationService(redis_client) jenkins_service = JenkinsService() webhook_service = WebhookService( dedup_service=dedup_service, jenkins_service=jenkins_service, celery_app=celery_app ) logger.info("All services initialized successfully") yield except Exception as e: logger.error("Failed to initialize services", error=str(e)) raise finally: # 关闭时清理 logger.info("Shutting down Gitea Webhook Ambassador") if redis_client: await redis_client.close() logger.info("Redis connection closed") # 创建 FastAPI 应用 app = FastAPI( title="Gitea Webhook Ambassador", description="高性能的 Gitea 到 Jenkins 的 Webhook 服务", version="1.0.0", lifespan=lifespan ) # 添加 CORS 中间件 app.add_middleware( CORSMiddleware, allow_origins=["*"], # 生产环境应该限制具体域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 依赖注入 def get_dedup_service() -> DeduplicationService: if dedup_service is None: raise HTTPException(status_code=503, detail="Deduplication service not available") return dedup_service def get_webhook_service() -> WebhookService: if webhook_service is None: raise HTTPException(status_code=503, detail="Webhook service not available") return webhook_service def get_celery_app_dep(): if celery_app is None: raise HTTPException(status_code=503, detail="Celery app not available") return celery_app # 中间件 @app.middleware("http") async def log_requests(request: Request, call_next): """请求日志中间件""" start_time = asyncio.get_event_loop().time() # 记录请求开始 logger.info("Request started", method=request.method, url=str(request.url), client_ip=request.client.host if request.client else None) try: response = await call_next(request) # 记录请求完成 process_time = asyncio.get_event_loop().time() - start_time logger.info("Request completed", method=request.method, url=str(request.url), status_code=response.status_code, process_time=process_time) return response except Exception as e: # 记录请求错误 process_time = asyncio.get_event_loop().time() - start_time logger.error("Request failed", method=request.method, url=str(request.url), error=str(e), process_time=process_time) raise @app.middleware("http") async def add_security_headers(request: Request, call_next): """添加安全头""" response = await call_next(request) # 添加安全相关的 HTTP 头 response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-XSS-Protection"] = "1; mode=block" response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" return response # 异常处理器 @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): """全局异常处理器""" logger.error("Unhandled exception", method=request.method, url=str(request.url), error=str(exc), exc_info=True) return JSONResponse( status_code=500, content={ "success": False, "message": "Internal server error", "error": str(exc) if get_settings().debug else "An unexpected error occurred" } ) # 健康检查端点 @app.get("/health") async def health_check(): """基础健康检查""" try: # 检查 Redis 连接 if redis_client: await redis_client.ping() redis_healthy = True else: redis_healthy = False # 检查 Celery 连接 if celery_app: inspect = celery_app.control.inspect() celery_healthy = bool(inspect.active() is not None) else: celery_healthy = False return { "status": "healthy" if redis_healthy and celery_healthy else "unhealthy", "timestamp": asyncio.get_event_loop().time(), "services": { "redis": "healthy" if redis_healthy else "unhealthy", "celery": "healthy" if celery_healthy else "unhealthy" } } except Exception as e: logger.error("Health check failed", error=str(e)) return JSONResponse( status_code=503, content={ "status": "unhealthy", "error": str(e) } ) @app.get("/health/queue") async def queue_health_check(): """队列健康检查""" try: if celery_app is None: return JSONResponse( status_code=503, content={"status": "unhealthy", "error": "Celery not available"} ) inspect = celery_app.control.inspect() # 获取队列统计 active = inspect.active() reserved = inspect.reserved() registered = inspect.registered() active_count = sum(len(tasks) for tasks in (active or {}).values()) reserved_count = sum(len(tasks) for tasks in (reserved or {}).values()) worker_count = len(registered or {}) # 更新监控指标 QUEUE_SIZE.labels(queue_type="active").set(active_count) QUEUE_SIZE.labels(queue_type="reserved").set(reserved_count) return { "status": "healthy", "queue_stats": { "active_tasks": active_count, "queued_tasks": reserved_count, "worker_count": worker_count, "total_queue_length": active_count + reserved_count } } except Exception as e: logger.error("Queue health check failed", error=str(e)) return JSONResponse( status_code=503, content={ "status": "unhealthy", "error": str(e) } ) # 监控指标端点 @app.get("/metrics") async def metrics(): """Prometheus 监控指标""" return Response( content=generate_latest(), media_type=CONTENT_TYPE_LATEST ) # 包含路由模块 try: from app.handlers import webhook, health, admin app.include_router( webhook.router, prefix="/webhook", tags=["webhook"] ) app.include_router( health.router, prefix="/health", tags=["health"] ) app.include_router( admin.router, prefix="/admin", tags=["admin"] ) except ImportError as e: # 如果模块不存在,记录警告但不中断应用启动 logger.warning(f"Some handlers not available: {e}") # 根路径 @app.get("/") async def root(): """根路径""" return { "name": "Gitea Webhook Ambassador", "version": "1.0.0", "description": "高性能的 Gitea 到 Jenkins 的 Webhook 服务", "endpoints": { "webhook": "/webhook/gitea", "health": "/health", "metrics": "/metrics", "admin": "/admin" } } if __name__ == "__main__": import uvicorn settings = get_settings() uvicorn.run( "app.main:app", host=settings.host, port=settings.port, reload=settings.debug, log_level=settings.logging.level.lower() )