feat: add monitor history 24h table

This commit is contained in:
zaunist committed 2025-04-28 19:17:22 +08:00
1 parent 61a59f8a4d
commit ca4ddeac65
8 files changed
+181 -84

No files matched your search

+14
View File
@@ -6,6 +6,8 @@ DROP TABLE IF EXISTS status_page_agents;
DROP TABLE IF EXISTS status_page_monitors;
DROP TABLE IF EXISTS status_page_config;
DROP TABLE IF EXISTS monitor_status_history;
DROP TABLE IF EXISTS monitor_status_history_24h;
DROP TABLE IF EXISTS monitor_daily_stats;
DROP TABLE IF EXISTS agents;
DROP TABLE IF EXISTS monitors;
DROP TABLE IF EXISTS users;
@@ -55,6 +57,18 @@ CREATE TABLE IF NOT EXISTS monitor_status_history (
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
);
-- 24小时监控状态历史表(热表)
CREATE TABLE IF NOT EXISTS monitor_status_history_24h (
id INTEGER PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
status TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
response_time INTEGER,
status_code INTEGER,
error TEXT,
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
);
-- 监控每日统计表
CREATE TABLE IF NOT EXISTS monitor_daily_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
+5
View File
@@ -24,6 +24,11 @@ export async function createTables(env: Bindings): Promise<void> {
"CREATE TABLE IF NOT EXISTS monitor_status_history (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, response_time INTEGER, status_code INTEGER, error TEXT, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
);
console.log("创建24小时监控状态历史表(热表)...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS monitor_status_history_24h (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, response_time INTEGER, status_code INTEGER, error TEXT, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
);
console.log("创建监控每日统计表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS monitor_daily_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, date TEXT NOT NULL, total_checks INTEGER NOT NULL DEFAULT 0, up_checks INTEGER NOT NULL DEFAULT 0, down_checks INTEGER NOT NULL DEFAULT 0, avg_response_time INTEGER DEFAULT 0, min_response_time INTEGER DEFAULT 0, max_response_time INTEGER DEFAULT 0, availability REAL DEFAULT 0, created_at TEXT NOT NULL, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
+3 -8
View File
@@ -3,13 +3,7 @@
* 用于应用启动时检测数据库是否为空,如果为空则初始化
*/
import { Bindings } from "../models/db";
import {
createTables,
createAdminUser,
createDefaultStatusPage,
createNotificationTemplates,
createNotificationChannelsAndSettings,
} from "./database";
import { createTables } from "./database";
import { runMigrations } from "../migrations/migration";
import { cleanupOldRecords } from "../repositories/monitor";
@@ -41,6 +35,8 @@ export async function checkAndInitializeDatabase(
"users",
"monitors",
"monitor_status_history",
"monitor_status_history_24h",
"monitor_daily_stats",
"agents",
"status_page_config",
"status_page_monitors",
@@ -49,7 +45,6 @@ export async function checkAndInitializeDatabase(
"notification_templates",
"notification_settings",
"notification_history",
"monitor_daily_stats",
];
// 检查每个表是否存在
+144 -62
View File
@@ -163,38 +163,39 @@ async function handleMonitorNotification(
async function generateDailyStats(c: any) {
try {
console.log("开始生成每日监控统计数据...");
// 获取前一天的日期 (YYYY-MM-DD 格式)
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T')[0];
const dateStr = yesterday.toISOString().split("T")[0];
console.log(`正在处理日期 ${dateStr} 的数据`);
// 时间范围
const startTime = `${dateStr}T00:00:00.000Z`;
const endTime = `${dateStr}T23:59:59.999Z`;
// 一次性获取所有监控
const monitorsResult = await c.env.DB.prepare(
"SELECT id, name FROM monitors"
).all();
if (!monitorsResult.results || monitorsResult.results.length === 0) {
console.log("没有找到监控");
return { success: true, message: "没有监控", processed: 0 };
}
const monitors = monitorsResult.results;
console.log(`找到 ${monitors.length} 个监控`);
// 创建监控ID列表
const monitorIds = monitors.map((m: any) => m.id);
// 一次性获取所有监控历史记录(指定日期范围内)
console.log(`一次性查询所有监控在 ${startTime} 至 ${endTime} 的历史记录`);
const historyResult = await c.env.DB.prepare(`
const historyResult = await c.env.DB.prepare(
`
SELECT
monitor_id,
status,
@@ -204,20 +205,21 @@ async function generateDailyStats(c: any) {
WHERE
timestamp >= ? AND
timestamp <= ?
`)
.bind(startTime, endTime)
.all();
`
)
.bind(startTime, endTime)
.all();
if (!historyResult.results || historyResult.results.length === 0) {
console.log(`在 ${dateStr} 没有找到任何监控历史记录`);
return { success: true, message: "没有历史记录", processed: 0 };
}
console.log(`找到 ${historyResult.results.length} 条历史记录`);
// 按监控ID分组处理数据
const statsMap = new Map();
// 初始化每个监控的统计数据结构
for (const monitorId of monitorIds) {
statsMap.set(monitorId, {
@@ -229,69 +231,81 @@ async function generateDailyStats(c: any) {
avgResponseTime: 0,
minResponseTime: 0,
maxResponseTime: 0,
availability: 0
availability: 0,
});
}
// 处理所有历史记录
for (const record of historyResult.results) {
const monitorId = record.monitor_id;
// 如果这个监控ID不在我们的列表中,跳过
if (!statsMap.has(monitorId)) continue;
const stats = statsMap.get(monitorId);
stats.totalChecks++;
// 统计状态
if (record.status === 'up') {
if (record.status === "up") {
stats.upChecks++;
} else if (record.status === 'down') {
} else if (record.status === "down") {
stats.downChecks++;
}
// 收集响应时间数据
if (record.response_time != null && record.response_time > 0) {
stats.responseTimes.push(record.response_time);
}
}
// 处理每个监控的响应时间统计和可用率计算
for (const [monitorId, stats] of statsMap.entries()) {
// 只处理有记录的监控
if (stats.totalChecks === 0) continue;
// 计算响应时间统计数据
if (stats.responseTimes.length > 0) {
stats.avgResponseTime = stats.responseTimes.reduce((sum: number, time: number) => sum + time, 0) / stats.responseTimes.length;
stats.avgResponseTime =
stats.responseTimes.reduce(
(sum: number, time: number) => sum + time,
0
) / stats.responseTimes.length;
stats.minResponseTime = Math.min(...stats.responseTimes);
stats.maxResponseTime = Math.max(...stats.responseTimes);
}
// 计算可用率
stats.availability = stats.totalChecks > 0 ? (stats.upChecks / stats.totalChecks) * 100 : 0;
stats.availability =
stats.totalChecks > 0 ? (stats.upChecks / stats.totalChecks) * 100 : 0;
// 删除临时数据
delete stats.responseTimes;
}
// 将数据写入数据库
const now = new Date().toISOString();
let processed = 0;
// 批量构建 INSERT 语句
for (const [monitorId, stats] of statsMap.entries()) {
// 跳过没有记录的监控
if (stats.totalChecks === 0) continue;
const monitor = monitors.find((m: any) => m.id === monitorId);
const monitorName = monitor ? monitor.name : `ID: ${monitorId}`;
try {
console.log(`监控 ${monitorName} (ID: ${monitorId}) 数据: 总检查=${stats.totalChecks}, 正常=${stats.upChecks}, 故障=${stats.downChecks}, 可用率=${stats.availability.toFixed(2)}%`);
console.log(
`监控 ${monitorName} (ID: ${monitorId}) 数据: 总检查=${
stats.totalChecks
}, 正常=${stats.upChecks}, 故障=${
stats.downChecks
}, 可用率=${stats.availability.toFixed(2)}%`
);
// 使用 UPSERT 操作
await c.env.DB.prepare(`
await c.env.DB.prepare(
`
INSERT INTO monitor_daily_stats (
monitor_id,
date,
@@ -312,61 +326,129 @@ async function generateDailyStats(c: any) {
min_response_time = excluded.min_response_time,
max_response_time = excluded.max_response_time,
availability = excluded.availability
`).bind(
monitorId,
dateStr,
stats.totalChecks,
stats.upChecks,
stats.downChecks,
stats.avgResponseTime,
stats.minResponseTime,
stats.maxResponseTime,
stats.availability,
now
).run();
`
)
.bind(
monitorId,
dateStr,
stats.totalChecks,
stats.upChecks,
stats.downChecks,
stats.avgResponseTime,
stats.minResponseTime,
stats.maxResponseTime,
stats.availability,
now
)
.run();
processed++;
console.log(`成功更新监控 ID ${monitorId} 的每日统计数据`);
} catch (error) {
console.error(`更新监控 ID ${monitorId} 的每日统计数据时出错:`, error);
}
}
console.log(`每日统计数据生成完成,成功处理了 ${processed} 个监控`);
return {
success: true,
message: "每日统计数据生成完成",
processed: processed,
date: dateStr
date: dateStr,
};
} catch (error) {
console.error("生成每日统计数据时出错:", error);
return {
success: false,
message: "生成每日统计数据时出错",
error: String(error)
return {
success: false,
message: "生成每日统计数据时出错",
error: String(error),
};
}
}
// 迁移24小时以前的监控历史数据到冷表
async function migrateMonitorHistoryData(c: any) {
try {
console.log("开始迁移24小时以前的监控历史数据...");
const oneDayAgo = new Date();
oneDayAgo.setHours(oneDayAgo.getHours() - 24);
const timestamp = oneDayAgo.toISOString();
// 获取24小时前的监控历史数据
const historyResult = await c.env.DB.prepare(
`
SELECT * FROM monitor_status_history_24h
WHERE timestamp < ?
`
)
.bind(timestamp)
.all();
console.log(
`找到 ${historyResult.results.length} 条24小时前的监控历史数据`
);
// 将数据迁移到冷表
await c.env.DB.prepare(
`
INSERT INTO monitor_status_history (
monitor_id,
status,
timestamp,
response_time,
status_code,
error
) VALUES (?, ?, ?, ?, ?, ?)
`
)
.bind(
historyResult.results.map((record: any) => [
record.monitor_id,
record.status,
record.timestamp,
record.response_time,
record.status_code,
record.error,
])
)
.run();
// 删除热表中的数据
await c.env.DB.prepare(
`
DELETE FROM monitor_status_history_24h
WHERE timestamp < ?
`
)
.bind(timestamp)
.run();
console.log("迁移完成,成功迁移了 ${historyResult.results.length} 条数据");
} catch (error) {
console.error("迁移24小时以前的监控历史数据时出错:", error);
}
}
// 在 Cloudflare Workers 中设置定时触发器
export default {
async scheduled(event: any, env: any, ctx: any) {
const c = { env };
// 默认执行监控检查任务
let result: any = await checkMonitors(c);
const now = new Date();
const hour = now.getUTCHours();
const minute = now.getUTCMinutes();
if (hour === 0 && minute === 5) {
// 迁移24小时以前的监控历史数据到冷表
await migrateMonitorHistoryData(c);
// 生成每日监控统计数据
const statsResult = await generateDailyStats(c);
result = { monitorCheck: result, dailyStats: statsResult };
}
return result;
},
fetch: monitorTask.fetch,
+6 -8
View File
@@ -138,15 +138,14 @@ export async function getMonitorsByIds(
}
// 获取单个监控状态历史 24小时内
export async function getMonitorStatusHistory(
export async function getMonitorStatusHistoryIn24h(
db: Bindings["DB"],
monitorId: number
) {
return await db
.prepare(
`SELECT * FROM monitor_status_history
`SELECT * FROM monitor_status_history_24h
WHERE monitor_id = ?
AND timestamp > datetime('now', '-24 hours')
ORDER BY timestamp ASC`
)
.bind(monitorId)
@@ -154,16 +153,15 @@ export async function getMonitorStatusHistory(
}
// 获取所有监控状态历史 24小时内
export async function getAllMonitorStatusHistory(db: Bindings["DB"]) {
export async function getAllMonitorStatusHistoryIn24h(db: Bindings["DB"]) {
return await db
.prepare(
`SELECT * FROM monitor_status_history
WHERE timestamp > datetime('now', '-24 hours')
`SELECT * FROM monitor_status_history_24h
ORDER BY timestamp ASC`
)
.all<MonitorStatusHistory>();
}
// 记录监控状态历史
// 记录监控状态历史到热表
export async function insertMonitorStatusHistory(
db: Bindings["DB"],
monitorId: number,
@@ -177,7 +175,7 @@ export async function insertMonitorStatusHistory(
return await db
.prepare(
`INSERT INTO monitor_status_history (monitor_id, status, timestamp, response_time, status_code, error)
`INSERT INTO monitor_status_history_24h (monitor_id, status, timestamp, response_time, status_code, error)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(monitorId, status, now, response_time, status_code, error)
+7 -4
View File
@@ -148,7 +148,7 @@ export async function getMonitorDetails(db: Bindings["DB"], id: number) {
}
// 获取历史状态数据
const historyResult = await repositories.getMonitorStatusHistory(db, id);
const historyResult = await repositories.getMonitorStatusHistoryIn24h(db, id);
return {
...monitor,
@@ -198,7 +198,7 @@ export async function getMonitorById(db: Bindings["DB"], id: number) {
}
// 获取历史状态数据
const historyResult = await repositories.getMonitorStatusHistory(db, id);
const historyResult = await repositories.getMonitorStatusHistoryIn24h(db, id);
return {
success: true,
@@ -392,7 +392,10 @@ export async function getMonitorStatusHistoryById(
}
// 获取历史状态
const historyResult = await repositories.getMonitorStatusHistory(db, id);
const historyResult = await repositories.getMonitorStatusHistoryIn24h(
db,
id
);
return {
success: true,
@@ -417,7 +420,7 @@ export async function getMonitorStatusHistoryById(
* @returns 所有监控状态历史
*/
export async function getAllMonitorStatusHistory(db: Bindings["DB"]) {
const result = await repositories.getAllMonitorStatusHistory(db);
const result = await repositories.getAllMonitorStatusHistoryIn24h(db);
return {
success: true,
history: result.results || [],
+1 -1
View File
@@ -165,7 +165,7 @@ export async function deleteNotificationTemplate(
// 通知设置相关服务,获取所有的通知设置
export async function getNotificationConfig(
db: D1Database,
db: D1Database
): Promise<models.NotificationConfig> {
return await repositories.getNotificationConfig(db);
}
+1 -1
View File
@@ -227,7 +227,7 @@ export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
env.DB,
monitorId
);
const monitorHistory = await repositories.getMonitorStatusHistory(
const monitorHistory = await repositories.getMonitorStatusHistoryIn24h(
env.DB,
monitorId
);