Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
6f9ea87
initial approach
xBis7 Mar 18, 2026
532fa39
fix back compat in providers
xBis7 Mar 20, 2026
24371e9
reduce diffs
xBis7 Mar 20, 2026
0e616ee
remove the Stats shim & handle backcompat in providers
xBis7 Mar 20, 2026
17142a0
fix stats references in providers tests
xBis7 Mar 23, 2026
159d7d8
fix mypy check errors for core and providers
xBis7 Mar 23, 2026
14f6337
fix test_sdk.py
xBis7 Mar 24, 2026
831ed70
cleanup after rebase
xBis7 Mar 24, 2026
eda0802
remove legacy_map for providers backcompat and use a Stats shim
xBis7 Mar 27, 2026
8c9b7e1
fix stats imports in airflow-core and task-sdk
xBis7 Mar 27, 2026
3604d40
fix test_scheduler_job
xBis7 Mar 27, 2026
30e65db
trigger CI again
xBis7 Mar 27, 2026
359e4f6
fix test_dagrun, core/test_stats, test_edge_executor, test_task_runner
xBis7 Mar 27, 2026
61068ac
fix test_celery_executor + test_kubernetes_executor
xBis7 Mar 28, 2026
9cf0883
read export_legacy_names in stats.initialize from config + add more u…
xBis7 Mar 30, 2026
d5e1c14
cleanup
xBis7 Mar 30, 2026
4b07e6c
cleanup airflow/stats.py
xBis7 Mar 30, 2026
1f384db
fix static check error
xBis7 Apr 1, 2026
74587a6
change providers version checks to AIRFLOW_V_3_2_1_PLUS
xBis7 Apr 16, 2026
dba89fb
add '# use next version' for common-compat
xBis7 Apr 16, 2026
325f312
fix static check errors
xBis7 Apr 21, 2026
b826a33
restore pytest.mark.db_test in test_jobs
xBis7 Apr 21, 2026
a253f5f
lazily initialize the registry at the first use + fix mypy observabil…
xBis7 Apr 21, 2026
3e228fe
use a timer wrapper for exit stack
xBis7 Apr 21, 2026
d8893fd
cleanup comment in daemon_utils.py
xBis7 Apr 21, 2026
710bd66
remove the legacy_name_tags param
xBis7 Apr 22, 2026
da96235
add DualStatsManager shim for forward compat of the edge3 provider wi…
xBis7 Apr 22, 2026
296042f
fix registry discrepancy for metric 'dag_processing.last_duration'
xBis7 Apr 22, 2026
fd78355
remove the DualStatsManager shim
xBis7 Apr 22, 2026
899444f
fix task_runner discrepancies with the registry + test_task_runner er…
xBis7 Apr 22, 2026
970fb15
fix edge3 test_jobs.py
xBis7 Apr 22, 2026
76a1b58
fix base_executor metric discrepancies with the registry
xBis7 Apr 22, 2026
7e9a591
fix test references for executor metrics
xBis7 Apr 22, 2026
22791da
restore accidentally removed dynamic metrics in edge3 worker
xBis7 Apr 23, 2026
20c685f
fix back compat errors in the CI
xBis7 Apr 23, 2026
a6ed145
cleanup
xBis7 Apr 23, 2026
ef461ca
remove leftover Stats references in airflow-core and task-sdk
xBis7 Apr 24, 2026
135e936
replace pid instance check by os.register_at_fork() in stats
xBis7 Apr 24, 2026
152c608
replace _ExitStackTimer by _DualTimer
xBis7 Apr 27, 2026
63ddcee
make timer() return TimerProtocol
xBis7 Apr 27, 2026
a58b5ab
update serialization/definitions/dag after rebase
xBis7 Apr 27, 2026
c790aa8
remove 'Stats.' from airflow-core test_stats
xBis7 Apr 27, 2026
4d6d21c
update Stats.initialize in the openlineage listener
xBis7 Apr 27, 2026
d7e2dbf
replace . with __ when replacing variables in legacy names
xBis7 Apr 28, 2026
1792287
fix failures in test_triggerer_job.py after rebase
xBis7 Apr 28, 2026
40f53bb
cleanup openlineage listener.py
xBis7 Apr 28, 2026
8360b79
provide a spec in all backend mocks in tests
xBis7 Apr 28, 2026
bb7e83f
fix test_triggerer_job after rebase
xBis7 Apr 30, 2026
316f612
Addressing minor code review directly
ashb Apr 30, 2026
1658e0e
remove unused import after rebase
xBis7 Apr 30, 2026
1caaf88
fix mypy error in shared/observability
xBis7 Apr 30, 2026
1193e14
fix mypy error + observability tests
xBis7 Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion airflow-core/src/airflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@
# Deprecated lazy imports
"AirflowException": (".exceptions", "AirflowException", True),
"Dataset": (".sdk", "Asset", True),
Comment thread
xBis7 marked this conversation as resolved.
"Stats": (".observability.stats", "Stats", True),
"Trace": (".observability.trace", "Trace", True),
"metrics": (".observability.metrics", "", True),
"traces": ("._shared.observability.traces", "", True),
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/assets/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from sqlalchemy import exc, or_, select
from sqlalchemy.orm import joinedload

