From 393424fa88bc20e36a80df356f4e4f579bdbf85e Mon Sep 17 00:00:00 2001 From: TheOtherAdam Date: Fri, 26 Jun 2026 23:45:55 +0800 Subject: [PATCH] Allow disabling managed log file (#170374) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- homeassistant/bootstrap.py | 39 ++++++++++++++-- homeassistant/const.py | 3 +- homeassistant/core_config.py | 6 +++ tests/test_bootstrap.py | 86 +++++++++++++++++++++++++++++++----- tests/test_core_config.py | 3 ++ 5 files changed, 123 insertions(+), 14 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 370fc77dd26..81a9fca6160 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -66,6 +66,7 @@ from .const import ( BASE_PLATFORMS, FORMAT_DATETIME, KEY_DATA_LOGGING as DATA_LOGGING, + KEY_DATA_LOGGING_DISABLED_REASON as DATA_LOGGING_DISABLED_REASON, SIGNAL_BOOTSTRAP_INTEGRATIONS, ) from .core_config import async_process_ha_core_config @@ -129,6 +130,11 @@ SETUP_ORDER_SORT_KEY = partial(contains, BASE_PLATFORMS) ERROR_LOG_FILENAME = "home-assistant.log" +ENV_DISABLE_LOG_FILE = "HA_DISABLE_LOG_FILE" +ENV_DUPLICATE_LOG_FILE = "HA_DUPLICATE_LOG_FILE" +ENV_SUPERVISOR = "SUPERVISOR" +LOG_FILE_DISABLED_REASON_ENVIRONMENT = "environment" +LOG_FILE_DISABLED_REASON_SUPERVISOR = "supervisor" # hass.data key for logging information. DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded") @@ -642,10 +648,12 @@ async def async_enable_logging( logger.setLevel(logging.INFO if verbose else logging.WARNING) if log_file is None: + disabled_log_file_reason = _log_file_disabled_reason() default_log_path = hass.config.path(ERROR_LOG_FILENAME) - if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ: + if disabled_log_file_reason: # Rename the default log file if it exists, since previous versions created - # it even on Supervisor + # it before Supervisor disabled duplicate file logging or + # HA_DISABLE_LOG_FILE disabled the log file. def rename_old_file() -> None: """Rename old log file in executor.""" if os.path.isfile(default_log_path): @@ -657,6 +665,7 @@ async def async_enable_logging( else: err_log_path = default_log_path else: + disabled_log_file_reason = None err_log_path = os.path.abspath(log_file) if err_log_path: @@ -669,10 +678,34 @@ async def async_enable_logging( # Save the log file location for access by other components. hass.data[DATA_LOGGING] = err_log_path + elif disabled_log_file_reason == LOG_FILE_DISABLED_REASON_ENVIRONMENT: + hass.data[DATA_LOGGING_DISABLED_REASON] = disabled_log_file_reason async_activate_log_queue_handler(hass) +def _log_file_disabled_reason() -> str | None: + """Return why the log file is disabled.""" + if ENV_SUPERVISOR in os.environ and ENV_DUPLICATE_LOG_FILE not in os.environ: + return LOG_FILE_DISABLED_REASON_SUPERVISOR + + disable_log_file = os.environ.get(ENV_DISABLE_LOG_FILE) + if disable_log_file is None: + return None + + try: + if cv.boolean(disable_log_file): + return LOG_FILE_DISABLED_REASON_ENVIRONMENT + except vol.Invalid: + _LOGGER.warning( + "Ignoring invalid %s value: %s. Expected a boolean value: " + "1/0, true/false, yes/no, on/off, or enable/disable", + ENV_DISABLE_LOG_FILE, + disable_log_file, + ) + return None + + def _create_log_file( err_log_path: str, log_rotate_days: int | None ) -> RotatingFileHandler | TimedRotatingFileHandler: @@ -734,7 +767,7 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: domains.update(DEFAULT_INTEGRATIONS_RECOVERY_MODE) # Add domains depending on if the Supervisor is used or not - if "SUPERVISOR" in os.environ: + if ENV_SUPERVISOR in os.environ: domains.update(DEFAULT_INTEGRATIONS_SUPERVISOR) return domains diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f624a889c1..8749f8c50fb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1031,8 +1031,9 @@ SIGNAL_BOOTSTRAP_INTEGRATIONS: SignalType[dict[str, float]] = SignalType( ) -# hass.data key for logging information. +# hass.data keys for logging information. KEY_DATA_LOGGING: HassKey[str] = HassKey("logging") +KEY_DATA_LOGGING_DISABLED_REASON: HassKey[str] = HassKey("logging_disabled_reason") # Date/Time formats diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py index aa8f89d9b9a..de958b97d41 100644 --- a/homeassistant/core_config.py +++ b/homeassistant/core_config.py @@ -50,6 +50,7 @@ from .const import ( CONF_URL, CONF_USERNAME, EVENT_CORE_CONFIG_UPDATE, + KEY_DATA_LOGGING_DISABLED_REASON, LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, UnitOfLength, __version__, @@ -698,6 +699,11 @@ class Config: "language": self.language, "latitude": self.latitude, "location_name": self.location_name, + "logging": { + "log_file_disabled_reason": self.hass.data.get( + KEY_DATA_LOGGING_DISABLED_REASON + ), + }, "longitude": self.longitude, "radius": self.radius, "recovery_mode": self.recovery_mode, diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e1136cc9b66..e509788053f 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -131,25 +131,78 @@ async def test_async_enable_logging( @pytest.mark.parametrize( - ("extra_env", "log_file_count", "old_log_file_count"), - [({}, 0, 1), ({"HA_DUPLICATE_LOG_FILE": "1"}, 1, 0)], + ( + "env", + "log_file_count", + "old_log_file_count", + "data_logging", + "data_logging_disabled_reason", + ), + [ + pytest.param( + {"SUPERVISOR": "1"}, + 0, + 1, + None, + None, + id="supervisor", + ), + pytest.param( + {"SUPERVISOR": "1", "HA_DUPLICATE_LOG_FILE": "1"}, + 1, + 0, + CONFIG_LOG_FILE, + None, + id="supervisor-duplicate-log-file", + ), + pytest.param( + {"HA_DISABLE_LOG_FILE": "1"}, + 0, + 1, + None, + "environment", + id="disable-log-file", + ), + pytest.param( + {"HA_DISABLE_LOG_FILE": "0"}, + 1, + 0, + CONFIG_LOG_FILE, + None, + id="disable-log-file-false", + ), + pytest.param( + {"HA_DISABLE_LOG_FILE": "invalid"}, + 1, + 0, + CONFIG_LOG_FILE, + None, + id="disable-log-file-invalid", + ), + ], ) -async def test_async_enable_logging_supervisor( +async def test_async_enable_logging_log_file_disable_control( hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - extra_env: dict[str, str], + monkeypatch: pytest.MonkeyPatch, + env: dict[str, str], log_file_count: int, old_log_file_count: int, + data_logging: str | None, + data_logging_disabled_reason: str | None, ) -> None: - """Test the default log file is not created on Supervisor.""" + """Test the default log file disable controls.""" # Ensure we start with a clean slate cleanup_log_files() assert len(glob.glob(CONFIG_LOG_FILE)) == 0 assert len(glob.glob(ARG_LOG_FILE)) == 0 + for env_var in ("SUPERVISOR", "HA_DUPLICATE_LOG_FILE", "HA_DISABLE_LOG_FILE"): + monkeypatch.delenv(env_var, raising=False) + for env_var, value in env.items(): + monkeypatch.setenv(env_var, value) + with ( - patch.dict(os.environ, {"SUPERVISOR": "1", **extra_env}), patch( "homeassistant.bootstrap.async_activate_log_queue_handler" ) as mock_async_activate_log_queue_handler, @@ -157,11 +210,19 @@ async def test_async_enable_logging_supervisor( ): await bootstrap.async_enable_logging(hass) assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count + assert hass.data.get(bootstrap.DATA_LOGGING) == data_logging + assert ( + hass.data.get(bootstrap.DATA_LOGGING_DISABLED_REASON) + == data_logging_disabled_reason + ) + assert hass.config.as_dict()["logging"] == { + "log_file_disabled_reason": data_logging_disabled_reason, + } mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() # Check that if the log file exists, it is renamed - def write_log_file(): + def write_log_file() -> None: with open( get_test_config_dir("home-assistant.log"), "w", encoding="utf8" ) as f: @@ -174,6 +235,11 @@ async def test_async_enable_logging_supervisor( await bootstrap.async_enable_logging(hass) assert len(glob.glob(CONFIG_LOG_FILE)) == log_file_count assert len(glob.glob(f"{CONFIG_LOG_FILE}.old")) == old_log_file_count + assert hass.data.get(bootstrap.DATA_LOGGING) == data_logging + assert ( + hass.data.get(bootstrap.DATA_LOGGING_DISABLED_REASON) + == data_logging_disabled_reason + ) mock_async_activate_log_queue_handler.assert_called_once() mock_async_activate_log_queue_handler.reset_mock() @@ -183,9 +249,9 @@ async def test_async_enable_logging_supervisor( log_file="test.log", ) mock_async_activate_log_queue_handler.assert_called_once() - # Even on Supervisor, the log file should be created - # if it is explicitly specified + # The log file should be created if it is explicitly specified. assert len(glob.glob(ARG_LOG_FILE)) > 0 + assert bootstrap.DATA_LOGGING in hass.data cleanup_log_files() diff --git a/tests/test_core_config.py b/tests/test_core_config.py index 69ac332a127..5615012884c 100644 --- a/tests/test_core_config.py +++ b/tests/test_core_config.py @@ -928,6 +928,9 @@ async def test_config_as_dict() -> None: "currency": "EUR", "country": None, "language": "en", + "logging": { + "log_file_disabled_reason": None, + }, "safe_mode": False, "debug": False, "radius": 100,