把每日巡检脚本从「一次性命令」改成可复用小工具:argparse + logging 实战笔记
为什么要改
每天手工敲一遍 ssh ... && grep ... 这种巡检命令,短期没什么问题,三个月后就会冒出三种典型症状:命令记不住、参数顺序搞错、出了问题没日志可查。这篇记录一下我把一套每日巡检脚本从"临时命令拼凑"重构成一个小工具的过程,重点是两个标准库——argparse 和 logging,以及几处踩过的坑。
第一步:先把命令行契约定下来
重构最容易犯的错是先写实现。我的做法是先在 README 里写清这个工具怎么被调用:
inspect.py --target /var/log/app --days 7 --pattern ERROR --json
四个参数分别是巡检目录、回看天数、关注的关键字、是否输出机器可读的 JSON。定义清楚之后再写代码,argparse 的声明几乎是照抄这份说明。
第二步:argparse 的骨架
import argparse
import sys
def build_parser():
p = argparse.ArgumentParser(
prog="inspect",
description="对指定目录下近 N 天的日志做关键字巡检",
)
p.add_argument("--target", required=True, help="待巡检的目录")
p.add_argument("--days", type=int, default=7, help="回看天数,默认 7")
p.add_argument("--pattern", default="ERROR", help="匹配关键字,默认 ERROR")
p.add_argument("--json", action="store_true", help="以 JSON 输出结果")
return p
def main(argv=None):
args = build_parser().parse_args(argv)
if args.days <= 0:
build_parser().error("--days 必须是正整数")
return 0
if __name__ == "__main__":
sys.exit(main())
三点经验:
main(argv=None)这个签名值得保留。测试或者别的脚本调用时可以传自己的参数列表,不必去改sys.argv。required=True只加在真正没有合理默认值的参数上。--days有默认值就别逼调用方每次都写。- 参数校验失败用
parser.error(),它会打印用法并返回退出码 2,比自己print完sys.exit(1)更符合命令行惯例。
第三步:用 logging 取代 print
import logging
def setup_logging(verbose=False):
logging.basicConfig(
level=logging.DEBUG if verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
return logging.getLogger("inspect")
为什么不用 print:
- 级别可以调。平时 INFO,排查问题时加个
--verbose就变 DEBUG,不用改代码。 - 输出目标可以换。想落文件就加一个
FileHandler,print做不到。 - 库代码里用
logging.getLogger(__name__),被别人 import 时不会擅自改掉全局配置。
我踩过的坑是在模块顶层调用 basicConfig()。这样只要有人 import 这个模块,全局日志配置就被顺手改掉了。正确做法是在 main() 里配置一次。
第四步:退出码和错误处理
一个能被 cron 或者 CI 调用的工具,退出码比输出内容更重要。我最后定的约定是:
0 正常运行,没有命中任何告警
1 正常运行,有命中(需要人看)
2 参数错误
3 运行期错误(目录不存在、权限不足等)
这样上层脚本只判断 $? 就能决定要不要发通知,不需要去解析 stdout。运行期错误统一用 try/except 包住并调用 log.exception(),堆栈进日志,屏幕上只留一行人能读懂的说明。
第五步:结果分成人看和机器看两路
人看的时候希望是整齐的表格,机器消费时希望是 JSON,两条路走,别混在一起:
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
for row in result["hits"]:
print(f'{row["time"]} {row["file"]}:{row["line"]} {row["text"]}')
顺手一条:ensure_ascii=False。不加的话中文日志在 JSON 里全是 \uXXXX,肉眼没法看。
复盘
整个重构加上注释不到 150 行,收益却很直接:
- 命令从"记不住的一长串"变成一个带
--help的子命令; - 出问题时有日志级别和堆栈可查,不用再猜是哪个环节挂的;
- 退出码让它能无脑塞进 crontab,有人值守的脚本变成无人值守;
- 纯标准库,扔到任何一台有 Python 3.8+ 的机器上就能跑,不需要配环境。
如果下一步继续演进,我会做两件事:把参数定义抽到单独的模块,支持从配置文件读默认值;把命中结果写进结构化存储,方便做趋势对比。但在那之前,先把"跑得稳、错得起"做扎实,比堆功能有用得多。
- 点赞
- 收藏
- 关注作者
评论(0)