from airflow._shared.observability.metrics.stats import Stats
from airflow._shared.observability.metrics import stats
from airflow.configuration import conf
from airflow.listeners.listener import get_listener_manager
from airflow.listeners.types import AssetEvent as ListenerAssetEvent
Expand Down Expand Up @@ -283,7 +283,7 @@ def register_asset_change(
)
)

Stats.incr("asset.updates")
stats.incr("asset.updates")

dags_to_queue = (
dags_to_queue_from_asset | dags_to_queue_from_asset_alias | dags_to_queue_from_asset_ref
Expand Down
4 changes: 0 additions & 4 deletions airflow-core/src/airflow/cli/commands/daemon_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ def run_command_with_daemon_option(
)

with ctx:
# in daemon context stats client needs to be reinitialized.
from airflow._shared.observability.metrics.stats import Stats

Stats.instance = None
callback()
else:
signal.signal(signal.SIGINT, sigint_handler)
Expand Down
48 changes: 25 additions & 23 deletions airflow-core/src/airflow/dag_processing/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
from tabulate import tabulate
from uuid6 import uuid7

from airflow._shared.observability.metrics.stats import Stats, normalize_name_for_stats
from airflow._shared.observability.metrics import stats
from airflow._shared.observability.metrics.stats import normalize_name_for_stats
from airflow._shared.timezones import timezone
from airflow.api_fastapi.execution_api.app import InProcessExecutionAPI
from airflow.configuration import conf
Expand Down Expand Up @@ -344,8 +345,10 @@ def prepare_process_context(self) -> None:
# selector implementations. Also see _StubSelector documentation.
self.selector = selectors.DefaultSelector()

stats_factory = stats_utils.get_stats_factory(Stats)
Stats.initialize(factory=stats_factory)
stats.initialize(
factory=stats_utils.get_stats_factory(),
export_legacy_names=conf.getboolean("metrics", "legacy_names_on"),
)

def prepare_bundles(self) -> None:
"""Sync bundle configuration to the DB and load bundles for parsing."""
Expand Down Expand Up @@ -649,7 +652,7 @@ def _add_callback_to_queue(self, request: CallbackRequest) -> None:
)
self._callback_to_execute[file_info].append(request)
self._add_files_to_queue([file_info], mode="front")
Stats.incr("dag_processing.other_callback_count")
stats.incr("dag_processing.other_callback_count")

@provide_session
def get_bundle_state(self, bundle_name: str, *, session: Session = NEW_SESSION) -> BundleState | None:
Expand Down Expand Up @@ -934,7 +937,7 @@ def _log_file_processing_stats(self, known_files: dict[str, set[DagFileInfo]]):
last_run = stat.last_finish_time
if last_run:
seconds_ago = (utcnow - last_run).total_seconds()
Stats.gauge(f"dag_processing.last_run.seconds_ago.{file_name}", seconds_ago)
stats.gauge(f"dag_processing.last_run.seconds_ago.{file_name}", seconds_ago)

rows.append(
(
Expand Down Expand Up @@ -1012,7 +1015,7 @@ def handle_removed_files(self, known_files: dict[str, set[DagFileInfo]]):
def purge_removed_files_from_queue(self, present: set[DagFileInfo]):
"""Remove from queue any files no longer observed locally."""
self._file_queue = deque(x for x in self._file_queue if x in present)
Stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))
stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))

def remove_orphaned_file_stats(self, present: set[DagFileInfo]):
"""Remove the stats for any dag files that don't exist anymore."""
Expand All @@ -1030,7 +1033,7 @@ def terminate_orphan_processes(self, present: set[DagFileInfo]):
continue
file_name = str(file.rel_path)
self.log.warning("Stopping processor for %s", file_name)
Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "stop"})
stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "stop"})
processor.kill(signal.SIGKILL)
processor.logger_filehandle.close()
self._file_stats.pop(file, None)
Expand Down Expand Up @@ -1246,10 +1249,10 @@ def _start_new_processes(self):
continue

processor = self._create_process(file)
Stats.incr("dag_processing.processes", tags={"file_path": str(file.rel_path), "action": "start"})
stats.incr("dag_processing.processes", tags={"file_path": str(file.rel_path), "action": "start"})

self._processors[file] = processor
Stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))
stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))

