diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d0ef2..3f942ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and versions are tracked in the repo-root `VERSION` file. - Align the Typer support floor with the tested matrix and cover representative minimum/maximum Typer and Click version pairings. +- Add the namespaced `BASE_CLI_LOG_UTC` environment variable and deprecate + `LOG_UTC` with a migration warning; the legacy alias is scheduled for removal + no earlier than 0.7. ### Fixed diff --git a/README.md b/README.md index cf7f8cf..20c13dd 100644 --- a/README.md +++ b/README.md @@ -568,6 +568,11 @@ Every `base_cli.App` command gets these options: - `--json`: opt-in machine output, when `LifecycleOptions.json` is enabled; emits the versioned envelopes described in [`docs/json-contracts.md`](https://basefoundry.github.io/base-cli/json-contracts/). +Set `BASE_CLI_LOG_UTC=1` when the default text formatter should use UTC +timestamps. The older `LOG_UTC` name remains a temporary compatibility alias +and emits a deprecation warning; new deployments should use the namespaced +variable. + `LifecycleOptions()` preserves this default set. Its `debug`, `quiet`, `environment`, `config`, `keep_temp`, `log_file`, and `version` fields are enabled by default; `dry_run` and `json` are opt-in. Set one field to `None` to diff --git a/docs/integrations.md b/docs/integrations.md index 0bfb68c..2c27178 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -52,3 +52,10 @@ outcome, exit code, and duration. Raw argv, configuration values, filesystem paths, and secrets are never attached. A missing API package, invalid provider, or failing exporter is logged at debug level and treated as a no-op; it cannot change the command's exit status or cleanup behavior. + +## Log timestamp environment variable + +Set `BASE_CLI_LOG_UTC=1` to make the default text formatter use UTC +timestamps. The namespaced variable takes precedence over the legacy setting. +`LOG_UTC` remains recognized during the 0.5 compatibility window, but emits a +`BaseCliDeprecationWarning` and is scheduled for removal no earlier than 0.7. diff --git a/lib/python/base_cli/logging.py b/lib/python/base_cli/logging.py index 35aa824..a733287 100644 --- a/lib/python/base_cli/logging.py +++ b/lib/python/base_cli/logging.py @@ -5,6 +5,7 @@ import platform import sys import time +import warnings from io import TextIOWrapper from pathlib import Path from typing import BinaryIO, TextIO, cast @@ -21,6 +22,7 @@ from ._private_files import restrict_file from .context import get_current_context +from .deprecations import BaseCliDeprecationWarning from .history import compact_home_text from .json_contracts import JsonLogFormatter from .paths import current_working_dir @@ -213,7 +215,22 @@ def _secure_log_file_open_flags(mode: str) -> int: class CliFormatter(logging.Formatter): def __init__(self, *, use_utc: bool | None = None, use_color: bool = False) -> None: - self.use_utc = use_utc if use_utc is not None else os.environ.get("LOG_UTC") == "1" + if use_utc is not None: + resolved_use_utc = use_utc + else: + configured_use_utc = os.environ.get("BASE_CLI_LOG_UTC") + legacy_use_utc = os.environ.get("LOG_UTC") + if legacy_use_utc: + warnings.warn( + "LOG_UTC is deprecated since 0.5 and will be removed in 0.7; use BASE_CLI_LOG_UTC instead.", + BaseCliDeprecationWarning, + stacklevel=2, + ) + if configured_use_utc is not None: + resolved_use_utc = configured_use_utc == "1" + else: + resolved_use_utc = legacy_use_utc == "1" + self.use_utc = resolved_use_utc self.use_color = use_color datefmt = "%Y-%m-%d %H:%M:%S UTC" if self.use_utc else "%Y-%m-%d %H:%M:%S %z" super().__init__(datefmt=datefmt) diff --git a/tests/test_logging.py b/tests/test_logging.py index 6f85b75..b7c4e6c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -6,6 +6,7 @@ import sys import tempfile import unittest +import warnings from pathlib import Path from unittest import mock @@ -122,6 +123,42 @@ def test_configure_logger_honors_log_utc(self) -> None: self.assertRegex(stream.getvalue(), r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC INFO") + def test_configure_logger_honors_namespaced_log_utc_and_precedence(self) -> None: + stream = io.StringIO() + + with mock.patch.dict(os.environ, {"BASE_CLI_LOG_UTC": "1", "LOG_UTC": "0"}): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", base_cli.BaseCliDeprecationWarning) + logger = base_cli.configure_logger("namespaced-utc-stream", None, debug=False, stream=stream) + logger.info("hello utc") + + self.assertRegex(stream.getvalue(), r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC INFO") + self.assertEqual(len(caught), 1) + + def test_legacy_log_utc_emits_deprecation_warning(self) -> None: + with mock.patch.dict(os.environ, {"LOG_UTC": "1"}, clear=True): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", base_cli.BaseCliDeprecationWarning) + CliFormatter() + + self.assertEqual(len(caught), 1) + self.assertIn("BASE_CLI_LOG_UTC", str(caught[0].message)) + + def test_empty_legacy_log_utc_does_not_warn(self) -> None: + with mock.patch.dict(os.environ, {"LOG_UTC": ""}, clear=True): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always", base_cli.BaseCliDeprecationWarning) + CliFormatter() + + self.assertEqual(caught, []) + + def test_deprecation_warning_respects_an_explicit_error_filter(self) -> None: + with mock.patch.dict(os.environ, {"LOG_UTC": "1"}, clear=True): + with warnings.catch_warnings(): + warnings.simplefilter("error", base_cli.BaseCliDeprecationWarning) + with self.assertRaises(base_cli.BaseCliDeprecationWarning): + CliFormatter() + def test_configure_logger_colors_python_user_stream_when_requested(self) -> None: stream = self._TtyStream()