Log formatter that prints out the log message nicely formatted, with colored level and stringified extra fields. It formats the log records so that they: - start with the level (colorized, and padded to 5 chars so that it is nicely aligned) - then have the actual log message, if
| 82 | |
| 83 | |
| 84 | class CrawleeLogFormatter(logging.Formatter): |
| 85 | """Log formatter that prints out the log message nicely formatted, with colored level and stringified extra fields. |
| 86 | |
| 87 | It formats the log records so that they: |
| 88 | - start with the level (colorized, and padded to 5 chars so that it is nicely aligned) |
| 89 | - then have the actual log message, if it's multiline then it's nicely indented |
| 90 | - then have the stringified extra log fields |
| 91 | - then, if an exception is a part of the log record, prints the formatted exception. |
| 92 | """ |
| 93 | |
| 94 | # The fields that are added to the log record with `logger.log(..., extra={...})` are just merged in the log record |
| 95 | # with the other log record properties, and you can't get them in some nice, isolated way. So, to get the extra |
| 96 | # fields, we just compare all the properties present in the log record with properties present in an empty log |
| 97 | # record, and extract all the extra ones not present in the empty log record. |
| 98 | empty_record = logging.LogRecord('dummy', 0, 'dummy', 0, 'dummy', None, None) |
| 99 | |
| 100 | def __init__( |
| 101 | self, |
| 102 | include_logger_name: bool = True, # noqa: FBT001, FBT002 |
| 103 | *args: Any, |
| 104 | **kwargs: Any, |
| 105 | ) -> None: |
| 106 | """Initialize a new instance. |
| 107 | |
| 108 | Args: |
| 109 | include_logger_name: Include logger name at the beginning of the log line. |
| 110 | args: Arguments passed to the parent class. |
| 111 | kwargs: Keyword arguments passed to the parent class. |
| 112 | """ |
| 113 | super().__init__(*args, **kwargs) |
| 114 | self.include_logger_name = include_logger_name |
| 115 | |
| 116 | def _get_extra_fields(self, record: logging.LogRecord) -> dict[str, Any]: |
| 117 | extra_fields: dict[str, Any] = {} |
| 118 | for key, value in record.__dict__.items(): |
| 119 | if key not in self.empty_record.__dict__: |
| 120 | extra_fields[key] = value # noqa: PERF403 |
| 121 | |
| 122 | return extra_fields |
| 123 | |
| 124 | def format(self, record: logging.LogRecord) -> str: |
| 125 | """Format the log record nicely. |
| 126 | |
| 127 | This formats the log record so that it: |
| 128 | - starts with the level (colorized, and padded to 5 chars so that it is nicely aligned) |
| 129 | - then has the actual log message, if it's multiline then it's nicely indented |
| 130 | - then has the stringified extra log fields |
| 131 | - then, if an exception is a part of the log record, prints the formatted exception. |
| 132 | """ |
| 133 | logger_name_string = f'{_LOG_NAME_COLOR}[{record.name}]{Style.RESET_ALL} ' |
| 134 | |
| 135 | # Colorize the log level, and shorten it to 6 chars tops |
| 136 | level_color_code = _LOG_LEVEL_COLOR.get(record.levelno, '') |
| 137 | level_short_alias = _LOG_LEVEL_SHORT_ALIAS.get(record.levelno, record.levelname) |
| 138 | level_string = f'{level_color_code}{level_short_alias}{Style.RESET_ALL} ' |
| 139 | |
| 140 | # Format the extra log record fields, if there were some |
| 141 | # Just stringify them to JSON and color them gray |
no outgoing calls