网站和API的故障比我们想象的更频繁。如果能在您最喜欢或最常用的服务出现宕机时收到WhatsApp的通知,那不是很好吗?
在本教程中,你将学习如何为你喜欢的服务设置自动监测,并在服务状态发生变化时收到WhatsApp通知。我们将使用Notion做数据库,使用Twilio的WhatsApp Business API接收通知,使用GitHub动作按计划运行我们的工作,我们将用Python编写所有代码。让我们开始吧!
关于使用WhatsApp的特别说明
WhatsApp必须正式批准你的账户,然后你才能以生产身份用WhatsApp发送消息,即使是个人项目。不过,这并不意味着你必须等待开始建设Twilio Sandbox for WhatsApp让你在开发环境中测试你的应用程序。你可以使用沙盒来完成这个教程,但你需要让你的WhatsApp账户得到批准,以便服务状态监控能够全天候运行。这是因为WhatsApp沙盒会话在3天后过期,必须重新启用。
请阅读我们的帮助中心文章《如何获得我自己的WhatsApp Twilio号码在生产中使用?以了解更多信息。
在Notion中创建一个数据库
我们要做的第一件事是创建一个Notion数据库。一旦你登录到Notion,并且你在正确的工作区工作(其中你是管理员),创建一个新的页面,并给它命名为 "监控"。点击新页面的文本区,输入*/table full*。一个模式将出现。选择表--完整页:

给这个表的标题和你给页面的标题一样("监控")。
Notion中的每个数据库表都有字段,这些字段被显示为列。
首先,将Name列的名称改为URL。稍后,我们将使用这一列来跟踪我们想要监控的服务。
接下来,删除Tags列,因为我们不会使用它。
点击表格标题行的加号(+)图标,将这些列添加到表格中:
- Identifier(属性类型: Text) - 用于检查响应中是否存在预定义的字符串
- 状态(属性类型:选择) - 用于存储服务状态

在状态列中,点击该列中的第一个空白单元。键入以下每个值,将它们添加为状态字段的选项(或创建你自己的):
- 运行中
- 怀疑
- 警告
- 维护
- 下降
要编辑颜色,点击包含新状态的单元格。会出现一个下拉菜单,显示所有可供选择的状态。当你把鼠标悬停在一个选项上时,右边会出现一个带有三个点的图标。点击该图标,将出现另一个下拉菜单,你可以从中选择你想要的颜色。

在创建列标题之后,添加一些你想监控的服务的URL。在本教程中,我添加了GitHub、我的网站和谷歌。我包括了每个URL的标识符,这是一个字符串,我希望在一个服务正常运行的情况下,它能出现在响应中。要确定哪些字符串可以作为标识符,可以从你的CLI运行一个curl ,比如。curl https://www.google.com.(如果你还不熟悉,请在这里阅读更多关于curl 。)

获得一个Notion API Token
创建一个Notion集成
要使用Notion API,你需要创建一个Notion集成,以便获得Notion API令牌。登录Notion后,进入你的集成页面,点击创建新的集成瓦片或左侧边栏的黑色按钮。

一个表格会出现,你可以配置一些关于集成的基本信息。将其命名为 "服务监控",选择适当的工作区,然后点击提交。

一旦整合完成,你将看到一个秘密的API令牌,将在后面的教程中使用。暂时把它复制并粘贴到某个地方。我们将在后面的教程中使用它。

与新的Notion集成共享监控数据库
默认情况下,Notion集成不能访问工作区的页面或数据库。你需要分享你想连接到Notion集成的特定页面或数据库。
要做到这一点,在你的Notion工作区打开监控数据库。点击右上角的共享链接。将会出现一个模式。使用邀请按钮左边的搜索字段,找到 "服务监测 "集成,然后点击邀请。

