在现代 DevOps 实践中,服务可用性监控状态页面展示已成为保障用户体验与提升服务可信度的关键组成部分。尤其是在多服务、多节点架构下,透明化的状态展示不仅方便团队内部快速定位问题,也能向用户传达专业性和责任感。

本篇将基于两个优秀的开源/商用工具 —— Uptime Kuma 和 Instatus,构建一套高可用、自动更新、视觉美观的服务状态系统:

  • Uptime Kuma:部署、功能全面的监控系统,支持 HTTP(s)、TCP、Ping 等多种探测方式,以及通知集成(如 Telegram、Email、Webhook 等);
  • Instatus:商用,集成,美观

对于 Instatus 来说,它本身并不能监控服务状况,我们可以通过 Uptime Kuma 做一个 Webhook 推送,从而实现面板状况监控功能

为什么选择 Uptime Kuma?

Instatus 官方其实支持使用 Uptime Robot,Uptime Robot 是不需要自托管的,我一直觉得单机部署 Uptime Kuma 没啥必要,因为一旦机器挂了的话最后也还是收不到任何推送,监控就不起意义了

但是,Uptime Robot 的 Webhook 是要收费的

好在自建的 Uptime Kuma 能支持 Webhook,在这里我们就使用 Kuma 吧

搭建

这里需要先搭建一个 Uptime Kuma,使用 docker-compose

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    volumes:
      - ./data:/app/data
      # 暴露docker sock是可选项,没有监控docker的需求可以删掉
      - /var/run/docker.sock:/var/run/docker.sock
    ports:
      # <Host Port>:<Container Port>
      - 3001:3001
    restart: unless-stopped

启动容器,然后反向代理

之后新添加一个监控

Uptime Kuma 的 Webhook 格式与 Instatus 要求的不一致,在这里我通过 Cloudflare Worker 对其进行转换,使其兼容

先创建一个新的 Worker

在这里选择默认的 Hello World 模版就 OK 了

image.png

之后将里面的代码全部都删掉,复制粘贴下面的进去:

export default {
    async fetch(request, env, ctx) {
        try {
            const data = await request.json();
            const { status, url } = data;

            const endpointMap = {
                "example.com": "https://api.instatus.com/v3/integrations/webhook/<apikey>",
            };

            let instatusEndpoint = null;

            if (url) {
                try {
                    const parsed = new URL(url);
                    const hostname = parsed.hostname;
                    if (hostname in endpointMap) {
                        instatusEndpoint = endpointMap[hostname];
                        console.log(`为URL ${url} 匹配的域名为 ${hostname},使用 endpoint: ${instatusEndpoint}`);
                    } else {
                        console.log(`未匹配到域名 ${hostname}`);
                    }
                } catch (e) {
                    return new Response("Invalid URL: " + url, { status: 400 });
                }
            }

            if (!instatusEndpoint) {
                return new Response("No matching endpoint for URL: " + url, { status: 400 });
            }

            let trigger = null;
            if (status === 0) trigger = "down";
            else if (status === 1) trigger = "up";
            else return new Response("Ignored status: " + status, { status: 200 });

            const res = await fetch(instatusEndpoint, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ trigger })
            });

            if (!res.ok) {
                const err = await res.text();
                return new Response("Instatus error: " + err, { status: 500 });
            }

            return new Response("Forwarded to Instatus", { status: 200 });

        } catch (err) {
            return new Response("Error: " + err.message, { status: 500 });
        }
    }
}

最开头的 endpointMap 需要你自己从 Instatus 里面申请 Webhook 的 Endpoint

找不到的话就一直往下面翻,然后点击,里面有一个 Creat Webhook

按照要求添加前面展示面板的组件就 OK,endpointMap 的填写规则就和键值对差不多,前面写域名(不需要带协议头和路径),后面填写 URL,请注意两个都是字符串类型的,都需要使用双引号

回到 Cloudflare Worker 界面,点击部署即可,之后最好添加一个自定义的域名,这样 Worker 就部署好了

再来到本地部署的 Uptime Kuma,新建一个通知

这里的 Post URL 使用上面 Cloudflare Worker 的地址,请求体我们自定义一下:

{
"url": "{{monitorJSON['url']}}",
"status": {{heartbeatJSON['status']}}
}

image.png

保存就 OK,测试不通过很正常,因为测试发送的是空请求

Debug

要捕捉 Webhook 来分析可以使用:Webhook.site - Test, transform and automate Web requests and emails

Uptime Kuma 的完整默认 Webhook 请求为:

{"heartbeat":{"monitorID":1,"status":1,"time":"2025-04-11 03:40:35.997","msg":"All children up and running","important":true,"duration":60,"timezone":"Asia/Shanghai","timezoneOffset":"+08:00","localDateTime":"2025-04-11 11:40:35"},"monitor":{"id":1,"name":"Web","description":null,"pathName":"Web","parent":null,"childrenIDs":[2],"url":"https://","method":"GET","hostname":null,"port":null,"maxretries":0,"weight":2000,"active":true,"forceInactive":false,"type":"group","timeout":48,"interval":60,"retryInterval":60,"resendInterval":0,"keyword":null,"invertKeyword":false,"expiryNotification":false,"ignoreTls":false,"upsideDown":false,"packetSize":56,"maxredirects":10,"accepted_statuscodes":["200-299"],"dns_resolve_type":"A","dns_resolve_server":"1.1.1.1","dns_last_result":null,"docker_container":"","docker_host":null,"proxyId":null,"notificationIDList":{"1":true,"2":true,"3":true},"tags":[],"maintenance":false,"mqttTopic":"","mqttSuccessMessage":"","databaseQuery":null,"authMethod":null,"grpcUrl":null,"grpcProtobuf":null,"grpcMethod":null,"grpcServiceName":null,"grpcEnableTls":false,"radiusCalledStationId":null,"radiusCallingStationId":null,"game":null,"gamedigGivenPortOnly":true,"httpBodyEncoding":null,"jsonPath":null,"expectedValue":null,"kafkaProducerTopic":null,"kafkaProducerBrokers":[],"kafkaProducerSsl":false,"kafkaProducerAllowAutoTopicCreation":false,"kafkaProducerMessage":null,"screenshot":null,"includeSensitiveData":false},"msg":"[Web] [✅ Up] All children up and running"}

在这里我使用自定义,将其简化为:

{
  "url": "https://example.com",
  "status": 1 // 0为故障,1为正常
}

此外在测试的过程中,出现被限制的问题,报错大致意思为 5 分钟内请求限制为 50 次,个人认为这个大概是因为 Cloudflare 的 IP 被风控导致,解决方法为自建一个代理服务器,让 Worker 通过代理地址来请求 Instatus 服务器

最后修改:2025 年 04 月 12 日
如果觉得我的文章对你有用,请随意赞赏