refactor: improve log formatting — logger name prefix and table alignment
1. Add _prefix_logger_name processor that moves [module.name] before event text for consistent format: timestamp [level] [module] message 2. Fix startup summary table alignment by using display width calculation instead of len() — properly accounts for wide emoji and variation selectors that render as 2 terminal cells
This commit is contained in:
@@ -48,6 +48,14 @@ def _clean_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]
|
||||
return event_dict
|
||||
|
||||
|
||||
def _prefix_logger_name(logger: Any, method_name: str, event_dict: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Move logger name before event text: [module.name] event text."""
|
||||
logger_name = event_dict.pop('logger', None)
|
||||
if logger_name:
|
||||
event_dict['event'] = f'[{logger_name}] {event_dict.get("event", "")}'
|
||||
return event_dict
|
||||
|
||||
|
||||
def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
"""Configure structlog and return formatters + notifier.
|
||||
|
||||
@@ -103,6 +111,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
foreign_pre_chain=shared_processors,
|
||||
processors=[
|
||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||
_prefix_logger_name,
|
||||
structlog.dev.ConsoleRenderer(
|
||||
colors=False,
|
||||
pad_event_to=0,
|
||||
@@ -118,6 +127,7 @@ def setup_logging() -> tuple[logging.Formatter, logging.Formatter, Any]:
|
||||
foreign_pre_chain=shared_processors,
|
||||
processors=[
|
||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||
_prefix_logger_name,
|
||||
structlog.dev.ConsoleRenderer(
|
||||
pad_event_to=0,
|
||||
pad_level=False,
|
||||
|
||||
@@ -1,11 +1,58 @@
|
||||
import platform
|
||||
import time
|
||||
import unicodedata
|
||||
from collections.abc import Iterable, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _char_width(ch: str) -> int:
|
||||
"""Return terminal display width of a single character."""
|
||||
cp = ord(ch)
|
||||
# Variation selector U+FE0F / U+FE0E — zero width (handled by caller)
|
||||
if cp in (0xFE0E, 0xFE0F, 0x200D):
|
||||
return 0
|
||||
# Combining marks — zero width
|
||||
if unicodedata.category(ch).startswith('M'):
|
||||
return 0
|
||||
# East Asian Wide / Fullwidth
|
||||
if unicodedata.east_asian_width(ch) in ('W', 'F'):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
def _display_width(text: str) -> int:
|
||||
"""Calculate terminal display width accounting for wide chars and emoji."""
|
||||
width = 0
|
||||
prev_base = 0
|
||||
for ch in text:
|
||||
cp = ord(ch)
|
||||
# U+FE0F emoji presentation selector — upgrades previous char to 2 cells
|
||||
if cp == 0xFE0F:
|
||||
if prev_base == 1:
|
||||
width += 1 # upgrade 1 → 2
|
||||
prev_base = 2
|
||||
continue
|
||||
cw = _char_width(ch)
|
||||
if cw > 0:
|
||||
prev_base = cw
|
||||
width += cw
|
||||
return width
|
||||
|
||||
|
||||
def _ljust(text: str, width: int) -> str:
|
||||
"""Left-justify text to given display width."""
|
||||
return text + ' ' * max(0, width - _display_width(text))
|
||||
|
||||
|
||||
def _center(text: str, width: int) -> str:
|
||||
"""Center text to given display width."""
|
||||
pad = max(0, width - _display_width(text))
|
||||
left = pad // 2
|
||||
return ' ' * left + text + ' ' * (pad - left)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StepRecord:
|
||||
title: str
|
||||
@@ -85,25 +132,25 @@ class StartupTimeline:
|
||||
subtitle_parts.append(f'{key}: {value}')
|
||||
subtitle_text = ' | '.join(subtitle_parts)
|
||||
|
||||
width = max(len(title_text), len(subtitle_text))
|
||||
width = max(_display_width(title_text), _display_width(subtitle_text))
|
||||
border = '╔' + '═' * (width + 2) + '╗'
|
||||
self.logger.info(border)
|
||||
self.logger.info('║ ' + title_text.ljust(width) + ' ║')
|
||||
self.logger.info('║ ' + subtitle_text.ljust(width) + ' ║')
|
||||
self.logger.info('║ ' + _ljust(title_text, width) + ' ║')
|
||||
self.logger.info('║ ' + _ljust(subtitle_text, width) + ' ║')
|
||||
self.logger.info('╚' + '═' * (width + 2) + '╝')
|
||||
|
||||
def log_section(self, title: str, lines: Iterable[str], icon: str = '📄') -> None:
|
||||
items = [f'{icon} {title}'] + [f'• {line}' for line in lines]
|
||||
width = max(len(item) for item in items)
|
||||
width = max(_display_width(item) for item in items)
|
||||
top = '┌ ' + '─' * width + ' ┐'
|
||||
middle = '├ ' + '─' * width + ' ┤'
|
||||
bottom = '└ ' + '─' * width + ' ┘'
|
||||
|
||||
self.logger.info(top)
|
||||
self.logger.info('│ ' + items[0].ljust(width) + ' │')
|
||||
self.logger.info('│ ' + _ljust(items[0], width) + ' │')
|
||||
self.logger.info(middle)
|
||||
for item in items[1:]:
|
||||
self.logger.info('│ ' + item.ljust(width) + ' │')
|
||||
self.logger.info('│ ' + _ljust(item, width) + ' │')
|
||||
self.logger.info(bottom)
|
||||
|
||||
def add_manual_step(
|
||||
@@ -169,15 +216,15 @@ class StartupTimeline:
|
||||
base += f' :: {step.message}'
|
||||
lines.append(base)
|
||||
|
||||
width = max(len(line) for line in lines)
|
||||
width = max(_display_width(line) for line in lines)
|
||||
border_top = '┏' + '━' * (width + 2) + '┓'
|
||||
border_mid = '┣' + '━' * (width + 2) + '┫'
|
||||
border_bottom = '┗' + '━' * (width + 2) + '┛'
|
||||
title = 'РЕЗЮМЕ ЗАПУСКА'
|
||||
|
||||
self.logger.info(border_top)
|
||||
self.logger.info('┃ ' + title.center(width) + ' ┃')
|
||||
self.logger.info('┃ ' + _center(title, width) + ' ┃')
|
||||
self.logger.info(border_mid)
|
||||
for line in lines:
|
||||
self.logger.info('┃ ' + line.ljust(width) + ' ┃')
|
||||
self.logger.info('┃ ' + _ljust(line, width) + ' ┃')
|
||||
self.logger.info(border_bottom)
|
||||
|
||||
Reference in New Issue
Block a user