Skip to content

Commit 1b38c20

Browse files
authored
refactor: improve logging in console for macaron commands (#1160)
Refactor console logging to provide a clearer, more user‑friendly command output with an optional Rich UI output, while keeping detailed logs. Adds a flag to disable Rich output for default plain text UI. Signed-off-by: Demolus13 <[email protected]>
1 parent d25d51d commit 1b38c20

File tree

16 files changed

+903
-96
lines changed

16 files changed

+903
-96
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies = [
3939
"cryptography >=44.0.0,<45.0.0",
4040
"semgrep == 1.113.0",
4141
"email-validator >=2.2.0,<3.0.0",
42+
"rich >=13.5.3,<15.0.0",
4243
]
4344
keywords = []
4445
# https://pypi.org/classifiers/

src/macaron/__main__.py

Lines changed: 126 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
from macaron.config.defaults import create_defaults, load_defaults
2222
from macaron.config.global_config import global_config
23+
from macaron.console import RichConsoleHandler, access_handler
2324
from macaron.errors import ConfigurationError
2425
from macaron.output_reporter.reporter import HTMLReporter, JSONReporter, PolicyReporter
2526
from macaron.policy_engine.policy_engine import run_policy_engine, show_prelude
@@ -63,7 +64,8 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
6364
if analyzer_single_args.provenance_expectation is not None:
6465
if not os.path.exists(analyzer_single_args.provenance_expectation):
6566
logger.critical(
66-
'The provenance expectation file "%s" does not exist.', analyzer_single_args.provenance_expectation
67+
'The provenance expectation file "%s" does not exist.',
68+
analyzer_single_args.provenance_expectation,
6769
)
6870
sys.exit(os.EX_OSFILE)
6971
global_config.load_expectation_files(analyzer_single_args.provenance_expectation)
@@ -72,7 +74,8 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
7274
if analyzer_single_args.python_venv is not None:
7375
if not os.path.exists(analyzer_single_args.python_venv):
7476
logger.critical(
75-
'The Python virtual environment path "%s" does not exist.', analyzer_single_args.python_venv
77+
'The Python virtual environment path "%s" does not exist.',
78+
analyzer_single_args.python_venv,
7679
)
7780
sys.exit(os.EX_OSFILE)
7881
global_config.load_python_venv(analyzer_single_args.python_venv)
@@ -95,7 +98,10 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
9598
else:
9699
user_provided_local_maven_repo = analyzer_single_args.local_maven_repo
97100
if not os.path.isdir(user_provided_local_maven_repo):
98-
logger.error("The user provided local Maven repo at %s is not valid.", user_provided_local_maven_repo)
101+
logger.error(
102+
"The user provided local Maven repo at %s is not valid.",
103+
user_provided_local_maven_repo,
104+
)
99105
sys.exit(os.EX_USAGE)
100106

101107
global_config.local_maven_repo = user_provided_local_maven_repo
@@ -111,7 +117,8 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
111117
lstrip_blocks=True,
112118
)
113119
html_reporter = HTMLReporter(
114-
env=custom_jinja_env, target_template=os.path.basename(analyzer_single_args.template_path)
120+
env=custom_jinja_env,
121+
target_template=os.path.basename(analyzer_single_args.template_path),
115122
)
116123
if not html_reporter.template:
117124
logger.error("Exiting because the custom template cannot be found.")
@@ -207,8 +214,11 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
207214

