把每日巡检脚本从「一次性命令」改成可复用小工具:argparse + logging 实战笔记

举报
yd_238692410 发表于 2026/11/03 23:53:46 2026/11/03
【摘要】 把每天手工敲的巡检命令重构成一个小工具:先用命令行契约定接口,再用 argparse 收参数、logging 分级记日志、退出码约定让上层脚本可判断,最后把结果拆成人看的表格和机器读的 JSON 两路输出。全程只用 Python 标准库。

为什么要改

每天手工敲一遍 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+ 的机器上就能跑,不需要配环境。

如果下一步继续演进,我会做两件事:把参数定义抽到单独的模块,支持从配置文件读默认值;把命中结果写进结构化存储,方便做趋势对比。但在那之前,先把"跑得稳、错得起"做扎实,比堆功能有用得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。