Python中优雅包装脚本实现错误邮件告警并保留原命令行API
优雅实现脚本错误告警包装器的几种方案
针对你想要的python email_wrap.py script_to_run.py <arguments>调用方式,同时保留原脚本命令行API、避免把所有代码塞进try块的繁琐需求,这里有几个实用又优雅的实现思路:
方案1:独立包装脚本 + runpy模块(首推,零侵入原脚本)
这个方案完全不需要改动原脚本,通过一个独立的包装脚本接管执行流程,捕获异常后发送告警,还能完美传递原脚本的所有命令行参数。我自己在生产环境处理定时任务告警时经常用这种方式,非常省心。
代码实现(email_wrap.py):
import sys import runpy def send_email(exception: Exception) -> None: # 替换成你的实际邮件发送逻辑,比如调用SMTP服务 print(f"⚠️ 发送告警邮件:脚本 {sys.argv[1]} 执行失败,异常信息:\n{str(exception)}") def main() -> None: if len(sys.argv) < 2: print("用法:python email_wrap.py <目标脚本路径> [脚本参数...]") sys.exit(1) # 拆分目标脚本路径和它的参数 target_script = sys.argv[1] target_args = sys.argv[2:] # 临时替换sys.argv,让原脚本的参数解析器(比如argparse)能正常工作 original_argv = sys.argv.copy() sys.argv = [target_script] + target_args try: # 执行目标脚本,效果等同于直接运行 `python script_to_run.py arg1 arg2` runpy.run_path(target_script, run_name="__main__") except Exception as e: # 捕获所有未处理的异常,发送告警 send_email(e) # 重新抛出异常,保留原脚本的错误退出行为(比如返回非0退出码) raise finally: # 恢复原sys.argv,避免影响后续可能的操作 sys.argv = original_argv if __name__ == "__main__": main()
为什么这个方案好用?
- 零侵入:原脚本完全不需要修改,不管是自己写的还是第三方脚本都能直接用
- 参数无缝传递:原脚本的命令行API完全保留,参数解析逻辑不用做任何调整
- 逻辑解耦:错误告警逻辑集中在包装脚本,和业务代码完全分离
方案2:装饰器模式(适合可修改原脚本的场景)
如果你有权限修改原脚本,用装饰器包装核心入口函数(比如main())是更简洁的方式,把错误告警逻辑抽象成可复用的组件,其他脚本也能快速复用。
代码示例:
首先定义一个通用的告警装饰器(可以放在单独的工具模块里):
# decorators.py def email_on_failure(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: send_email(e) raise # 保留原异常的传播,不吞掉错误 return wrapper
然后在原脚本中引入并使用:
# script_to_run.py import argparse from decorators import email_on_failure @email_on_failure def main(): parser = argparse.ArgumentParser(description="原脚本的功能描述") parser.add_argument("--input", required=True, help="输入文件路径") parser.add_argument("--verbose", action="store_true", help="打印详细日志") args = parser.parse_args() # 原脚本的核心业务代码... if args.verbose: print("开始执行核心逻辑...") # 模拟异常 # raise ValueError("测试异常:数据格式错误") if __name__ == "__main__": main()
优势:
- 复用性强:一个装饰器可以给N个脚本加告警逻辑
- 代码整洁:原脚本的核心代码不用嵌套在大try块里,结构更清晰
- 灵活可控:可以选择只给特定函数加告警,不是整个脚本
方案3:上下文管理器(适合代码块级别的精准捕获)
如果只需要对脚本中的某一段核心业务代码进行告警包装,上下文管理器是更灵活的选择,能精准控制捕获异常的范围。
代码示例:
# context_utils.py class EmailOnFailure: def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): # 如果有异常发生,发送告警 if exc_type is not None: send_email(exc_val) # 返回False让异常继续传播,返回True则吞掉异常(根据需求选择) return False
在原脚本中使用:
# script_to_run.py import argparse from context_utils import EmailOnFailure def main(): parser = argparse.ArgumentParser() args = parser.parse_args() # 只有with块内的代码触发异常时才会发告警 with EmailOnFailure(): print("执行核心业务逻辑...") # 模拟异常 # raise RuntimeError("测试:数据库连接失败") # 这段代码的异常不会触发告警 print("执行后续清理逻辑...") if __name__ == "__main__": main()
关于命令行参数处理的说明
在方案1的包装脚本中,我们通过临时替换sys.argv来确保原脚本的参数解析器(比如argparse、click)能正常工作。举个例子:
当你执行:
python email_wrap.py script_to_run.py --input data.txt --verbose
包装脚本会把sys.argv临时改成["script_to_run.py", "--input", "data.txt", "--verbose"],原脚本的参数解析逻辑会和直接运行python script_to_run.py --input data.txt --verbose时完全一致,完美保留原脚本的命令行API。
内容的提问来源于stack exchange,提问作者Yuval Atzmon
相关产品推荐
相关产品推荐

