mirror of https://github.com/interlegis/sapl.git
8 changed files with 167 additions and 21 deletions
@ -0,0 +1,106 @@ |
|||||
|
import logging |
||||
|
import traceback |
||||
|
|
||||
|
from django.http import HttpResponse, JsonResponse |
||||
|
from django.utils import timezone |
||||
|
from rest_framework.views import APIView |
||||
|
from rest_framework.response import Response |
||||
|
from rest_framework import status |
||||
|
from django.conf import settings |
||||
|
from sapl.health import check_app, check_db, check_cache |
||||
|
|
||||
|
COMMON_HEADERS = { |
||||
|
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", |
||||
|
"Pragma": "no-cache", |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def _format_plain(ok: bool) -> HttpResponse: |
||||
|
return HttpResponse("OK\n" if ok else "UNHEALTHY\n", |
||||
|
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
||||
|
content_type="text/plain") |
||||
|
|
||||
|
|
||||
|
class HealthzView(APIView): |
||||
|
authentication_classes = [] |
||||
|
permission_classes = [] |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
def get(self, request): |
||||
|
try: |
||||
|
ok, msg, ms = check_app() |
||||
|
payload = { |
||||
|
"status": "OK" if ok else "UNHEALTHY", |
||||
|
"checks": {"app": {"ok": ok, "latency_ms": round(ms, 1), "error": msg}}, |
||||
|
"version": settings.SAPL_VERSION, |
||||
|
"time": timezone.now().isoformat(), |
||||
|
} |
||||
|
if request.query_params.get("fmt") == "txt": |
||||
|
return _format_plain(ok) |
||||
|
return Response(payload, |
||||
|
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
||||
|
headers=COMMON_HEADERS) |
||||
|
except Exception as e: |
||||
|
self.logger.error(traceback.format_exc()) |
||||
|
return "An internal error has occurred!" |
||||
|
|
||||
|
|
||||
|
|
||||
|
class ReadyzView(APIView): |
||||
|
authentication_classes = [] |
||||
|
permission_classes = [] |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
def get(self, request): |
||||
|
try: |
||||
|
checks = { |
||||
|
"app": check_app(), |
||||
|
"db": check_db(), |
||||
|
"cache": check_cache(), |
||||
|
} |
||||
|
payload_checks = { |
||||
|
name: {"ok": r[0], "latency_ms": round(r[2], 1), "error": r[1]} |
||||
|
for name, r in checks.items() |
||||
|
} |
||||
|
ok = all(r[0] for r in checks.values()) |
||||
|
payload = { |
||||
|
"status": "ok" if ok else "unhealthy", |
||||
|
"checks": payload_checks, |
||||
|
"version": settings.SAPL_VERSION, |
||||
|
"time": timezone.now().isoformat(), |
||||
|
} |
||||
|
if request.query_params.get("fmt") == "txt": |
||||
|
return _format_plain(ok) |
||||
|
return Response(payload, |
||||
|
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
||||
|
headers=COMMON_HEADERS) |
||||
|
except Exception as e: |
||||
|
self.logger.error(traceback.format_exc()) |
||||
|
return "An internal error has occurred!" |
||||
|
|
||||
|
|
||||
|
class AppzVersionView(APIView): |
||||
|
authentication_classes = [] |
||||
|
permission_classes = [] |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
def get(self, request): |
||||
|
try: |
||||
|
payload = { |
||||
|
'name': 'SAPL', |
||||
|
'description': 'Sistema de Apoio ao Processo Legislativo', |
||||
|
'version': settings.SAPL_VERSION, |
||||
|
} |
||||
|
if request.query_params.get("fmt") == "txt": |
||||
|
return HttpResponse(f"{payload['version']} {payload['name']}", |
||||
|
status=status.HTTP_200_OK, |
||||
|
content_type="text/plain") |
||||
|
return Response(payload, |
||||
|
status=status.HTTP_200_OK, |
||||
|
headers=COMMON_HEADERS) |
||||
|
except Exception as e: |
||||
|
self.logger.error(traceback.format_exc()) |
||||
|
return "An internal error has occurred!" |
@ -0,0 +1,36 @@ |
|||||
|
# core/health.py |
||||
|
import logging |
||||
|
import time |
||||
|
from typing import Tuple, Optional |
||||
|
from django.db import connection |
||||
|
from django.core.cache import cache |
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
def check_app() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
return True, None, (time.monotonic() - t0) * 1000 |
||||
|
|
||||
|
|
||||
|
def check_db() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
try: |
||||
|
with connection.cursor() as cur: |
||||
|
cur.execute("SELECT 1") |
||||
|
cur.fetchone() |
||||
|
return True, None, (time.monotonic() - t0) * 1000 |
||||
|
except Exception as e: |
||||
|
logging.error(e) |
||||
|
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
||||
|
|
||||
|
|
||||
|
def check_cache() -> Tuple[bool, Optional[str], float]: |
||||
|
t0 = time.monotonic() |
||||
|
try: |
||||
|
cache.set("_hc", "1", 5) |
||||
|
ok = cache.get("_hc") == "1" |
||||
|
return ok, None if ok else "Cache get/set failed", (time.monotonic() - t0) * 1000 |
||||
|
except Exception as e: |
||||
|
logging.error(e) |
||||
|
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
@ -0,0 +1,12 @@ |
|||||
|
from prometheus_client import CollectorRegistry, Gauge, generate_latest, CONTENT_TYPE_LATEST |
||||
|
|
||||
|
|
||||
|
def health_registry(check_results: dict) -> bytes: |
||||
|
""" |
||||
|
check_results: {"app": True/False, "db": True/False, ...} |
||||
|
""" |
||||
|
reg = CollectorRegistry() |
||||
|
g = Gauge("app_health", "1 if healthy, 0 if unhealthy", ["component"], registry=reg) |
||||
|
for comp, ok in check_results.items(): |
||||
|
g.labels(component=comp).set(1 if ok else 0) |
||||
|
return generate_latest(reg) |
Loading…
Reference in new issue