课题7:编译器日志增强器
难度:低 | 类型:项目实战 | 源文件:
scratchv/utils/logger.py| 行数:~300 状态:✅ 已完成
概述
为编译器的各个阶段(解析、IR生成、优化、代码生成)添加分级、带颜色的日志输出,支持--log-level和--log-file命令行参数。
理解背景
是什么?
日志增强器(logger.py)为 ScratchV 编译器提供了统一的结构化日志系统,替代散落在各处的 print() 调用。
核心功能:
- 彩色终端输出:不同级别用不同颜色(DEBUG 灰、INFO 绿、WARNING 黄、ERROR 红)
- 级别过滤:开发时看 DEBUG,正式跑只看 INFO
- 文件输出:同时写日志文件(无颜色,纯文本)
- 阶段计时:自动记录每个编译阶段的耗时
- 进度显示:长任务显示 [当前/总数] 百分比
为什么?
编译器是一个多阶段流水线(解析 → 优化 → 代码生成 → ...)。每个阶段都可能出问题。如果只是到处 print("ok") 和 print("error"):
- 找不到问题:不知道哪个阶段报的错、花了多少时间
- 日志太多:正式使用时无法关闭调试输出
- 格式不统一:每个人加的 print 格式都不一样
统一日志系统让调试效率大幅提升。
核心概念
日志级别(从低到高)
| 级别 | 用途 | 颜色 | 什么时候用 |
|---|---|---|---|
| DEBUG | 详细追踪信息 | 灰色 | 开发调试时 |
| INFO | 正常操作消息 | 绿色 | 报告进度 |
| WARNING | 非关键问题 | 黄色 | 可恢复的异常 |
| ERROR | 严重错误 | 红色 | 编译失败 |
| CRITICAL | 致命错误 | 粗体红 | 必须立即停止 |
log_phase 上下文管理器
自动记录一个阶段的开始和结束时间:
with log_phase("parse", "Parsing DSL input"):
program = parser.parse(source)
# 自动输出:
# 12:34:56 INFO [scratchv.parse] Parsing DSL input...
# 12:34:56 INFO [scratchv.parse] Parsing DSL input... done (0.032s)
如果阶段抛异常,会自动输出 FAILED。
详细任务
- 使用Python
logging模块,创建多个logger(按模块)。
- 添加
argparse参数:--log-level {DEBUG,INFO,WARN,ERROR},--log-file FILE。
- 替换现有
print语句为logger.info或logger.debug。
- 为关键操作添加日志:开始解析、优化Pass应用、指令数统计、代码生成完成。
- 实现彩色输出(使用
colorlog或ANSI码),不同级别不同颜色。
- 支持同时输出到控制台和文件(文件可保留DEBUG级别)。
- 确保高日志级别时低级别字符串不会被构造(使用
logger.isEnabledFor)。
交付产物
- 修改后的编译器主文件和各个模块
- 使用示例:
python main.py test.dsl --log-level DEBUG --log-file build.log
- 文档:日志级别含义、如何为新增模块添加日志
代码走读
一步步使用
Step 1: 初始化日志系统
from scratchv.utils.logger import init_logger, get_logger
# 初始化(通常在编译器入口点调用一次)
init_logger(
level="DEBUG", # 显示所有级别的日志
log_file="build.log", # 同时写入文件
use_color=True, # 终端彩色输出
)
Step 2: 获取日志器并使用
log = get_logger("scratchv.parser") # 按模块命名
log.debug("Tokenizing line %d...", 42)
log.info("Parsed %d nodes", 150)
log.warning("Unknown attribute: %s", attr_name)
log.error("Syntax error at line %d: %s", line, msg)
Step 3: 使用阶段计时
from scratchv.utils.logger import log_phase
with log_phase("optimize", "Running optimization passes"):
for pass_name in ["constant_folding", "dead_code", "peephole"]:
run_pass(pass_name)
# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)
Step 4: 运行时切换日志级别
from scratchv.utils.logger import set_level
set_level("WARNING") # 只看警告和错误
set_level("DEBUG") # 恢复详细输出
核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
logging模块,创建多个logger(按模块)。argparse参数:--log-level {DEBUG,INFO,WARN,ERROR},--log-file FILE。print语句为logger.info或logger.debug。colorlog或ANSI码),不同级别不同颜色。logger.isEnabledFor)。- 修改后的编译器主文件和各个模块
- 使用示例:
python main.py test.dsl --log-level DEBUG --log-file build.log - 文档:日志级别含义、如何为新增模块添加日志
代码走读
一步步使用
Step 1: 初始化日志系统
from scratchv.utils.logger import init_logger, get_logger
# 初始化(通常在编译器入口点调用一次)
init_logger(
level="DEBUG", # 显示所有级别的日志
log_file="build.log", # 同时写入文件
use_color=True, # 终端彩色输出
)
Step 2: 获取日志器并使用
log = get_logger("scratchv.parser") # 按模块命名
log.debug("Tokenizing line %d...", 42)
log.info("Parsed %d nodes", 150)
log.warning("Unknown attribute: %s", attr_name)
log.error("Syntax error at line %d: %s", line, msg)
Step 3: 使用阶段计时
from scratchv.utils.logger import log_phase
with log_phase("optimize", "Running optimization passes"):
for pass_name in ["constant_folding", "dead_code", "peephole"]:
run_pass(pass_name)
# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)
Step 4: 运行时切换日志级别
from scratchv.utils.logger import set_level
set_level("WARNING") # 只看警告和错误
set_level("DEBUG") # 恢复详细输出
核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
Step 1: 初始化日志系统
from scratchv.utils.logger import init_logger, get_logger
# 初始化(通常在编译器入口点调用一次)
init_logger(
level="DEBUG", # 显示所有级别的日志
log_file="build.log", # 同时写入文件
use_color=True, # 终端彩色输出
)
Step 2: 获取日志器并使用
log = get_logger("scratchv.parser") # 按模块命名
log.debug("Tokenizing line %d...", 42)
log.info("Parsed %d nodes", 150)
log.warning("Unknown attribute: %s", attr_name)
log.error("Syntax error at line %d: %s", line, msg)
Step 3: 使用阶段计时
from scratchv.utils.logger import log_phase
with log_phase("optimize", "Running optimization passes"):
for pass_name in ["constant_folding", "dead_code", "peephole"]:
run_pass(pass_name)
# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)
Step 4: 运行时切换日志级别
from scratchv.utils.logger import set_level
set_level("WARNING") # 只看警告和错误
set_level("DEBUG") # 恢复详细输出
核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
from scratchv.utils.logger import init_logger, get_logger
# 初始化(通常在编译器入口点调用一次)
init_logger(
level="DEBUG", # 显示所有级别的日志
log_file="build.log", # 同时写入文件
use_color=True, # 终端彩色输出
)
log = get_logger("scratchv.parser") # 按模块命名
log.debug("Tokenizing line %d...", 42)
log.info("Parsed %d nodes", 150)
log.warning("Unknown attribute: %s", attr_name)
log.error("Syntax error at line %d: %s", line, msg)
Step 3: 使用阶段计时
from scratchv.utils.logger import log_phase
with log_phase("optimize", "Running optimization passes"):
for pass_name in ["constant_folding", "dead_code", "peephole"]:
run_pass(pass_name)
# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)
Step 4: 运行时切换日志级别
from scratchv.utils.logger import set_level
set_level("WARNING") # 只看警告和错误
set_level("DEBUG") # 恢复详细输出
核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
from scratchv.utils.logger import log_phase
with log_phase("optimize", "Running optimization passes"):
for pass_name in ["constant_folding", "dead_code", "peephole"]:
run_pass(pass_name)
# 输出:
# INFO [scratchv.optimize] Running optimization passes...
# INFO [scratchv.optimize] Running optimization passes... done (0.156s)
from scratchv.utils.logger import set_level
set_level("WARNING") # 只看警告和错误
set_level("DEBUG") # 恢复详细输出
核心设计:单例 + 层级命名
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
_root_logger: Optional[logging.Logger] = None # 全局根日志器
def init_logger(level, log_file, use_color):
_root_logger = logging.getLogger("scratchv") # 根
_root_logger.addHandler(console_handler) # → 终端
if log_file:
_root_logger.addHandler(file_handler) # → 文件
def get_logger(name: str):
if not name.startswith("scratchv"):
name = f"scratchv.{name}"
return logging.getLogger(name) # Python logging 自动继承层级
所有 scratchv.xxx 日志器都继承根日志器的 handlers 和级别。
彩色格式化器
class _ColorFormatter(logging.Formatter):
def format(self, record):
color = _COLORS.get(record.levelname, "")
return f"{color}{record.levelname:<8}\033[0m [{record.name}] {record.getMessage()}"
class _ColorFormatter(logging.Formatter):
def format(self, record):
color = _COLORS.get(record.levelname, "")
return f"{color}{record.levelname:<8}\033[0m [{record.name}] {record.getMessage()}"
核心就是用 ANSI 转义码包裹日志级别名。\033[32m = 绿色开始,\033[0m = 重置。
动手练习
练习 1: 给一个模块加日志
找一个你正在开发的模块(比如优化 pass),把里面的 print() 替换成 log.debug() / log.info()。
练习 2: 比较不同日志级别
分别用 DEBUG、INFO、WARNING 级别运行同一个编译任务,观察输出量的差异。
练习 3: 添加 log_phase
给编译器的某个阶段加上 log_phase,看看这个阶段实际耗时多少。
常见坑
| 坑 | 说明 |
|---|---|
| 忘记 init_logger | get_logger() 会自动用默认参数初始化,但建议显式调用 init_logger() 以获得预期行为 |
| 日志太多 | 默认级别是 INFO,如果你调成 DEBUG,可能输出大量内部信息 |
| 文件 handler 总是 DEBUG | 文件输出固定为 DEBUG 级别(file_handler.setLevel(logging.DEBUG)),确保文件里有完整记录 |
| 彩色输出在管道中 | 如果输出被管道到文件(python compile.py > out.txt),ANSI 颜色码会变成乱码。用 use_color=False 关闭 |
进阶阅读
- Python logging 官方文档:logging — Logging facility for Python
- ANSI 颜色码参考:ANSI escape code
- 相关课题: 课题5:RISC-V汇编代码美化器 | 课题9:DSL错误提示美化器
12周每周目标
- W1:学习
logging模块基础(Logger、Handler、Formatter、级别)。编写demo。
- W2:分析编译器现有
print语句,规划哪些应转为日志,划分级别。
- W3:在
main.py中初始化logging,添加控制台Handler,设置格式%(asctime)s - %(name)s - %(levelname)s - %(message)s。
- W4:替换前端(解析)中的
print为logger.info/debug,添加--log-level参数。
- W5:替换IR生成和优化阶段的
print,为每个阶段创建子logger(如logger = logging.getLogger('ir'))。
- W6:替换后端代码生成的
print,确保所有输出通过日志。
- W7:安装
colorlog,根据级别设置颜色(ERROR红色,WARNING黄色,INFO绿色,DEBUG灰色)。
- W8:添加
--log-file参数,将日志同时写入文件。
- W9:优化日志信息,避免噪音,关键步骤输出简洁的统计信息。
- W10:测试不同级别和参数组合,确保性能影响小(使用
if logger.isEnabledFor)。
- W11:添加进度指示(如"Parsing... Done in 0.02s")。
- W12:撰写文档:日志级别说明、配置方法、常见使用场景。
- W1:学习
logging模块基础(Logger、Handler、Formatter、级别)。编写demo。 - W2:分析编译器现有
print语句,规划哪些应转为日志,划分级别。 - W3:在
main.py中初始化logging,添加控制台Handler,设置格式%(asctime)s - %(name)s - %(levelname)s - %(message)s。 - W4:替换前端(解析)中的
print为logger.info/debug,添加--log-level参数。 - W5:替换IR生成和优化阶段的
print,为每个阶段创建子logger(如logger = logging.getLogger('ir'))。 - W6:替换后端代码生成的
print,确保所有输出通过日志。 - W7:安装
colorlog,根据级别设置颜色(ERROR红色,WARNING黄色,INFO绿色,DEBUG灰色)。 - W8:添加
--log-file参数,将日志同时写入文件。 - W9:优化日志信息,避免噪音,关键步骤输出简洁的统计信息。
- W10:测试不同级别和参数组合,确保性能影响小(使用
if logger.isEnabledFor)。 - W11:添加进度指示(如"Parsing... Done in 0.02s")。
- W12:撰写文档:日志级别说明、配置方法、常见使用场景。