获得Notion数据库的ID
如果你的Notion数据库在一个工作区中,数据库页面的URL结构可能看起来像这样:
www.notion.so/{工作区\_名称}/{…? v={view_id}。
隔离URL中36个字符的**{database_id}**部分。暂时把它复制并粘贴到某个地方。我们稍后会把它设置为一个环境变量。
如果你的Notion数据库不在一个工作区里,或者它根本不符合上面显示的URL,它可能看起来像这样:
注意,当试图找到数据库ID时,应该在左边的导航栏中选择监控数据库表 ,因为它的URL与整个监控数据库相关的URL不同(在左边的导航栏中高一级)。

请访问Notion的数据库工作文档页面,了解更多信息。
创建Python虚拟环境
为这个项目创建一个新的目录,叫做service-monitoring,然后导航到这个新目录:
$ mkdir service-monitoring
$ cd service-monitoring
我们将为这个项目创建一个新的虚拟环境,这样我们需要安装的依赖项就不会干扰到你电脑上的全局设置。要创建一个名为 "env "的新环境,运行以下命令:
$ python3 -m venv env
$ source env/bin/activate
在你为虚拟环境提供源码之后,你会看到你的命令提示符的输入行以环境的名称 ("env") 开始。Python 在 service-monitoring 目录下创建了一个名为env/的新文件夹,你可以通过在命令提示符下运行ls 命令来查看。如果你使用 git 作为你的版本控制系统,你应该把这个新的env/目录添加到一个.gitignore文件中,以便 git 知道不要跟踪它。要在service-monitoring/目录下创建.gitignore文件,运行这个命令:
(env) $ touch .gitignore
在你选择的文本编辑器中打开*.gitignore文件:,然后在.gitignore文件的内容中添加env/*文件夹:
env/
安全地存储环境变量
你需要使用你在本教程开始时找到的账户SID和Auth Token,以便与Twilio API互动。这两个环境变量应该是私有的,这意味着我们不应该把它们的值放在代码中。相反,我们可以把它们存储在一个*.env文件中,并在我们的.gitignore文件中列出.env*文件,这样git就不会跟踪它。只要有环境变量需要提供给操作系统,就可以使用.env文件。
注意 Python 为虚拟环境创建的env/文件夹与为存储秘密而创建的.env文件不是一回事。
首先,创建*.env*文件:
(env) $ touch .env
然后,将 .env文件作为一个行项添加到*.gitignore*文件中:
env/
.env # Add this
接下来,在你喜欢的文本编辑器中打开*.env*文件,添加以下几行,用你自己的值替换随机字符串占位符的值:
export ACCOUNT_SID=AzLdMHvYEn0iKSJz
export AUTH_TOKEN=thFGzjqudVwDJDga
export NOTION_API_TOKEN=FOkpd3tM9I4nylOO
export NOTION_DATABASE_ID=kZsIdYEJ7Y1j7Dzj
对*.env*文件进行源化,使其成为你的操作系统可用的,然后将环境变量值打印到你的控制台,以确认它们被成功源化了:
(env) $ source .env
(env) $ echo $ACCOUNT_SID
(env) $ echo $AUTH_TOKEN
(env) $ echo $NOTION_API_TOKEN
(env) $ echo $NOTION_DATABASE_ID
安装Python依赖项
该项目所需的Python包是:
- twilio- 提供对 What'sApp API 的访问。
- requests- 发送和接收HTTP请求
- python-dotenv- 访问环境变量
Python项目所需的依赖项通常列在一个叫做requirements.txt的文件中。在service-monitoring/目录下创建一个requirements.txt文件:
(env) $ touch requirements.txt
使用你喜欢的文本编辑器将这个 Python 包的列表复制并粘贴到你的requirements.txt文件中:
twilio
requests
python-dotenv
用下面的命令安装所有的依赖项,确保你仍然有你的虚拟环境 ("env") 资源:
(env) $ pip install -r requirements.txt
创建一个新的Python文件
现在是写代码的时候了!让我们从创建Python文件开始:
(env) $ touch main.py
在你喜欢的代码编辑器中打开main.py。包括将需要的导入和环境变量的代码:
import json
import os
import requests
from dotenv import load_dotenv
from requests.models import Response
from twilio.rest import Client
load_dotenv()
TWILIO_ACCOUNT_SID = os.getenv('ACCOUNT_SID')
TWILIO_AUTH_TOKEN = os.getenv('AUTH_TOKEN')
NOTION_API_BASE_URL = 'https://api.notion.com/v1'
NOTION_API_TOKEN = os.getenv('NOTION_API_TOKEN')
NOTION_DATABASE_ID = os.getenv('NOTION_DATABASE_ID')
写一个get_services_to_monitor()函数
我们要写的第一个函数将对Notion API进行调用,并返回我们要监控的服务的列表(这些服务在我们的数据库中列出)。复制并粘贴这个函数到main.py文件的底部。下面的代码片段后面有一个解释:
def get_services_to_monitor():
"""
Calls the notion API to get the services that we need to monitor
and returns a list of the services.
"""
headers: dict = {
'Authorization': f'Bearer {NOTION_API_TOKEN}',
'Content-Type': 'application/json',
'Notion-Version': '2021-08-16',
}
# uses https://developers.notion.com/reference/post-database-query
response: Response = requests.post(
f'{NOTION_API_BASE_URL}/databases/{NOTION_DATABASE_ID}/query', headers=headers)
if response.status_code == 200:
json_response: dict = response.json()['results']
else:
print("Something went wrong.")
return
services: list = []
for item in json_response:
service: dict = {
'id': item['id'],
'url': item['properties']['URL']['title'][0]['text']['content'],
'identifier': item['properties']['Identifier']['rich_text'][0]['text']['content'],
}
services.append(service)
print(services)
return services
get_services_to_monitor()
在**get_services_to_monitor()**函数中,我们通过Notion API查询监控数据库,以获得其中存储的数据。POST *api.notion.com/v1/database…
{
"id": "",
"url": "",
"identifier": ""
}
我们将每个新的字典追加到服务列表中。这个字典列表在函数结束时被返回,每个字典将被传递给我们要写的下一个函数,叫做get_status()。
这里要问的一个好问题是,那个id字段是什么?Notion 数据库中的每个条目都可以作为一个 Page 打开,每个 Page 都有其独特的 ID。这个id字段将在下一节中被用来在找到服务的状态后更新Notion数据库中的特定条目。
现在运行main.py文件应该会导致在你的CLI窗口中打印出一个字典列表,假设你所有的环境变量都已经设置好了,因为文件的最后一行调用了get_services_to_monitor()函数:

编写一个get_status()函数
接下来,我们需要一个函数,向每个服务发送HTTP请求,并返回一个有意义的字符串,描述服务的状态。在get_services_to_monitor()函数定义的下面,以及目前在文件最后一行的get_services_to_monitor()函数调用的上面,添加这个代码段:
def get_status(service: dict):
"""
This function returns a status string based on the status code
and the presence of the identifier in the response.
"""
response: Response = requests.get(service['url'])
if response is not None:
status_code: int = response.status_code
response_body: str = response.text
if status_code >= 200 and status_code < 400 and service['identifier'].lower() in response_body.lower():
return 'Operational'
elif status_code >= 200 and status_code < 400:
return 'Doubtful'
elif status_code >= 400 and status_code < 500:
return 'Warning'
elif status_code == 503:
return 'Maintenance'
else:
return 'Down'
else:
print("Something went wrong.")
return
这个函数接收一个字典(代表一个服务)作为参数,并返回一个代表该服务状态的字符串。该函数向该服务的URL发送一个GET 请求,并检查响应的状态代码。基于状态代码的值,该函数返回一个代表服务状态的字符串。
需要注意的一件事是if-else 块中标识符的使用。标识符是一个字符串,你确信它存在于URL发送的响应中。如果标识符缺失,即使状态代码是200,你也不能肯定地说,服务是正确运行的。
用这段代码替换main.py文件最后一行的函数调用,这样当你运行main.py时,函数会按照必要的顺序被调用:
def main():
services: list = get_services_to_monitor()
for service in services:
status: str = get_status(service)
print(status)
if __name__ == '__main__':
main()
再次运行该文件。
(env) $ python3 main.py
你应该在输出中看到服务字典的列表和状态字符串:

用服务的状态更新Notion数据库
现在,每个服务的状态已经确定,可以在Notion数据库中更新。在现有的get_status ()函数之后添加这个新的update_service_status( ) 函数:
def update_service_status(service: dict, status: str):
"""
This function updates the service's status using Notion API.
"""
payload: dict = {
'properties': {
'Status': {
'select': {
'name': status
}
}
}
}
headers: dict = {
'Authorization': f'Bearer {NOTION_API_TOKEN}',
'Content-Type': 'application/json',
'Notion-Version': '2021-05-13',
}
# uses https://developers.notion.com/reference/patch-page
requests.patch(
f'{NOTION_API_BASE_URL}/pages/{service["id"]}',
headers=headers,
data=json.dumps(payload),
)
更新你的**main()**函数,使其看起来像这样。
def main():
services: list = get_services_to_monitor()
for service in services:
status: str = get_status(service)
update_service_status(service, status) # add this line
print("{} status is {} and has been updated in Notion.".format(service['url'], status)) # Add this
update_service_status()函数接受两个输入,服务和状态。该函数构建一个有效载荷,其中包括与状态相关的数据,并向api.notion.com/v1/pages/{P…
现在,如果你通过在CLI中输入以下命令来运行该文件,你会看到你的数据库已经被更新:
(env) $ python3 main.py


发送WhatsApp通知
当一个服务的状态被更新时,以某种形式接收通知真的很重要。在本教程中,我们将使用Twilio的WhatsApp Business API来执行这项任务。
在你的main.py文件中,就在update_service_status()函数之后和main()函数之前,复制并粘贴以下新函数:
def send_notification(service: dict, status: str):
"""
This function sends a Whatsapp notification using the Twilio WhatsApp API
"""
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
from_whatsapp_number = 'whatsapp:+14155238886', # This is the Twilio Sandbox number. Don't change it.
to_whatsapp_number = 'whatsapp:+<YOUR_WHATSAPP_NUMBER>' # Customize this
body: str = f'Status for {service["url"]} is {status}.'
message: MessageInstance = client.messages.create(body=body,
from_=from_whatsapp_number,
to=to_whatsapp_number)
return message.sid
在上面的代码中,send_notification() 函数需要两个参数,服务和状态。它使用Twilio的Python SDK从Twilio的WhatsApp沙盒电话号码向你的个人电话号码发送一条WhatsApp信息。来自WhatsApp的号码是在你的Twilio WhatsApp沙盒中提供的。用你自己的号码替换到WhatsApp号码,包括国家代码。信息的主体只包含服务的URL和服务状态。
接下来,更新main()函数,使其调用send_notification()函数:
def main():
services: list = get_services_to_monitor()
for service in services:
status: str = get_status(service)
update_service_status(service, status)
send_notification(service, status) # Add this line
确保你已经激活了你的WhatsApp沙盒,然后在你的CLI中运行python3 main.py ,再次执行该程序,你将会收到WhatsApp的通知

有条件地发送WhatsApp通知
如果您现在再次运行代码,您将再次收到WhatsApp通知,尽管服务状态很可能在这么短的时间内没有改变。在现实世界中,你只想在服务状态发生变化时收到通知,比如从运行到下降。让我们更新代码来微调通知的发送时间。
在main.py中,更新get_services_to_monitor()函数,在for-loop中添加一个try-except block:
def get_services_to_monitor():
# . . .
for item in json_response:
service: dict = {
'id': item['id'],
'url': item['properties']['URL']['title'][0]['text']['content'],
'identifier': item['properties']['Identifier']['rich_text'][0]['text']['content'],
}
# Add this block
# Since status of a service can be empty, we need to use try except block
# to get the last recorded status of a service
try:
service['last_recorded_status'] = item['properties']['Status']['select']['name']
except KeyError:
service['last_recorded_status'] = ''
services.append(service)
return services
上面的代码片段在服务字典中添加了一个名为last_recorded_status 的新键。记住,这个字典将作为一个参数传递给send_notification()函数。
现在,更新send_notification()函数中的第一行代码,包括突出显示的if语句,并缩进函数中的其他代码:
def send_notification(service: dict, status: str):
"""
This function sends a WhatsApp notification using the Twilio WhatsApp API.
"""
if service['last_recorded_status'] != status: # add this line
# Indent all following lines
client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
from_whatsapp_number = 'whatsapp:+<TWILIO_WHATSAPP_NUMBER>'
to_whatsapp_number = 'whatsapp:+<YOUR_WHATSAPP_NUMBER>'
body: str = f'Status for {service["url"]} is {status}.'
message: MessageInstance = client.messages.create(body=body,
from_=from_whatsapp_number,
to=to_whatsapp_number)
return message.sid
如果你通过运行python3 main.py ,再次执行该代码,你将得到一个通知,如果并且仅当你的任何服务的状态发生了变化。
自动监测服务
本教程的最后一步是使我们的main.py文件在没有我们干预的情况下按常规时间表运行。有很多方法可以做到这一点,比如在Heroku上托管脚本并使用Heroku Scheduler;在自营机器上运行Crontab;或者使用GitHub Actions。在本教程中,我们将使用GitHub Actions。
创建一个GitHub动作
在service-monitoring/目录中,创建一个名为.github/workflows/*的新目录:
(env) $ mkdir .github
(env) $ mkdir .github/workflows
然后,在.github/workflows中创建一个文件,称为main.yml:
(env) $ touch .github/workflows/main.yml
用你喜欢的文本编辑器在main.yml中添加以下代码:
name: Monitoring
on:
schedule:
- cron: "*/10 * * * *"
jobs:
monitor:
runs-on: ubuntu-latest
env:
NOTION_API_TOKEN: ${{ secrets.NOTION_API_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
steps:
- name: Setup Repository
uses: actions/checkout@v2
with:
ref: main
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: |-
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Monitoring
run: |-
python src/main.py
上述GitHub动作每10分钟运行一次,如果需要,你可以通过用不同的分钟数替换10来改变代码的运行频率。
创建一个GitHub仓库来设置GitHub动作
在service-monitoring项目目录下初始化一个Git仓库。然后,将代码推送到你刚刚创建的GitHub仓库:
(env) $ git init
(env) $ git remote add origin https://github.com/<your-GitHub-username>/<your-repo-name>.git
(env) $ git add .
(env) $ git commit -m "Create service monitoring"
(env) $ git branch -M main
(env) $ git push -u origin main
提示:确保你在GitHub上使用的个人访问令牌(PAT)可以访问工作流范围,如下图所示:

在GitHub仪表盘的GitHub repo页面上,进入设置>秘密,添加GitHub Actions工作流程中使用的秘密及其值。你可以在之前创建的*.env*文件中找到这些值。

一旦你把代码推送到GitHub repo并设置了秘密,你将看到你的行动每10分钟运行一次。
测试项目
为了验证一切是否按预期运行,我把我的网站(ravgeet.in)置于维护模式下10分钟,检查是否收到通知。
正如你在下面的截图中看到的,我的网站在Notion数据库中的状态变成了维护:
我也收到了WhatsApp的通知:

总结
祝贺你!你已经写了一个Python脚本,它将监控你的服务,并在其运行状态发生变化时向你发送通知。如果你喜欢这个教程或有任何反馈,我很想听到你的意见。
我迫不及待地想看到你所构建的东西!