Skip to content

Commit 48528f9

Browse files
committed
feature health checks
1 parent 9f436c1 commit 48528f9

6 files changed

Lines changed: 107 additions & 0 deletions

File tree

backend/healthcheck/__init__.py

Whitespace-only changes.

backend/healthcheck/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class HealthcheckConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "healthcheck"

backend/healthcheck/urls.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.urls import path
2+
3+
from healthcheck.views import FeatureHealth
4+
5+
6+
urlpatterns = [
7+
path("features/", FeatureHealth.as_view(), name="feature-health"),
8+
]

backend/healthcheck/views.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import time
2+
3+
import requests
4+
from django.conf import settings
5+
from django.utils import timezone
6+
from requests.auth import HTTPBasicAuth
7+
from rest_framework import status
8+
from rest_framework.response import Response
9+
from rest_framework.views import APIView
10+
11+
from laundry.api_wrapper import check_is_working
12+
13+
14+
class FeatureHealth(APIView):
15+
"""
16+
GET: Unauthenticated endpoint for healthchecks.
17+
Polled by status.pennlabs.org.
18+
"""
19+
20+
def _check(self, name, check_fn):
21+
start = time.time()
22+
try:
23+
check_fn()
24+
return {"status": "ok", "response_time_ms": round((time.time() - start) * 1000)}
25+
except Exception as e:
26+
return {
27+
"status": "error",
28+
"response_time_ms": round((time.time() - start) * 1000),
29+
"error": str(e),
30+
}
31+
32+
def _check_laundry(self):
33+
if not check_is_working():
34+
raise Exception("Laundry API is not responding")
35+
36+
def _check_dining(self):
37+
from dining.api_wrapper import DiningAPIWrapper
38+
39+
d = DiningAPIWrapper()
40+
d.update_token()
41+
response = d.request(
42+
"GET", "https://3scale-public-prod-open-data.apps.k8s.upenn.edu/api/v1/dining/venues"
43+
)
44+
if response.status_code != 200:
45+
raise Exception(f"Dining API returned {response.status_code}")
46+
47+
def _check_wharton_gsr(self):
48+
response = requests.get(
49+
"https://apps.wharton.upenn.edu/gsr/api/v1/privileges",
50+
headers={"Authorization": f"Token {settings.WHARTON_TOKEN}"},
51+
timeout=10,
52+
)
53+
if response.status_code not in (200, 403, 404):
54+
raise Exception(f"Wharton API returned {response.status_code}")
55+
56+
def _check_libcal_gsr(self):
57+
body = {
58+
"client_id": settings.GENERAL_LIBCAL_ID,
59+
"client_secret": settings.GENERAL_LIBCAL_SECRET,
60+
"grant_type": "client_credentials",
61+
}
62+
response = requests.post("https://api2.libcal.com/1.1/oauth/token", data=body, timeout=10)
63+
if response.status_code != 200:
64+
raise Exception(f"LibCal token endpoint returned {response.status_code}")
65+
66+
def _check_penngroups_gsr(self):
67+
response = requests.get(
68+
"https://grouperWs.apps.upenn.edu/grouperWs/servicesRest/4.9.3/subjects",
69+
auth=HTTPBasicAuth(settings.PENNGROUPS_USERNAME, settings.PENNGROUPS_PASSWORD),
70+
timeout=10,
71+
)
72+
if response.status_code >= 500:
73+
raise Exception(f"PennGroups API returned {response.status_code}")
74+
75+
def get(self, request):
76+
features = {
77+
"laundry": self._check("laundry", self._check_laundry),
78+
"dining": self._check("dining", self._check_dining),
79+
"wharton_gsr": self._check("wharton_gsr", self._check_wharton_gsr),
80+
"libcal_gsr": self._check("libcal_gsr", self._check_libcal_gsr),
81+
"penngroups_gsr": self._check("penngroups_gsr", self._check_penngroups_gsr),
82+
}
83+
all_ok = all(f["status"] == "ok" for f in features.values())
84+
return Response(
85+
{
86+
"status": "ok" if all_ok else "degraded",
87+
"features": features,
88+
"timestamp": timezone.now().isoformat(),
89+
},
90+
status=status.HTTP_200_OK if all_ok else status.HTTP_503_SERVICE_UNAVAILABLE,
91+
)

backend/pennmobile/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"phonenumber_field",
5757
"market",
5858
"games",
59+
"healthcheck",
5960
]
6061

6162
MIDDLEWARE = [

backend/pennmobile/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def universal_identifier_link(request):
5757
urlpatterns = [
5858
path("api/", include(urlpatterns)),
5959
path("", include((urlpatterns, "apex"))),
60+
path("health/", include("healthcheck.urls")),
6061
path(
6162
".well-known/apple-app-site-association", universal_identifier_link, name="universal-links"
6263
),

0 commit comments

Comments
 (0)