This is an automated email from the ASF dual-hosted git repository.

vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 9da89e70ac0 Feature expand health endpoint reporting (#70416)
9da89e70ac0 is described below

commit 9da89e70ac036e8d8c23208777f9575cf6eee22d
Author: Jung-Hyun Andrew Kim <[email protected]>
AuthorDate: Fri Aug 28 05:35:05 2026 -0700

    Feature expand health endpoint reporting (#70416)
---
 .../logging-monitoring/check-health.rst            | 100 +++--
 airflow-core/docs/migrations-ref.rst               |   4 +-
 .../src/airflow/api/common/airflow_health.py       | 138 ++++--
 .../airflow/api_fastapi/core_api/datamodels/job.py |   2 +
 .../api_fastapi/core_api/datamodels/monitor.py     |  29 ++
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  12 +
 .../core_api/openapi/v2-rest-api-generated.yaml    | 137 ++++++
 .../airflow/cli/commands/dag_processor_command.py  |   2 +-
 .../src/airflow/cli/commands/triggerer_command.py  |   4 +-
 airflow-core/src/airflow/jobs/job.py               |   8 +-
 .../src/airflow/jobs/triggerer_job_runner.py       |   4 +-
 .../0133_3_4_0_add_scope_columns_to_job.py         |  58 +++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 257 ++++++++++-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  37 ++
 airflow-core/src/airflow/utils/db.py               |   2 +-
 .../tests/unit/api/common/test_airflow_health.py   | 494 +++++++++++++++++----
 .../api_fastapi/core_api/routes/public/test_job.py |  21 +
 .../core_api/routes/public/test_monitor.py         |  63 ++-
 .../core_api/routes/public/test_task_instances.py  |   2 +
 .../cli/commands/test_dag_processor_command.py     |   1 +
 .../unit/cli/commands/test_triggerer_command.py    |  22 +-
 airflow-core/tests/unit/jobs/test_triggerer_job.py |  21 +-
 .../src/airflowctl/api/datamodels/generated.py     |  74 ++-
 23 files changed, 1313 insertions(+), 179 deletions(-)

diff --git 
a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst
 
b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst
index c9b1966714a..260c0ada751 100644
--- 
a/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst
+++ 
b/airflow-core/docs/administration-and-deployment/logging-monitoring/check-health.rst
@@ -36,48 +36,94 @@ Webserver Health Check Endpoint
 -------------------------------
 
 To check the health status of your Airflow instance, you can simply access the 
endpoint
-``/api/v2/monitor/health``. It will return a JSON object that provides a 
high-level glance at the health status across multiple Airflow components.
+``/api/v2/monitor/health``. It will return a JSON object that provides a 
high-level glance at the health status across multiple Airflow components,
+including per-instance details when multiple schedulers, triggerers, or Dag 
processors are running.
 
 .. code-block:: JSON
 
   {
-    "metadatabase":{
-      "status":"healthy"
+    "metadatabase": {
+      "status": "healthy"
     },
-    "scheduler":{
-      "status":"healthy",
-      "latest_scheduler_heartbeat":"2018-12-26 17:15:11+00:00"
+    "scheduler": {
+      "status": "healthy",
+      "latest_scheduler_heartbeat": "2018-12-26T17:15:11+00:00",
+      "detailed_status": "healthy",
+      "instances": [
+        {
+          "status": "healthy",
+          "hostname": "scheduler-1.example.com",
+          "latest_scheduler_heartbeat": "2018-12-26T17:15:11+00:00"
+        }
+      ]
     },
-    "triggerer":{
-      "status":"healthy",
-      "latest_triggerer_heartbeat":"2018-12-26 17:16:12+00:00"
+    "triggerer": {
+      "status": "healthy",
+      "latest_triggerer_heartbeat": "2018-12-26T17:16:12+00:00",
+      "detailed_status": "degraded",
+      "instances": [
+        {
+          "status": "healthy",
+          "hostname": "triggerer-1.example.com",
+          "latest_triggerer_heartbeat": "2018-12-26T17:16:12+00:00",
+          "team_name": "team-a"
+        },
+        {
+          "status": "unhealthy",
+          "hostname": "triggerer-2.example.com",
+          "latest_triggerer_heartbeat": "2018-12-26T17:10:00+00:00",
+          "team_name": null
+        }
+      ]
     },
-    "dag_processor":{
-      "status":"healthy",
-      "latest_dag_processor_heartbeat":"2018-12-26 17:16:12+00:00"
+    "dag_processor": {
+      "status": "healthy",
+      "latest_dag_processor_heartbeat": "2018-12-26T17:16:12+00:00",
+      "detailed_status": "healthy",
+      "instances": [
+        {
+          "status": "healthy",
+          "hostname": "dag-processor-1.example.com",
+          "latest_dag_processor_heartbeat": "2018-12-26T17:16:12+00:00",
+          "bundle_names": ["dags-team-a"]
+        }
+      ]
     }
   }
 
-* The ``status`` of each component can be either "healthy" or "unhealthy"
+* ``metadatabase``
 
-  * The status of ``metadatabase`` depends on whether a valid connection can 
be initiated with the database
+  * ``status`` is ``"healthy"`` when a valid connection can be initiated with 
the database, otherwise ``"unhealthy"``.
 
-  * The status of ``scheduler`` depends on when the latest scheduler heartbeat 
was received
+* Component-level fields for ``scheduler``, ``triggerer``, and 
``dag_processor``
 
-    * If the last heartbeat was received more than 30 seconds (default value) 
earlier than the current time, the scheduler is
-      considered unhealthy
-    * This threshold value can be specified using the option 
``scheduler_health_check_threshold`` within the
-      ``[scheduler]`` section in ``airflow.cfg``
-    * If you run more than one scheduler, only the state of one scheduler will 
be reported, i.e. only one working scheduler is enough
-      for the scheduler state to be considered healthy
+  * ``status`` (legacy aggregate): ``"healthy"`` if **any** running instance 
is alive, otherwise ``"unhealthy"``
+    (including when no running jobs exist for that component).
 
-  * The status of the ``triggerer`` behaves exactly like that of the 
``scheduler`` as described above.
-    Note that the ``status`` and ``latest_triggerer_heartbeat`` fields in the 
health check response will be null for
-    deployments that do not include a ``triggerer`` component.
+  * ``detailed_status``: reflects the full set of running instances:
 
-  * The status of the ``dag_processor`` behaves exactly like that of the 
``scheduler`` as described above.
-    Note that the ``status`` and ``latest_dag_processor_heartbeat`` fields in 
the health check response will be null for
-    deployments that do not include a ``dag_processor`` component.
+    * ``"healthy"`` — every running instance is alive
+    * ``"degraded"`` — some instances are alive and some are not
+    * ``"down"`` — no running instance is alive (including when no jobs exist)
+
+  * ``latest_*_heartbeat``: the most recent heartbeat among running jobs of 
that type (ordered by heartbeat descending),
+    or ``null`` when there are none.
+    An instance is considered alive when its latest heartbeat is within the 
component health-check threshold
+    (defaults and config options: ``[scheduler] 
scheduler_health_check_threshold``,
+    ``[triggerer] triggerer_health_check_threshold``,
+    ``[dag_processor] health_check_threshold``).
+
+  * ``instances``: one entry per **running** job of that type (``null`` when 
there are none). Each entry includes:
+
+    * ``status``: ``"healthy"`` or ``"unhealthy"`` for that instance
+    * ``hostname``: host where the component is running
+    * the corresponding ``latest_*_heartbeat`` for that instance
+    * ``team_name`` (triggerer only): team the triggerer is scoped to, or 
``null`` when unscoped
+    * ``bundle_names`` (Dag processor only): Dag bundles that processor is 
configured to parse, or ``null`` when unset
+
+  * For HA deployments, prefer ``detailed_status`` and ``instances`` when you 
need to see every scheduler,
+    triggerer, or Dag processor. The top-level ``status`` remains useful for 
simple probes that only care
+    whether at least one instance is healthy.
 
 Please keep in mind that the HTTP response code of ``/api/v2/monitor/health`` 
endpoint **should not** be used to determine the health
 status of the application. The return code is only indicative of the state of 
the rest call (200 for success).
diff --git a/airflow-core/docs/migrations-ref.rst 
b/airflow-core/docs/migrations-ref.rst
index 8e03f37ef54..e8bef749a6e 100644
--- a/airflow-core/docs/migrations-ref.rst
+++ b/airflow-core/docs/migrations-ref.rst
@@ -39,7 +39,9 @@ Here's the list of all the Database Migrations that are 
executed via when you ru
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | Revision ID             | Revises ID       | Airflow Version   | Description 
                                                 |
 
+=========================+==================+===================+==============================================================+
-| ``8d3f1a6b2c47`` (head) | ``c7f0a5d2e9b4`` | ``3.4.0``         | Add 
team_name to log.                                        |
+| ``f8c2a1d94e03`` (head) | ``8d3f1a6b2c47`` | ``3.4.0``         | Add 
team_name and bundle_names scope columns to job table.   |
++-------------------------+------------------+-------------------+--------------------------------------------------------------+
+| ``8d3f1a6b2c47``        | ``c7f0a5d2e9b4`` | ``3.4.0``         | Add 
team_name to log.                                        |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
 | ``c7f0a5d2e9b4``        | ``76c46545c91e`` | ``3.4.0``         | Lower case 
team names.                                       |
 
+-------------------------+------------------+-------------------+--------------------------------------------------------------+
diff --git a/airflow-core/src/airflow/api/common/airflow_health.py 
b/airflow-core/src/airflow/api/common/airflow_health.py
index 1086e22df57..e95a2966b3a 100644
--- a/airflow-core/src/airflow/api/common/airflow_health.py
+++ b/airflow-core/src/airflow/api/common/airflow_health.py
@@ -16,77 +16,159 @@
 # under the License.
 from __future__ import annotations
 
-from typing import Any
+from typing import TYPE_CHECKING, Any
+
+from sqlalchemy import select
 
 from airflow.jobs.dag_processor_job_runner import DagProcessorJobRunner
+from airflow.jobs.job import Job
 from airflow.jobs.scheduler_job_runner import SchedulerJobRunner
 from airflow.jobs.triggerer_job_runner import TriggererJobRunner
+from airflow.utils.session import NEW_SESSION, provide_session
+
+if TYPE_CHECKING:
+    from sqlalchemy.orm import Session
 
 HEALTHY = "healthy"
 UNHEALTHY = "unhealthy"
+DEGRADED = "degraded"
+DOWN = "down"
+
+
+@provide_session
+def get_jobs_health(job_runner_class, *, session: Session = NEW_SESSION) -> 
list[Job]:
+    """Return unfinished jobs for the runner class, ordered by latest 
heartbeat."""
+    return list(
+        session.scalars(
+            select(Job)
+            .where(
+                Job.job_type == job_runner_class.job_type,
+                Job.end_date.is_(None),
+            )
+            .order_by(Job.latest_heartbeat.desc())
+        )
+    )
+
+
+def _job_instance_health(job: Job, heartbeat_field_name: str) -> dict[str, 
Any]:
+    heartbeat = job.latest_heartbeat.isoformat() if job.latest_heartbeat else 
None
+    return {
+        "hostname": job.hostname,
+        "status": HEALTHY if job.is_alive() else UNHEALTHY,
+        heartbeat_field_name: heartbeat,
+    }
+
+
+def _legacy_status(jobs: list[Job]) -> str:
+    """Top-level status: healthy if any instance is alive."""
+    return HEALTHY if any(job.is_alive() for job in jobs) else UNHEALTHY
+
+
+def _triggerer_instance_health(job: Job) -> dict[str, Any]:
+    return {
+        **_job_instance_health(job, "latest_triggerer_heartbeat"),
+        "team_name": job.team_name,
+    }
+
+
+def _dag_processor_instance_health(job: Job) -> dict[str, Any]:
+    return {
+        **_job_instance_health(job, "latest_dag_processor_heartbeat"),
+        "bundle_names": job.bundle_names,
+    }
+
+
+def _aggregate_detailed_status(jobs: list[Job]) -> str:
+    """detailed_status: healthy (all alive), degraded (some alive), down (none 
alive)."""
+    alive_count = sum(1 for job in jobs if job.is_alive())
+    if alive_count == 0:
+        return DOWN
+    if alive_count == len(jobs):
+        return HEALTHY
+    return DEGRADED
 
 
 def get_airflow_health() -> dict[str, Any]:
-    """Get the health for Airflow metadatabase, scheduler and triggerer."""
+    """Get the health for Airflow metadatabase, scheduler, triggerer, and dag 
processor."""
     metadatabase_status = HEALTHY
+
     latest_scheduler_heartbeat = None
     latest_triggerer_heartbeat = None
     latest_dag_processor_heartbeat = None
 
+    scheduler_instances: list[dict[str, Any]] | None = None
+    triggerer_instances: list[dict[str, Any]] | None = None
+    dag_processor_instances: list[dict[str, Any]] | None = None
+
     scheduler_status = UNHEALTHY
-    triggerer_status: str | None = UNHEALTHY
-    dag_processor_status: str | None = UNHEALTHY
+    triggerer_status = UNHEALTHY
+    dag_processor_status = UNHEALTHY
+
+    scheduler_detailed_status = DOWN
+    triggerer_detailed_status = DOWN
+    dag_processor_detailed_status = DOWN
 
     try:
-        latest_scheduler_job = SchedulerJobRunner.most_recent_job()
+        scheduler_jobs = get_jobs_health(SchedulerJobRunner)
+        scheduler_status = _legacy_status(scheduler_jobs)
+        scheduler_detailed_status = _aggregate_detailed_status(scheduler_jobs)
+        if scheduler_jobs:
+            scheduler_instances = [
+                _job_instance_health(job, "latest_scheduler_heartbeat") for 
job in scheduler_jobs
+            ]
 
-        if latest_scheduler_job:
-            if latest_scheduler_job.latest_heartbeat:
-                latest_scheduler_heartbeat = 
latest_scheduler_job.latest_heartbeat.isoformat()
-            if latest_scheduler_job.is_alive():
-                scheduler_status = HEALTHY
+            if scheduler_jobs[0].latest_heartbeat:
+                latest_scheduler_heartbeat = 
scheduler_jobs[0].latest_heartbeat.isoformat()
     except Exception:
         metadatabase_status = UNHEALTHY
 
     try:
-        latest_triggerer_job = TriggererJobRunner.most_recent_job()
-
-        if latest_triggerer_job:
-            if latest_triggerer_job.latest_heartbeat:
-                latest_triggerer_heartbeat = 
latest_triggerer_job.latest_heartbeat.isoformat()
-            if latest_triggerer_job.is_alive():
-                triggerer_status = HEALTHY
-        else:
-            triggerer_status = None
+        triggerer_jobs = get_jobs_health(TriggererJobRunner)
+        triggerer_status = _legacy_status(triggerer_jobs)
+        triggerer_detailed_status = _aggregate_detailed_status(triggerer_jobs)
+        if triggerer_jobs:
+            triggerer_instances = [_triggerer_instance_health(job) for job in 
triggerer_jobs]
+
+            if triggerer_jobs[0].latest_heartbeat:
+                latest_triggerer_heartbeat = 
triggerer_jobs[0].latest_heartbeat.isoformat()
     except Exception:
         metadatabase_status = UNHEALTHY
+        triggerer_status = UNHEALTHY
+        triggerer_detailed_status = DOWN
 
     try:
-        latest_dag_processor_job = DagProcessorJobRunner.most_recent_job()
-
-        if latest_dag_processor_job:
-            if latest_dag_processor_job.latest_heartbeat:
-                latest_dag_processor_heartbeat = 
latest_dag_processor_job.latest_heartbeat.isoformat()
-            if latest_dag_processor_job.is_alive():
-                dag_processor_status = HEALTHY
-        else:
-            dag_processor_status = None
+        dag_processor_jobs = get_jobs_health(DagProcessorJobRunner)
+        dag_processor_status = _legacy_status(dag_processor_jobs)
+        dag_processor_detailed_status = 
_aggregate_detailed_status(dag_processor_jobs)
+        if dag_processor_jobs:
+            dag_processor_instances = [_dag_processor_instance_health(job) for 
job in dag_processor_jobs]
+
+            if dag_processor_jobs[0].latest_heartbeat:
+                latest_dag_processor_heartbeat = 
dag_processor_jobs[0].latest_heartbeat.isoformat()
     except Exception:
         metadatabase_status = UNHEALTHY
+        dag_processor_status = UNHEALTHY
+        dag_processor_detailed_status = DOWN
 
     airflow_health_status = {
         "metadatabase": {"status": metadatabase_status},
         "scheduler": {
             "status": scheduler_status,
             "latest_scheduler_heartbeat": latest_scheduler_heartbeat,
+            "detailed_status": scheduler_detailed_status,
+            "instances": scheduler_instances,
         },
         "triggerer": {
             "status": triggerer_status,
             "latest_triggerer_heartbeat": latest_triggerer_heartbeat,
+            "detailed_status": triggerer_detailed_status,
+            "instances": triggerer_instances,
         },
         "dag_processor": {
             "status": dag_processor_status,
             "latest_dag_processor_heartbeat": latest_dag_processor_heartbeat,
+            "detailed_status": dag_processor_detailed_status,
+            "instances": dag_processor_instances,
         },
     }
 
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py
index f97be99191a..8e316d7b2e3 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/job.py
@@ -37,6 +37,8 @@ class JobResponse(BaseModel):
     executor_class: str | None
     hostname: str | None
     unixname: str | None
+    team_name: str | None = None
+    bundle_names: list[str] | None = None
     dag_display_name: str | None = Field(
         validation_alias=AliasPath("dag_model", "dag_display_name"), 
default=None
     )
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py
index 3bfc5425fd7..5bf60bd83bd 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/monitor.py
@@ -25,22 +25,51 @@ class BaseInfoResponse(BaseModel):
     status: str | None
 
 
+class SchedulerInstanceInfoResponse(BaseInfoResponse):
+    """Scheduler instance info serializer for responses."""
+
+    hostname: str | None
+    latest_scheduler_heartbeat: str | None
+
+
+class TriggererInstanceInfoResponse(BaseInfoResponse):
+    """Triggerer instance info serializer for responses."""
+
+    hostname: str | None
+    latest_triggerer_heartbeat: str | None
+    team_name: str | None
+
+
+class DagProcessorInstanceInfoResponse(BaseInfoResponse):
+    """Dag processor instance info serializer for responses."""
+
+    hostname: str | None
+    latest_dag_processor_heartbeat: str | None
+    bundle_names: list[str] | None
+
+
 class SchedulerInfoResponse(BaseInfoResponse):
     """Scheduler info serializer for responses."""
 
     latest_scheduler_heartbeat: str | None
+    detailed_status: str | None
+    instances: list[SchedulerInstanceInfoResponse] | None = None
 
 
 class TriggererInfoResponse(BaseInfoResponse):
     """Triggerer info serializer for responses."""
 
     latest_triggerer_heartbeat: str | None
+    detailed_status: str | None
+    instances: list[TriggererInstanceInfoResponse] | None = None
 
 
 class DagProcessorInfoResponse(BaseInfoResponse):
     """DagProcessor info serializer for responses."""
 
     latest_dag_processor_heartbeat: str | None
+    detailed_status: str | None
+    instances: list[DagProcessorInstanceInfoResponse] | None = None
 
 
 class HealthInfoResponse(BaseModel):
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 6a8af48ecf6..25b50d9e825 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -3864,6 +3864,18 @@ components:
           - type: string
           - type: 'null'
           title: Unixname
+        team_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Team Name
+        bundle_names:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Bundle Names
         dag_display_name:
           anyOf:
           - type: string
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index 0eb4d2423dc..099e1b9cb5e 100644
--- 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++ 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -13938,12 +13938,57 @@ components:
           - type: string
           - type: 'null'
           title: Latest Dag Processor Heartbeat
+        detailed_status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Detailed Status
+        instances:
+          anyOf:
+          - items:
+              $ref: '#/components/schemas/DagProcessorInstanceInfoResponse'
+            type: array
+          - type: 'null'
+          title: Instances
       type: object
       required:
       - status
       - latest_dag_processor_heartbeat
+      - detailed_status
       title: DagProcessorInfoResponse
       description: DagProcessor info serializer for responses.
+    DagProcessorInstanceInfoResponse:
+      properties:
+        status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Status
+        hostname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Hostname
+        latest_dag_processor_heartbeat:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Latest Dag Processor Heartbeat
+        bundle_names:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Bundle Names
+      type: object
+      required:
+      - status
+      - hostname
+      - latest_dag_processor_heartbeat
+      - bundle_names
+      title: DagProcessorInstanceInfoResponse
+      description: Dag processor instance info serializer for responses.
     DagRunAssetReference:
       properties:
         run_id:
@@ -14838,6 +14883,18 @@ components:
           - type: string
           - type: 'null'
           title: Unixname
+        team_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Team Name
+        bundle_names:
+          anyOf:
+          - items:
+              type: string
+            type: array
+          - type: 'null'
+          title: Bundle Names
         dag_display_name:
           anyOf:
           - type: string
@@ -15459,12 +15516,49 @@ components:
           - type: string
           - type: 'null'
           title: Latest Scheduler Heartbeat
+        detailed_status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Detailed Status
+        instances:
+          anyOf:
+          - items:
+              $ref: '#/components/schemas/SchedulerInstanceInfoResponse'
+            type: array
+          - type: 'null'
+          title: Instances
       type: object
       required:
       - status
       - latest_scheduler_heartbeat
+      - detailed_status
       title: SchedulerInfoResponse
       description: Scheduler info serializer for responses.
+    SchedulerInstanceInfoResponse:
+      properties:
+        status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Status
+        hostname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Hostname
+        latest_scheduler_heartbeat:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Latest Scheduler Heartbeat
+      type: object
+      required:
+      - status
+      - hostname
+      - latest_scheduler_heartbeat
+      title: SchedulerInstanceInfoResponse
+      description: Scheduler instance info serializer for responses.
     StructuredLogMessage:
       properties:
         timestamp:
@@ -16590,12 +16684,55 @@ components:
           - type: string
           - type: 'null'
           title: Latest Triggerer Heartbeat
+        detailed_status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Detailed Status
+        instances:
+          anyOf:
+          - items:
+              $ref: '#/components/schemas/TriggererInstanceInfoResponse'
+            type: array
+          - type: 'null'
+          title: Instances
       type: object
       required:
       - status
       - latest_triggerer_heartbeat
+      - detailed_status
       title: TriggererInfoResponse
       description: Triggerer info serializer for responses.
+    TriggererInstanceInfoResponse:
+      properties:
+        status:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Status
+        hostname:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Hostname
+        latest_triggerer_heartbeat:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Latest Triggerer Heartbeat
+        team_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Team Name
+      type: object
+      required:
+      - status
+      - hostname
+      - latest_triggerer_heartbeat
+      - team_name
+      title: TriggererInstanceInfoResponse
+      description: Triggerer instance info serializer for responses.
     UpdateHITLDetailPayload:
       properties:
         chosen_options:
diff --git a/airflow-core/src/airflow/cli/commands/dag_processor_command.py 
b/airflow-core/src/airflow/cli/commands/dag_processor_command.py
index 04e410a5d66..7e7df45a8d6 100644
--- a/airflow-core/src/airflow/cli/commands/dag_processor_command.py
+++ b/airflow-core/src/airflow/cli/commands/dag_processor_command.py
@@ -38,7 +38,7 @@ def _create_dag_processor_job_runner(args: Any) -> 
DagProcessorJobRunner:
     if args.bundle_name:
         cli_utils.validate_dag_bundle_arg(args.bundle_name)
     return DagProcessorJobRunner(
-        job=Job(),
+        job=Job(bundle_names=args.bundle_name),
         processor=DagFileProcessorManager(
             max_runs=args.num_runs,
             bundle_names_to_parse=args.bundle_name,
diff --git a/airflow-core/src/airflow/cli/commands/triggerer_command.py 
b/airflow-core/src/airflow/cli/commands/triggerer_command.py
index 7e1d98f44db..87200ff57be 100644
--- a/airflow-core/src/airflow/cli/commands/triggerer_command.py
+++ b/airflow-core/src/airflow/cli/commands/triggerer_command.py
@@ -62,7 +62,9 @@ def triggerer_run(
     set_component_mp_start_method("triggerer")
     with _serve_logs(skip_serve_logs):
         triggerer_job_runner = TriggererJobRunner(
-            job=Job(heartrate=triggerer_heartrate), capacity=capacity, 
queues=queues, team_name=team_name
+            job=Job(heartrate=triggerer_heartrate, team_name=team_name),
+            capacity=capacity,
+            queues=queues,
         )
         run_job(job=triggerer_job_runner.job, 
execute_callable=triggerer_job_runner._execute)
 
diff --git a/airflow-core/src/airflow/jobs/job.py 
b/airflow-core/src/airflow/jobs/job.py
index b19cd103de9..fa59a440abc 100644
--- a/airflow-core/src/airflow/jobs/job.py
+++ b/airflow-core/src/airflow/jobs/job.py
@@ -24,7 +24,7 @@ from functools import cached_property, lru_cache
 from time import sleep
 from typing import TYPE_CHECKING, NoReturn
 
-from sqlalchemy import Index, Integer, String, case, select
+from sqlalchemy import ForeignKey, Index, Integer, String, case, select
 from sqlalchemy.exc import OperationalError
 from sqlalchemy.orm import Mapped, backref, foreign, mapped_column, 
relationship
 from sqlalchemy.orm.session import make_transient
@@ -40,7 +40,7 @@ from airflow.utils.log.logging_mixin import LoggingMixin
 from airflow.utils.net import get_hostname
 from airflow.utils.platform import getuser
 from airflow.utils.session import NEW_SESSION, create_session, provide_session
-from airflow.utils.sqlalchemy import UtcDateTime
+from airflow.utils.sqlalchemy import ExtendedJSON, UtcDateTime
 
 
 class JobState(str, Enum):
@@ -101,6 +101,10 @@ class Job(Base, LoggingMixin):
     executor_class: Mapped[str | None] = mapped_column(String(500))
     hostname: Mapped[str | None] = mapped_column(String(500))
     unixname: Mapped[str | None] = mapped_column(String(1000))
+    team_name: Mapped[str | None] = mapped_column(
+        String(50), ForeignKey("team.name", ondelete="SET NULL"), nullable=True
+    )
+    bundle_names: Mapped[list[str] | None] = mapped_column(ExtendedJSON, 
nullable=True)
 
     __table_args__ = (
         Index("job_type_heart", job_type, latest_heartbeat),
diff --git a/airflow-core/src/airflow/jobs/triggerer_job_runner.py 
b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
index 8f8e930b07f..6af0d1d5dfc 100644
--- a/airflow-core/src/airflow/jobs/triggerer_job_runner.py
+++ b/airflow-core/src/airflow/jobs/triggerer_job_runner.py
@@ -202,7 +202,6 @@ class TriggererJobRunner(BaseJobRunner, LoggingMixin):
         job: Job,
         capacity=None,
         queues: set[str] | None = None,
-        team_name: str | None = None,
     ):
         super().__init__(job)
         if capacity is None:
@@ -212,7 +211,6 @@ class TriggererJobRunner(BaseJobRunner, LoggingMixin):
         else:
             raise ValueError(f"Capacity number {capacity!r} is invalid")
         self.queues = queues
-        self.team_name = team_name
         # Set up only when _execute() starts the subprocess; keep it defined 
so that
         # signal handlers (or other code) firing before startup don't hit 
AttributeError.
         self.trigger_runner: TriggerRunnerSupervisor | None = None
@@ -274,7 +272,7 @@ class TriggererJobRunner(BaseJobRunner, LoggingMixin):
                 capacity=self.capacity,
                 logger=log,
                 queues=self.queues,
-                team_name=self.team_name,
+                team_name=self.job.team_name,
             )
             # Run the main DB comms loop in this process
             self.trigger_runner.run()
diff --git 
a/airflow-core/src/airflow/migrations/versions/0133_3_4_0_add_scope_columns_to_job.py
 
b/airflow-core/src/airflow/migrations/versions/0133_3_4_0_add_scope_columns_to_job.py
new file mode 100644
index 00000000000..ba8fa778610
--- /dev/null
+++ 
b/airflow-core/src/airflow/migrations/versions/0133_3_4_0_add_scope_columns_to_job.py
@@ -0,0 +1,58 @@
+#
+# 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.
+
+"""
+Add team_name and bundle_names scope columns to job table.
+
+Revision ID: f8c2a1d94e03
+Revises: 8d3f1a6b2c47
+Create Date: 2026-07-08 02:00:00.000000
+
+"""
+
+from __future__ import annotations
+
+import sqlalchemy as sa
+from alembic import op
+
+from airflow.utils.sqlalchemy import ExtendedJSON
+
+# revision identifiers, used by Alembic.
+revision = "f8c2a1d94e03"
+down_revision = "8d3f1a6b2c47"
+branch_labels = None
+depends_on = None
+airflow_version = "3.4.0"
+
+
+def upgrade():
+    """Add triggerer team and dag-processor bundle scope columns to job 
table."""
+    with op.batch_alter_table("job", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("team_name", sa.String(length=50), 
nullable=True))
+        batch_op.add_column(sa.Column("bundle_names", ExtendedJSON(), 
nullable=True))
+        batch_op.create_foreign_key(
+            batch_op.f("job_team_name_fkey"), "team", ["team_name"], ["name"], 
ondelete="SET NULL"
+        )
+
+
+def downgrade():
+    """Remove scope columns from job table."""
+    with op.batch_alter_table("job", schema=None) as batch_op:
+        batch_op.drop_constraint(batch_op.f("job_team_name_fkey"), 
type_="foreignkey")
+        batch_op.drop_column("bundle_names")
+        batch_op.drop_column("team_name")
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index ca7eee96aee..888427fae8e 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -4411,14 +4411,95 @@ export const $DagProcessorInfoResponse = {
                 }
             ],
             title: 'Latest Dag Processor Heartbeat'
+        },
+        detailed_status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Detailed Status'
+        },
+        instances: {
+            anyOf: [
+                {
+                    items: {
+                        '$ref': 
'#/components/schemas/DagProcessorInstanceInfoResponse'
+                    },
+                    type: 'array'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Instances'
         }
     },
     type: 'object',
-    required: ['status', 'latest_dag_processor_heartbeat'],
+    required: ['status', 'latest_dag_processor_heartbeat', 'detailed_status'],
     title: 'DagProcessorInfoResponse',
     description: 'DagProcessor info serializer for responses.'
 } as const;
 
+export const $DagProcessorInstanceInfoResponse = {
+    properties: {
+        status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Status'
+        },
+        hostname: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Hostname'
+        },
+        latest_dag_processor_heartbeat: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Latest Dag Processor Heartbeat'
+        },
+        bundle_names: {
+            anyOf: [
+                {
+                    items: {
+                        type: 'string'
+                    },
+                    type: 'array'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Bundle Names'
+        }
+    },
+    type: 'object',
+    required: ['status', 'hostname', 'latest_dag_processor_heartbeat', 
'bundle_names'],
+    title: 'DagProcessorInstanceInfoResponse',
+    description: 'Dag processor instance info serializer for responses.'
+} as const;
+
 export const $DagRunAssetReference = {
     properties: {
         run_id: {
@@ -5697,6 +5778,31 @@ export const $JobResponse = {
             ],
             title: 'Unixname'
         },
+        team_name: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Team Name'
+        },
+        bundle_names: {
+            anyOf: [
+                {
+                    items: {
+                        type: 'string'
+                    },
+                    type: 'array'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Bundle Names'
+        },
         dag_display_name: {
             anyOf: [
                 {
@@ -6605,14 +6711,81 @@ export const $SchedulerInfoResponse = {
                 }
             ],
             title: 'Latest Scheduler Heartbeat'
+        },
+        detailed_status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Detailed Status'
+        },
+        instances: {
+            anyOf: [
+                {
+                    items: {
+                        '$ref': 
'#/components/schemas/SchedulerInstanceInfoResponse'
+                    },
+                    type: 'array'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Instances'
         }
     },
     type: 'object',
-    required: ['status', 'latest_scheduler_heartbeat'],
+    required: ['status', 'latest_scheduler_heartbeat', 'detailed_status'],
     title: 'SchedulerInfoResponse',
     description: 'Scheduler info serializer for responses.'
 } as const;
 
+export const $SchedulerInstanceInfoResponse = {
+    properties: {
+        status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Status'
+        },
+        hostname: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Hostname'
+        },
+        latest_scheduler_heartbeat: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Latest Scheduler Heartbeat'
+        }
+    },
+    type: 'object',
+    required: ['status', 'hostname', 'latest_scheduler_heartbeat'],
+    title: 'SchedulerInstanceInfoResponse',
+    description: 'Scheduler instance info serializer for responses.'
+} as const;
+
 export const $StructuredLogMessage = {
     properties: {
         timestamp: {
@@ -8387,14 +8560,92 @@ export const $TriggererInfoResponse = {
                 }
             ],
             title: 'Latest Triggerer Heartbeat'
+        },
+        detailed_status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Detailed Status'
+        },
+        instances: {
+            anyOf: [
+                {
+                    items: {
+                        '$ref': 
'#/components/schemas/TriggererInstanceInfoResponse'
+                    },
+                    type: 'array'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Instances'
         }
     },
     type: 'object',
-    required: ['status', 'latest_triggerer_heartbeat'],
+    required: ['status', 'latest_triggerer_heartbeat', 'detailed_status'],
     title: 'TriggererInfoResponse',
     description: 'Triggerer info serializer for responses.'
 } as const;
 
+export const $TriggererInstanceInfoResponse = {
+    properties: {
+        status: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Status'
+        },
+        hostname: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Hostname'
+        },
+        latest_triggerer_heartbeat: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Latest Triggerer Heartbeat'
+        },
+        team_name: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Team Name'
+        }
+    },
+    type: 'object',
+    required: ['status', 'hostname', 'latest_triggerer_heartbeat', 
'team_name'],
+    title: 'TriggererInstanceInfoResponse',
+    description: 'Triggerer instance info serializer for responses.'
+} as const;
+
 export const $UpdateHITLDetailPayload = {
     properties: {
         chosen_options: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 1b280d7a567..be90b462bdb 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1141,6 +1141,18 @@ export type DAGWarningResponse = {
 export type DagProcessorInfoResponse = {
     status: string | null;
     latest_dag_processor_heartbeat: string | null;
+    detailed_status: string | null;
+    instances?: Array<DagProcessorInstanceInfoResponse> | null;
+};
+
+/**
+ * Dag processor instance info serializer for responses.
+ */
+export type DagProcessorInstanceInfoResponse = {
+    status: string | null;
+    hostname: string | null;
+    latest_dag_processor_heartbeat: string | null;
+    bundle_names: Array<(string)> | null;
 };
 
 /**
@@ -1492,6 +1504,8 @@ export type JobResponse = {
     executor_class: string | null;
     hostname: string | null;
     unixname: string | null;
+    team_name?: string | null;
+    bundle_names?: Array<(string)> | null;
     dag_display_name?: string | null;
 };
 
@@ -1724,6 +1738,17 @@ export type ReprocessBehavior = 'failed' | 'completed' | 
'none';
 export type SchedulerInfoResponse = {
     status: string | null;
     latest_scheduler_heartbeat: string | null;
+    detailed_status: string | null;
+    instances?: Array<SchedulerInstanceInfoResponse> | null;
+};
+
+/**
+ * Scheduler instance info serializer for responses.
+ */
+export type SchedulerInstanceInfoResponse = {
+    status: string | null;
+    hostname: string | null;
+    latest_scheduler_heartbeat: string | null;
 };
 
 /**
@@ -2070,6 +2095,18 @@ export type TriggerResponse = {
 export type TriggererInfoResponse = {
     status: string | null;
     latest_triggerer_heartbeat: string | null;
+    detailed_status: string | null;
+    instances?: Array<TriggererInstanceInfoResponse> | null;
+};
+
+/**
+ * Triggerer instance info serializer for responses.
+ */
+export type TriggererInstanceInfoResponse = {
+    status: string | null;
+    hostname: string | null;
+    latest_triggerer_heartbeat: string | null;
+    team_name: string | null;
 };
 
 /**
diff --git a/airflow-core/src/airflow/utils/db.py 
b/airflow-core/src/airflow/utils/db.py
index 71ec2d2004b..f0779c99690 100644
--- a/airflow-core/src/airflow/utils/db.py
+++ b/airflow-core/src/airflow/utils/db.py
@@ -117,7 +117,7 @@ _REVISION_HEADS_MAP: dict[str, str] = {
     "3.1.8": "509b94a1042d",
     "3.2.0": "1d6611b6ab7c",
     "3.3.0": "d2f4e1b3c5a7",
-    "3.4.0": "8d3f1a6b2c47",
+    "3.4.0": "f8c2a1d94e03",
 }
 
 # Prefix used to identify tables holding data moved during migration.
diff --git a/airflow-core/tests/unit/api/common/test_airflow_health.py 
b/airflow-core/tests/unit/api/common/test_airflow_health.py
index 43c17e41088..79648ac99ac 100644
--- a/airflow-core/tests/unit/api/common/test_airflow_health.py
+++ b/airflow-core/tests/unit/api/common/test_airflow_health.py
@@ -16,127 +16,471 @@
 # under the License.
 from __future__ import annotations
 
-from datetime import datetime
+from datetime import datetime, timedelta
 from unittest.mock import MagicMock, patch
 
 import pytest
 
+from airflow._shared.timezones import timezone
 from airflow.api.common.airflow_health import (
+    DEGRADED,
+    DOWN,
     HEALTHY,
     UNHEALTHY,
     get_airflow_health,
+    get_jobs_health,
 )
-from airflow.jobs.job import Job
+from airflow.jobs.job import Job, JobState
+from airflow.jobs.scheduler_job_runner import SchedulerJobRunner
+from airflow.jobs.triggerer_job_runner import TriggererJobRunner
+from airflow.utils.session import provide_session
+
+from tests_common.test_utils.db import clear_db_jobs
 
 pytestmark = pytest.mark.db_test
 
+STALE_HEARTBEAT_AGE = timedelta(minutes=5)
 
-@patch("airflow.api.common.airflow_health.SchedulerJobRunner.most_recent_job", 
return_value=None)
-@patch("airflow.api.common.airflow_health.TriggererJobRunner.most_recent_job", 
return_value=None)
-@patch("airflow.api.common.airflow_health.DagProcessorJobRunner.most_recent_job",
 return_value=None)
-def test_get_airflow_health_only_metadatabase_healthy(
-    latest_scheduler_job_mock,
-    latest_triggerer_job_mock,
-    latest_dag_processor_job_mock,
-):
-    health_status = get_airflow_health()
-    expected_status = {
-        "metadatabase": {"status": HEALTHY},
-        "scheduler": {"status": UNHEALTHY, "latest_scheduler_heartbeat": None},
-        "triggerer": {"status": None, "latest_triggerer_heartbeat": None},
-        "dag_processor": {"status": None, "latest_dag_processor_heartbeat": 
None},
+
+def _mock_job(
+    *,
+    hostname: str,
+    heartbeat: datetime,
+    alive: bool,
+    team_name: str | None = None,
+    bundle_names: list[str] | None = None,
+) -> MagicMock:
+    job = MagicMock(spec=Job)
+    job.hostname = hostname
+    job.latest_heartbeat = heartbeat
+    job.is_alive = MagicMock(return_value=alive)
+    job.team_name = team_name
+    job.bundle_names = bundle_names
+    return job
+
+
+def _empty_component(heartbeat_field: str) -> dict:
+    return {
+        "status": UNHEALTHY,
+        heartbeat_field: None,
+        "detailed_status": DOWN,
+        "instances": None,
     }
 
-    assert health_status == expected_status
+
+def _create_job(
+    session,
+    runner_class,
+    *,
+    hostname: str,
+    heartbeat: datetime,
+    state: JobState = JobState.RUNNING,
+    end_date: datetime | None = None,
+    team_name: str | None = None,
+    bundle_names: list[str] | None = None,
+) -> Job:
+    job = Job(
+        state=state,
+        latest_heartbeat=heartbeat,
+        hostname=hostname,
+        end_date=end_date,
+        team_name=team_name,
+        bundle_names=bundle_names,
+    )
+    if runner_class is TriggererJobRunner:
+        runner_class(job=job, capacity=1)
+    else:
+        runner_class(job=job)
+    session.add(job)
+    return job
+
+
+ALIVE_SCHEDULER_JOB_MOCK = _mock_job(hostname="scheduler-alive", 
heartbeat=datetime(2024, 2, 1), alive=True)
+STALE_SCHEDULER_JOB_MOCK = _mock_job(hostname="scheduler-stale", 
heartbeat=datetime(2024, 1, 1), alive=False)
+STALE_SCHEDULER_JOB_MOCK_2 = _mock_job(
+    hostname="scheduler-stale-2", heartbeat=datetime(2023, 12, 1), alive=False
+)
+STALE_SCHEDULER_JOB_MOCK_3 = _mock_job(
+    hostname="scheduler-stale-3", heartbeat=datetime(2023, 11, 1), alive=False
+)
+
+ALIVE_TRIGGERER_JOB_MOCK = _mock_job(
+    hostname="triggerer-alive",
+    heartbeat=datetime(2024, 2, 1),
+    alive=True,
+    team_name="team-a",
+)
+STALE_TRIGGERER_JOB_MOCK = _mock_job(
+    hostname="triggerer-stale",
+    heartbeat=datetime(2024, 1, 1),
+    alive=False,
+    team_name="team-b",
+)
+
+ALIVE_DAG_PROCESSOR_JOB_MOCK = _mock_job(
+    hostname="dag-processor-host",
+    heartbeat=datetime(2024, 1, 3),
+    alive=True,
+    bundle_names=["bundle-a"],
+)
 
 
-@patch("airflow.api.common.airflow_health.SchedulerJobRunner.most_recent_job", 
return_value=Exception)
-@patch("airflow.api.common.airflow_health.TriggererJobRunner.most_recent_job", 
return_value=Exception)
-@patch("airflow.api.common.airflow_health.DagProcessorJobRunner.most_recent_job",
 return_value=Exception)
-def test_get_airflow_health_metadatabase_unhealthy(
-    latest_scheduler_job_mock,
-    latest_triggerer_job_mock,
-    latest_dag_processor_job_mock,
-):
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def test_get_airflow_health_no_jobs(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [[], [], []]
     health_status = get_airflow_health()
 
-    expected_status = {
-        "metadatabase": {"status": UNHEALTHY},
-        "scheduler": {"status": UNHEALTHY, "latest_scheduler_heartbeat": None},
-        "triggerer": {"status": UNHEALTHY, "latest_triggerer_heartbeat": None},
-        "dag_processor": {"status": UNHEALTHY, 
"latest_dag_processor_heartbeat": None},
+    assert health_status == {
+        "metadatabase": {"status": HEALTHY},
+        "scheduler": _empty_component("latest_scheduler_heartbeat"),
+        "triggerer": _empty_component("latest_triggerer_heartbeat"),
+        "dag_processor": _empty_component("latest_dag_processor_heartbeat"),
     }
 
-    assert health_status == expected_status
 
+@patch("airflow.api.common.airflow_health.get_jobs_health", 
side_effect=Exception)
+def test_get_airflow_health_metadatabase_unhealthy(mock_get_jobs_health):
+    health_status = get_airflow_health()
 
-LATEST_SCHEDULER_JOB_MOCK = MagicMock(spec=Job)
-LATEST_SCHEDULER_JOB_MOCK.latest_heartbeat = datetime.now()
-LATEST_SCHEDULER_JOB_MOCK.is_alive = MagicMock(return_value=True)
+    assert health_status == {
+        "metadatabase": {"status": UNHEALTHY},
+        "scheduler": _empty_component("latest_scheduler_heartbeat"),
+        "triggerer": _empty_component("latest_triggerer_heartbeat"),
+        "dag_processor": _empty_component("latest_dag_processor_heartbeat"),
+    }
 
 
-@patch(
-    "airflow.api.common.airflow_health.SchedulerJobRunner.most_recent_job",
-    return_value=LATEST_SCHEDULER_JOB_MOCK,
-)
-@patch("airflow.api.common.airflow_health.TriggererJobRunner.most_recent_job", 
return_value=None)
-@patch("airflow.api.common.airflow_health.DagProcessorJobRunner.most_recent_job",
 return_value=None)
-def test_get_airflow_health_scheduler_healthy_no_triggerer(
-    latest_scheduler_job_mock,
-    latest_triggerer_job_mock,
-    latest_dag_processor_job_mock,
-):
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def test_get_airflow_health_one_alive_job(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [[ALIVE_SCHEDULER_JOB_MOCK], [], []]
     health_status = get_airflow_health()
 
-    expected_status = {
+    assert health_status == {
         "metadatabase": {"status": HEALTHY},
         "scheduler": {
             "status": HEALTHY,
-            "latest_scheduler_heartbeat": 
LATEST_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "latest_scheduler_heartbeat": 
ALIVE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "detailed_status": HEALTHY,
+            "instances": [
+                {
+                    "hostname": ALIVE_SCHEDULER_JOB_MOCK.hostname,
+                    "status": HEALTHY,
+                    "latest_scheduler_heartbeat": 
ALIVE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+                }
+            ],
         },
-        "triggerer": {"status": None, "latest_triggerer_heartbeat": None},
-        "dag_processor": {"status": None, "latest_dag_processor_heartbeat": 
None},
+        "triggerer": _empty_component("latest_triggerer_heartbeat"),
+        "dag_processor": _empty_component("latest_dag_processor_heartbeat"),
     }
 
-    assert health_status == expected_status
 
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def test_get_airflow_health_mixed_alive_and_stale_jobs(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [
+        [ALIVE_SCHEDULER_JOB_MOCK, STALE_SCHEDULER_JOB_MOCK, 
STALE_SCHEDULER_JOB_MOCK_2],
+        [],
+        [],
+    ]
+    health_status = get_airflow_health()
 
-LATEST_TRIGGERER_JOB_MOCK = MagicMock(spec=Job)
-LATEST_TRIGGERER_JOB_MOCK.latest_heartbeat = datetime.now()
-LATEST_TRIGGERER_JOB_MOCK.is_alive = MagicMock(return_value=True)
+    assert health_status["scheduler"]["status"] == HEALTHY
+    assert health_status["scheduler"]["detailed_status"] == DEGRADED
+    assert health_status["scheduler"]["instances"] == [
+        {
+            "hostname": ALIVE_SCHEDULER_JOB_MOCK.hostname,
+            "status": HEALTHY,
+            "latest_scheduler_heartbeat": 
ALIVE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+        },
+        {
+            "hostname": STALE_SCHEDULER_JOB_MOCK.hostname,
+            "status": UNHEALTHY,
+            "latest_scheduler_heartbeat": 
STALE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+        },
+        {
+            "hostname": STALE_SCHEDULER_JOB_MOCK_2.hostname,
+            "status": UNHEALTHY,
+            "latest_scheduler_heartbeat": 
STALE_SCHEDULER_JOB_MOCK_2.latest_heartbeat.isoformat(),
+        },
+    ]
+    assert (
+        health_status["scheduler"]["latest_scheduler_heartbeat"]
+        == ALIVE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat()
+    )
+    assert health_status["triggerer"] == 
_empty_component("latest_triggerer_heartbeat")
+    assert health_status["dag_processor"] == 
_empty_component("latest_dag_processor_heartbeat")
 
-LATEST_DAG_PROCESSOR_JOB_MOCK = MagicMock(spec=Job)
-LATEST_DAG_PROCESSOR_JOB_MOCK.latest_heartbeat = datetime.now()
-LATEST_DAG_PROCESSOR_JOB_MOCK.is_alive = MagicMock(return_value=True)
 
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def test_get_airflow_health_all_stale_jobs(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [
+        [STALE_SCHEDULER_JOB_MOCK, STALE_SCHEDULER_JOB_MOCK_2, 
STALE_SCHEDULER_JOB_MOCK_3],
+        [],
+        [],
+    ]
+    health_status = get_airflow_health()
 
-@patch("airflow.api.common.airflow_health.SchedulerJobRunner.most_recent_job", 
return_value=None)
-@patch(
-    "airflow.api.common.airflow_health.TriggererJobRunner.most_recent_job",
-    return_value=LATEST_TRIGGERER_JOB_MOCK,
-)
-@patch(
-    "airflow.api.common.airflow_health.DagProcessorJobRunner.most_recent_job",
-    return_value=LATEST_DAG_PROCESSOR_JOB_MOCK,
-)
-def test_get_airflow_health_triggerer_healthy_no_scheduler_job_record(
-    latest_scheduler_job_mock,
-    latest_triggerer_job_mock,
-    latest_dag_processor_job_mock,
-):
+    assert health_status["scheduler"]["status"] == UNHEALTHY
+    assert health_status["scheduler"]["detailed_status"] == DOWN
+    assert health_status["scheduler"]["instances"] == [
+        {
+            "hostname": STALE_SCHEDULER_JOB_MOCK.hostname,
+            "status": UNHEALTHY,
+            "latest_scheduler_heartbeat": 
STALE_SCHEDULER_JOB_MOCK.latest_heartbeat.isoformat(),
+        },
+        {
+            "hostname": STALE_SCHEDULER_JOB_MOCK_2.hostname,
+            "status": UNHEALTHY,
+            "latest_scheduler_heartbeat": 
STALE_SCHEDULER_JOB_MOCK_2.latest_heartbeat.isoformat(),
+        },
+        {
+            "hostname": STALE_SCHEDULER_JOB_MOCK_3.hostname,
+            "status": UNHEALTHY,
+            "latest_scheduler_heartbeat": 
STALE_SCHEDULER_JOB_MOCK_3.latest_heartbeat.isoformat(),
+        },
+    ]
+
+
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def 
test_get_airflow_health_mixed_triggerers_include_team_name(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [[], [ALIVE_TRIGGERER_JOB_MOCK, 
STALE_TRIGGERER_JOB_MOCK], []]
     health_status = get_airflow_health()
 
-    expected_status = {
+    assert health_status["triggerer"]["status"] == HEALTHY
+    assert health_status["triggerer"]["detailed_status"] == DEGRADED
+    assert health_status["triggerer"]["instances"] == [
+        {
+            "hostname": ALIVE_TRIGGERER_JOB_MOCK.hostname,
+            "status": HEALTHY,
+            "latest_triggerer_heartbeat": 
ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "team_name": ALIVE_TRIGGERER_JOB_MOCK.team_name,
+        },
+        {
+            "hostname": STALE_TRIGGERER_JOB_MOCK.hostname,
+            "status": UNHEALTHY,
+            "latest_triggerer_heartbeat": 
STALE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "team_name": STALE_TRIGGERER_JOB_MOCK.team_name,
+        },
+    ]
+    assert (
+        health_status["triggerer"]["latest_triggerer_heartbeat"]
+        == ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat()
+    )
+
+
+@patch("airflow.api.common.airflow_health.get_jobs_health")
+def 
test_get_airflow_health_triggerer_and_dag_processor_healthy(mock_get_jobs_health):
+    mock_get_jobs_health.side_effect = [[], [ALIVE_TRIGGERER_JOB_MOCK], 
[ALIVE_DAG_PROCESSOR_JOB_MOCK]]
+    health_status = get_airflow_health()
+
+    assert health_status == {
         "metadatabase": {"status": HEALTHY},
-        "scheduler": {"status": UNHEALTHY, "latest_scheduler_heartbeat": None},
+        "scheduler": _empty_component("latest_scheduler_heartbeat"),
         "triggerer": {
             "status": HEALTHY,
-            "latest_triggerer_heartbeat": 
LATEST_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "latest_triggerer_heartbeat": 
ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(),
+            "detailed_status": HEALTHY,
+            "instances": [
+                {
+                    "hostname": ALIVE_TRIGGERER_JOB_MOCK.hostname,
+                    "status": HEALTHY,
+                    "latest_triggerer_heartbeat": 
ALIVE_TRIGGERER_JOB_MOCK.latest_heartbeat.isoformat(),
+                    "team_name": ALIVE_TRIGGERER_JOB_MOCK.team_name,
+                }
+            ],
         },
         "dag_processor": {
             "status": HEALTHY,
-            "latest_dag_processor_heartbeat": 
LATEST_DAG_PROCESSOR_JOB_MOCK.latest_heartbeat.isoformat(),
+            "latest_dag_processor_heartbeat": 
ALIVE_DAG_PROCESSOR_JOB_MOCK.latest_heartbeat.isoformat(),
+            "detailed_status": HEALTHY,
+            "instances": [
+                {
+                    "hostname": ALIVE_DAG_PROCESSOR_JOB_MOCK.hostname,
+                    "status": HEALTHY,
+                    "latest_dag_processor_heartbeat": 
ALIVE_DAG_PROCESSOR_JOB_MOCK.latest_heartbeat.isoformat(),
+                    "bundle_names": ALIVE_DAG_PROCESSOR_JOB_MOCK.bundle_names,
+                }
+            ],
         },
     }
 
-    assert health_status == expected_status
+
+class TestAirflowHealthFromDb:
+    @pytest.fixture(autouse=True)
+    def cleanup_jobs(self):
+        clear_db_jobs()
+        yield
+        clear_db_jobs()
+
+    @provide_session
+    def 
test_get_jobs_health_returns_unfinished_jobs_ordered_by_heartbeat(self, *, 
session):
+        older = timezone.utcnow() - timedelta(minutes=5)
+        newer = timezone.utcnow()
+        older_job = _create_job(session, SchedulerJobRunner, hostname="older", 
heartbeat=older)
+        newer_job = _create_job(session, SchedulerJobRunner, hostname="newer", 
heartbeat=newer)
+        _create_job(
+            session,
+            SchedulerJobRunner,
+            hostname="ended",
+            heartbeat=newer,
+            end_date=newer,
+        )
+        unfinished_failed = _create_job(
+            session,
+            SchedulerJobRunner,
+            hostname="failed-unfinished",
+            heartbeat=older - timedelta(minutes=1),
+            state=JobState.FAILED,
+        )
+        _create_job(session, TriggererJobRunner, hostname="triggerer", 
heartbeat=newer)
+        session.commit()
+
+        jobs = get_jobs_health(SchedulerJobRunner, session=session)
+
+        assert [job.hostname for job in jobs] == [
+            newer_job.hostname,
+            older_job.hostname,
+            unfinished_failed.hostname,
+        ]
+
+    def test_get_airflow_health_no_jobs(self):
+        health_status = get_airflow_health()
+
+        assert health_status == {
+            "metadatabase": {"status": HEALTHY},
+            "scheduler": _empty_component("latest_scheduler_heartbeat"),
+            "triggerer": _empty_component("latest_triggerer_heartbeat"),
+            "dag_processor": 
_empty_component("latest_dag_processor_heartbeat"),
+        }
+
+    @provide_session
+    def test_get_airflow_health_one_alive_job(self, *, session):
+        heartbeat = timezone.utcnow()
+        job = _create_job(session, SchedulerJobRunner, 
hostname="scheduler-alive", heartbeat=heartbeat)
+        _create_job(
+            session,
+            SchedulerJobRunner,
+            hostname="scheduler-ended",
+            heartbeat=heartbeat,
+            end_date=heartbeat,
+        )
+        session.commit()
+
+        health_status = get_airflow_health()
+
+        assert health_status["metadatabase"]["status"] == HEALTHY
+        assert health_status["scheduler"]["status"] == HEALTHY
+        assert health_status["scheduler"]["detailed_status"] == HEALTHY
+        assert health_status["scheduler"]["instances"] == [
+            {
+                "hostname": job.hostname,
+                "status": HEALTHY,
+                "latest_scheduler_heartbeat": heartbeat.isoformat(),
+            }
+        ]
+        assert health_status["scheduler"]["latest_scheduler_heartbeat"] == 
heartbeat.isoformat()
+        assert health_status["triggerer"] == 
_empty_component("latest_triggerer_heartbeat")
+        assert health_status["dag_processor"] == 
_empty_component("latest_dag_processor_heartbeat")
+
+    @provide_session
+    def test_get_airflow_health_mixed_alive_and_stale_jobs(self, *, session):
+        alive_heartbeat = timezone.utcnow()
+        stale_heartbeat = timezone.utcnow() - STALE_HEARTBEAT_AGE
+        older_stale_heartbeat = stale_heartbeat - timedelta(minutes=1)
+        alive = _create_job(
+            session, SchedulerJobRunner, hostname="scheduler-alive", 
heartbeat=alive_heartbeat
+        )
+        stale = _create_job(
+            session, SchedulerJobRunner, hostname="scheduler-stale", 
heartbeat=stale_heartbeat
+        )
+        older_stale = _create_job(
+            session, SchedulerJobRunner, hostname="scheduler-stale-2", 
heartbeat=older_stale_heartbeat
+        )
+        session.commit()
+
+        health_status = get_airflow_health()
+
+        assert health_status["scheduler"]["status"] == HEALTHY
+        assert health_status["scheduler"]["detailed_status"] == DEGRADED
+        assert health_status["scheduler"]["instances"] == [
+            {
+                "hostname": alive.hostname,
+                "status": HEALTHY,
+                "latest_scheduler_heartbeat": alive_heartbeat.isoformat(),
+            },
+            {
+                "hostname": stale.hostname,
+                "status": UNHEALTHY,
+                "latest_scheduler_heartbeat": stale_heartbeat.isoformat(),
+            },
+            {
+                "hostname": older_stale.hostname,
+                "status": UNHEALTHY,
+                "latest_scheduler_heartbeat": 
older_stale_heartbeat.isoformat(),
+            },
+        ]
+        assert health_status["scheduler"]["latest_scheduler_heartbeat"] == 
alive_heartbeat.isoformat()
+
+    @provide_session
+    def test_get_airflow_health_all_stale_jobs(self, *, session):
+        first = timezone.utcnow() - STALE_HEARTBEAT_AGE
+        second = first - timedelta(minutes=1)
+        third = second - timedelta(minutes=1)
+        jobs = [
+            _create_job(session, SchedulerJobRunner, hostname="stale-1", 
heartbeat=first),
+            _create_job(session, SchedulerJobRunner, hostname="stale-2", 
heartbeat=second),
+            _create_job(session, SchedulerJobRunner, hostname="stale-3", 
heartbeat=third),
+        ]
+        session.commit()
+
+        health_status = get_airflow_health()
+
+        assert health_status["scheduler"]["status"] == UNHEALTHY
+        assert health_status["scheduler"]["detailed_status"] == DOWN
+        assert health_status["scheduler"]["instances"] == [
+            {
+                "hostname": job.hostname,
+                "status": UNHEALTHY,
+                "latest_scheduler_heartbeat": job.latest_heartbeat.isoformat(),
+            }
+            for job in jobs
+        ]
+
+    @provide_session
+    def test_get_airflow_health_mixed_triggerers_include_team_name(self, 
testing_team, *, session):
+        alive_heartbeat = timezone.utcnow()
+        stale_heartbeat = timezone.utcnow() - STALE_HEARTBEAT_AGE
+        alive = _create_job(
+            session,
+            TriggererJobRunner,
+            hostname="triggerer-alive",
+            heartbeat=alive_heartbeat,
+            team_name=testing_team.name,
+        )
+        stale = _create_job(
+            session,
+            TriggererJobRunner,
+            hostname="triggerer-stale",
+            heartbeat=stale_heartbeat,
+            team_name=testing_team.name,
+        )
+        session.commit()
+
+        health_status = get_airflow_health()
+
+        assert health_status["triggerer"]["status"] == HEALTHY
+        assert health_status["triggerer"]["detailed_status"] == DEGRADED
+        assert health_status["triggerer"]["instances"] == [
+            {
+                "hostname": alive.hostname,
+                "status": HEALTHY,
+                "latest_triggerer_heartbeat": alive_heartbeat.isoformat(),
+                "team_name": testing_team.name,
+            },
+            {
+                "hostname": stale.hostname,
+                "status": UNHEALTHY,
+                "latest_triggerer_heartbeat": stale_heartbeat.isoformat(),
+                "team_name": testing_team.name,
+            },
+        ]
+        assert health_status["triggerer"]["latest_triggerer_heartbeat"] == 
alive_heartbeat.isoformat()
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py
index 8e9538d3f63..cb4c7254492 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_job.py
@@ -157,6 +157,7 @@ class TestGetJobs(TestJobEndpoint):
             assert len(matched) == 1
             expected_job = {
                 "id": matched[0].id,
+                "bundle_names": None,
                 "dag_display_name": None,
                 "dag_id": None,
                 "state": matched[0].state,
@@ -166,6 +167,7 @@ class TestGetJobs(TestJobEndpoint):
                 "latest_heartbeat": 
from_datetime_to_zulu(matched[0].latest_heartbeat),
                 "executor_class": None,
                 "hostname": matched[0].hostname,
+                "team_name": None,
                 "unixname": matched[0].unixname,
             }
             assert resp_job == expected_job
@@ -187,6 +189,25 @@ class TestGetJobs(TestJobEndpoint):
         assert response_json["total_entries"] == 1
         assert response_json["jobs"][0]["dag_id"] == "target_dag"
 
+    def test_get_jobs_includes_team_name_and_bundle_names(self, test_client, 
session: Session, testing_team):
+        clear_db_jobs()
+        job = Job(
+            state=JobState.RUNNING,
+            job_type="TriggererJob",
+            team_name=testing_team.name,
+            bundle_names=["bundle-a", "bundle-b"],
+        )
+        session.add(job)
+        session.commit()
+
+        response = test_client.get("/jobs")
+
+        assert response.status_code == 200
+        response_json = response.json()
+        assert response_json["total_entries"] == 1
+        assert response_json["jobs"][0]["team_name"] == testing_team.name
+        assert response_json["jobs"][0]["bundle_names"] == ["bundle-a", 
"bundle-b"]
+
     def test_should_raises_401_unauthenticated(self, 
unauthenticated_test_client):
         response = unauthenticated_test_client.get("/jobs")
         assert response.status_code == 401
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py
index d4b3d97f932..e696ec43811 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_monitor.py
@@ -94,9 +94,9 @@ class TestGetHealth(TestMonitorEndpoint):
         assert body["scheduler"]["status"] == "unhealthy"
         assert body["scheduler"]["latest_scheduler_heartbeat"] is None
 
-    @mock.patch.object(SchedulerJobRunner, "most_recent_job")
-    def test_unhealthy_metadatabase_status(self, most_recent_job_mock, 
test_client):
-        most_recent_job_mock.side_effect = Exception
+    @mock.patch("airflow.api.common.airflow_health.get_jobs_health")
+    def test_unhealthy_metadatabase_status(self, mock_get_jobs_health, 
test_client):
+        mock_get_jobs_health.side_effect = Exception
         response = test_client.get("/monitor/health")
 
         assert response.status_code == 200
@@ -112,14 +112,17 @@ class TestGetHealth(TestMonitorEndpoint):
             "scheduler": {
                 "status": HEALTHY,
                 "latest_scheduler_heartbeat": 
"2024-11-23T11:09:16.663124+00:00",
+                "detailed_status": HEALTHY,
             },
             "triggerer": {
                 "status": HEALTHY,
                 "latest_triggerer_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                "detailed_status": HEALTHY,
             },
             "dag_processor": {
                 "status": HEALTHY,
                 "latest_dag_processor_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                "detailed_status": HEALTHY,
             },
         }
 
@@ -133,6 +136,58 @@ class TestGetHealth(TestMonitorEndpoint):
         assert body["scheduler"]["status"] == HEALTHY
         assert body["triggerer"]["status"] == HEALTHY
 
+    
@mock.patch("airflow.api_fastapi.core_api.routes.public.monitor.get_airflow_health")
+    def test_health_includes_instance_team_name_and_bundle_names(self, 
mock_get_airflow_health, test_client):
+        mock_get_airflow_health.return_value = {
+            "metadatabase": {"status": HEALTHY},
+            "scheduler": {
+                "status": HEALTHY,
+                "latest_scheduler_heartbeat": 
"2024-11-23T11:09:16.663124+00:00",
+                "detailed_status": HEALTHY,
+                "instances": [
+                    {
+                        "status": HEALTHY,
+                        "hostname": "scheduler-1",
+                        "latest_scheduler_heartbeat": 
"2024-11-23T11:09:16.663124+00:00",
+                    }
+                ],
+            },
+            "triggerer": {
+                "status": HEALTHY,
+                "latest_triggerer_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                "detailed_status": HEALTHY,
+                "instances": [
+                    {
+                        "status": HEALTHY,
+                        "hostname": "triggerer-1",
+                        "latest_triggerer_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                        "team_name": "team-a",
+                    }
+                ],
+            },
+            "dag_processor": {
+                "status": HEALTHY,
+                "latest_dag_processor_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                "detailed_status": HEALTHY,
+                "instances": [
+                    {
+                        "status": HEALTHY,
+                        "hostname": "dag-processor-1",
+                        "latest_dag_processor_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                        "bundle_names": ["bundle-a"],
+                    }
+                ],
+            },
+        }
+
+        response = test_client.get("/monitor/health")
+
+        assert response.status_code == 200
+        body = response.json()
+        assert body["triggerer"]["instances"][0]["team_name"] == "team-a"
+        assert body["dag_processor"]["instances"][0]["bundle_names"] == 
["bundle-a"]
+        assert body["scheduler"]["instances"][0]["hostname"] == "scheduler-1"
+
     
@mock.patch("airflow.api_fastapi.core_api.routes.public.monitor.get_airflow_health")
     def test_health_without_dag_processor(self, mock_get_airflow_health, 
test_client):
         mock_get_airflow_health.return_value = {
@@ -140,10 +195,12 @@ class TestGetHealth(TestMonitorEndpoint):
             "scheduler": {
                 "status": HEALTHY,
                 "latest_scheduler_heartbeat": 
"2024-11-23T11:09:16.663124+00:00",
+                "detailed_status": HEALTHY,
             },
             "triggerer": {
                 "status": HEALTHY,
                 "latest_triggerer_heartbeat": 
"2024-11-23T11:09:15.815483+00:00",
+                "detailed_status": HEALTHY,
             },
         }
 
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
index 7c945edf954..3784a57f062 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_instances.py
@@ -456,11 +456,13 @@ class TestGetTaskInstance(TestTaskInstanceEndpoint):
                 "queue": None,
             },
             "triggerer_job": {
+                "bundle_names": None,
                 "dag_display_name": None,
                 "dag_id": None,
                 "end_date": None,
                 "job_type": "TriggererJob",
                 "state": "running",
+                "team_name": None,
                 "unixname": getuser(),
             },
             "team_name": None,
diff --git a/airflow-core/tests/unit/cli/commands/test_dag_processor_command.py 
b/airflow-core/tests/unit/cli/commands/test_dag_processor_command.py
index abdf52ad125..6b6e429b600 100644
--- a/airflow-core/tests/unit/cli/commands/test_dag_processor_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_dag_processor_command.py
@@ -56,6 +56,7 @@ class TestDagProcessorCommand:
         with configure_testing_dag_bundle(os.devnull):
             dag_processor_command.dag_processor(args)
         assert mock_runner.call_args.kwargs["processor"].bundle_names_to_parse 
== ["testing"]
+        assert mock_runner.call_args.kwargs["job"].bundle_names == ["testing"]
 
     @conf_vars({("core", "load_examples"): "False"})
     
@mock.patch("airflow.cli.commands.dag_processor_command.DagProcessorJobRunner")
diff --git a/airflow-core/tests/unit/cli/commands/test_triggerer_command.py 
b/airflow-core/tests/unit/cli/commands/test_triggerer_command.py
index 0cc9a10b52a..f88dc90ea5c 100644
--- a/airflow-core/tests/unit/cli/commands/test_triggerer_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_triggerer_command.py
@@ -51,9 +51,8 @@ class TestTriggererCommand:
         triggerer_command.triggerer(args)
         mock_serve.return_value.__enter__.assert_called_once()
         mock_serve.return_value.__exit__.assert_called_once()
-        mock_triggerer_job_runner.assert_called_once_with(
-            job=mock.ANY, capacity=42, queues=None, team_name=None
-        )
+        mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, 
capacity=42, queues=None)
+        assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is 
None
 
     @conf_vars({("triggerer", "queues_enabled"): "True"})
     @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner")
@@ -66,8 +65,9 @@ class TestTriggererCommand:
         mock_serve.return_value.__enter__.assert_called_once()
         mock_serve.return_value.__exit__.assert_called_once()
         mock_triggerer_job_runner.assert_called_once_with(
-            job=mock.ANY, capacity=4, queues=set(["my_queue", "other_queue"]), 
team_name=None
+            job=mock.ANY, capacity=4, queues=set(["my_queue", "other_queue"])
         )
+        assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is 
None
 
     @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner")
     @mock.patch("airflow.cli.commands.triggerer_command.run_job")
@@ -120,22 +120,20 @@ class TestTriggererCommand:
     @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner")
     @mock.patch("airflow.cli.commands.triggerer_command._serve_logs")
     def test_team_name_passed_through(self, mock_serve, 
mock_triggerer_job_runner, mock_get_team):
-        """--team-name should be passed to TriggererJobRunner when valid"""
+        """--team-name should be set on the Job when valid"""
         mock_triggerer_job_runner.return_value.job_type = "TriggererJob"
         args = self.parser.parse_args(["triggerer", "--team-name", "team_a"])
         triggerer_command.triggerer(args)
-        mock_triggerer_job_runner.assert_called_once_with(
-            job=mock.ANY, capacity=mock.ANY, queues=None, team_name="team_a"
-        )
+        mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, 
capacity=mock.ANY, queues=None)
+        assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name == 
"team_a"
 
     @conf_vars({("core", "multi_team"): "False"})
     @mock.patch("airflow.cli.commands.triggerer_command.TriggererJobRunner")
     @mock.patch("airflow.cli.commands.triggerer_command._serve_logs")
     def test_no_team_name_passes_none(self, mock_serve, 
mock_triggerer_job_runner):
-        """Without --team-name, team_name=None is passed"""
+        """Without --team-name, Job.team_name is None"""
         mock_triggerer_job_runner.return_value.job_type = "TriggererJob"
         args = self.parser.parse_args(["triggerer"])
         triggerer_command.triggerer(args)
-        mock_triggerer_job_runner.assert_called_once_with(
-            job=mock.ANY, capacity=mock.ANY, queues=None, team_name=None
-        )
+        mock_triggerer_job_runner.assert_called_once_with(job=mock.ANY, 
capacity=mock.ANY, queues=None)
+        assert mock_triggerer_job_runner.call_args.kwargs["job"].team_name is 
None
diff --git a/airflow-core/tests/unit/jobs/test_triggerer_job.py 
b/airflow-core/tests/unit/jobs/test_triggerer_job.py
index 07f861f4f91..5c4c247d2a0 100644
--- a/airflow-core/tests/unit/jobs/test_triggerer_job.py
+++ b/airflow-core/tests/unit/jobs/test_triggerer_job.py
@@ -265,11 +265,22 @@ def test_capacity_decode():
 
 
 @pytest.mark.parametrize("team_name", ["team_a", None])
-def test_triggerer_job_runner_stores_team_name(team_name):
-    """TriggererJobRunner stores team_name as-is (validated at CLI layer)."""
-    job = Job()
-    runner = TriggererJobRunner(job, capacity=10, team_name=team_name)
-    assert runner.team_name == team_name
[email protected](TriggerRunnerSupervisor, "start")
+def test_execute_passes_job_team_name_to_supervisor(mock_supervisor_start, 
team_name):
+    mock_supervisor = MagicMock(spec=TriggerRunnerSupervisor)
+    mock_supervisor._exit_code = 0
+    mock_supervisor_start.return_value = mock_supervisor
+
+    job = Job(team_name=team_name)
+    job_runner = TriggererJobRunner(job)
+    with (
+        patch.object(job_runner, "register_signals"),
+        patch("airflow.jobs.triggerer_job_runner.stats.initialize"),
+    ):
+        job_runner._execute()
+
+    mock_supervisor_start.assert_called_once()
+    assert mock_supervisor_start.call_args.kwargs["team_name"] == team_name
 
 
 @pytest.mark.parametrize("platform_uses_exec", [True, False])
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index b5ef5665ac0..f2e0546f264 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -507,13 +507,15 @@ class DAGTagCollectionResponse(BaseModel):
     total_entries: Annotated[int, Field(title="Total Entries")]
 
 
-class DagProcessorInfoResponse(BaseModel):
+class DagProcessorInstanceInfoResponse(BaseModel):
     """
-    DagProcessor info serializer for responses.
+    Dag processor instance info serializer for responses.
     """
 
     status: Annotated[str | None, Field(title="Status")]
+    hostname: Annotated[str | None, Field(title="Hostname")]
     latest_dag_processor_heartbeat: Annotated[str | None, Field(title="Latest 
Dag Processor Heartbeat")]
+    bundle_names: Annotated[list[str] | None, Field(title="Bundle Names")]
 
 
 class DagRunAssetReference(BaseModel):
@@ -785,6 +787,8 @@ class JobResponse(BaseModel):
     executor_class: Annotated[str | None, Field(title="Executor Class")]
     hostname: Annotated[str | None, Field(title="Hostname")]
     unixname: Annotated[str | None, Field(title="Unixname")]
+    team_name: Annotated[str | None, Field(title="Team Name")] = None
+    bundle_names: Annotated[list[str] | None, Field(title="Bundle Names")] = 
None
     dag_display_name: Annotated[str | None, Field(title="Dag Display Name")] = 
None
 
 
@@ -975,12 +979,13 @@ class ReprocessBehavior(str, Enum):
     NONE = "none"
 
 
-class SchedulerInfoResponse(BaseModel):
+class SchedulerInstanceInfoResponse(BaseModel):
     """
-    Scheduler info serializer for responses.
+    Scheduler instance info serializer for responses.
     """
 
     status: Annotated[str | None, Field(title="Status")]
+    hostname: Annotated[str | None, Field(title="Hostname")]
     latest_scheduler_heartbeat: Annotated[str | None, Field(title="Latest 
Scheduler Heartbeat")]
 
 
@@ -1188,13 +1193,15 @@ class TriggerResponse(BaseModel):
     triggerer_id: Annotated[int | None, Field(title="Triggerer Id")]
 
 
-class TriggererInfoResponse(BaseModel):
+class TriggererInstanceInfoResponse(BaseModel):
     """
-    Triggerer info serializer for responses.
+    Triggerer instance info serializer for responses.
     """
 
     status: Annotated[str | None, Field(title="Status")]
+    hostname: Annotated[str | None, Field(title="Hostname")]
     latest_triggerer_heartbeat: Annotated[str | None, Field(title="Latest 
Triggerer Heartbeat")]
+    team_name: Annotated[str | None, Field(title="Team Name")]
 
 
 class UpdateHITLDetailPayload(BaseModel):
@@ -1923,6 +1930,17 @@ class DAGWarningResponse(BaseModel):
     dag_display_name: Annotated[str, Field(title="Dag Display Name")]
 
 
+class DagProcessorInfoResponse(BaseModel):
+    """
+    DagProcessor info serializer for responses.
+    """
+
+    status: Annotated[str | None, Field(title="Status")]
+    latest_dag_processor_heartbeat: Annotated[str | None, Field(title="Latest 
Dag Processor Heartbeat")]
+    detailed_status: Annotated[str | None, Field(title="Detailed Status")]
+    instances: Annotated[list[DagProcessorInstanceInfoResponse] | None, 
Field(title="Instances")] = None
+
+
 class DagStatsResponse(BaseModel):
     """
     Dag Stats serializer for responses.
@@ -1985,17 +2003,6 @@ class HTTPValidationError(BaseModel):
     detail: Annotated[list[ValidationError] | None, Field(title="Detail")] = 
None
 
 
-class HealthInfoResponse(BaseModel):
-    """
-    Health serializer for responses.
-    """
-
-    metadatabase: BaseInfoResponse
-    scheduler: SchedulerInfoResponse
-    triggerer: TriggererInfoResponse
-    dag_processor: DagProcessorInfoResponse | None = None
-
-
 class ImportErrorCollectionResponse(BaseModel):
     """
     Import Error Collection Response.
@@ -2098,6 +2105,17 @@ class QueuedEventCollectionResponse(BaseModel):
     total_entries: Annotated[int, Field(title="Total Entries")]
 
 
+class SchedulerInfoResponse(BaseModel):
+    """
+    Scheduler info serializer for responses.
+    """
+
+    status: Annotated[str | None, Field(title="Status")]
+    latest_scheduler_heartbeat: Annotated[str | None, Field(title="Latest 
Scheduler Heartbeat")]
+    detailed_status: Annotated[str | None, Field(title="Detailed Status")]
+    instances: Annotated[list[SchedulerInstanceInfoResponse] | None, 
Field(title="Instances")] = None
+
+
 class TaskDependencyCollectionResponse(BaseModel):
     """
     Task scheduling dependencies collection serializer for responses.
@@ -2226,6 +2244,17 @@ class TaskStateStoreCollectionResponse(BaseModel):
     total_entries: Annotated[int, Field(title="Total Entries")]
 
 
+class TriggererInfoResponse(BaseModel):
+    """
+    Triggerer info serializer for responses.
+    """
+
+    status: Annotated[str | None, Field(title="Status")]
+    latest_triggerer_heartbeat: Annotated[str | None, Field(title="Latest 
Triggerer Heartbeat")]
+    detailed_status: Annotated[str | None, Field(title="Detailed Status")]
+    instances: Annotated[list[TriggererInstanceInfoResponse] | None, 
Field(title="Instances")] = None
+
+
 class VariableCollectionResponse(BaseModel):
     """
     Variable Collection serializer for responses.
@@ -2491,6 +2520,17 @@ class HITLDetailHistory(BaseModel):
     task_instance: TaskInstanceHistoryResponse
 
 
+class HealthInfoResponse(BaseModel):
+    """
+    Health serializer for responses.
+    """
+
+    metadatabase: BaseInfoResponse
+    scheduler: SchedulerInfoResponse
+    triggerer: TriggererInfoResponse
+    dag_processor: DagProcessorInfoResponse | None = None
+
+
 class PluginCollectionResponse(BaseModel):
     """
     Plugin Collection serializer.

Reply via email to