在众多项目中,一个常见痛点就是系统发生异常时开发人员未能及时发现,直到用户反馈才后知后觉。因此,自动捕获异常并通过邮件报警已成为运维的标准配置。实现方案本身并不复杂,但其中细节往往容易踩坑。接下来,我们直接梳理一套工业级的最佳实践。

装饰器里怎么加异常捕获逻辑
核心做法是使用 try...except 包裹被装饰函数的执行,从而捕获所有未处理的异常。但切勿直接使用 except Exception ——这样会误吞 SystemExit、KeyboardInterrupt 等本应正常退出的信号,需要将它们排除在外。具体建议:
- 使用
except BaseException as e捕获所有异常,然后通过if isinstance(e, (SystemExit, KeyboardInterrupt)):判断后raise重新抛出。 - 记录异常时必须包含
traceback.format_exc(),否则邮件中只有错误类型和消息,缺少堆栈信息无法定位问题。 - 装饰器参数建议支持传入白名单异常类型,例如
ignore_exceptions=(requests.Timeout,),避免网络超时这类预期异常也触发告警。
这样设计的好处是:意外中断不会被误吞,常规异常能获取完整调用链,并且可以按场景过滤掉不必要的噪声。
发邮件前要检查哪些配置项
很多失败并非代码本身问题,而是环境配置不当。关键配置必须显式校验,不能等到报错才暴露:
SMTP_SERVER、SMTP_PORT、EMAIL_USER、EMAIL_PASSWORD这四个项缺一不可,应在启动时使用assert进行检查,而不是等到发送邮件时遇到smtplib.SMTPAuthenticationError。- 密码不要硬编码在代码中,应通过环境变量读取:
os.getenv("EMAIL_PASSWORD"),否则一旦打包进镜像或上传到 Git 就会导致泄露。 - 测试连接可使用
smtplib.SMTP(server, port).connect()+login(),但不要在装饰器每次调用时都建立新连接——应提前创建连接池或复用已认证的 SMTP 实例。
简单来说:配置检查前置、密码脱敏、连接复用,这些细节决定了报警系统的稳定性。
报警邮件内容怎么写才有效
运维人员看到邮件的第一反应是“这问题归谁负责”,因此邮件内容必须携带上下文,而不能仅仅丢出一个 traceback:
- 主题采用固定格式:
f"[ALERT] {func.__name__} failed in {os.environ.get('ENV', 'unknown')} env",方便邮件客户端按主题规则进行过滤。 - 正文应至少包含:发生时间(
datetime.now().isoformat())、所在服务器(socket.gethostname())、触发函数名、完整的 traceback、调用时的args和kwargs(注意过滤 password、token 等敏感字段)。 - 避免用纯文本发送长 traceback——应使用
MIMEText(..., 'plain', 'utf-8')并设置Content-Transfer-Encoding: base64,防止某些邮件客户端截断或出现乱码。
这些信息足以让运维现场还原问题,从而减少来回沟通的成本。
为什么不能在装饰器里直接发邮件
同步发送邮件会阻塞主业务,尤其在 SMTP 延迟较高或网络抖动时,可能将一个原本 100ms 的接口拖慢到几秒钟。在生产环境中必须采用异步方式:
- 使用
threading.Thread(target=send_mail, args=(...)).start()是最简单的办法,但要注意线程安全问题(例如共享的 SMTP 连接需要加锁)。 - 更稳定的做法是将邮件投递到队列(例如 Redis +
rq或celery),但这会引入额外的依赖;对于小型项目,使用线程已经足够。 - 务必添加超时控制:
smtp.send_message(msg, timeout=5),否则网络卡死会导致线程永久挂起。
最容易忽略的一点是异常的二次抛出——装饰器捕获异常后,如果不重新 raise,上游完全感知不到失败,业务逻辑可能继续执行,从而产生脏数据。因此,该重新抛出的异常必须重新抛出,邮件只是辅助通知的副产物。
