diff --git a/airflow-core/src/airflow/logging_config.py b/airflow-core/src/airflow/logging_config.py index e6d837bc22077..a391c1c6361a8 100644 --- a/airflow-core/src/airflow/logging_config.py +++ b/airflow-core/src/airflow/logging_config.py @@ -19,6 +19,7 @@ import logging import warnings +from importlib import import_module from logging.config import dictConfig from typing import TYPE_CHECKING, Any @@ -73,7 +74,9 @@ def load_logging_config() -> tuple[dict[str, Any], str]: else: modpath = logging_class_path.rsplit(".", 1)[0] try: - mod = import_string(modpath) + mod = import_module(modpath) + + # Load remote logging configuration from the custom module REMOTE_TASK_LOG = getattr(mod, "REMOTE_TASK_LOG") DEFAULT_REMOTE_CONN_ID = getattr(mod, "DEFAULT_REMOTE_CONN_ID", None) except Exception as err: diff --git a/airflow-core/tests/unit/core/test_logging_config.py b/airflow-core/tests/unit/core/test_logging_config.py index 72864b066b34a..b92a7f0bcfb0c 100644 --- a/airflow-core/tests/unit/core/test_logging_config.py +++ b/airflow-core/tests/unit/core/test_logging_config.py @@ -24,7 +24,7 @@ import pathlib import sys import tempfile -from unittest.mock import patch +from unittest.mock import call, patch import pytest @@ -96,6 +96,11 @@ # Other settings here """ +SETTINGS_FILE_WITH_REMOTE_VARS = f"""{SETTINGS_FILE_VALID} +REMOTE_TASK_LOG = None +DEFAULT_REMOTE_CONN_ID = "test_conn_id" +""" + SETTINGS_DEFAULT_NAME = "custom_airflow_local_settings" @@ -191,6 +196,15 @@ def teardown_method(self): importlib.reload(airflow_local_settings) configure_logging() + def _verify_basic_logging_config( + self, logging_config: dict, logging_class_path: str, expected_path: str + ) -> None: + """Helper method to verify basic logging config structure""" + assert isinstance(logging_config, dict) + assert logging_config["version"] == 1 + assert "airflow.task" in logging_config["loggers"] + assert logging_class_path == expected_path + # When we try to load an invalid config file, we expect an error def test_loading_invalid_local_settings(self): from airflow.logging_config import configure_logging, log @@ -365,3 +379,44 @@ def test_loading_remote_logging_with_hdfs_handler(self): airflow.logging_config.configure_logging() assert isinstance(airflow.logging_config.REMOTE_TASK_LOG, HdfsRemoteLogIO) + + @pytest.mark.parametrize( + "settings_content,module_structure,expected_path", + [ + (SETTINGS_FILE_WITH_REMOTE_VARS, None, f"{SETTINGS_DEFAULT_NAME}.LOGGING_CONFIG"), + ( + SETTINGS_FILE_WITH_REMOTE_VARS, + "nested.config.module", + f"nested.config.module.{SETTINGS_DEFAULT_NAME}.LOGGING_CONFIG", + ), + ], + ) + def test_load_logging_config_module_paths( + self, settings_content: str, module_structure: str, expected_path: str + ): + """Test that load_logging_config works with different module path structures""" + dir_structure = module_structure.replace(".", "/") if module_structure else None + + with settings_context(settings_content, dir_structure): + from airflow.logging_config import load_logging_config + + logging_config, logging_class_path = load_logging_config() + self._verify_basic_logging_config(logging_config, logging_class_path, expected_path) + + def test_load_logging_config_fallback_behavior(self): + """Test that load_logging_config falls back gracefully when remote logging vars are missing""" + with settings_context(SETTINGS_FILE_VALID): + from airflow.logging_config import load_logging_config + + with patch("airflow.logging_config.log") as mock_log: + logging_config, logging_class_path = load_logging_config() + + self._verify_basic_logging_config( + logging_config, logging_class_path, f"{SETTINGS_DEFAULT_NAME}.LOGGING_CONFIG" + ) + + mock_log.info.mock_calls = [ + call( + "Remote task logs will not be available due to an error: %s", + ) + ]