日志模块 logging 实战

print 是新手最爱的调试工具,但一到生产环境就成了灾难——没有时间戳、没法分级、没法按模块过滤、没法输出到文件……日志(logging)是任何”能长期运行的程序”的必备基础设施。Python 内置的 logging 模块功能极强,但学习曲线也陡(对新手不友好)。这一篇讲清楚:正确的日志姿势、常见的三种配置方式、生产环境的最佳实践。

一、为什么不能用 print

看这段”用 print 调试”的代码:

1
2
3
4
def do_task(user_id):
print(f"开始处理用户 {user_id}")
...
print(f"处理完成")

问题:

  • 没有时间戳;
  • 没有日志级别(哪些是错误,哪些是提示?);
  • 关不掉(生产环境不想看这些还得改代码);
  • 没法输出到文件;
  • 多进程一起打时序全乱;
  • 无法按模块过滤。

用 logging

1
2
3
4
5
6
7
import logging
logger = logging.getLogger(__name__)

def do_task(user_id):
logger.info(f"开始处理用户 {user_id}")
...
logger.info("处理完成")

同样一行,但获得所有优点。

二、五个日志级别

1
2
3
4
5
logger.debug("详细的调试信息")
logger.info("正常的运行信息")
logger.warning("警告,程序还能跑")
logger.error("错误,某个功能挂了")
logger.critical("严重错误,程序快挂了")

对应数字:DEBUG(10) < INFO(20) < WARNING(30) < ERROR(40) < CRITICAL(50)。

默认级别是 WARNING——所以直接 logger.info(...) 什么都不打印。要配置。

三、最简配置:basicConfig

新手一行代码搞定:

1
2
3
4
5
import logging

logging.basicConfig(level=logging.INFO)
logging.info("hello")
# INFO:root:hello

带时间戳:

1
2
3
4
5
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

basicConfig 只对 root logger 生效,且只能调一次。所以只用来在脚本入口做初始化,别在库里用。

四、getLogger:正确使用日志的姿势

每个模块用自己的 logger(用 __name__),而不是直接用 logging.info

1
2
3
4
5
6
# mymodule.py
import logging
logger = logging.getLogger(__name__) # __name__ = 'mymodule'

def foo():
logger.info("foo called")

为什么

  • 日志里能看到”是哪个模块打的”;
  • 可以按模块配置级别(例如 myapp.db 打 DEBUG,其他 INFO);
  • 库不应该硬性配置日志——用户来配。

五、格式化占位符

1
format = "%(asctime)s %(levelname)-8s [%(name)s] %(message)s"

常用占位符:

占位符 含义
%(asctime)s 时间戳
%(levelname)s 级别名
%(name)s logger 名(模块名)
%(message)s 日志内容
%(filename)s 源文件
%(lineno)d 行号
%(funcName)s 函数名
%(process)d 进程 ID
%(thread)d 线程 ID

六、Handler:日志送到哪里

Logger 只是”入口”,真正把日志写出去的是 Handler

  • StreamHandler:终端;
  • FileHandler:文件;
  • RotatingFileHandler:按大小滚动;
  • TimedRotatingFileHandler:按时间滚动;
  • SysLogHandler:syslog;
  • SMTPHandler:邮件(错误告警);
  • HTTPHandler:HTTP 接口。

一个 logger 可以有多个 handler——同时输出到终端和文件很常见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import logging

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

# 终端:INFO 及以上
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(console)

# 文件:DEBUG 及以上,滚动
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler("app.log", maxBytes=10*1024*1024, backupCount=5, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s:%(lineno)d] %(message)s"
))
logger.addHandler(file_handler)


logger.info("hello") # 终端 + 文件都有
logger.debug("debug") # 只有文件

七、异常日志

有异常时用 logger.exception——自动附加完整 traceback:

1
2
3
4
try:
do_stuff()
except Exception:
logger.exception("do_stuff 出错")

日志里会有类似:

1
2
3
4
5
ERROR do_stuff 出错
Traceback (most recent call last):
File "app.py", line 10, in do_stuff
...
ValueError: ...

logger.error(str(e)) 有用得多。

八、传参 vs 拼字符串

推荐%s + 参数而不是 f-string:

1
2
3
4
5
# ✅ 推荐
logger.info("user %s login from %s", user_id, ip)

# ❌ 不推荐(每次都格式化,即使日志级别没开)
logger.info(f"user {user_id} login from {ip}")

原因:如果 debug 日志没开启,logger.debug(msg, arg) 的格式化根本不会执行;f-string 无论如何都会先格式化。虽然差别不大,但大量 debug 日志时能省 20-30% CPU

九、字典配置

复杂配置用字典(dictConfig)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import logging.config

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s %(levelname)-8s [%(name)s] %(message)s",
},
"detailed": {
"format": "%(asctime)s %(levelname)-8s [%(name)s:%(lineno)d] %(funcName)s(): %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "default",
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "app.log",
"maxBytes": 10 * 1024 * 1024,
"backupCount": 5,
"encoding": "utf-8",
"level": "DEBUG",
"formatter": "detailed",
},
},
"loggers": {
"myapp": {
"level": "DEBUG",
"handlers": ["console", "file"],
"propagate": False,
},
"myapp.db": {
"level": "INFO",
},
},
"root": {
"level": "WARNING",
"handlers": ["console"],
},
}

