Skip to content

Commit 5b45a98

Browse files
authored
FIX(config) Respect log_level from settings.yaml and suppress premature debug output (#9)
* FIX(config) Respect log_level from settings.yaml and suppress premature debug output Settings.yaml log_level was ignored because config was loaded after --log-level argument default was set. Additionally, debug messages from config loading appeared before the logger was properly configured. Changes: - Load config defaults before creating argparse arguments - Use config file log_level as default instead of hardcoded "INFO" - Remove loguru default handler before parsing args to suppress early debug output - Add log_config_status() to log config info AFTER logger is configured - Remove redundant --log-level from trigger/summary subcommands - Note: --log-level must now come BEFORE subcommand (e.g., rptool --log-level DEBUG write) Fixes: #6 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Signed-off-by: Zdenek Kraus <zkraus@redhat.com>
1 parent 2f3848e commit 5b45a98

6 files changed

Lines changed: 232 additions & 65 deletions

File tree

src/reportportal/ap.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def create_main_parser() -> argparse.ArgumentParser:
4545
except PackageNotFoundError:
4646
pkg_version = 'unknown (not installed)'
4747

48+
# Get configuration defaults (config file + env vars + built-in defaults)
49+
# Must be loaded before creating arguments that use these defaults
50+
defaults = _get_config_defaults()
51+
4852
parser = argparse.ArgumentParser(
4953
prog='rptool',
5054
description='Unified command-line interface for ReportPortal tools',
@@ -58,11 +62,17 @@ def create_main_parser() -> argparse.ArgumentParser:
5862
version=f'rptool {pkg_version}'
5963
)
6064

65+
# Validation of config file log_level
66+
valid_log_levels = {"DEBUG", "INFO", "WARNING", "ERROR"}
67+
configured_log_level = str(defaults.get("log_level", "INFO")).upper()
68+
if configured_log_level not in valid_log_levels:
69+
raise ValueError(f'Invalid log level in config: {configured_log_level}. Must be one of {valid_log_levels}')
70+
6171
parser.add_argument(
6272
"--log-level",
6373
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
64-
default="INFO",
65-
help="Set the logging level (default: INFO)"
74+
default=configured_log_level,
75+
help="Set the logging level (default: from config or INFO)"
6676
)
6777

6878
# Create subparsers for each command
@@ -74,9 +84,6 @@ def create_main_parser() -> argparse.ArgumentParser:
7484
required=True
7585
)
7686

77-
# Get configuration defaults (config file + env vars + built-in defaults)
78-
defaults = _get_config_defaults()
79-
8087
# Adding subparsers' arguments
8188
subparsers_hanlers = [
8289
_add_write_arguments,
@@ -124,14 +131,14 @@ def _add_write_arguments(subparsers: argparse.ArgumentParser, defaults: dict) ->
124131
_add_common_rp_args(parser, defaults)
125132

126133
parser.add_argument(
127-
"--launch-name",
134+
"--launch-name",
128135
help="Override Launch name that will be reported, otherwise filename will be used",
129136
default=defaults['rp_launch_name']
130137
)
131138
parser.add_argument(
132-
"--launch-description",
139+
"--launch-description",
133140
help="Custom head section to launch description, passthrough description will be added from the junit if available",
134-
# The empty string from defaults is necessary to enable additional description to be added on .finish_launch()
141+
# The empty string from defaults is necessary to enable additional description to be added on .finish_launch()
135142
default=defaults['rp_launch_description'],
136143
)
137144
parser.add_argument(
@@ -147,7 +154,7 @@ def _add_write_arguments(subparsers: argparse.ArgumentParser, defaults: dict) ->
147154
default=False
148155
)
149156
parser.add_argument("junits", nargs='+', help="path to all junit results, multiple files will be reportes as one launch")
150-
157+
151158

152159
def _add_query_arguments(subparsers: argparse.ArgumentParser, defaults: dict) -> None:
153160
"""Add arguments for query command."""
@@ -240,13 +247,6 @@ def _add_trigger_arguments(subparsers: argparse.ArgumentParser, defaults: dict)
240247
)
241248
_add_common_rp_args(parser, defaults)
242249

243-
parser.add_argument(
244-
"--log-level",
245-
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
246-
default=defaults.get("log_level", "INFO"),
247-
help="Set the logging level (default: INFO)"
248-
)
249-
250250

251251
def _add_summary_arguments(subparsers: argparse.ArgumentParser, defaults: dict) -> None:
252252
"""Add arguments for summary command."""
@@ -260,12 +260,6 @@ def _add_summary_arguments(subparsers: argparse.ArgumentParser, defaults: dict)
260260

261261
_add_common_rp_args(parser, defaults)
262262

