找回密码
 立即注册
搜索
查看: 9|回复: 0

[Linux] 用Uptime Kuma+Cloudflare Tunnel打造网络设备监控神器

[复制链接]
发表于 2025-6-25 21:56 | 显示全部楼层 |阅读模式
项目背景:
手上维护了一个整个地市的众多网络设备,以硬盘录像机(NVR)为主
时常要查询他们在线状态,因为是散布在各个学校,需要在市,区平台上查看视频监控。
遇到不在线的,要让学校处理。
这里我是在内网部署了Uptime Kuma,并设置了状态页面
地址比如:http://172.20.0.32:3001/status/zkwg
20250625212110.png
效果还不错,唯一的缺点就是只能在内网查看,外网是打不开的,而且还有多个内网,他们之间又不互通。
查起来有点费劲。
于是就用它自带的Cloudflare Tunnel搞了个内网穿透,用API整合数据。
以下是最终的效果:
20250625212236.png
很直观的展示在线状态,ping值,最后一次上线时间,还把离线的排最上面。

说不多说,开始今天的教程:
打开Zero Trust页面
https://one.dash.cloudflare.com/
创建Tunnels隧道
20250625212647.png
20250625212821.png
20250625212936.png
复制上面的,下一步。
  1. cloudflared.exe service install mQyOGFlMzM0NWExZDFiMDAzZWI1ZTgiLCJ0IjoiOGZmMGFj
复制代码
注意install后面的一长串就是Cloudflare Tunnel 令牌
20250625213136.png
CF上的配置暂时就设置好了。
回到Uptime Kuma后台
把上面的Cloudflare Tunnel 令牌贴进来,启动Cloudflare。  当然机器是要联互联网的。。。
20250625213533.png
回到CF看隧道状态,显示正常就表示OK了
20250625213745.png
如果不讲究太多,直接用域名就可以访问了!
效果图是这个:


