Repository: apache/airflow

Issue:
API Endpoint - Health - Spec and impelementataion
**Description**

There is no endpoint to check if the instance is in good condition

We need to prepare the change to the API specification and then implement this change.

More information about API Endpoints:
https://github.com/apache/airflow/issues/8118

**Use case / motivation**

N/A

**Related Issues**

N/a


Discussion:
Hey @mik-laj I find this issue interesting and I would like to work on it. I will start by preparing the health endpoint specification and getting it approved before implementing it. What do you suggest, should I go ahead with it ?
Hello @OmairK 
Sure. You can rely on
https://github.com/apache/airflow/blob/e405be0141a996c5bb3659e3f19cab0e1ac8dc8d/docs/howto/check-health.rst

Resolving patch:
diff --git a/airflow/api_connexion/endpoints/health_endpoint.py b/airflow/api_connexion/endpoints/health_endpoint.py
--- a/airflow/api_connexion/endpoints/health_endpoint.py
+++ b/airflow/api_connexion/endpoints/health_endpoint.py
@@ -14,13 +14,36 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+from airflow.api_connexion.schemas.health_schema import health_schema
+from airflow.jobs.scheduler_job import SchedulerJob
 
-# TODO(mik-laj): We have to implement it.
-#     Do you want to help? Please look at: https://github.com/apache/airflow/issues/8144
+HEALTHY = "healthy"
+UNHEALTHY = "unhealthy"
 
 
 def get_health():
     """
-    Checks if the API works
+    Return the health of the airflow scheduler and metadatabase
     """
-    return "OK"
+    metadatabase_status = HEALTHY
+    latest_scheduler_heartbeat = None
+    scheduler_status = UNHEALTHY
+    try:
+        scheduler_job = SchedulerJob.most_recent_job()
+
+        if scheduler_job:
+            latest_scheduler_heartbeat = scheduler_job.latest_heartbeat.isoformat()
+            if scheduler_job.is_alive():
+                scheduler_status = HEALTHY
+    except Exception:  # pylint: disable=broad-except
+        metadatabase_status = UNHEALTHY
+
+    payload = {
+        "metadatabase": {"status": metadatabase_status},
+        "scheduler": {
+            "status": scheduler_status,
+            "latest_scheduler_heartbeat": latest_scheduler_heartbeat,
+        },
+    }
+
+    return health_schema.dump(payload)
diff --git a/airflow/api_connexion/schemas/health_schema.py b/airflow/api_connexion/schemas/health_schema.py
new file mode 100644
--- /dev/null
+++ b/airflow/api_connexion/schemas/health_schema.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from marshmallow import Schema, fields
+
+
+class BaseInfoSchema(Schema):
+    """Base status field for metadabase and scheduler"""
+    status = fields.String(dump_only=True)
+
+
+class MetaDatabaseInfoSchema(BaseInfoSchema):
+    """Schema for Metadatabase info"""
+
+
+class SchedulerInfoSchema(BaseInfoSchema):
+    """ Schema for Metadatabase info"""
+    latest_scheduler_heartbeat = fields.String(dump_only=True)
+
+
+class HeathInfoSchema(Schema):
+    """ Schema for the Health endpoint """
+
+    metadatabase = fields.Nested(MetaDatabaseInfoSchema)
+    scheduler = fields.Nested(SchedulerInfoSchema)
+
+
+health_schema = HeathInfoSchema()


Test patch:
diff --git a/tests/api_connexion/endpoints/test_health_endpoint.py b/tests/api_connexion/endpoints/test_health_endpoint.py
--- a/tests/api_connexion/endpoints/test_health_endpoint.py
+++ b/tests/api_connexion/endpoints/test_health_endpoint.py
@@ -15,11 +15,19 @@
 # specific language governing permissions and limitations
 # under the License.
 import unittest
+from datetime import timedelta
 
+from airflow.jobs.base_job import BaseJob
+from airflow.utils import timezone
+from airflow.utils.session import create_session, provide_session
+from airflow.utils.state import State
 from airflow.www import app
 
+HEALTHY = "healthy"
+UNHEALTHY = "unhealthy"
 
-class TestGetHealthTest(unittest.TestCase):
+
+class TestHealthTestBase(unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         super().setUpClass()
@@ -27,8 +35,58 @@ def setUpClass(cls) -> None:
 
     def setUp(self) -> None:
         self.client = self.app.test_client()  # type:ignore
+        with create_session() as session:
+            session.query(BaseJob).delete()
+
+    def tearDown(self):
+        super().tearDown()
+        with create_session() as session:
+            session.query(BaseJob).delete()
+
+
+class TestGetHeath(TestHealthTestBase):
+    @provide_session
+    def test_healthy_scheduler_status(self, session):
+        last_scheduler_heartbeat_for_testing_1 = timezone.utcnow()
+        session.add(
+            BaseJob(
+                job_type="SchedulerJob",
+                state=State.RUNNING,
+                latest_heartbeat=last_scheduler_heartbeat_for_testing_1,
+            )
+        )
+        session.commit()
+        resp_json = self.client.get("/api/v1/health").json
+        self.assertEqual("healthy", resp_json["metadatabase"]["status"])
+        self.assertEqual("healthy", resp_json["scheduler"]["status"])
+        self.assertEqual(
+            last_scheduler_heartbeat_for_testing_1.isoformat(),
+            resp_json["scheduler"]["latest_scheduler_heartbeat"],
+        )
+
+    @provide_session
+    def test_unhealthy_scheduler_is_slow(self, session):
+        last_scheduler_heartbeat_for_testing_2 = timezone.utcnow() - timedelta(
+            minutes=1
+        )
+        session.add(
+            BaseJob(
+                job_type="SchedulerJob",
+                state=State.RUNNING,
+                latest_heartbeat=last_scheduler_heartbeat_for_testing_2,
+            )
+        )
+        session.commit()
+        resp_json = self.client.get("/api/v1/health").json
+        self.assertEqual("healthy", resp_json["metadatabase"]["status"])
+        self.assertEqual("unhealthy", resp_json["scheduler"]["status"])
+        self.assertEqual(
+            last_scheduler_heartbeat_for_testing_2.isoformat(),
+            resp_json["scheduler"]["latest_scheduler_heartbeat"],
+        )
 
-    def test_should_response_200_and_ok(self):
-        response = self.client.get("/api/v1/health")
-        assert response.status_code == 200
-        assert response.data == b"OK"
+    def test_unhealthy_scheduler_no_job(self):
+        resp_json = self.client.get("/api/v1/health").json
+        self.assertEqual("healthy", resp_json["metadatabase"]["status"])
+        self.assertEqual("unhealthy", resp_json["scheduler"]["status"])
+        self.assertIsNone(None, resp_json["scheduler"]["latest_scheduler_heartbeat"])
diff --git a/tests/api_connexion/schemas/test_health_schema.py b/tests/api_connexion/schemas/test_health_schema.py
new file mode 100644
--- /dev/null
+++ b/tests/api_connexion/schemas/test_health_schema.py
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import unittest
+
+from airflow.api_connexion.schemas.health_schema import health_schema
+
+
+class TestHeathSchema(unittest.TestCase):
+    def setUp(self):
+        self.default_datetime = "2020-06-10T12:02:44+00:00"
+
+    def test_serialize(self):
+        payload = {
+            "metadatabase": {"status": "healthy"},
+            "scheduler": {
+                "status": "healthy",
+                "latest_scheduler_heartbeat": self.default_datetime,
+            },
+        }
+        serialized_data = health_schema.dump(payload)
+        self.assertDictEqual(serialized_data, payload)