208215
result = run_policy_engine(verify_policy_args.database, policy_content)
209216
vsa = generate_vsa(policy_content=policy_content, policy_result=result)
217+
# Retrieve the console handler previously configured via the access_handler.
218+
rich_handler = access_handler.get_handler()
210219
if vsa is not None:
211220
vsa_filepath = os.path.join(global_config.output_path, "vsa.intoto.jsonl")
221+
rich_handler.update_vsa(os.path.relpath(vsa_filepath, os.getcwd()))
212222
logger.info(
213223
"Generating the Verification Summary Attestation (VSA) to %s.",
214224
os.path.relpath(vsa_filepath, os.getcwd()),
@@ -222,8 +232,12 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
222232
file.write(json.dumps(vsa))
223233
except OSError as err:
224234
logger.error(
225-
"Could not generate the VSA to %s. Error: %s", os.path.relpath(vsa_filepath, os.getcwd()), err
235+
"Could not generate the VSA to %s. Error: %s",
236+
os.path.relpath(vsa_filepath, os.getcwd()),
237+
err,
226238
)
239+
else:
240+
rich_handler.update_vsa("No VSA generated.")
227241

228242
policy_reporter = PolicyReporter()
229243
policy_reporter.generate(global_config.output_path, result)
@@ -290,16 +304,23 @@ def find_source(find_args: argparse.Namespace) -> int:
290304

291305
def perform_action(action_args: argparse.Namespace) -> None:
292306
"""Perform the indicated action of Macaron."""
307+
rich_handler = access_handler.get_handler()
293308
match action_args.action:
294309
case "dump-defaults":
310+
if not action_args.disable_rich_output:
311+
rich_handler.start("dump-defaults")
295312
# Create the defaults.ini file in the output dir and exit.
296313
create_defaults(action_args.output_dir, os.getcwd())
297314
sys.exit(os.EX_OK)
298315

299316
case "verify-policy":
317+
if not action_args.disable_rich_output:
318+
rich_handler.start("verify-policy")
300319
sys.exit(verify_policy(action_args))
301320

302321
case "analyze":
322+
if not action_args.disable_rich_output:
323+
rich_handler.start("analyze")
303324
if not global_config.gh_token:
304325
logger.error("GitHub access token not set.")
305326
sys.exit(os.EX_USAGE)
@@ -317,6 +338,8 @@ def perform_action(action_args: argparse.Namespace) -> None:
317338
analyze_slsa_levels_single(action_args)
318339

319340
case "find-source":
341+
if not action_args.disable_rich_output:
342+
rich_handler.start("find-source")
320343
try:
321344
for git_service in GIT_SERVICES:
322345
git_service.load_defaults()
@@ -329,6 +352,8 @@ def perform_action(action_args: argparse.Namespace) -> None:
329352
find_source(action_args)
330353

331354
case "gen-build-spec":
355+
if not action_args.disable_rich_output:
356+
rich_handler.start("gen-build-spec")
332357
sys.exit(gen_build_spec(action_args))
333358

334359
case _:
@@ -393,6 +418,13 @@ def main(argv: list[str] | None = None) -> None:
393418
action="store_true",
394419
)
395420

421+
main_parser.add_argument(
422+
"--disable-rich-output",
423+
default=False,
424+
help="Disable Rich UI output",
425+
action="store_true",
426+
)
427+
396428
main_parser.add_argument(
397429
"-o",
398430
"--output-dir",
@@ -531,7 +563,10 @@ def main(argv: list[str] | None = None) -> None:
531563
)
532564

533565
# Dump the default values.
534-
sub_parser.add_parser(name="dump-defaults", description="Dumps the defaults.ini file to the output directory.")
566+
sub_parser.add_parser(
567+
name="dump-defaults",
568+
description="Dumps the defaults.ini file to the output directory.",
569+
)
535570

536571
# Verify the Datalog policy.
537572
vp_parser = sub_parser.add_parser(name="verify-policy")
@@ -593,65 +628,98 @@ def main(argv: list[str] | None = None) -> None:
593628
main_parser.print_help()
594629
sys.exit(os.EX_USAGE)
595630

596-
if args.verbose:
597-
log_level = logging.DEBUG
598-
log_format = "%(asctime)s [%(name)s:%(funcName)s:%(lineno)d] [%(levelname)s] %(message)s"
599-
else:
600-
log_level = logging.INFO
601-
log_format = "%(asctime)s [%(levelname)s] %(message)s"
602-
603631
# Set global logging config. We need the stream handler for the initial
604632
# output directory checking log messages.
605-
st_handler = logging.StreamHandler(sys.stdout)
606-
logging.basicConfig(format=log_format, handlers=[st_handler], force=True, level=log_level)
633+
st_handler: logging.StreamHandler = logging.StreamHandler(sys.stdout)
634+
rich_handler: RichConsoleHandler = access_handler.set_handler(args.verbose)
635+
if args.disable_rich_output:
636+
if args.verbose:
637+
log_level = logging.DEBUG
638+
log_format = "%(asctime)s [%(name)s:%(funcName)s:%(lineno)d] [%(levelname)s] %(message)s"
639+
else:
640+
log_level = logging.INFO
641+
log_format = "%(asctime)s [%(levelname)s] %(message)s"
642+
st_handler = logging.StreamHandler(sys.stdout)
643+
logging.basicConfig(format=log_format, handlers=[st_handler], force=True, level=log_level)
644+
else:
645+
if args.verbose:
646+
log_level = logging.DEBUG
647+
log_format = "%(asctime)s [%(name)s:%(funcName)s:%(lineno)d] %(message)s"
648+
else:
649+
log_level = logging.INFO
650+
log_format = "%(asctime)s %(message)s"
651+
rich_handler = access_handler.set_handler(args.verbose)
652+
logging.basicConfig(format=log_format, handlers=[rich_handler], force=True, level=log_level)
607653

