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:
Fringg
2026-02-16 18:33:40 +03:00
parent 516be6e600
commit f63720467a
2 changed files with 66 additions and 9 deletions
+10
View File
@@ -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,
+56 -9
View File
@@ -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)