def _add_new_files_to_queue(self, known_files: dict[str, set[DagFileInfo]]):
"""
Expand Down Expand Up @@ -1326,7 +1329,7 @@ def prepare_file_queue(self, known_files: dict[str, set[DagFileInfo]]):
if self._parsing_start_time is not None:
emit_metrics(
parse_time=time.perf_counter() - self._parsing_start_time,
stats=list(self._file_stats.values()),
dag_file_stats=list(self._file_stats.values()),
Comment thread
xBis7 marked this conversation as resolved.
)
self._parsing_start_time = None

Expand Down Expand Up @@ -1379,7 +1382,7 @@ def prepare_file_queue(self, known_files: dict[str, set[DagFileInfo]]):
"\n\t".join(str(f.rel_path) for f in to_queue),
)
self._add_files_to_queue(to_queue, mode="back")
Stats.incr("dag_processing.file_path_queue_update_count")
stats.incr("dag_processing.file_path_queue_update_count")

def _kill_timed_out_processors(self):
"""Kill any file processors that timeout to defend against process hangs."""
Expand All @@ -1396,8 +1399,8 @@ def _kill_timed_out_processors(self):
self.processor_timeout,
)
file_name = str(file.rel_path)
Stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "timeout"})
Stats.incr("dag_processing.processor_timeouts", tags={"file_path": file_name})
stats.decr("dag_processing.processes", tags={"file_path": file_name, "action": "timeout"})
stats.incr("dag_processing.processor_timeouts", tags={"file_path": file_name})
processor.kill(signal.SIGKILL)

processors_to_remove.append(file)
Expand Down Expand Up @@ -1445,7 +1448,7 @@ def _add_files_to_queue(
if self._parsing_start_time is None and self._file_queue:
self._parsing_start_time = time.perf_counter()

Stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))
stats.gauge("dag_processing.file_path_queue_size", len(self._file_queue))

def max_runs_reached(self):
""":return: whether all file paths have been processed max_runs times."""
Expand All @@ -1459,7 +1462,7 @@ def terminate(self):
"""Stop all running processors."""
for file, processor in self._processors.items():
# todo: AIP-66 what to do about file_path tag? replace with bundle name and rel path?
Stats.decr(
stats.decr(
"dag_processing.processes", tags={"file_path": str(file.rel_path), "action": "terminate"}
)
# SIGTERM, wait 5s, SIGKILL if still alive
Expand All @@ -1472,16 +1475,16 @@ def end(self):
kill_child_processes_by_pids(pids_to_kill)


def emit_metrics(*, parse_time: float, stats: Sequence[DagFileStat]):
def emit_metrics(*, parse_time: float, dag_file_stats: Sequence[DagFileStat]):
"""
Emit metrics about dag parsing summary.

