Skip to content

Commit f35afc7

Browse files
Add configurable log level option to CLI (#3129)
Co-authored-by: Vinicius D. Cerutti <[email protected]>
1 parent e596184 commit f35afc7

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

src/_nebari/cli.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import logging
2+
import os
13
import typing
24

35
import typer
@@ -29,6 +31,49 @@ def exclude_default_stages(ctx: typer.Context, exclude_default_stages: bool):
2931
return exclude_default_stages
3032

3133

34+
def configure_logging(log_level: None | str) -> None:
35+
"""Configure logging level based on log level string."""
36+
if not log_level:
37+
return
38+
39+
level_map = {
40+
"trace": logging.DEBUG,
41+
"debug": logging.DEBUG,
42+
"info": logging.INFO,
43+
"warning": logging.WARNING,
44+
"error": logging.ERROR,
45+
"critical": logging.CRITICAL,
46+
}
47+
48+
# Map Python logging levels to Terraform log levels
49+
tf_log_map = {
50+
"trace": "TRACE",
51+
"debug": "DEBUG",
52+
"info": "INFO",
53+
"warning": "WARN",
54+
"error": "ERROR",
55+
"critical": "ERROR",
56+
}
57+
58+
level = level_map.get(log_level.lower(), logging.WARNING)
59+
60+
# Set TF_LOG environment variable if not already set
61+
if "TF_LOG" not in os.environ:
62+
os.environ["TF_LOG"] = tf_log_map.get(log_level.lower(), "WARN")
63+
64+
if level == logging.DEBUG:
65+
logging.basicConfig(
66+
level=level,
67+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
68+
force=True,
69+
)
70+
else:
71+
logging.basicConfig(
72+
level=level, format="%(levelname)s - %(message)s", force=True
73+
)
74+
return
75+
76+
3277
def import_plugin(plugins: typing.List[str]):
3378
try:
3479
nebari_plugin_manager.load_plugins(plugins)
@@ -61,6 +106,13 @@ def common(
61106
help="Nebari version number",
62107
callback=version_callback,
63108
),
109+
log_level: str = typer.Option(
110+
None,
111+
"-l",
112+
"--log-level",
113+
help="Set logging level (trace, debug, info, warning, error, critical)",
114+
callback=configure_logging,
115+
),
64116
plugins: typing.List[str] = typer.Option(
65117
[],
66118
"--import-plugin",

tests/tests_unit/test_cli.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import logging
2+
import os
13
import subprocess
24

35
import pytest
46

7+
from _nebari.cli import configure_logging
58
from _nebari.subcommands.init import InitInputs
69
from nebari.plugins import nebari_plugin_manager
710

@@ -65,3 +68,118 @@ def test_nebari_init(tmp_path, namespace, auth_provider, ci_provider, ssl_cert_e
6568
)
6669
def test_nebari_commands_no_args(command):
6770
subprocess.run(command, check=True, capture_output=True, text=True).stdout.strip()
71+
72+
73+
@pytest.mark.parametrize(
74+
"log_level,expected_python_level,expected_tf_level",
75+
[
76+
("trace", logging.DEBUG, "TRACE"),
77+
("debug", logging.DEBUG, "DEBUG"),
78+
("info", logging.INFO, "INFO"),
79+
("warning", logging.WARNING, "WARN"),
80+
("error", logging.ERROR, "ERROR"),
81+
("critical", logging.CRITICAL, "ERROR"),
82+
],
83+
)
84+
def test_configure_logging_levels(
85+
log_level, expected_python_level, expected_tf_level, monkeypatch
86+
):
87+
"""Test that configure_logging sets correct Python and Terraform log levels."""
88+
# Remove TF_LOG from environment if it exists
89+
monkeypatch.delenv("TF_LOG", raising=False)
90+
91+
# Configure logging with the specified level
92+
configure_logging(log_level)
93+
94+
# Check that Python logging level is set correctly
95+
root_logger = logging.getLogger()
96+
assert root_logger.level == expected_python_level
97+
98+
# Check that TF_LOG environment variable is set correctly
99+
assert os.environ.get("TF_LOG") == expected_tf_level
100+
101+
102+
def test_configure_logging_with_none(monkeypatch):
103+
"""Test that configure_logging returns early when log_level is None."""
104+
# Remove TF_LOG from environment if it exists
105+
monkeypatch.delenv("TF_LOG", raising=False)
106+
107+
# Get initial logging level
108+
initial_level = logging.getLogger().level
109+
110+
# Call with None
111+
configure_logging(None)
112+
113+
# Verify logging level hasn't changed
114+
assert logging.getLogger().level == initial_level
115+
116+
# Verify TF_LOG wasn't set
117+
assert "TF_LOG" not in os.environ
118+
119+
120+
def test_configure_logging_preserves_existing_tf_log(monkeypatch):
121+
"""Test that configure_logging doesn't override existing TF_LOG variable."""
122+
# Set TF_LOG to a specific value
123+
monkeypatch.setenv("TF_LOG", "TRACE")
124+
125+
# Configure logging with a different level
126+
configure_logging("error")
127+
128+
# Verify TF_LOG wasn't changed
129+
assert os.environ["TF_LOG"] == "TRACE"
130+
131+
132+
def test_configure_logging_debug_format():
133+
"""Test that DEBUG level uses detailed format with timestamp."""
134+
configure_logging("debug")
135+
136+
# Get the root logger
137+
root_logger = logging.getLogger()
138+
139+
# Check that level is DEBUG
140+
assert root_logger.level == logging.DEBUG
141+
142+
# Check that at least one handler exists (basicConfig creates one)
143+
assert len(root_logger.handlers) > 0
144+
145+
146+
def test_configure_logging_non_debug_format():
147+
"""Test that non-DEBUG levels use simpler format without timestamp."""
148+
configure_logging("info")
149+
150+
# Get the root logger
151+
root_logger = logging.getLogger()
152+
153+
# Check that level is INFO
154+
assert root_logger.level == logging.INFO
155+
156+
# Check that at least one handler exists
157+
assert len(root_logger.handlers) > 0
158+
159+
160+
@pytest.mark.parametrize(
161+
"log_level",
162+
["trace", "debug", "info", "warning", "error"],
163+
)
164+
def test_cli_with_log_level(log_level, monkeypatch):
165+
"""Test that the CLI accepts and processes the --log-level option."""
166+
# Remove TF_LOG from environment if it exists
167+
monkeypatch.delenv("TF_LOG", raising=False)
168+
169+
command = ["nebari", "--log-level", log_level, "info"]
170+
result = subprocess.run(command, check=True, capture_output=True, text=True)
171+
172+
# Command should succeed
173+
assert result.returncode == 0
174+
175+
176+
def test_cli_with_short_log_level_option(monkeypatch):
177+
"""Test that the CLI accepts the -l short option for log level."""
178+
# Remove TF_LOG from environment if it exists
179+
monkeypatch.delenv("TF_LOG", raising=False)
180+
181+
command = ["nebari", "-l", "debug", "info"]
182+
result = subprocess.run(command, check=True, capture_output=True, text=True)
183+
184+
# Command should succeed
185+
assert result.returncode == 0

0 commit comments

Comments
 (0)