- 新增完整的 Python 实现,替代 Go 版本 - 添加 Web 登录界面和仪表板 - 实现 JWT 认证和 API 密钥管理 - 添加数据库存储功能 - 保持与 Go 版本一致的目录结构和启动脚本 - 包含完整的文档和测试脚本
383 lines
11 KiB
Python
383 lines
11 KiB
Python
"""
|
|
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()
|
|
) |