在Python项目开发中,日志记录是必不可少的基础设施。虽然Python标准库的logging模块功能强大,但原生并不支持Java风格的.properties文件配置方式。本文将详细介绍如何通过自定义解析器实现.properties文件配置日志系统,并提供两个完整可运行的示例。
注意:本文所有代码示例基于Python 3.12版本测试通过,但核心逻辑适用于Python 3.7及以上版本。
.properties文件与Python dictConfig所需的字典结构之间存在三个主要差异需要处理:
formatters.simple.formatroot.handlers=console,file以下是完整的解析器实现:
python复制def load_properties(filepath, encoding='utf-8'):
"""解析properties文件为平面字典"""
props = {}
with open(filepath, 'r', encoding=encoding) as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
props[key.strip()] = value.strip()
return props
def props_to_dict_config(props):
"""将平面properties字典转换为logging模块所需的嵌套字典结构"""
config = {}
for key, value in props.items():
parts = key.split('.')
current = config
# 处理嵌套键
for part in parts[:-1]:
current = current.setdefault(part, {})
# 类型转换逻辑
final_value = value
if isinstance(value, str):
# 布尔值转换
if value.lower() in ('true', 'false'):
final_value = value.lower() == 'true'
# 整数转换
elif value.isdigit():
final_value = int(value)
# 浮点数转换
elif value.replace('.', '', 1).isdigit():
final_value = float(value)
# 列表处理(特殊字段handlers和filters)
if parts[-1] in ('handlers', 'filters') and isinstance(final_value, str):
final_value = [v.strip() for v in final_value.split(',') if v.strip()]
current[parts[-1]] = final_value
return config
解析器对以下数据类型进行了自动转换:
true/false(不区分大小写)并转换为Python布尔值handlers和filters的逗号分隔值会被拆分为列表下面是一个基础配置示例logging1.properties,仅配置控制台输出:
properties复制# 基础日志配置
version=1
disable_existing_loggers=false
# 格式化器配置
formatters.simple.format=[%(asctime)s] %(levelname)s - %(message)s
formatters.simple.datefmt=%H:%M:%S
# 处理器配置
handlers.console.class=logging.StreamHandler
handlers.console.level=DEBUG
handlers.console.formatter=simple
handlers.console.stream=ext://sys.stdout
# 根日志器配置
root.level=INFO
root.handlers=console
关键配置项说明:
version: 必须设为1,这是dictConfig的版本号要求disable_existing_loggers: 建议设为false以避免意外禁用已有日志器formatters: 定义日志格式,支持format和datefmt两个子项handlers: 定义处理器,本例使用StreamHandler输出到控制台root: 根日志器配置,设置全局日志级别和处理器python复制import logging.config
def setup_logging(prop_file):
props = load_properties(prop_file)
dict_config = props_to_dict_config(props)
logging.config.dictConfig(dict_config)
if __name__ == '__main__':
setup_logging('logging1.properties')
logger = logging.getLogger()
logger.debug("这条DEBUG日志不会显示") # 因为root级别是INFO
logger.info("这是一条INFO日志")
logger.warning("这是一条WARNING日志")
运行上述代码后,控制台将显示如下输出:
code复制[14:30:22] INFO - 这是一条INFO日志
[14:30:22] WARNING - 这是一条WARNING日志
下面展示一个更复杂的配置示例logging2.properties,包含:
properties复制version=1
disable_existing_loggers=false
# 格式化器
formatters.detailed.format=%(asctime)s [%(levelname)s] %(name)s - %(message)s
formatters.detailed.datefmt=%Y-%m-%d %H:%M:%S
# 过滤器
filters.sensitive.()=__main__.SensitiveFilter
# 处理器
handlers.console.class=logging.StreamHandler
handlers.console.level=INFO
handlers.console.formatter=detailed
handlers.console.stream=ext://sys.stdout
handlers.file.class=logging.handlers.RotatingFileHandler
handlers.file.level=DEBUG
handlers.file.formatter=detailed
handlers.file.filename=app.log
handlers.file.maxBytes=10485760 # 10MB
handlers.file.backupCount=3
handlers.file.encoding=utf8
handlers.file.filters=sensitive
# 日志器层级
loggers.app.level=DEBUG
loggers.app.handlers=console
loggers.app.propagate=false
loggers.app.module.level=INFO
loggers.app.module.handlers=file
loggers.app.module.propagate=true
loggers.app.module.sub.level=DEBUG
loggers.app.module.sub.handlers=
loggers.app.module.sub.propagate=true
# 根日志器
root.level=WARNING
root.handlers=console
需要在配置加载前定义过滤器类:
python复制class SensitiveFilter(logging.Filter):
"""过滤包含敏感词的日志记录"""
def filter(self, record):
sensitive_words = ['password', 'secret', 'token']
msg = record.getMessage()
return not any(word in msg for word in sensitive_words)
python复制import logging.config
import logging.handlers
class SensitiveFilter(logging.Filter):
"""过滤包含敏感词的日志记录"""
def filter(self, record):
sensitive_words = ['password', 'secret', 'token']
msg = record.getMessage()
return not any(word in msg for word in sensitive_words)
def setup_logging(prop_file):
props = load_properties(prop_file)
dict_config = props_to_dict_config(props)
logging.config.dictConfig(dict_config)
if __name__ == '__main__':
setup_logging('logging2.properties')
# 获取不同层级的日志器
app_logger = logging.getLogger('app')
module_logger = logging.getLogger('app.module')
sub_logger = logging.getLogger('app.module.sub')
# 测试日志输出
app_logger.debug("App debug message")
app_logger.info("App info message")
module_logger.debug("Module debug message (filtered by level)")
module_logger.info("Module info message")
module_logger.warning("Module warning with password") # 会被过滤器过滤
sub_logger.debug("Submodule debug message")
sub_logger.info("Submodule info message")
sub_logger.error("Submodule error with secret") # 会被过滤器过滤
在这个配置中,我们设置了三个层级的日志器:
app: 直接使用console处理器,不传播app.module: 使用file处理器,允许传播app.module.sub: 无专属处理器,完全依赖传播传播行为说明:
app日志只会出现在控制台(配置了console处理器)app.module日志会出现在文件(自己的file处理器)和控制台(传播给app)app.module.sub日志会出现在文件(传播给app.module)和控制台(传播给app)配置未生效:
日志重复输出:
权限问题:
同样的原理可以应用于其他配置格式。例如,使用PyYAML库解析YAML:
python复制import yaml
def load_yaml_config(filepath):
with open(filepath, 'r') as f:
return yaml.safe_load(f)
实现配置热更新需要额外工作:
python复制import time
import threading
def watch_config(filepath, interval=30):
"""监视配置文件变化并自动重载"""
last_mtime = 0
while True:
try:
mtime = os.path.getmtime(filepath)
if mtime > last_mtime:
reload_config(filepath)
last_mtime = mtime
except Exception as e:
logging.error(f"Config watch error: {e}")
time.sleep(interval)
def reload_config(filepath):
"""安全重载配置"""
try:
props = load_properties(filepath)
dict_config = props_to_dict_config(props)
logging.config.dictConfig(dict_config)
logging.info("Configuration reloaded successfully")
except Exception as e:
logging.error(f"Failed to reload config: {e}")
要使用自定义处理器,只需在配置中指定完整导入路径:
properties复制handlers.custom.class=my_package.my_module.MyCustomHandler
handlers.custom.some_param=value
在实际项目中使用.properties文件配置Python日志系统时,我总结了以下几点经验:
这种配置方式特别适合需要将配置与代码分离的中大型项目,也便于运维人员在不接触代码的情况下调整日志行为。