Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Policy violation: docs/api-stability.md requires every deprecation to "remain supported for at least two minor releases and 90 calendar days, whichever is longer." Current VERSION is 0.4.3, so introducing the warning in 0.5 and scheduling removal in 0.6 is only one minor release apart — a user upgrading from 0.5.x straight to 0.6.0 loses LOG_UTC support without the two-minor-release window this project's own policy promises.

`BaseCliDeprecationWarning` and is scheduled for removal no earlier than 0.7.
19 changes: 18 additions & 1 deletion lib/python/base_cli/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness (silent behavior): when both BASE_CLI_LOG_UTC and LOG_UTC are set, the legacy variable is silently ignored with no deprecation warning at all, since the legacy_use_utc check is only reached in the else branch. A user mid-migration with both set gets correct behavior (new var wins) but no signal that LOG_UTC is deprecated and currently doing nothing — they may keep the stale variable indefinitely.

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)
Expand Down
37 changes: 37 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
import tempfile
import unittest
import warnings
from pathlib import Path
from unittest import mock

Expand Down Expand Up @@ -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()

Expand Down
Loading