Added logic to retrieve messages via API call

This commit is contained in:
florian committed 2026-10-11 17:06:44 +02:00
1 parent 2bbb9dff3c
commit 85d6a552c6
2 files changed
+77 -44

No files matched your search

+76 -43
View File
@@ -1,42 +1,49 @@
from fastapi import FastAPI, Depends, HTTPException, Request, Response from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.security.api_key import APIKeyHeader from fastapi.security.api_key import APIKeyHeader
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from typing import Dict from pydantic import BaseModel, Field
from pydantic import BaseModel
from validator import verify_api_key from validator import verify_api_key
from db_module import get_db, create_connection_pool, close_connection_pool, start_healthcheck_thread from db_module import get_db, create_connection_pool, close_connection_pool, start_healthcheck_thread
from simple_logger_handler import setup_logger, LOG_LEVEL from simple_logger_handler import setup_logger, LOG_LEVEL
from rabbitmq_handler import RabbitMQProducer from rabbitmq_handler import RabbitMQConsumer
import uvicorn import uvicorn
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import signal
from metrics_server import REQUEST_COUNTER from metrics_server import REQUEST_COUNTER
import asyncio import asyncio
from uvicorn_logger_config import LOGGING_CONFIG from uvicorn_logger_config import LOGGING_CONFIG
from secret_manager import cleanup_secret_files from secret_manager import cleanup_secret_files
from aio_pika.exceptions import AMQPException
logger = setup_logger(__name__) logger = setup_logger(__name__)
api_key_header_internal = APIKeyHeader(name="X-API-Key-Internal") api_key_header_internal = APIKeyHeader(name="X-API-Key-Internal")
MAX_MESSAGES = 100
class Notification(BaseModel): class PullRequest(BaseModel):
receipent_user_id : int user_id: int = Field(gt=0)
class PullResponse(BaseModel):
messages: list[dict]
count: int
has_more: bool
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
logger.info("Starting application...") logger.info("[FastAPI] Starting application...")
logger.info("Creating MySQL connection pool...") logger.info("[FastAPI] Creating MySQL connection pool...")
create_connection_pool("ApiInternalPool") create_connection_pool("ApiPullPool")
start_healthcheck_thread() start_healthcheck_thread()
logger.info("MySQL healthcheck thread started.") logger.info("[FastAPI] MySQL healthcheck thread started.")
producer = RabbitMQProducer() consumer = RabbitMQConsumer()
try: try:
logger.info("Starting RabbitMQ producer...") logger.info("[FastAPI] Starting RabbitMQ consumer...")
await producer.connect() await consumer.connect()
app.state.rmq_producer = producer app.state.rmq_consumer = consumer
logger.info("[FastAPI] RabbitMQ producer initialized.") logger.info("[FastAPI] RabbitMQ consumer initialized.")
SECRET_PATHS = frozenset({ SECRET_PATHS = frozenset({
"/etc/secrets/db_username", "/etc/secrets/db_username",
@@ -48,49 +55,56 @@ async def lifespan(app: FastAPI):
yield yield
finally: finally:
logger.info("Closing RabbitMQ producer...") logger.info("[FastAPI] Closing RabbitMQ producer...")
await producer.close() await consumer.close()
logger.info("Closing MySQL connection pool...") logger.info("[FastAPI] Closing MySQL connection pool...")
close_connection_pool() close_connection_pool()
api = FastAPI( api = FastAPI(
title="Internal Notifier API", title="Backend Pull Notifications",
description="API to forward messages to RabbitMQ", description="API to retrieve pull notifications on invocation",
version="1.0.0", version="1.0.0",
lifespan=lifespan lifespan=lifespan,
docs_url=None,
redoc_url=None,
openapi_url=None
) )
KNOWN_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
@api.middleware("http") @api.middleware("http")
async def prometheus_middleware(request: Request, call_next): async def prometheus_middleware(request: Request, call_next):
logger.debug(f"Incoming request: {request.method} {request.url.path}") logger.debug(f"[FastAPI] Incoming request: {request.method} {request.url.path}")
status = 500 status = 500
try: try:
response = await call_next(request) response = await call_next(request)
status = response.status_code status = response.status_code
logger.debug(f"Request processed with status: {status}") logger.debug(f"[FastAPI] Request processed with status: {status}")
except Exception as e: except Exception as e:
logger.exception(f"Exception during request processing: {e}") logger.exception(f"Exception during request processing: {e}")
raise raise
finally: finally:
REQUEST_COUNTER.labels(request.method, request.url.path, status).inc() route = request.scope.get("route")
logger.debug(f"Prometheus counter incremented: {request.method} {request.url.path} {status}") path = route.path if route else "unmatched"
method = request.method if request.method in KNOWN_METHODS else "OTHER"
REQUEST_COUNTER.labels(method, path, status).inc()
logger.debug(f"[FastAPI] Prometheus counter incremented: {method} {path} {status}")
return response return response
def verify_api_key_dependency_internal(db=Depends(get_db), api_key: str = Depends(api_key_header_internal)) -> str: def verify_api_key_dependency_internal(db=Depends(get_db), api_key: str = Depends(api_key_header_internal)) -> str:
logger.debug(f"Verifying internal API key: {api_key}")
cursor = db.cursor() cursor = db.cursor()
cursor.execute("SELECT program_name, api_key FROM internal_api_keys WHERE status = 'active'") cursor.execute("SELECT program_name, api_key FROM internal_api_keys WHERE status = 'active'")
for program_name, hashed_key in cursor.fetchall(): for program_name, hashed_key in cursor.fetchall():
if verify_api_key(api_key=api_key, hashed=hashed_key): if verify_api_key(api_key=api_key, hashed=hashed_key):
logger.debug(f"API key verified for program: {program_name}") logger.debug(f"[FastAPI] API key verified for program: {program_name}")
return program_name return program_name
logger.warning("Internal API key verification failed") logger.warning("Internal API key verification failed")
raise HTTPException(status_code=403, detail="Unauthorized") raise HTTPException(status_code=403, detail="Unauthorized")
@api.exception_handler(StarletteHTTPException) @api.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request,exc): async def custom_http_exception_handler(request,exc):
logger.debug(f"Handling HTTP exception: {exc}") logger.debug(f"[FastAPI] Handling HTTP exception: {exc}")
if exc.status_code == 404: if exc.status_code == 404:
return JSONResponse( return JSONResponse(
status_code=403, status_code=403,
@@ -101,30 +115,49 @@ async def custom_http_exception_handler(request,exc):
content={"detail": exc.detail} content={"detail": exc.detail}
) )
@api.get("/notifications/pull") @api.post("/notifications/pull", response_model=PullResponse)
async def receive_notifications( async def pull_notifications(
notification_data: Notification, pull_request: PullRequest,
request: Request, request: Request,
db = Depends(get_db), program_name: str = Depends(verify_api_key_dependency_internal),
program_name: str = Depends(verify_api_key_dependency_internal)
): ):
user_id = pull_request.user_id
logger.debug(f"[FastAPI] User id {user_id} is requesting messages")
logger.info(f"Received notifcation data from {program_name} for RMQ") try:
messages = await request.app.state.rmq_consumer.fetch_messages(
user_id=user_id, max_messages=MAX_MESSAGES
)
except (AMQPException, OSError) as e:
logger.error(f"[FastAPI] RabbitMQ unavailable for user_id={user_id}: {e}")
raise HTTPException(status_code=503, detail="Message broker unavailable")
except RuntimeError as e:
logger.error(f"[FastAPI] Consumer not ready: {e}")
raise HTTPException(status_code=503, detail="Service not ready")
messages = await request.app.state.rmq_producer.fetch_messages() logger.info(f"[FastAPI] Retrieved {len(messages)} message(s) for user_id={user_id}")
logger.info("Successfully retrieved messages for ") return PullResponse(messages=messages, count=len(messages), has_more=len(messages) >= MAX_MESSAGES)
print (messages)
class QuietServer(uvicorn.Server):
def install_signal_handlers(self):
pass
async def start_servers(): async def start_servers():
logger.debug("Starting FastAPI and metrics servers") config_main = uvicorn.Config(api, host="0.0.0.0", port=8101, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG)
config_main = uvicorn.Config("main:api", host="0.0.0.0", port=8101, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG)
config_metrics = uvicorn.Config("metrics_server:metrics_api", host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG) config_metrics = uvicorn.Config("metrics_server:metrics_api", host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG)
servers = [QuietServer(config_main), QuietServer(config_metrics)]
server_main = uvicorn.Server(config_main) def stop():
server_metrics = uvicorn.Server(config_metrics) for s in servers:
s.should_exit = True
await asyncio.gather(server_main.serve(), server_metrics.serve()) loop = asyncio.get_running_loop()
logger.debug("Servers started") for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, stop)
await asyncio.gather(*(s.serve() for s in servers))
logger.debug("[FastAPI] Servers started")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(start_servers()) asyncio.run(start_servers())
+1 -1
View File
@@ -71,7 +71,7 @@ class RabbitMQConsumer:
await queue.bind(self.exchange, routing_key=f"{self.pull_routing_prefix}.{user_id}") await queue.bind(self.exchange, routing_key=f"{self.pull_routing_prefix}.{user_id}")
return queue return queue
async def fetch_messages(self, user_id: int, max_messages: int = 1000) -> list[dict]: async def fetch_messages(self, user_id: int, max_messages: int = 100) -> list[dict]:
"""Drain up to max_messages queued messages for this user and return their """Drain up to max_messages queued messages for this user and return their
decoded JSON bodies. decoded JSON bodies.