263-
parser.add_argument(
264-
"--log-level",
265-
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
266-
default=defaults.get("log_level", "INFO"),
267-
help="Set the logging level (default: INFO)"
268-
)
269263
parser.add_argument(
270264
"--attribute",
271265
action="append",

src/reportportal/config.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,29 @@ def load_config_file() -> Dict[str, Any]:
3737
3838
Returns:
3939
Dictionary with configuration values, empty dict if file doesn't exist
40-
or can't be loaded
40+
or is empty.
41+
42+
Raises:
43+
ValueError: When config file cannot be parsed properly.
4144
"""
4245

4346
config_file = get_config_file_path()
4447

4548
if not config_file.exists():
46-
logger.debug(f"Config file not found: {config_file}")
49+
logger.debug("No config file present, using defaults")
4750
return {}
4851

4952
try:
5053
with open(config_file, 'r') as f:
5154
config = yaml.safe_load(f)
5255
if config is None:
53-
logger.debug(f"Config file is empty: {config_file}")
56+
logger.debug("Config file empty")
5457
return {}
55-
logger.debug(f"Loaded config from: {config_file}")
58+
logger.info("Config file loaded successfully")
5659
return config
5760
except Exception as e:
58-
logger.warning(f"Failed to load config file {config_file}: {e}")
59-
return {}
61+
# need to raise ValueError to indicate critical problem
62+
raise ValueError(f"Error reading config file {config_file} {e}")
6063

6164

6265
def get_config_defaults() -> Dict[str, Any]:
@@ -162,6 +165,7 @@ def get_effective_defaults() -> Dict[str, Any]:
162165
# Inject REQUESTS_CA_BUNDLE into environment if configured but not already set
163166
if merged.get("requests_ca_bundle") and not os.environ.get("REQUESTS_CA_BUNDLE"):
164167
os.environ["REQUESTS_CA_BUNDLE"] = merged["requests_ca_bundle"]
165-
logger.debug(f"Set REQUESTS_CA_BUNDLE from config: {merged['requests_ca_bundle']}")
168+
logger.debug("Set REQUESTS_CA_BUNDLE from config: {}", merged['requests_ca_bundle'])
166169

167170
return merged
171+

src/reportportal/rp_dispatcher.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,28 @@ def main(argv: Optional[List[str]] = None) -> int:
164164
Returns:
165165
Exit code (0 for success, 1 for error)
166166
"""
167-
parser = ap.create_main_parser()
167+
# Remove default loguru handler immediately to prevent premature debug messages
168+
# (e.g., during config file loading before log level is determined)
169+
logger.remove()
170+
# Setup intermittent WARNING logger for any configuration logs
171+
# or set to environmnet variable LOG_LEVEL if defined
172+
logger.add(sink=sys.stderr, level=os.environ.get('LOG_LEVEL', "").upper() or 'WARNING')
173+
174+
try:
175+
parser = ap.create_main_parser()
176+
except ValueError as e:
177+
logger.error('Improper configuration {}', str(e))
178+
return 1
179+
168180

169181
# Parse arguments
170182
try:
171183
args = parser.parse_args(argv)
172184
except SystemExit as e:
173185
return e.code if e.code is not None else 1
174186

175-
# setup logging handlers
176-
logger.remove() # remove default one
187+
# Setup logging handler with configured log level
188+
logger.remove()
177189
logger.add(sink=sys.stderr, level=args.log_level)
178190

179191
# Dispatch to appropriate command handler

tests/unit/test_ap.py

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
import os
7+
from pathlib import Path
78
from unittest.mock import patch
89

910
from reportportal.ap import (
@@ -321,11 +322,11 @@ def test_release_parser_all_options(self):
321322
parser = create_main_parser()
322323

323324
args = parser.parse_args([
325+
'--log-level', 'DEBUG',
324326
'summary',
325327
'--rp-project', 'test_project',
326328
'--rp-url', 'https://test.reportportal.com',
327329
'--rp-token', 'test_key',
328-
'--log-level', 'DEBUG',
329330
'--attribute', 'kuadrant:v1.3.1',
330331
'--attribute', 'platform:aws',
331332
'--days', '7',
@@ -373,7 +374,7 @@ def test_release_parser_log_levels(self):
373374
parser = create_main_parser()
374375

375376
for level in ['DEBUG', 'INFO', 'WARNING', 'ERROR']:
376-
args = parser.parse_args(['summary', '--attribute', 'kuadrant:v1.3.1', '--log-level', level])
377+
args = parser.parse_args(['--log-level', level, 'summary', '--attribute', 'kuadrant:v1.3.1'])
377378
assert args.log_level == level
378379

379380
def test_release_parser_days_parameter(self):
@@ -535,4 +536,136 @@ def test_multi_criteria_filtering(self):
535536
assert 'platform:gcp' in args.attribute
536537
assert 'component:controller' in args.attribute
537538
assert 'env:staging' in args.attribute
538-
assert args.days == 7
539+
assert args.days == 7
540+
541+
542+
@pytest.mark.unit
543+
class TestLogLevelConfig:
544+
"""Test log level configuration (Issue #6)."""
545+
546+
def test_log_level_from_config_file(self):
547+
"""Test that log_level from config file is used as default."""
548+
with patch.dict(os.environ, {}, clear=True):
549+
with patch('reportportal.config.load_config_file') as mock_load:
550+
# Simulate config file with DEBUG log level
551+
mock_load.return_value = {'log_level': 'DEBUG'}
552+
parser = create_main_parser()
553+
554+
# Parse without --log-level argument
555+
args = parser.parse_args(['write', 'test.xml'])
556+
557+
# Should use config file value
558+
assert args.log_level == 'DEBUG'
559+
560+
def test_log_level_cli_overrides_config(self):
561+
"""Test that CLI --log-level overrides config file."""
562+
with patch.dict(os.environ, {}, clear=True):
563+
with patch('reportportal.config.load_config_file') as mock_load:
564+
# Config has DEBUG
565+
mock_load.return_value = {'log_level': 'DEBUG'}
566+
parser = create_main_parser()
567+
568+
# CLI specifies ERROR
569+
args = parser.parse_args(['--log-level', 'ERROR', 'write', 'test.xml'])
570+
571+
# Should use CLI value
572+
assert args.log_level == 'ERROR'
573+
574+
def test_log_level_default_when_no_config(self):
575+
"""Test that INFO is used when no config is provided."""
576+
with patch.dict(os.environ, {}, clear=True):
577+
with patch('reportportal.config.load_config_file') as mock_load:
578+
# No log_level in config
579+
mock_load.return_value = {}
580+
parser = create_main_parser()
581+
582+
# Parse without --log-level argument
583+
args = parser.parse_args(['write', 'test.xml'])
584+
585+
# Should use built-in default (INFO)
586+
assert args.log_level == 'INFO'
587+
588+
def test_log_level_works_with_all_commands(self):
589+
"""Test that config log_level works for all commands."""
590+
with patch.dict(os.environ, {}, clear=True):
591+
with patch('reportportal.config.load_config_file') as mock_load:
592+
mock_load.return_value = {'log_level': 'WARNING'}
593+
parser = create_main_parser()
594+
595+
# Test write command
596+
args = parser.parse_args(['write', 'test.xml'])
597+
assert args.log_level == 'WARNING'
598+
599+
# Test query command
600+
args = parser.parse_args(['query'])
601+
assert args.log_level == 'WARNING'
602+
603+
# Test trigger command
604+
args = parser.parse_args(['trigger'])
605+
assert args.log_level == 'WARNING'
606+
607+
# Test summary command
608+
args = parser.parse_args(['summary', '--attribute', 'test:v1'])
609+
assert args.log_level == 'WARNING'
610+
611+
def test_log_level_invalid_in_config(self):
612+
"""Test that invalid log level in config raises ValueError."""
613+
with patch.dict(os.environ, {}, clear=True):
614+
with patch('reportportal.config.load_config_file') as mock_load:
615+
# Simulate config file with invalid log level
616+
mock_load.return_value = {'log_level': 'VERBOSE'}
617+
618+
# Creating parser should raise ValueError
619+
with pytest.raises(ValueError) as exc_info:
620+
create_main_parser()
621+
622+
# Check error message contains the invalid value
623+
error_msg = str(exc_info.value)
624+
assert 'Invalid log level in config' in error_msg
625+
assert 'VERBOSE' in error_msg
626+
assert 'Must be one of' in error_msg
627+
628+
def test_log_level_invalid_in_config_case_insensitive(self):
629+
"""Test that invalid log level works with case normalization."""
630+
with patch.dict(os.environ, {}, clear=True):
631+
with patch('reportportal.config.load_config_file') as mock_load:
632+
# Lowercase 'trace' should also be rejected
633+
mock_load.return_value = {'log_level': 'trace'}
634+
635+
with pytest.raises(ValueError) as exc_info:
636+
create_main_parser()
637+
638+
error_msg = str(exc_info.value)
639+
assert 'Invalid log level in config' in error_msg
640+
# Should show uppercase version in error
641+
assert 'TRACE' in error_msg
642+
643+
def test_no_premature_debug_messages_during_config_load(self):
644+
"""Test that debug messages during config loading are suppressed until log level is set."""
645+
import io
646+
from unittest.mock import patch
647+
from reportportal.rp_dispatcher import main
648+
649+
# Capture stderr to check for debug messages
650+
captured_stderr = io.StringIO()
651+
652+
with patch.dict(os.environ, {}, clear=True):
653+
with patch('reportportal.config.load_config_file') as mock_load:
654+
# Simulate config file exists and has values
655+
mock_load.return_value = {'log_level': 'INFO', 'rp_url': 'http://test.com'}
656+
657+
# Redirect stderr
658+
with patch('sys.stderr', captured_stderr):
659+
try:
660+
# Run with INFO level (default from config)
661+
# This will fail because we don't have valid args, but we just want to check logging
662+
main(['write', 'test.xml'])
663+
except SystemExit:
664+
pass
665+
666+
# Check that no "Loaded config from" or "Config file" debug messages appear
667+
stderr_output = captured_stderr.getvalue()
668+
assert "Loaded config from" not in stderr_output, \
669+
"Debug message 'Loaded config from' should not appear with INFO log level"
670+
assert "Config file not found" not in stderr_output, \
671+
"Debug message 'Config file not found' should not appear with INFO log level"

0 commit comments

Comments
 (0)