logging.config.dictConfig(LOGGING)

字典配置的好处:

  • 全部信息一处写清;
  • 可从 JSON/YAML 加载;
  • 复杂多 logger 场景不容易乱。

十、logger 的层级

logger 名字用 . 分层:

  • myapp
  • myapp.db
  • myapp.db.query

配置 myapp 会自动作用于所有以 myapp. 开头的 logger(除非子 logger 显式覆盖)。

日志会沿着层级向上传播(propagate=True 默认),最终到 root。避免同一条日志被父子 logger 打印两次:给子 logger 设 propagate=False

十一、生产环境最佳实践

  1. 入口配置一次,模块用 getLogger(__name__)
  2. 格式化用 %s,不要 f-string
  3. DEBUG 到文件,INFO 到终端
  4. 文件按大小或时间滚动(RotatingFileHandler);
  5. 异常用 logger.exception
  6. 敏感信息不打日志(密码、令牌、身份证);
  7. 性能敏感的路径别打大量日志
  8. 多进程用 QueueHandler(避免文件写入冲突);
  9. 接错误告警(SMTPHandler、Sentry、云平台);
  10. 加请求 id / trace id(用 LoggerAdapter 或第三方 structlog)。

十二、第三方推荐

  • structlog:结构化日志(每条日志是 JSON),日志聚合平台首选;
  • loguru:极简 API,一行搞定日志;比原生 logging 好用得多;
  • rich:让终端日志变彩色、更好看;
  • Sentry / Datadog:错误监控。

loguru 的极简 API:

1
2
3
4
5
6
from loguru import logger

logger.info("hello")
logger.debug("debug 信息")
logger.add("app.log", rotation="10 MB") # 一行加文件 handler
logger.exception("出错了")

新项目非要用原生 logging,除非有历史包袱或严格要求。

十三、加自定义字段:LoggerAdapter

想在每条日志里带上 request_id、user_id 等业务字段:

1
2
3
4
5
6
7
8
9
class RequestAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
return f"[req={self.extra['request_id']}] {msg}", kwargs


logger = logging.getLogger("myapp")
req_logger = RequestAdapter(logger, {"request_id": "abc-123"})
req_logger.info("处理请求")
# [req=abc-123] 处理请求

十四、一个完整例子:Web 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# main.py
"""一个生产级的日志初始化脚本。"""

import logging
import logging.config
import sys
from pathlib import Path


def setup_logging(log_dir: Path, verbose: bool = False):
log_dir.mkdir(parents=True, exist_ok=True)

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
},
"detailed": {
"format": "%(asctime)s %(levelname)-8s [%(name)s:%(lineno)d] "
"%(funcName)s(): %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG" if verbose else "INFO",
"formatter": "simple",
"stream": sys.stdout,
},
"app_file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": str(log_dir / "app.log"),
"when": "midnight",
"backupCount": 30,
"encoding": "utf-8",
"level": "DEBUG",
"formatter": "detailed",
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"filename": str(log_dir / "error.log"),
"maxBytes": 20 * 1024 * 1024,
"backupCount": 10,
"encoding": "utf-8",
"level": "ERROR",
"formatter": "detailed",
},
},
"loggers": {
"myapp": {
"level": "DEBUG",
"handlers": ["console", "app_file", "error_file"],
"propagate": False,
},
"urllib3": {
"level": "WARNING", # 屏蔽 requests 的 DEBUG 噪音
},
},
"root": {
"level": "WARNING",
"handlers": ["console"],
},
}

logging.config.dictConfig(LOGGING)


if __name__ == "__main__":
setup_logging(Path("logs"), verbose=True)

logger = logging.getLogger("myapp")
logger.debug("debug 消息")
logger.info("服务启动")
try:
1 / 0
except Exception:
logger.exception("除零错误")

十五、常见陷阱

  1. logging.info() 而不是 logger.info():前者用 root logger,配置不清晰。
  2. 用 f-string 传消息:级别不开也执行格式化。用 %s + 参数。
  3. basicConfig 调多次:只有第一次生效。
  4. 多进程写同一文件:会乱序或丢日志。用 QueueHandler 或按进程分文件。
  5. DEBUG 日志上生产:文件涨飞,可能包含敏感信息。
  6. disable_existing_loggers: True(默认!):会禁用已定义的 logger。dictConfig 里推荐设 False
  7. propagate=True + 父子都有 handler:日志重复打。

十六、小结与延伸阅读

  • 每个模块用 logger = logging.getLogger(__name__)
  • 五级:DEBUG < INFO < WARNING < ERROR < CRITICAL;
  • 一个 logger 可以有多个 handler;
  • 生产环境:console + rotating file + error file;
  • 异常用 logger.exception
  • 传参用 %s + 参数
  • 复杂配置用 dictConfig
  • 新项目推荐 loguru,更简洁。

延伸阅读:

到这里模块六(中级进阶)10 篇结束! 下一篇进入 模块七:实用工具与并发,从 HTTP 请求:requests 库入门 开始。