开发技巧

用 Sentry Crons 监控每晚 02:30(北京时间)运行的订单日报脚本

用 Sentry Crons 监控每晚 02:30(北京时间)运行的订单日报脚本:任务开始和结束各发一次 check-in,没按时启动、运行超过 20 分钟、执行报错,Sentry 都会创建问题并告警。监控配置写在代码里,首次运行自动创建。

BINBHO · 图文教程

用 Sentry Crons 监控每晚 02:30(北京时间)运行的订单日报脚本:任务开始和结束各发一次 check-in,没按时启动、运行超过 20 分钟、执行报错,Sentry 都会创建问题并告警。监控配置写在代码里,首次运行自动创建。

先看清成品

本机用 sentry-sdk 2.72.0 跑了 2 个测试,全部通过:正常运行时截获 in_progress → ok 两个 check-in(同一个 check_in_id,带时区和时长),出错时截获 in_progress → error;脚本实际生成了日报文件。没有发送到真实 Sentry,也没有配置真实的 cron 定时任务。

注意:Sentry 有免费和付费计划,额度与价格以官方价格页为准。错误数据里可能包含用户个人信息,上线前确认采集范围符合你所在地区的个人信息保护法规(如《个人信息保护法》),并在隐私政策中说明。

做完之后你会得到什么?

成果 说明
日报脚本 汇总前一天已支付订单
Cron 监控 nightly-order-report,时区 Asia/Shanghai
三类告警 错过、超时、失败
离线测试 验证 check-in 顺序和状态

需要准备哪些环境?

项目 要求 说明
Sentry 项目 任意 Python 项目 用 DSN
sentry-sdk 1.45.0 以上才能用 monitor_config;本文 2.72.0
定时器 crontab 或 systemd timer

文件结构

25-sentry-C-src/
├── nightly_report.py
├── orders_sample.csv
└── test_nightly.py

第 1 步:脚本与监控配置

nightly_report.py:

"""每天 02:30(北京时间)汇总前一天订单,生成日报文件。用 Sentry Crons 监控:没按时跑、跑太久、报错都会告警。"""
import csv, os, sys
from datetime import date, timedelta
from pathlib import Path
import sentry_sdk
from sentry_sdk.crons import monitor

MONITOR_CONFIG = {
    "schedule": {"type": "crontab", "value": "30 2 * * *"},
    "timezone": "Asia/Shanghai",
    "checkin_margin": 15,         # 02:45 前没开始算“错过”
    "max_runtime": 20,            # 超过 20 分钟算失败
    "failure_issue_threshold": 1, # 失败 1 次就建问题
    "recovery_threshold": 1,
}

def summarize(rows):
    paid = [r for r in rows if r["status"] == "paid"]
    if not rows:
        raise RuntimeError("订单导出为空,可能上游导出失败")
    return {"orders": len(paid), "revenue": round(sum(float(r["amount"]) for r in paid), 2)}

@monitor(monitor_slug="nightly-order-report", monitor_config=MONITOR_CONFIG)
def run(src: Path, outdir: Path, day: date):
    with open(src, encoding="utf-8-sig", newline="") as f:
        stats = summarize(list(csv.DictReader(f)))
    outdir.mkdir(exist_ok=True)
    out = outdir / f"report_{day.isoformat()}.txt"
    out.write_text(f"{day} 已支付订单 {stats['orders']} 笔,金额 {stats['revenue']:.2f} 元\n", encoding="utf-8")
    return out

if __name__ == "__main__":
    sentry_sdk.init(dsn=os.environ.get("SENTRY_DSN"), environment=os.environ.get("APP_ENV", "production"))
    day = date.today() - timedelta(days=1)
    print("写出", run(Path(sys.argv[1]), Path("reports"), day))

orders_sample.csv:

order_id,status,amount
1001,paid,59.90
1002,refunded,89.00
1003,paid,15.00

要点:

  • schedule 与 crontab 写法相同;timezone 设为 Asia/Shanghai,Sentry 按北京时间判断是否错过。
  • 导出为空时主动抛错,让“数据不对”也算失败,而不是生成一份 0 元日报。

第 2 步:测试

test_nightly.py:

