mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-19 18:56:48 +02:00
remove client-side aggregation and add server-side support for it in new util
This commit is contained in:
parent
2c6d410c60
commit
8d2e3f130a
3 changed files with 69 additions and 116 deletions
|
@ -3,7 +3,7 @@ const apicache = require("../modules/apicache");
|
|||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const StatusPage = require("../model/status_page");
|
||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||
const { rangeToDatabaseDate } = require("../util/heartbeat-range");
|
||||
const { getAggregatedHeartbeatData } = require("../util/heartbeat-range");
|
||||
const { R } = require("redbean-node");
|
||||
const { badgeConstants } = require("../../src/util");
|
||||
const { makeBadge } = require("badge-maker");
|
||||
|
@ -89,13 +89,17 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
|||
let statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]);
|
||||
let heartbeatRange = statusPage ? statusPage.heartbeat_bar_range : "auto";
|
||||
|
||||
// Calculate the date range for heartbeats based on range setting
|
||||
let dateFrom = rangeToDatabaseDate(heartbeatRange);
|
||||
|
||||
for (let monitorID of monitorIDList) {
|
||||
let list;
|
||||
if (dateFrom === null) {
|
||||
// Auto mode: use original logic with LIMIT 100
|
||||
|
||||
// Try to use aggregated data from stat tables for better performance
|
||||
const aggregatedData = await getAggregatedHeartbeatData(monitorID, heartbeatRange);
|
||||
|
||||
if (aggregatedData) {
|
||||
// Use pre-aggregated stat data
|
||||
heartbeatList[monitorID] = aggregatedData;
|
||||
} else {
|
||||
// Fall back to raw heartbeat data for auto mode
|
||||
list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ?
|
||||
|
@ -104,20 +108,10 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
|||
`, [
|
||||
monitorID,
|
||||
]);
|
||||
} else {
|
||||
// Time-based filtering
|
||||
list = await R.getAll(`
|
||||
SELECT * FROM heartbeat
|
||||
WHERE monitor_id = ? AND time >= ?
|
||||
ORDER BY time DESC
|
||||
`, [
|
||||
monitorID,
|
||||
dateFrom,
|
||||
]);
|
||||
}
|
||||
|
||||
list = R.convertToBeans("heartbeat", list);
|
||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||
list = R.convertToBeans("heartbeat", list);
|
||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||
}
|
||||
|
||||
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||
uptimeList[`${monitorID}_24`] = uptimeCalculator.get24Hour().uptime;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const { R } = require("redbean-node");
|
||||
const dayjs = require("dayjs");
|
||||
|
||||
/**
|
||||
* Utility functions for heartbeat range handling
|
||||
*/
|
||||
|
@ -23,22 +26,63 @@ function parseRangeHours(range) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert range to database-compatible date string
|
||||
* Get aggregated heartbeat data using stat tables for better performance
|
||||
* @param {number} monitorId - Monitor ID
|
||||
* @param {string} range - Range string like "6h", "7d", "auto"
|
||||
* @returns {string|null} Date string or null for auto
|
||||
* @returns {Promise<Array>} Aggregated heartbeat data
|
||||
*/
|
||||
function rangeToDatabaseDate(range) {
|
||||
const hours = parseRangeHours(range);
|
||||
if (hours === null) {
|
||||
async function getAggregatedHeartbeatData(monitorId, range) {
|
||||
if (!range || range === "auto") {
|
||||
// Fall back to regular heartbeat query for auto mode
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - hours);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
const now = dayjs();
|
||||
const hours = parseRangeHours(range);
|
||||
|
||||
if (hours <= 24) {
|
||||
// Use hourly stats for ranges up to 24 hours
|
||||
const startTime = now.subtract(hours, "hours");
|
||||
const timestampKey = Math.floor(startTime.valueOf() / (60 * 60 * 1000)) * (60 * 60 * 1000);
|
||||
|
||||
const stats = await R.getAll(`
|
||||
SELECT * FROM stat_hourly
|
||||
WHERE monitor_id = ? AND timestamp >= ?
|
||||
ORDER BY timestamp ASC
|
||||
`, [monitorId, timestampKey]);
|
||||
|
||||
// Convert to heartbeat-like format
|
||||
return stats.map(stat => ({
|
||||
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
||||
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
||||
up: stat.up,
|
||||
down: stat.down,
|
||||
ping: stat.ping
|
||||
}));
|
||||
} else {
|
||||
// Use daily stats for ranges over 24 hours
|
||||
const days = Math.ceil(hours / 24);
|
||||
const startTime = now.subtract(days, "days");
|
||||
const timestampKey = Math.floor(startTime.valueOf() / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000);
|
||||
|
||||
const stats = await R.getAll(`
|
||||
SELECT * FROM stat_daily
|
||||
WHERE monitor_id = ? AND timestamp >= ?
|
||||
ORDER BY timestamp ASC
|
||||
`, [monitorId, timestampKey]);
|
||||
|
||||
// Convert to heartbeat-like format
|
||||
return stats.map(stat => ({
|
||||
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
||||
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
||||
up: stat.up,
|
||||
down: stat.down,
|
||||
ping: stat.ping
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseRangeHours,
|
||||
rangeToDatabaseDate
|
||||
getAggregatedHeartbeatData
|
||||
};
|
|
@ -129,94 +129,9 @@ export default {
|
|||
},
|
||||
|
||||
aggregatedBeatList() {
|
||||
if (!this.beatList || !this.heartbeatBarRange || this.heartbeatBarRange === "auto") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const now = dayjs();
|
||||
const buckets = [];
|
||||
|
||||
// Parse the range to get total time and determine bucket size
|
||||
// Parse range to get total hours
|
||||
let totalHours;
|
||||
if (this.heartbeatBarRange.endsWith("h")) {
|
||||
totalHours = parseInt(this.heartbeatBarRange);
|
||||
} else if (this.heartbeatBarRange.endsWith("d")) {
|
||||
totalHours = parseInt(this.heartbeatBarRange) * 24;
|
||||
} else {
|
||||
totalHours = 90 * 24; // Fallback
|
||||
}
|
||||
|
||||
// Calculate bucket size and count
|
||||
const totalBuckets = this.maxBeat || 50;
|
||||
const bucketSize = totalHours / totalBuckets;
|
||||
|
||||
// Create time buckets from oldest to newest
|
||||
const startTime = now.subtract(totalHours, "hours");
|
||||
for (let i = 0; i < totalBuckets; i++) {
|
||||
let bucketStart;
|
||||
let bucketEnd;
|
||||
if (bucketSize < 1) {
|
||||
// Handle sub-hour buckets (minutes)
|
||||
const minutes = bucketSize * 60;
|
||||
bucketStart = startTime.add(i * minutes, "minutes");
|
||||
bucketEnd = bucketStart.add(minutes, "minutes");
|
||||
} else {
|
||||
// Handle hour+ buckets
|
||||
bucketStart = startTime.add(i * bucketSize, "hours");
|
||||
bucketEnd = bucketStart.add(bucketSize, "hours");
|
||||
}
|
||||
|
||||
buckets.push({
|
||||
start: bucketStart,
|
||||
end: bucketEnd,
|
||||
beats: [],
|
||||
status: 1, // default to up
|
||||
time: bucketEnd.toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Group heartbeats into buckets
|
||||
this.beatList.forEach(beat => {
|
||||
const beatTime = dayjs.utc(beat.time).local();
|
||||
const bucket = buckets.find(b =>
|
||||
(beatTime.isAfter(b.start) || beatTime.isSame(b.start)) &&
|
||||
(beatTime.isBefore(b.end) || beatTime.isSame(b.end))
|
||||
);
|
||||
if (bucket) {
|
||||
bucket.beats.push(beat);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate status for each bucket
|
||||
buckets.forEach(bucket => {
|
||||
if (bucket.beats.length === 0) {
|
||||
bucket.status = null; // no data - will be rendered as empty/grey
|
||||
bucket.time = bucket.end.toISOString();
|
||||
} else {
|
||||
// If any beat is down, bucket is down
|
||||
// If any beat is maintenance, bucket is maintenance
|
||||
// Otherwise bucket is up
|
||||
const hasDown = bucket.beats.some(b => b.status === 0);
|
||||
const hasMaintenance = bucket.beats.some(b => b.status === 3);
|
||||
|
||||
if (hasDown) {
|
||||
bucket.status = 0;
|
||||
} else if (hasMaintenance) {
|
||||
bucket.status = 3;
|
||||
} else {
|
||||
bucket.status = 1;
|
||||
}
|
||||
|
||||
// Use the latest beat time in the bucket
|
||||
const latestBeat = bucket.beats.reduce((latest, beat) =>
|
||||
dayjs(beat.time).isAfter(dayjs(latest.time)) ? beat : latest
|
||||
);
|
||||
bucket.time = latestBeat.time;
|
||||
}
|
||||
});
|
||||
|
||||
return buckets;
|
||||
// Data is now pre-aggregated by the server using stat tables
|
||||
// No client-side processing needed for non-auto ranges
|
||||
return this.beatList || [];
|
||||
},
|
||||
|
||||
wrapStyle() {
|
||||
|
|
Loading…
Add table
Reference in a new issue