Repository: CTFd/CTFd

Issue:
Add a healthcheck endpoint
Add a simple healthcheck endpoint. Likely something like `/healthcheck`. It should likely so a simple `SELECT 1` on the database and do a simple `get_config()` call to validate that everything is working and then return a 200 with "OK". On any failure it should return 500.  


Discussion:

Resolving patch:
diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py
--- a/CTFd/cache/__init__.py
+++ b/CTFd/cache/__init__.py
@@ -1,9 +1,43 @@
+from functools import lru_cache, wraps
+from time import monotonic_ns
+
 from flask import request
 from flask_caching import Cache, make_template_fragment_key
 
 cache = Cache()
 
 
+def timed_lru_cache(timeout: int = 300, maxsize: int = 64, typed: bool = False):
+    """
+    lru_cache implementation that includes a time based expiry
+
+    Parameters:
+    seconds (int): Timeout in seconds to clear the WHOLE cache, default = 5 minutes
+    maxsize (int): Maximum Size of the Cache
+    typed (bool): Same value of different type will be a different entry
+
+    Implmentation from https://gist.github.com/Morreski/c1d08a3afa4040815eafd3891e16b945?permalink_comment_id=3437689#gistcomment-3437689
+    """
+
+    def wrapper_cache(func):
+        func = lru_cache(maxsize=maxsize, typed=typed)(func)
+        func.delta = timeout * 10 ** 9
+        func.expiration = monotonic_ns() + func.delta
+
+        @wraps(func)
+        def wrapped_func(*args, **kwargs):
+            if monotonic_ns() >= func.expiration:
+                func.cache_clear()
+                func.expiration = monotonic_ns() + func.delta
+            return func(*args, **kwargs)
+
+        wrapped_func.cache_info = func.cache_info
+        wrapped_func.cache_clear = func.cache_clear
+        return wrapped_func
+
+    return wrapper_cache
+
+
 def make_cache_key(path=None, key_prefix="view/%s"):
     """
     This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses.
diff --git a/CTFd/utils/health/__init__.py b/CTFd/utils/health/__init__.py
new file mode 100644
--- /dev/null
+++ b/CTFd/utils/health/__init__.py
@@ -0,0 +1,18 @@
+from time import monotonic_ns
+
+from flask import current_app
+from sqlalchemy_utils import database_exists
+
+from CTFd.cache import cache, timed_lru_cache
+
+
+@timed_lru_cache(timeout=30)
+def check_database():
+    return database_exists(current_app.config["SQLALCHEMY_DATABASE_URI"])
+
+
+@timed_lru_cache(timeout=30)
+def check_config():
+    value = monotonic_ns()
+    cache.set("healthcheck", value)
+    return cache.get("healthcheck") == value
diff --git a/CTFd/views.py b/CTFd/views.py
--- a/CTFd/views.py
+++ b/CTFd/views.py
@@ -44,6 +44,7 @@
     DEFAULT_VERIFICATION_EMAIL_BODY,
     DEFAULT_VERIFICATION_EMAIL_SUBJECT,
 )
+from CTFd.utils.health import check_config, check_database
 from CTFd.utils.helpers import get_errors, get_infos, markup
 from CTFd.utils.modes import USERS_MODE
 from CTFd.utils.security.auth import login_user
@@ -504,3 +505,12 @@ def themes_beta(theme, path):
         if os.path.isfile(cand_path):
             return send_file(cand_path)
     abort(404)
+
+
+@views.route("/healthcheck")
+def healthcheck():
+    if check_database() is False:
+        return "ERR", 500
+    if check_config() is False:
+        return "ERR", 500
+    return "OK", 200


Test patch: