feat: Task 19 — Health-Check Blueprint mit Readiness-Endpunkt (closes #19)
This commit is contained in:
+9
-5
@@ -8,6 +8,10 @@ from src.config import settings
|
|||||||
from src.error_handler import register_error_handlers
|
from src.error_handler import register_error_handlers
|
||||||
from src.logging_config import setup_logging
|
from src.logging_config import setup_logging
|
||||||
from src.queue.handler import QueueHandler
|
from src.queue.handler import QueueHandler
|
||||||
|
from src.routes.admin import admin_bp
|
||||||
|
from src.routes.health import health_bp
|
||||||
|
from src.routes.telegram import telegram_bp
|
||||||
|
from src.routes.whatsapp import whatsapp_bp
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> Flask:
|
def create_app() -> Flask:
|
||||||
@@ -29,9 +33,9 @@ def create_app() -> Flask:
|
|||||||
redis_port=settings.REDIS_PORT,
|
redis_port=settings.REDIS_PORT,
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/health")
|
app.register_blueprint(telegram_bp, url_prefix="/webhook")
|
||||||
def health() -> dict:
|
app.register_blueprint(whatsapp_bp, url_prefix="/webhook")
|
||||||
"""Health-Check Endpoint für Monitoring."""
|
app.register_blueprint(admin_bp, url_prefix="/admin")
|
||||||
return {"status": "ok", "queue_size": app.queue_handler.queue_size()}
|
app.register_blueprint(health_bp)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""Health-Check Route mit Status und Readiness-Endpunkt."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from flask import Blueprint, current_app, jsonify
|
||||||
|
|
||||||
|
health_bp = Blueprint("health", __name__, url_prefix="/health")
|
||||||
|
|
||||||
|
|
||||||
|
@health_bp.route("")
|
||||||
|
def health_status() -> tuple:
|
||||||
|
"""GET /health — Liveness: Status, Queue-Size, Python-Version, Plattform."""
|
||||||
|
qh = getattr(current_app, "queue_handler", None)
|
||||||
|
queue_size = qh.queue_size() if qh else None
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"queue_size": queue_size,
|
||||||
|
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||||
|
"platform": platform.platform(),
|
||||||
|
}
|
||||||
|
), 200
|
||||||
|
|
||||||
|
|
||||||
|
@health_bp.route("/ready")
|
||||||
|
def health_ready() -> tuple:
|
||||||
|
"""GET /health/ready — Readiness: Redis-Verbindung prüfen."""
|
||||||
|
qh = getattr(current_app, "queue_handler", None)
|
||||||
|
if qh is None:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ready": False,
|
||||||
|
"reason": "QueueHandler nicht registriert",
|
||||||
|
}
|
||||||
|
), 503
|
||||||
|
|
||||||
|
try:
|
||||||
|
qh.redis.ping()
|
||||||
|
return jsonify({"ready": True}), 200
|
||||||
|
except Exception as exc:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"ready": False,
|
||||||
|
"reason": str(exc),
|
||||||
|
}
|
||||||
|
), 503
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""Tests für src.routes.health — Health-Check mit Readiness."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from src.app import create_app
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthStatus:
|
||||||
|
"""Tests für /health Endpoint."""
|
||||||
|
|
||||||
|
def setup_method(self) -> None:
|
||||||
|
"""Jeder Test bekommt eine frische App mit gemocktem Redis."""
|
||||||
|
with patch("src.queue.handler.redis.Redis"):
|
||||||
|
self.app = create_app()
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
|
||||||
|
def test_health_status_ok(self) -> None:
|
||||||
|
"""GET /health → 200 mit Status-JSON."""
|
||||||
|
self.app.queue_handler = MagicMock(queue_size=MagicMock(return_value=42)) # type: ignore[assignment]
|
||||||
|
response = self.client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.get_json()
|
||||||
|
assert data["status"] == "ok"
|
||||||
|
assert "queue_size" in data
|
||||||
|
assert "python_version" in data
|
||||||
|
assert "platform" in data
|
||||||
|
|
||||||
|
def test_health_queue_size_integer(self) -> None:
|
||||||
|
"""queue_size ist ein Integer."""
|
||||||
|
self.app.queue_handler = MagicMock(queue_size=MagicMock(return_value=7)) # type: ignore[assignment]
|
||||||
|
response = self.client.get("/health")
|
||||||
|
data = response.get_json()
|
||||||
|
assert isinstance(data["queue_size"], int)
|
||||||
|
assert data["queue_size"] == 7
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthReady:
|
||||||
|
"""Tests für /health/ready Endpoint."""
|
||||||
|
|
||||||
|
def setup_method(self) -> None:
|
||||||
|
"""Jeder Test bekommt eine frische App."""
|
||||||
|
with patch("src.queue.handler.redis.Redis"):
|
||||||
|
self.app = create_app()
|
||||||
|
self.client = self.app.test_client()
|
||||||
|
|
||||||
|
def test_health_ready_redis_alive(self) -> None:
|
||||||
|
"""Redis ping erfolgreich → ready=true."""
|
||||||
|
response = self.client.get("/health/ready")
|
||||||
|
# Redis wird gemockt, ping sollte successful sein
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.get_json()
|
||||||
|
assert data["ready"] is True
|
||||||
|
|
||||||
|
def test_health_ready_redis_fails(self) -> None:
|
||||||
|
"""Redis ping fehlschlägt → ready=false, 503."""
|
||||||
|
with patch.object(
|
||||||
|
self.app.queue_handler.redis, "ping", side_effect=Exception("Redis down")
|
||||||
|
):
|
||||||
|
response = self.client.get("/health/ready")
|
||||||
|
assert response.status_code == 503
|
||||||
|
data = response.get_json()
|
||||||
|
assert data["ready"] is False
|
||||||
|
assert "reason" in data
|
||||||
Reference in New Issue
Block a user