我这里并不想用这个域名访问,因为是我自己的域名。
我需要用公司的域名,我要把他们嵌入到html网页来展示。
因为换了域名,就涉及到跨域访问,要在CF上配置。
先点击域名进来,找到规则
20250625214144.png
创建 响应头转换规则
20250625215048.png
20250625215205.png
这样我就能用公司的域名来添加这些监控信息了
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.   <meta charset="UTF-8" />
  5.   <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  6.   <title>XHZK设备状态展示</title>
  7.   <style>
  8.     body {
  9.       font-family: Arial, sans-serif;
  10.       margin: 0;
  11.       padding: 0;
  12.       background-color: #f5f5f5;
  13.     }

  14.     .container {
  15.       max-width: 800px;
  16.       margin: 0 auto;
  17.       padding: 20px;
  18.     }

  19.     h1 {
  20.       text-align: center;
  21.       color: #333;
  22.       margin-bottom: 20px;
  23.     }

  24.     .status-box {
  25.       background-color: white;
  26.       border-radius: 8px;
  27.       padding: 15px;
  28.       box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  29.       border: 1px solid #ddd;
  30.     }

  31.     .status-box h2 {
  32.       margin-top: 0;
  33.       font-size: 1.1em;
  34.       color: #333;
  35.       border-bottom: 1px solid #eee;
  36.       padding-bottom: 10px;
  37.       margin-bottom: 15px;
  38.     }

  39.     .status-list {
  40.       list-style: none;
  41.       padding: 0;
  42.       margin: 0;
  43.     }

  44.     .status-list li {
  45.       padding: 10px 15px;
  46.       border-bottom: 1px solid #eee;
  47.       display: flex;
  48.       flex-direction: column;
  49.     }

  50.     .status-list li:last-child {
  51.       border-bottom: none;
  52.     }

  53.     .up::before {
  54.       content: '● ';
  55.       color: green;
  56.     }

  57.     .down::before {
  58.       content: '● ';
  59.       color: red;
  60.     }

  61.     .status-change {
  62.       font-size: 0.9em;
  63.       color: #666;
  64.       margin-top: 4px;
  65.     }
  66.   </style>
  67. </head>
  68. <body>

  69. <div class="container">
  70.   <h1>XHZK设备状态展示</h1>

  71.   <div class="status-box">
  72.     <h2>设备在线状态</h2>
  73.     <ul id="ts-list" class="status-list"></ul>
  74.   </div>
  75. </div>

  76. <script>
  77.   const tsPage = {
  78.     id: 'ts',
  79.     configUrl: 'https://xh.yourmain.xyz/api/status-page/zkwg',   
  80.     heartbeatUrl: 'https://xh.yourmain.xyz/api/status-page/heartbeat/zkwg',   
  81.     listId: 'ts-list'
  82.   };

  83.   function formatTime(timestamp) {
  84.     if (!timestamp) return '未知';
  85.     const date = new Date(timestamp);
  86.     return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
  87.   }

  88.   // 判断是否为“有效在线”
  89.   function isOffline(heartbeat) {
  90.     return heartbeat.status === 0 ||
  91.            (heartbeat.ping != null && heartbeat.ping > 1000) ||
  92.            /timeout|fail|offline/i.test(heartbeat.msg || '');
  93.   }

  94.   async function loadStatus(page) {
  95.     const list = document.getElementById(page.listId);
  96.     list.innerHTML = '<li>加载中...</li>';

  97.     try {
  98.       // 获取配置数据
  99.       const configResponse = await fetch(page.configUrl, {
  100.         method: 'GET',
  101.         headers: { 'Origin': 'https://status.jmxy.cc'     }
  102.       });
  103.       if (!configResponse.ok) throw new Error(`Config HTTP ${configResponse.status}`);
  104.       const configData = await configResponse.json();

  105.       // 获取心跳数据
  106.       const heartbeatResponse = await fetch(page.heartbeatUrl, {
  107.         method: 'GET',
  108.         headers: { 'Origin': 'https://status.jmxy.cc'     }
  109.       });
  110.       if (!heartbeatResponse.ok) throw new Error(`Heartbeat HTTP ${heartbeatResponse.status}`);
  111.       const heartbeatData = await heartbeatResponse.json();

  112.       list.innerHTML = '';

  113.       // 构建 ID → 名称映射
  114.       const monitorMap = {};
  115.       configData.publicGroupList?.forEach(group => {
  116.         group.monitorList?.forEach(monitor => {
  117.           monitorMap[monitor.id] = monitor.name;
  118.         });
  119.       });

  120.       const monitors = [];

  121.       for (const [id, heartbeats] of Object.entries(heartbeatData.heartbeatList || {})) {
  122.         if (!heartbeats.length || !monitorMap[id]) continue;

  123.         const latest = heartbeats[heartbeats.length - 1];
  124.         const currentStatus = isOffline(latest) ? 0 : 1;

  125.         let lastOnline = null;
  126.         let lastOffline = null;

  127.         // 倒序查找最后一次稳定状态变化
  128.         for (let i = heartbeats.length - 1; i >= 0; i--) {
  129.           const hb = heartbeats[i];
  130.           const status = isOffline(hb) ? 0 : 1;

  131.           if (currentStatus === 0 && status === 1 && lastOnline === null) {
  132.             lastOnline = hb.time;
  133.           }

  134.           if (currentStatus === 1 && status === 0 && lastOffline === null) {
  135.             lastOffline = hb.time;
  136.           }
  137.         }

  138.         monitors.push({
  139.           name: monitorMap[id],
  140.           status: currentStatus,
  141.           ping: latest.ping,
  142.           lastOnline,
  143.           lastOffline
  144.         });
  145.       }

  146.       // 排序:DOWN 在前,UP 在后;同状态按名称排序
  147.       monitors.sort((a, b) => {
  148.         if (a.status !== b.status) return a.status - b.status;
  149.         return a.name.localeCompare(b.name);
  150.       });

  151.       // 渲染列表
  152.       if (monitors.length === 0) {
  153.         list.innerHTML = '<li class="error">无监控数据</li>';
  154.       } else {
  155.         monitors.forEach(monitor => {
  156.           const li = document.createElement('li');
  157.           li.className = monitor.status === 1 ? 'up' : 'down';

  158.           let statusText = `状态: ${monitor.status === 1 ? '在线' : '离线'}`;
  159.           if (monitor.ping != null) {
  160.             statusText += ` (${monitor.ping.toFixed(2)}ms)`;
  161.           }

  162.           let changeText = '';
  163.           if (monitor.status === 1) {
  164.             changeText += `<div class="status-change">最近一次离线时间:${monitor.lastOffline ? formatTime(monitor.lastOffline) : '无记录'}</div>`;
  165.           } else {
  166.             changeText += `<div class="status-change">最近一次上线时间:${monitor.lastOnline ? formatTime(monitor.lastOnline) : '无记录'}</div>`;
  167.           }

  168.           li.innerHTML = `<span>${monitor.name}</span><span>${statusText}${changeText}</span>`;
  169.           list.appendChild(li);
  170.         });
  171.       }

  172.     } catch (error) {
  173.       console.error(`加载 ${page.id} 状态失败:`, error);
  174.       list.innerHTML = `<li class="error">错误: ${error.message}</li>`;
  175.     }
  176.   }

  177.   // 初次加载
  178.   loadStatus(tsPage);

  179.   // 每 30 秒刷新一次
  180.   setInterval(() => {
  181.     loadStatus(tsPage);
  182.   }, 30000);
  183. </script>
  184. </body>
  185. </html>
复制代码
用AI写的,也没什么技术含量,把域名替换成自己的就行。
至此我们就可以用  https://status.yourmain.com/test.html来查看监控状态了
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|═╬簡箪√嗳's BBS

GMT+8, 2025-6-27 16:20

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表