55destinations (console and/or file). It also offers a convenient way to
66retrieve logger instances for use in other modules.
77
8- Functions:
9- initialize_logger: Initializes the logging system with a console handler
10- and an optional file handler.
11- get_logger: Retrieves a logger instance with the specified name.
8+ The logging system is enhanced with recipe-level contextual information.
9+ When the caller sets the context variable in cloud_autopkg_runner.logging_context,
10+ every log line automatically includes the recipe name, even when running concurrently.
1211"""
1312
1413import logging
1514import sys
16- from typing import TextIO
15+ from typing import ClassVar , TextIO
16+
17+ from cloud_autopkg_runner .logging_context import recipe_context
18+
19+
20+ class ColorFormatter (logging .Formatter ):
21+ """Add ANSI coloring to log level names while preserving padding."""
22+
23+ COLORS : ClassVar [dict [str , str ]] = {
24+ "DEBUG" : "\033 [36m" , # Cyan
25+ "INFO" : "\033 [32m" , # Green
26+ "WARNING" : "\033 [33m" , # Yellow
27+ "ERROR" : "\033 [31m" , # Red
28+ "CRITICAL" : "\033 [41m" , # Red background
29+ }
30+ RESET : ClassVar [str ] = "\033 [0m"
31+
32+ def format (self , record : logging .LogRecord ) -> str :
33+ """Render log message with colorized level names."""
34+ # Let the base class format the record first
35+ msg = super ().format (record )
36+
37+ color = self .COLORS .get (record .levelname )
38+ if not color :
39+ return msg
40+
41+ # Replace the exact (already padded) level text inside msg
42+ padded = f"{ record .levelname :<7} "
43+ colored = f"{ color } { padded } { self .RESET } "
44+
45+ return msg .replace (padded , colored , 1 )
46+
47+
48+ class RecipeContextFilter (logging .Filter ):
49+ """Inject contextual recipe information into every log record."""
50+
51+ def filter (self , record : logging .LogRecord ) -> bool :
52+ """Add the current recipe context to the log record.
53+
54+ This method is called for each log record emitted. It sets
55+ `record.recipe` to the value stored in the recipe_context
56+ ContextVar, or "-" if the context variable is not set.
57+
58+ Returns:
59+ True always, so that the record is not filtered out.
60+ """
61+ try :
62+ record .recipe = recipe_context .get ()
63+ except LookupError :
64+ record .recipe = __package__
65+
66+ return True
1767
1868
1969def initialize_logger (verbosity_level : int , log_file : str | None = None ) -> None :
@@ -24,6 +74,9 @@ def initialize_logger(verbosity_level: int, log_file: str | None = None) -> None
2474 `verbosity_level` argument, while the file handler (if enabled) logs at
2575 the DEBUG level.
2676
77+ A logging Filter is added that inserts the current recipe context
78+ (if set) into each log record as `%(recipe)s`.
79+
2780 Args:
2881 verbosity_level: An integer representing the verbosity level. Maps to
2982 logging levels as follows:
@@ -32,11 +85,11 @@ def initialize_logger(verbosity_level: int, log_file: str | None = None) -> None
3285 will be written to this file in addition to the console. If None,
3386 no file logging will occur.
3487 """
35- logger = logging .getLogger ()
88+ logger = logging .getLogger (__name__ . split ( "." )[ 0 ] )
3689 logger .setLevel (logging .DEBUG )
37-
3890 logger .handlers .clear ()
3991
92+ # Map verbosity flags to log levels
4093 log_levels : list [int ] = [
4194 logging .ERROR ,
4295 logging .WARNING ,
@@ -45,24 +98,28 @@ def initialize_logger(verbosity_level: int, log_file: str | None = None) -> None
4598 ]
4699 level : int = log_levels [min (verbosity_level , len (log_levels ) - 1 )]
47100
101+ # Attach recipe context filter
102+ context_filter = RecipeContextFilter ()
103+ logger .addFilter (context_filter )
104+
48105 # Console handler
49106 console_handler : logging .StreamHandler [TextIO ] = logging .StreamHandler (sys .stdout )
50107 console_handler .setLevel (level )
51- console_formatter : logging .Formatter = logging .Formatter (
52- "%(module)-20s %(levelname)-8s %(message)s"
53- )
108+ console_formatter = ColorFormatter ("%(levelname)-7s %(recipe)-25s %(message)s" )
54109 console_handler .setFormatter (console_formatter )
110+ console_handler .addFilter (context_filter )
55111 logger .addHandler (console_handler )
56112
57113 # File handler (optional)
58114 if log_file :
59115 file_handler : logging .FileHandler = logging .FileHandler (log_file , mode = "w" )
60116 file_handler .setLevel (logging .DEBUG )
61117 file_formatter : logging .Formatter = logging .Formatter (
62- "%(asctime)s %(module)-20s %(levelname)-8s %(message)s" ,
118+ "%(asctime)s %(levelname)-7s %(recipe)-25s %(message)s" ,
63119 datefmt = "%m-%d %H:%M" ,
64120 )
65121 file_handler .setFormatter (file_formatter )
122+ file_handler .addFilter (context_filter )
66123 logger .addHandler (file_handler )
67124
68125
@@ -73,6 +130,13 @@ def get_logger(name: str) -> logging.Logger:
73130 simplifies the process of obtaining loggers for use in different
74131 modules of the application.
75132
133+ This wrapper allows for future enhancements, such as adding context
134+ filters or structured logging.
135+
136+ This does not configure handlers or log levels; it simply returns
137+ the logger with the given name. Use `initialize_logger()` at
138+ application startup to configure logging output.
139+
76140 Args:
77141 name: The name of the logger to retrieve (typically `__name__`).
78142
0 commit comments