This is called once every time around the parsing "loop" - i.e. after
all files have been parsed.
"""
Stats.gauge("dag_processing.total_parse_time", parse_time)
Stats.gauge("dagbag_size", sum(stat.num_dags for stat in stats))
Stats.gauge("dag_processing.import_errors", sum(stat.import_errors for stat in stats))
stats.gauge("dag_processing.total_parse_time", parse_time)
stats.gauge("dagbag_size", sum(stat.num_dags for stat in dag_file_stats))
stats.gauge("dag_processing.import_errors", sum(stat.import_errors for stat in dag_file_stats))


def process_parse_results(
Expand All @@ -1506,7 +1509,7 @@ def process_parse_results(
last_duration=run_duration,
run_count=run_count, # Don't increment for callback-only processing
)
Stats.incr("dag_processing.callback_only_count")
stats.incr("dag_processing.callback_only_count")
else:
# Actual DAG parsing or import error
stat = DagFileStat(
Expand All @@ -1521,11 +1524,10 @@ def process_parse_results(
file_name = normalize_name_for_stats(Path(relative_fileloc).stem)
# bundle_name is included to distinguish files with the same name across different bundles
normalized_bundle = normalize_name_for_stats(bundle_name)
Stats.timing(f"dag_processing.last_duration.{normalized_bundle}.{file_name}", stat.last_duration)
Stats.timing(
stats.timing(
"dag_processing.last_duration",
stat.last_duration,
tags={"file_name": file_name, "bundle_name": normalized_bundle},
tags={"bundle_name": normalized_bundle, "file_name": file_name},
)

if parsing_result is None:
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/dag_processing/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import attrs
from pydantic import BaseModel, Field, TypeAdapter

from airflow._shared.observability.metrics.stats import Stats
from airflow._shared.observability.metrics import stats
from airflow.callbacks.callback_requests import (
CallbackRequest,
DagCallbackRequest,
Expand Down Expand Up @@ -366,7 +366,7 @@ def _execute_dag_callbacks(dagbag: DagBag, request: DagCallbackRequest, log: Fil
callback(context)
except Exception:
log.exception("Callback failed", dag_id=request.dag_id)
Stats.incr("dag.callback_exceptions", tags={"dag_id": request.dag_id})
stats.incr("dag.callback_exceptions", tags={"dag_id": request.dag_id})


def _execute_task_callbacks(dagbag: DagBag, request: TaskCallbackRequest, log: FilteringBoundLogger) -> None:
Expand Down
20 changes: 11 additions & 9 deletions airflow-core/src/airflow/executors/base_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

import pendulum

from airflow._shared.observability.metrics.stats import Stats
from airflow._shared.observability.metrics import stats
from airflow.cli.cli_config import DefaultHelpParser
from airflow.configuration import conf
from airflow.executors import workloads
Expand Down Expand Up @@ -204,8 +204,10 @@ def jwt_generator(self) -> JWTGenerator:
return generator

def __init__(self, parallelism: int = PARALLELISM, team_name: str | None = None):
stats_factory = stats_utils.get_stats_factory(Stats)
Stats.initialize(factory=stats_factory)
stats.initialize(
factory=stats_utils.get_stats_factory(),
export_legacy_names=conf.getboolean("metrics", "legacy_names_on"),
)
super().__init__()
# Ensure we set this now, so that each subprocess gets the same value
from airflow.api_fastapi.auth.tokens import get_signing_args
Expand Down Expand Up @@ -371,20 +373,20 @@ def _emit_metrics(self, open_slots, num_running_tasks, num_queued_tasks):
else:
self.log.debug("%s open slots for executor %s", open_slots, name)

Stats.gauge(
stats.gauge(
open_slots_metric_name,
value=open_slots,
tags={"status": "open", "name": name},
tags={"status": "open", "executor_class_name": name},
)
Stats.gauge(
stats.gauge(
queued_tasks_metric_name,
value=num_queued_tasks,
tags={"status": "queued", "name": name},
tags={"status": "queued", "executor_class_name": name},
)
Stats.gauge(
stats.gauge(
running_tasks_metric_name,
value=num_running_tasks,
tags={"status": "running", "name": name},
tags={"status": "running", "executor_class_name": name},
)

def order_queued_tasks_by_priority(self) -> list[tuple[TaskInstanceKey, workloads.ExecuteTask]]:
Expand Down
4 changes: 2 additions & 2 deletions airflow-core/src/airflow/jobs/dag_processor_job_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from typing import TYPE_CHECKING

from airflow._shared.observability.metrics.stats import Stats
from airflow._shared.observability.metrics import stats
from airflow.jobs.base_job_runner import BaseJobRunner
from airflow.jobs.job import Job, perform_heartbeat
from airflow.utils.log.logging_mixin import LoggingMixin
Expand Down Expand Up @@ -68,4 +68,4 @@ def _execute(self) -> int | None:
return None

def heartbeat_callback(self, session: Session | None = None) -> None:
Stats.incr("dag_processor_heartbeat", 1, 1)
stats.incr("dag_processor_heartbeat", 1, 1)
8 changes: 4 additions & 4 deletions airflow-core/src/airflow/jobs/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from sqlalchemy.orm import Mapped, backref, foreign, mapped_column, relationship
from sqlalchemy.orm.session import make_transient

from airflow._shared.observability.metrics.stats import Stats
from airflow._shared.observability.metrics import stats
from airflow._shared.timezones import timezone
from airflow.configuration import conf
from airflow.exceptions import AirflowException
Expand Down Expand Up @@ -245,7 +245,7 @@ def heartbeat(
self.log.debug("[heartbeat]")
self.heartbeat_failed = False
except OperationalError:
Stats.incr(convert_camel_to_snake(self.__class__.__name__) + "_heartbeat_failure", 1, 1)
stats.incr(convert_camel_to_snake(self.__class__.__name__) + "_heartbeat_failure", 1, 1)
if not self.heartbeat_failed:
self.log.exception("%s heartbeat failed with error", self.__class__.__name__)
self.heartbeat_failed = True
Expand All @@ -268,7 +268,7 @@ def heartbeat(
@provide_session
def prepare_for_execution(self, session: Session = NEW_SESSION):
"""Prepare the job for execution."""
Stats.incr(self.__class__.__name__.lower() + "_start", 1, 1)
stats.incr(self.__class__.__name__.lower() + "_start", 1, 1)
self.state = JobState.RUNNING
self.start_date = timezone.utcnow()
session.add(self)
Expand All @@ -284,7 +284,7 @@ def complete_execution(self, session: Session = NEW_SESSION):
self.end_date = timezone.utcnow()
session.merge(self)
session.commit()
Stats.incr(self.__class__.__name__.lower() + "_end", 1, 1)
stats.incr(self.__class__.__name__.lower() + "_end", 1, 1)

@provide_session
def most_recent_job(self, session: Session = NEW_SESSION) -> Job | None:
Expand Down
Loading
Loading