docs(memory): Uptime Kuma 第一批监控上线 + git 证书更新记录
This commit is contained in:
parent
f2bbcd28d1
commit
b7246c4d47
@ -1,5 +1,8 @@
|
|||||||
# 2026-09-13
|
# 2026-09-13
|
||||||
|
|
||||||
|
2026-09-13 | CHG | Uptime Kuma 第一批监控上线:9 个站点 HTTP 监控(60/120/300s 三档 + 证书到期通知),因官方 API 不支持创建监控,采用「直写 SQLite monitor 表 + 重启容器」批量添加
|
||||||
|
2026-09-13 | FIX | 修正 service.xpcool.com 监控误报:后端纯 POST 无根路由,可接受状态码放宽为 ["200-399","404"](进程挂掉时 nginx 返 502,仍判 DOWN)
|
||||||
|
2026-09-13 | CFG | git.xpcool.com 证书更新(至 2026-12-12);旧证书备份为 fullchain.pem.bak.202609131719;注意该站证书文件名是 fullchain.pem/privkey.pem,与别的站不一致
|
||||||
2026-09-13 | CFG | status.xpcool.com 切换 HTTPS:腾讯云 DV 证书落到 /data/nginx/ssl/status.xpcool.com/(key 600),80→301→443 生效,公网验证 ssl_verify=0、证书有效期至 2026-12-12
|
2026-09-13 | CFG | status.xpcool.com 切换 HTTPS:腾讯云 DV 证书落到 /data/nginx/ssl/status.xpcool.com/(key 600),80→301→443 生效,公网验证 ssl_verify=0、证书有效期至 2026-12-12
|
||||||
2026-09-13 | FIX | 沉淀 SSH 远程部署两个坑:多行脚本经 ssh.exe 传参会因括号导致 bash 语法错误;多文件 scp 静默丢文件 → 改为「本地 .sh + 单文件 scp + 幂等脚本」
|
2026-09-13 | FIX | 沉淀 SSH 远程部署两个坑:多行脚本经 ssh.exe 传参会因括号导致 bash 语法错误;多文件 scp 静默丢文件 → 改为「本地 .sh + 单文件 scp + 幂等脚本」
|
||||||
2026-09-13 | CFG | status.xpcool.com:新增 nginx 反代配置(先 HTTP,含 WebSocket 透传),nginx -t 通过并已 reload
|
2026-09-13 | CFG | status.xpcool.com:新增 nginx 反代配置(先 HTTP,含 WebSocket 透传),nginx -t 通过并已 reload
|
||||||
|
|||||||
@ -58,3 +58,13 @@
|
|||||||
- **证书**:腾讯云免费 DV,CN/SAN 均为 `status.xpcool.com`,**有效期仅 3 个月(至 2026-12-12),到期必须重新申请并替换**;落点 `/data/nginx/ssl/status.xpcool.com/`(crt/pem/csr 644、key 600),本地源 `E:\CentOS\ssl\status.xpcool.com_nginx\`。部署时用 `openssl x509 -pubkey | md5` 与 `openssl pkey -pubout | md5` 比对,确认证书与私钥配套。
|
- **证书**:腾讯云免费 DV,CN/SAN 均为 `status.xpcool.com`,**有效期仅 3 个月(至 2026-12-12),到期必须重新申请并替换**;落点 `/data/nginx/ssl/status.xpcool.com/`(crt/pem/csr 644、key 600),本地源 `E:\CentOS\ssl\status.xpcool.com_nginx\`。部署时用 `openssl x509 -pubkey | md5` 与 `openssl pkey -pubout | md5` 比对,确认证书与私钥配套。
|
||||||
- 访问形态:`http:// → 301 → https://`;公网实测 `ssl_verify=0`、响应约 0.45s(2026-09-13)。
|
- 访问形态:`http:// → 301 → https://`;公网实测 `ssl_verify=0`、响应约 0.45s(2026-09-13)。
|
||||||
- nginx 配置模板见 `.workbuddy/tmp/status.xpcool.com.conf`。
|
- nginx 配置模板见 `.workbuddy/tmp/status.xpcool.com.conf`。
|
||||||
|
- **监控的添加方式(2026-09-13 实测)**:Uptime Kuma 官方 REST API **不支持创建监控**(只能读/暂停/恢复),只能走 Socket.IO(需面板账号密码,社区库 `uptime-kuma-api`)或**直接写 SQLite**。本项目采用后者:向 `monitor` 表 INSERT(字段均有默认值,只需给 `name/type/url/interval/retry_interval/maxretries/weight/description/accepted_statuscodes_json`,`user_id=1` 即 `xpcool`),再 `docker restart uptime-kuma` 让服务端重新加载并开始探测。脚本:`.workbuddy/tmp/add-monitors.sh`(幂等,按 name 去重);**改库前先备份** `/tmp/kuma.db.backup.<时间戳>`。
|
||||||
|
- **heartbeat.status 含义**:`0=DOWN`、`1=UP`、`2=PENDING`、`3=MAINTENANCE`;`monitor_tls_info` 表会自动填充各站点证书信息(可用于证书到期监控)。
|
||||||
|
- **第一批监控(9 个,均 HTTP(s)、`accepted_statuscodes_json=["200-399"]`、开启证书到期通知)**:
|
||||||
|
| 站点 | 间隔 | 重试 | 权重 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| admin / service / git | 60s | 3 | 1000 |
|
||||||
|
| file / read / tool / sub2api | 120s | 2 | 2000 |
|
||||||
|
| code / status | 300s | 2 | 3000 |
|
||||||
|
- **`service.xpcool.com` 单独放宽为 `["200-399","404"]`**:该后端是纯 POST 接口(遵循本项目"接口全 POST"规范),任何 GET 都 404;返回 404 说明 nginx + GoFrame 存活,**后端进程真挂时 nginx 返回 502,仍会判 DOWN**。
|
||||||
|
- **待办**:通知渠道尚未配置(建议接 Bark,可复用现有 `bark` 容器及 `service.xpcool.com/bark/` 反代);「证书到期提前几天通知」需在面板设置里确认。
|
||||||
|
|||||||
82
.workbuddy/tmp/add-monitors.sh
Normal file
82
.workbuddy/tmp/add-monitors.sh
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 批量添加 Uptime Kuma 监控(2026-09-13 第一批)
|
||||||
|
# 说明:Uptime Kuma 官方 REST API 不支持创建监控,这里直接写入 SQLite(字段均有默认值),
|
||||||
|
# 写完后重启容器,服务端会从 monitor 表重新加载并开始探测。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DB=/data/uptime-kuma/kuma.db
|
||||||
|
|
||||||
|
echo "===== insert monitors ====="
|
||||||
|
sudo python3 - <<'PY'
|
||||||
|
import sqlite3, shutil, datetime
|
||||||
|
|
||||||
|
DB = "/data/uptime-kuma/kuma.db"
|
||||||
|
USER_ID = 1
|
||||||
|
|
||||||
|
# 先备份数据库(含 WAL),便于回滚
|
||||||
|
stamp = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
||||||
|
for ext in ("", "-wal", "-shm"):
|
||||||
|
try:
|
||||||
|
shutil.copy(DB + ext, "/tmp/kuma.db.backup.%s%s" % (stamp, ext))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print("backup -> /tmp/kuma.db.backup.%s" % stamp)
|
||||||
|
|
||||||
|
# (名称, URL, 探测间隔秒, 最大重试, 排序权重(越小越前), 描述)
|
||||||
|
MONITORS = [
|
||||||
|
("管理后台 · admin.xpcool.com", "https://admin.xpcool.com", 60, 3, 1000, "Vue 管理后台前端"),
|
||||||
|
("后端服务 · service.xpcool.com", "https://service.xpcool.com", 60, 3, 1000, "Go 后端服务"),
|
||||||
|
("代码托管 · git.xpcool.com", "https://git.xpcool.com", 60, 3, 1000, "Gitea 代码托管"),
|
||||||
|
("文件下载 · file.xpcool.com", "https://file.xpcool.com", 120, 2, 2000, "FileBrowser Quantum 只读下载"),
|
||||||
|
("阅读 · read.xpcool.com", "https://read.xpcool.com", 120, 2, 2000, "legado 自托管阅读"),
|
||||||
|
("工具站 · tool.xpcool.com", "https://tool.xpcool.com", 120, 2, 2000, "前端工具站"),
|
||||||
|
("sub2api · sub2api.xpcool.com", "https://sub2api.xpcool.com", 120, 2, 2000, "sub2api 服务"),
|
||||||
|
("网页 IDE · code.xpcool.com", "https://code.xpcool.com", 300, 2, 3000, "code-server 网页 IDE"),
|
||||||
|
("监控面板 · status.xpcool.com", "https://status.xpcool.com", 300, 2, 3000, "Uptime Kuma 自身可用性"),
|
||||||
|
]
|
||||||
|
|
||||||
|
con = sqlite3.connect(DB, timeout=30)
|
||||||
|
cur = con.cursor()
|
||||||
|
existing = {r[0] for r in cur.execute("select name from monitor")}
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
for name, url, interval, retries, weight, desc in MONITORS:
|
||||||
|
if name in existing:
|
||||||
|
print("SKIP exists:", name)
|
||||||
|
continue
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO monitor ("
|
||||||
|
" name, active, user_id, interval, retry_interval, maxretries, url, type, method,"
|
||||||
|
" weight, description, accepted_statuscodes_json, expiry_notification,"
|
||||||
|
" domain_expiry_notification, upside_down, maxredirects, invert_keyword"
|
||||||
|
") VALUES (?, 1, ?, ?, ?, ?, ?, 'http', 'GET', ?, ?, ?, 1, 0, 0, 10, 0)",
|
||||||
|
(name, USER_ID, interval, interval, retries, url, weight, desc, '["200-399"]'),
|
||||||
|
)
|
||||||
|
print("ADD:", name, "-> id", cur.lastrowid)
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
print("added =", added, " total =", cur.execute("select count(*) from monitor").fetchone()[0])
|
||||||
|
con.close()
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "===== restart uptime-kuma (reload monitors) ====="
|
||||||
|
sudo docker restart uptime-kuma >/dev/null
|
||||||
|
sleep 50
|
||||||
|
|
||||||
|
echo "===== verify ====="
|
||||||
|
sudo python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True)
|
||||||
|
print("monitors:")
|
||||||
|
for row in con.execute("select id, name, type, interval, active from monitor order by weight, id"):
|
||||||
|
print(" ", row)
|
||||||
|
print("heartbeat rows:", con.execute("select count(*) from heartbeat").fetchone()[0])
|
||||||
|
print("latest beats (monitor_id, status, time):")
|
||||||
|
for row in con.execute("select monitor_id, status, time from heartbeat order by id desc limit 15"):
|
||||||
|
print(" ", row)
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "===== container ====="
|
||||||
|
sudo docker ps --filter name=uptime-kuma --format '{{.Names}} {{.Status}}'
|
||||||
|
echo DONE
|
||||||
43
.workbuddy/tmp/fix-service-monitor.sh
Normal file
43
.workbuddy/tmp/fix-service-monitor.sh
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 修正 service.xpcool.com 监控的判定规则
|
||||||
|
# 背景:该后端为纯 POST 接口,任何 GET 都返回 404;404 说明 nginx+GoFrame 存活,
|
||||||
|
# 真正故障(后端进程挂掉)时 nginx 会返回 502,故把 404 纳入可接受状态码。
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "===== update monitor #2 ====="
|
||||||
|
sudo python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect("/data/uptime-kuma/kuma.db", timeout=30)
|
||||||
|
cur = con.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"update monitor set accepted_statuscodes_json = ?, description = ? where id = 2",
|
||||||
|
('["200-399","404"]', "Go 后端服务(无根路由,404 即存活;502/超时才算故障)"),
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
row = cur.execute("select id, name, accepted_statuscodes_json from monitor where id = 2").fetchone()
|
||||||
|
print("updated:", row[0], row[2])
|
||||||
|
con.close()
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "===== restart to reload ====="
|
||||||
|
sudo docker restart uptime-kuma >/dev/null
|
||||||
|
sleep 55
|
||||||
|
|
||||||
|
echo "===== final status ====="
|
||||||
|
sudo python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True)
|
||||||
|
label = {0: "DOWN", 1: "UP", 2: "PENDING", 3: "MAINTENANCE"}
|
||||||
|
q = """
|
||||||
|
select m.id, m.name, m.interval, m.accepted_statuscodes_json, h.status, h.msg
|
||||||
|
from monitor m
|
||||||
|
left join heartbeat h on h.id = (select max(id) from heartbeat where monitor_id = m.id)
|
||||||
|
order by m.weight, m.id
|
||||||
|
"""
|
||||||
|
for mid, name, interval, codes, status, msg in con.execute(q):
|
||||||
|
print(" id=%s %-4ss [%-11s] %-34s %s" % (mid, interval, label.get(status, status), name[:34], (msg or "")[:40]))
|
||||||
|
print()
|
||||||
|
print("heartbeat total:", con.execute("select count(*) from heartbeat").fetchone()[0])
|
||||||
|
PY
|
||||||
|
sudo docker ps --filter name=uptime-kuma --format '{{.Names}} {{.Status}}'
|
||||||
|
echo DONE
|
||||||
30
.workbuddy/tmp/kuma-schema.sh
Normal file
30
.workbuddy/tmp/kuma-schema.sh
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 读取 Uptime Kuma 数据库结构(只读),评估批量添加监控的可行路径
|
||||||
|
python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True)
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
print("===== all tables =====")
|
||||||
|
for (n,) in cur.execute("select name from sqlite_master where type='table' order by name"):
|
||||||
|
print(n)
|
||||||
|
|
||||||
|
for name in ("user", "monitor", "monitor_notification", "monitor_tag", "tag", "notification"):
|
||||||
|
r = cur.execute("select sql from sqlite_master where type='table' and name=?", (name,)).fetchone()
|
||||||
|
print("\n===== schema: %s =====" % name)
|
||||||
|
print(r[0] if r else "NOT FOUND")
|
||||||
|
|
||||||
|
print("\n===== users =====")
|
||||||
|
try:
|
||||||
|
for row in cur.execute("select id, username, active from user"):
|
||||||
|
print(row)
|
||||||
|
except Exception as e:
|
||||||
|
print("err:", e)
|
||||||
|
|
||||||
|
print("\n===== monitor count =====")
|
||||||
|
print(cur.execute("select count(*) from monitor").fetchone())
|
||||||
|
|
||||||
|
print("\n===== monitor columns (pragma) =====")
|
||||||
|
for row in cur.execute("pragma table_info(monitor)"):
|
||||||
|
print(row)
|
||||||
|
PY
|
||||||
18
.workbuddy/tmp/probe-service.sh
Normal file
18
.workbuddy/tmp/probe-service.sh
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 探测 service.xpcool.com 可用的健康检查路径
|
||||||
|
echo "=== service nginx conf ==="
|
||||||
|
cat /data/nginx/conf.d/service.xpcool.com.conf
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== path probe (via local nginx with Host header) ==="
|
||||||
|
for p in / /health /healthz /api/health /api/service/health /api/service/admin /api/service/open/health /index.html /favicon.ico; do
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" -k -H "Host: service.xpcool.com" "https://127.0.0.1$p")
|
||||||
|
echo " $p -> $code"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== direct backend probe (127.0.0.1:10100) ==="
|
||||||
|
for p in / /health /api/health; do
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:10100$p")
|
||||||
|
echo " $p -> $code"
|
||||||
|
done
|
||||||
52
.workbuddy/tmp/update-git-cert.sh
Normal file
52
.workbuddy/tmp/update-git-cert.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# git.xpcool.com 证书更新(2026-09-13)
|
||||||
|
# 自适应:从 nginx 配置里读出实际引用的证书路径,覆盖该路径,避免文件名不一致踩空
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DOMAIN=git.xpcool.com
|
||||||
|
CONF=/data/nginx/conf.d/$DOMAIN.conf
|
||||||
|
|
||||||
|
echo "=== conf ssl lines ==="
|
||||||
|
grep -n "listen\|server_name\|ssl_certificate" "$CONF"
|
||||||
|
|
||||||
|
echo "=== ssl dir (before) ==="
|
||||||
|
sudo ls -l /data/nginx/ssl/$DOMAIN/
|
||||||
|
|
||||||
|
# 从配置解析真实路径(ssl_certificate\s 不会误匹配 ssl_certificate_key)
|
||||||
|
CRT=$(grep -oP "ssl_certificate\s+\K[^;]+" "$CONF" | head -1 | tr -d ' \r')
|
||||||
|
KEY=$(grep -oP "ssl_certificate_key\s+\K[^;]+" "$CONF" | head -1 | tr -d ' \r')
|
||||||
|
echo "target crt = $CRT"
|
||||||
|
echo "target key = $KEY"
|
||||||
|
|
||||||
|
echo "=== old cert ==="
|
||||||
|
sudo openssl x509 -in "$CRT" -noout -subject -enddate
|
||||||
|
echo -n "old sha="; sudo sha256sum "$CRT" | cut -c1-16
|
||||||
|
|
||||||
|
# 备份旧证书(可回滚)
|
||||||
|
STAMP=$(date +%Y%m%d%H%M)
|
||||||
|
sudo cp -a "$CRT" "${CRT}.bak.$STAMP"
|
||||||
|
sudo cp -a "$KEY" "${KEY}.bak.$STAMP"
|
||||||
|
echo "backup -> ${CRT}.bak.$STAMP"
|
||||||
|
|
||||||
|
# 覆盖为新证书
|
||||||
|
sudo cp /tmp/${DOMAIN}_bundle.crt "$CRT"
|
||||||
|
sudo cp /tmp/${DOMAIN}.key "$KEY"
|
||||||
|
sudo chmod 644 "$CRT"
|
||||||
|
sudo chmod 600 "$KEY"
|
||||||
|
|
||||||
|
echo "=== key/cert match check ==="
|
||||||
|
c=$(sudo openssl x509 -in "$CRT" -noout -pubkey | openssl md5)
|
||||||
|
k=$(sudo openssl pkey -in "$KEY" -pubout 2>/dev/null | openssl md5)
|
||||||
|
echo "cert=$c"
|
||||||
|
echo "key =$k"
|
||||||
|
if [ "$c" = "$k" ]; then echo "MATCH_OK"; else echo "MATCH_FAIL"; exit 1; fi
|
||||||
|
|
||||||
|
echo "=== new cert ==="
|
||||||
|
sudo openssl x509 -in "$CRT" -noout -subject -enddate
|
||||||
|
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
echo "=== verify served cert ==="
|
||||||
|
echo | openssl s_client -connect 127.0.0.1:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -subject -enddate
|
||||||
|
echo "DONE"
|
||||||
28
.workbuddy/tmp/verify-monitors.sh
Normal file
28
.workbuddy/tmp/verify-monitors.sh
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 复核监控状态:状态码含义 0=DOWN 1=UP 2=PENDING 3=MAINTENANCE
|
||||||
|
sleep 25
|
||||||
|
sudo python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
con = sqlite3.connect("file:/data/uptime-kuma/kuma.db?mode=ro", uri=True)
|
||||||
|
|
||||||
|
print("=== name encoding check (chars, first bytes hex) ===")
|
||||||
|
for row in con.execute("select id, name, length(name), hex(substr(name,1,4)) from monitor order by id limit 3"):
|
||||||
|
print(" ", row)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== latest status per monitor ===")
|
||||||
|
q = """
|
||||||
|
select m.id, m.name, h.status, h.time, h.msg
|
||||||
|
from monitor m
|
||||||
|
left join heartbeat h on h.id = (select max(id) from heartbeat where monitor_id = m.id)
|
||||||
|
order by m.weight, m.id
|
||||||
|
"""
|
||||||
|
label = {0: "DOWN", 1: "UP", 2: "PENDING", 3: "MAINTENANCE"}
|
||||||
|
for mid, name, status, time, msg in con.execute(q):
|
||||||
|
st = label.get(status, status)
|
||||||
|
print(" id=%s [%-11s] %s (%s) %s" % (mid, st, name, time, (msg or "")[:60]))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("heartbeat total:", con.execute("select count(*) from heartbeat").fetchone()[0])
|
||||||
|
print("tls_info rows:", con.execute("select count(*) from monitor_tls_info").fetchone()[0])
|
||||||
|
PY
|
||||||
Loading…
Reference in New Issue
Block a user