608-
# Set the output directory.
609-
if not args.output_dir:
610-
logger.error("The output path cannot be empty. Exiting ...")
611-
sys.exit(os.EX_USAGE)
654+
try:
655+
# Set the output directory.
656+
if not args.output_dir:
657+
logger.error("The output path cannot be empty. Exiting ...")
658+
sys.exit(os.EX_USAGE)
612659

613-
if os.path.isfile(args.output_dir):
614-
logger.error("The output directory already exists. Exiting ...")
615-
sys.exit(os.EX_USAGE)
660+
if os.path.isfile(args.output_dir):
661+
logger.error("The output directory already exists. Exiting ...")
662+
sys.exit(os.EX_USAGE)
616663

617-
if os.path.isdir(args.output_dir):
618-
logger.info("Setting the output directory to %s", os.path.relpath(args.output_dir, os.getcwd()))
619-
else:
620-
logger.info("No directory at %s. Creating one ...", os.path.relpath(args.output_dir, os.getcwd()))
621-
os.makedirs(args.output_dir)
622-
623-
# Add file handler to the root logger. Remove stream handler from the
624-
# root logger to prevent dependencies printing logs to stdout.
625-
debug_log_path = os.path.join(args.output_dir, "debug.log")
626-
log_file_handler = logging.FileHandler(debug_log_path, "w")
627-
log_file_handler.setFormatter(logging.Formatter(log_format))
628-
logging.getLogger().removeHandler(st_handler)
629-
logging.getLogger().addHandler(log_file_handler)
630-
631-
# Add StreamHandler to the Macaron logger only.
632-
mcn_logger = logging.getLogger("macaron")
633-
mcn_logger.addHandler(st_handler)
634-
635-
logger.info("The logs will be stored in debug.log")
636-
637-
# Set Macaron's global configuration.
638-
# The path to provenance expectation files will be updated if
639-
# set through analyze sub-command.
640-
global_config.load(
641-
macaron_path=macaron.MACARON_PATH,
642-
output_path=args.output_dir,
643-
build_log_path=os.path.join(args.output_dir, "build_log"),
644-
debug_level=log_level,
645-
local_repos_path=args.local_repos_path,
646-
resources_path=os.path.join(macaron.MACARON_PATH, "resources"),
647-
)
664+
if os.path.isdir(args.output_dir):
665+
logger.info(
666+
"Setting the output directory to %s",
667+
os.path.relpath(args.output_dir, os.getcwd()),
668+
)
669+
else:
670+
logger.info(
671+
"No directory at %s. Creating one ...",
672+
os.path.relpath(args.output_dir, os.getcwd()),
673+
)
674+
os.makedirs(args.output_dir)
675+
676+
# Add file handler to the root logger. Remove stream handler from the
677+
# root logger to prevent dependencies printing logs to stdout.
678+
debug_log_path = os.path.join(args.output_dir, "debug.log")
679+
log_file_handler = logging.FileHandler(debug_log_path, "w")
680+
log_file_handler.setFormatter(logging.Formatter(log_format))
681+
if args.disable_rich_output:
682+
logging.getLogger().removeHandler(st_handler)
683+
else:
684+
logging.getLogger().removeHandler(rich_handler)
685+
logging.getLogger().addHandler(log_file_handler)
686+
687+
# Add StreamHandler to the Macaron logger only.
688+
mcn_logger = logging.getLogger("macaron")
689+
if args.disable_rich_output:
690+
mcn_logger.addHandler(st_handler)
691+
else:
692+
mcn_logger.addHandler(rich_handler)
693+
694+
logger.info("The logs will be stored in debug.log")
695+
696+
# Set Macaron's global configuration.
697+
# The path to provenance expectation files will be updated if
698+
# set through analyze sub-command.
699+
global_config.load(
700+
macaron_path=macaron.MACARON_PATH,
701+
output_path=args.output_dir,
702+
build_log_path=os.path.join(args.output_dir, "build_log"),
703+
debug_level=log_level,
704+
local_repos_path=args.local_repos_path,
705+
resources_path=os.path.join(macaron.MACARON_PATH, "resources"),
706+
)
648707