from datetime import date
from pathlib import Path
import pytest, sentry_sdk
from sentry_sdk.transport import Transport
import nightly_report as m

class Capture(Transport):
    def __init__(self, options=None): super().__init__(options); self.items = []
    def capture_envelope(self, env):
        for it in env.items: self.items.append((it.type, it.payload.json))

@pytest.fixture
def cap():
    t = Capture(); sentry_sdk.init(dsn="https://public@o0.ingest.sentry.io/0", transport=t); yield t

def checkins(t): return [p for typ, p in t.items if typ == "check_in"]

def test_ok(cap, tmp_path):
    out = m.run(Path("orders_sample.csv"), tmp_path, date(2026, 10, 20)); sentry_sdk.flush()
    assert out.read_text(encoding="utf-8") == "2026-10-20 已支付订单 2 笔,金额 74.90 元\n"
    c = checkins(cap); assert [x["status"] for x in c] == ["in_progress", "ok"]
    assert c[0]["monitor_slug"] == "nightly-order-report" and c[0]["monitor_config"]["timezone"] == "Asia/Shanghai"
    assert c[1]["check_in_id"] == c[0]["check_in_id"] and c[1]["duration"] >= 0

def test_error(cap, tmp_path):
    empty = tmp_path / "e.csv"; empty.write_text("order_id,status,amount\n")
    with pytest.raises(RuntimeError): m.run(empty, tmp_path, date(2026, 10, 20))
    sentry_sdk.flush()
    assert [x["status"] for x in checkins(cap)] == ["in_progress", "error"]
pip install sentry-sdk pytest
python -m pytest -q
python nightly_report.py orders_sample.csv

检查结果(本机实际输出):

2 passed
写出 reports/report_2026-10-20.txt

文件内容:2026-10-20 已支付订单 2 笔,金额 74.90 元。

第 3 步:配置定时任务(本文未执行)

服务器时区若为北京时间,crontab:

CRON_TZ=Asia/Shanghai
30 2 * * * cd /srv/report && SENTRY_DSN=https://xxx@oNNN.ingest.sentry.io/NNN /usr/bin/python3 nightly_report.py /srv/export/orders.csv >> report.log 2>&1

CRON_TZ 只有部分 cron 实现支持;不支持时按服务器时区换算。DSN 也可以写进只有运行用户可读的环境文件。

第 4 步:告警(本文未执行)

首次运行后,Sentry Crons 页面出现 nightly-order-report。在 Alerts → Create Alert → Issues 中添加筛选条件 monitor.slug equals nightly-order-report,通知到值班邮箱。

预期结果:

情况 Sentry 显示
02:45 前没有开始 Missed
运行超过 20 分钟 Failed(超时)
抛出异常 Failed(error)
正常完成 OK

调试示例

测试 test_error 中传入只有表头的 CSV,summarize 抛出 RuntimeError: 订单导出为空,可能上游导出失败,monitor 自动把第二个 check-in 设为 error。截获的状态序列为:

['in_progress', 'error']

这说明错误不需要手动上报 check-in,装饰器会处理。

出错了怎么办?

现象 原因 解决
Crons 页面没有监控 没有 check-in 到达,或 SDK 太旧 确认 DSN,升级到 1.45.0 以上
每天都显示 Missed 时区不一致 timezone 与 crontab 的实际时区保持一致
check-in 被丢弃 超过每个监控环境每分钟 6 次的限制 不要在循环中频繁调用
只有 in_progress 没有结束 进程被强制杀死 设置 max_runtime,超时会判为失败
interval 配置报错 value 写成了字符串 interval 的 value 必须是整数

模板规则

  • 每个定时任务一个 monitor_slug,配置写在代码里。
  • 数据异常主动抛错。
  • 时区写明,和定时器保持一致。

项目资源

用途 地址
Python Crons 设置 https://docs.sentry.io/platforms/python/crons/
Crons 产品说明 https://docs.sentry.io/product/crons/
sentry-python https://github.com/getsentry/sentry-python
Sentry 价格 https://sentry.io/pricing/
自建 Sentry https://develop.sentry.dev/self-hosted/

定时任务最常见的故障是“悄悄没跑”。有了 check-in,没跑、跑太久、跑出错都会有人知道,而不是等到第二天有人问日报去哪了。