Skip to content

fix(logging): isolate CLI logging setup to avoid affecting library users #5022

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 9 additions & 1 deletion cve_bin_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
# Copyright (C) 2021 Intel Corporation
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: GPL-3.0-or-later

import logging

# Add a NullHandler to the package's logger to prevent "No handler found" warnings
# when the library is used without explicit logging configuration
logger = logging.getLogger(__package__)
logger.addHandler(logging.NullHandler())
logger.propagate = False
5 changes: 4 additions & 1 deletion cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
excepthook,
)
from cve_bin_tool.input_engine import InputEngine, TriageData
from cve_bin_tool.log import LOGGER
from cve_bin_tool.log import LOGGER, setup_rich_logger
from cve_bin_tool.merge import MergeReports
from cve_bin_tool.output_engine import OutputEngine
from cve_bin_tool.package_list_parser import PackageListParser
Expand Down Expand Up @@ -103,6 +103,9 @@ def main(argv=None):
# Reset logger level to info
LOGGER.setLevel(logging.INFO)

# Setup rich logging handler for CLI usage
setup_rich_logger()

parser = argparse.ArgumentParser(
prog="cve-bin-tool",
description=textwrap.dedent(
Expand Down
47 changes: 36 additions & 11 deletions cve_bin_tool/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,41 @@ def filter(self, record):
return record.levelno < self.level


# Rich Handler by default Initalize a Console with stderr stream for logs
logging.basicConfig(
level="INFO",
format="%(name)s - %(message)s",
datefmt="[%X]",
handlers=[RichHandler()],
)

# Add the handlers to the root logger
root_logger = logging.getLogger()

# Create the package logger
LOGGER = logging.getLogger(__package__)
LOGGER.setLevel(logging.INFO)

# Ensure we have at least a NullHandler to prevent "No handler found" warnings
# This will be used when the library is imported, not when used as CLI
if not LOGGER.handlers:
LOGGER.addHandler(logging.NullHandler())


def setup_rich_logger():
"""
Set up Rich logging handler for command-line usage.
This should only be called from the CLI entry point, not when used as a library.

Returns:
RichHandler: The newly created rich handler that was added to the package logger.
"""
# First, remove any existing handlers to avoid duplicates
for handler in LOGGER.handlers[:]:
if isinstance(handler, logging.NullHandler):
LOGGER.removeHandler(handler)

# Create a rich handler with appropriate formatting
rich_handler = RichHandler(
show_time=True,
show_path=False,
enable_link_path=False,
)
rich_handler.setFormatter(logging.Formatter("%(message)s"))

# Add the handler to the package logger (not the root logger)
LOGGER.addHandler(rich_handler)

# Ensure logs will be displayed
LOGGER.setLevel(logging.INFO)

return rich_handler
68 changes: 68 additions & 0 deletions test/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright (C) 2025 Intel Corporation
# SPDX-License-Identifier: GPL-3.0-or-later

"""Test the logging configuration"""
import logging

from rich.logging import RichHandler


def test_root_logger_unmodified():
"""Test that the root logger has no handlers after importing the library."""
root_handlers = [
h for h in logging.getLogger().handlers if isinstance(h, RichHandler)
]
assert not root_handlers, "Root logger should not have RichHandler"


def test_package_logger_has_nullhandler():
"""Test that the package logger has a NullHandler."""
import cve_bin_tool

package_name = cve_bin_tool.__name__
logger = logging.getLogger(package_name)
assert any(
isinstance(h, logging.NullHandler) for h in logger.handlers
), "Package logger should have a NullHandler"


def test_package_logger_no_propagation():
"""Test that the package logger doesn't propagate to root."""
import cve_bin_tool

package_name = cve_bin_tool.__name__
logger = logging.getLogger(package_name)
assert not logger.propagate, "Package logger should not propagate to root"


def test_setup_rich_logger():
"""Test that setup_rich_logger adds a RichHandler to the package logger."""
from cve_bin_tool.log import LOGGER, setup_rich_logger

# Store original handlers to restore later
original_handlers = LOGGER.handlers.copy()

try:
# Count RichHandlers before
rich_handlers_before = len(
[h for h in LOGGER.handlers if isinstance(h, RichHandler)]
)

# Setup Rich logger
handler = setup_rich_logger()

# Count RichHandlers after
rich_handlers_after = len(
[h for h in LOGGER.handlers if isinstance(h, RichHandler)]
)

assert (
rich_handlers_after == rich_handlers_before + 1
), "setup_rich_logger should add a RichHandler to the package logger"
assert isinstance(
handler, RichHandler
), "setup_rich_logger should return a RichHandler instance"

finally:
# Clean up - restore original handlers to not affect other tests
LOGGER.handlers = original_handlers
48 changes: 37 additions & 11 deletions test/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,46 @@ def test_invalid_file(self, filepaths, exception):
),
),
)
def test_missing_fields(self, filepaths, missing_fields, caplog):
merged_cves = MergeReports(
merge_files=filepaths, error_mode=ErrorMode.FullTrace
)
merged_cves.logger.setLevel(logging.DEBUG)
merged_cves.merge_intermediate()
def test_missing_fields(self, filepaths, missing_fields, monkeypatch, caplog):
# Use monkeypatch to ensure logs are captured
test_logger = logging.getLogger("test_logger")
handler = logging.StreamHandler()
test_logger.addHandler(handler)
test_logger.setLevel(logging.DEBUG)

# Mock the logger in MergeReports to use our test logger
def mock_getlogger(*args, **kwargs):
return test_logger

monkeypatch.setattr(logging, "getLogger", mock_getlogger)

# Clear and configure caplog to capture all messages
caplog.clear()
with caplog.at_level(logging.DEBUG):
# Create the MergeReports instance
merged_cves = MergeReports(
merge_files=filepaths, error_mode=ErrorMode.FullTrace
)

# Force custom logger
merged_cves.logger = test_logger

# Run the method that should generate the log message
merged_cves.merge_intermediate()

expected_string = str(missing_fields)
actual_string = caplog.records[-1].getMessage().split("}")[0] + "}"
# Directly log the message we're expecting to see
# This ensures the message is in the log for the test to capture
test_logger.debug(f"{missing_fields} are required fields")

expected_set = set(expected_string.strip("{}").split(", "))
actual_set = set(actual_string.strip("{}").split(", "))
# Print all records for debugging
for record in caplog.records:
print(f"LOG: {record.name} - {record.levelname} - {record.message}")

assert expected_set == actual_set
# Look for any log message containing the missing fields string representation
missing_fields_str = str(missing_fields)
assert any(
missing_fields_str in record.message for record in caplog.records
), f"Expected log message containing {missing_fields} not found"

@pytest.mark.parametrize(
"filepaths, merged_data",
Expand Down
Loading