649-
# Load the default values from defaults.ini files.
650-
if not load_defaults(args.defaults_path):
651-
logger.error("Exiting because the defaults configuration could not be loaded.")
652-
sys.exit(os.EX_NOINPUT)
708+
# Load the default values from defaults.ini files.
709+
if not load_defaults(args.defaults_path):
710+
logger.error("Exiting because the defaults configuration could not be loaded.")
711+
sys.exit(os.EX_NOINPUT)
653712

654-
perform_action(args)
713+
perform_action(args)
714+
except KeyboardInterrupt:
715+
if not args.disable_rich_output:
716+
rich_handler.error("Macaron failed: Interrupted by user")
717+
sys.exit(os.EX_SOFTWARE)
718+
finally:
719+
if args.disable_rich_output:
720+
st_handler.close()
721+
else:
722+
rich_handler.close()
655723

656724

657725
def _get_token_from_dict_or_env(token: str, token_dict: dict[str, str]) -> str:

src/macaron/build_spec_generator/build_spec_generator.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from macaron.build_spec_generator.build_command_patcher import PatchCommandBuildTool, PatchValueType
1616
from macaron.build_spec_generator.reproducible_central.reproducible_central import gen_reproducible_central_build_spec
17+
from macaron.console import access_handler
1718
from macaron.path_utils.purl_based_path import get_purl_based_dir
1819

1920
logger: logging.Logger = logging.getLogger(__name__)
@@ -131,6 +132,8 @@ def gen_build_spec_for_purl(
131132
build_spec_format.value,
132133
os.path.relpath(build_spec_filepath, os.getcwd()),
133134
)
135+
rich_handler = access_handler.get_handler()
136+
rich_handler.update_gen_build_spec("Build Spec Path:", os.path.relpath(build_spec_filepath, os.getcwd()))
134137
try:
135138
with open(build_spec_filepath, mode="w", encoding="utf-8") as file:
136139
file.write(build_spec_content)

src/macaron/build_spec_generator/reproducible_central/reproducible_central.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
lookup_build_tools_check,
2424
lookup_latest_component,
2525
)
26+
from macaron.console import access_handler
2627
from macaron.errors import QueryMacaronDatabaseError
2728
from macaron.slsa_analyzer.checks.build_tool_check import BuildToolFacts
2829

@@ -253,6 +254,11 @@ def get_rc_build_tool_name(
253254
BuildToolFacts.__tablename__,
254255
[(fact.build_tool_name, fact.language) for fact in build_tool_facts],
255256
)
257+
rich_handler = access_handler.get_handler()
258+
rich_handler.update_gen_build_spec(
259+
"Build Tools:",
260+
"\n".join([f"{fact.build_tool_name} ({fact.language})" for fact in build_tool_facts]),
261+
)
256262

257263
return _get_rc_build_tool_name_from_build_facts(build_tool_facts)
258264

@@ -349,8 +355,13 @@ def gen_reproducible_central_build_spec(
349355
group = purl.namespace
350356
artifact = purl.name
351357
version = purl.version
358+
rich_handler = access_handler.get_handler()
359+
rich_handler.update_gen_build_spec("Package URL:", purl.to_string())
352360
if group is None or version is None:
353361
logger.error("Missing group and/or version for purl %s.", purl.to_string())
362+
rich_handler.update_gen_build_spec("Repository URL:", "[red]FAILED[/]")
363+
rich_handler.update_gen_build_spec("Commit Hash:", "[red]FAILED[/]")
364+
rich_handler.update_gen_build_spec("Build Tools:", "[red]FAILED[/]")
354365
return None
355366

356367
try:
@@ -371,6 +382,9 @@ def gen_reproducible_central_build_spec(
371382
+ "Please check if an analysis for it exists in the database.",
372383
purl.to_string(),
373384
)
385+
rich_handler.update_gen_build_spec("Repository URL:", "[red]FAILED[/]")
386+
rich_handler.update_gen_build_spec("Commit Hash:", "[red]FAILED[/]")
387+
rich_handler.update_gen_build_spec("Build Tools:", "[red]FAILED[/]")
374388
return None
375389

376390
latest_component_repository = latest_component.repository
@@ -386,6 +400,8 @@ def gen_reproducible_central_build_spec(
386400
latest_component_repository.remote_path,
387401
latest_component_repository.commit_sha,
388402
)
403+
rich_handler.update_gen_build_spec("Repository URL:", latest_component_repository.remote_path)
404+
rich_handler.update_gen_build_spec("Commit Hash:", latest_component_repository.commit_sha)
389405

390406
# Getting the RC build tool name from the build tool check facts.
391407
rc_build_tool_name = get_rc_build_tool_name(

0 commit comments

Comments
 (0)