mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-19 18:56:48 +02:00
try stat tables first, then add fallback if pre-aggregated data fails
This commit is contained in:
parent
8d2e3f130a
commit
dbf58b8650
3 changed files with 158 additions and 16 deletions
|
@ -3,7 +3,7 @@ const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
const { allowDevAllOrigin, sendHttpError } = require("../util-server");
|
||||||
const { getAggregatedHeartbeatData } = require("../util/heartbeat-range");
|
const { getAggregatedHeartbeatData, parseRangeHours } = require("../util/heartbeat-range");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { badgeConstants } = require("../../src/util");
|
const { badgeConstants } = require("../../src/util");
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
|
@ -89,6 +89,8 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||||
let statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]);
|
let statusPage = await R.findOne("status_page", " id = ? ", [ statusPageID ]);
|
||||||
let heartbeatRange = statusPage ? statusPage.heartbeat_bar_range : "auto";
|
let heartbeatRange = statusPage ? statusPage.heartbeat_bar_range : "auto";
|
||||||
|
|
||||||
|
console.log(`[STATUS-PAGE] Processing ${monitorIDList.length} monitors with range: ${heartbeatRange}`);
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
for (let monitorID of monitorIDList) {
|
||||||
let list;
|
let list;
|
||||||
|
|
||||||
|
@ -97,20 +99,44 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||||
|
|
||||||
if (aggregatedData) {
|
if (aggregatedData) {
|
||||||
// Use pre-aggregated stat data
|
// Use pre-aggregated stat data
|
||||||
|
console.log(`[STATUS-PAGE] Using aggregated data for monitor ${monitorID}: ${aggregatedData.length} records`);
|
||||||
heartbeatList[monitorID] = aggregatedData;
|
heartbeatList[monitorID] = aggregatedData;
|
||||||
} else {
|
} else {
|
||||||
// Fall back to raw heartbeat data for auto mode
|
// Fall back to raw heartbeat data (auto mode or no stat data)
|
||||||
list = await R.getAll(`
|
console.log(`[STATUS-PAGE] Using raw heartbeat data for monitor ${monitorID} (range: ${heartbeatRange})`);
|
||||||
SELECT * FROM heartbeat
|
|
||||||
WHERE monitor_id = ?
|
if (heartbeatRange === "auto") {
|
||||||
ORDER BY time DESC
|
// Auto mode - use original LIMIT 100 logic
|
||||||
LIMIT 100
|
list = await R.getAll(`
|
||||||
`, [
|
SELECT * FROM heartbeat
|
||||||
monitorID,
|
WHERE monitor_id = ?
|
||||||
]);
|
ORDER BY time DESC
|
||||||
|
LIMIT 100
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Non-auto range but no stat data - filter raw heartbeat data by time
|
||||||
|
const hours = parseRangeHours(heartbeatRange);
|
||||||
|
const date = new Date();
|
||||||
|
date.setHours(date.getHours() - hours);
|
||||||
|
const dateFrom = date.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
|
|
||||||
|
console.log(`[STATUS-PAGE] Filtering heartbeat data from ${dateFrom} for ${hours} hours`);
|
||||||
|
|
||||||
|
list = await R.getAll(`
|
||||||
|
SELECT * FROM heartbeat
|
||||||
|
WHERE monitor_id = ? AND time >= ?
|
||||||
|
ORDER BY time DESC
|
||||||
|
`, [
|
||||||
|
monitorID,
|
||||||
|
dateFrom
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
list = R.convertToBeans("heartbeat", list);
|
list = R.convertToBeans("heartbeat", list);
|
||||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
console.log(`[STATUS-PAGE] Raw heartbeat data for monitor ${monitorID}: ${heartbeatList[monitorID].length} records`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||||
|
|
|
@ -32,53 +32,82 @@ function parseRangeHours(range) {
|
||||||
* @returns {Promise<Array>} Aggregated heartbeat data
|
* @returns {Promise<Array>} Aggregated heartbeat data
|
||||||
*/
|
*/
|
||||||
async function getAggregatedHeartbeatData(monitorId, range) {
|
async function getAggregatedHeartbeatData(monitorId, range) {
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Getting aggregated data for monitor ${monitorId}, range: ${range}`);
|
||||||
|
|
||||||
if (!range || range === "auto") {
|
if (!range || range === "auto") {
|
||||||
// Fall back to regular heartbeat query for auto mode
|
console.log(`[HEARTBEAT-RANGE] Auto mode - returning null to use regular heartbeat query`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
const hours = parseRangeHours(range);
|
const hours = parseRangeHours(range);
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Parsed ${range} to ${hours} hours`);
|
||||||
|
|
||||||
if (hours <= 24) {
|
if (hours <= 24) {
|
||||||
// Use hourly stats for ranges up to 24 hours
|
// Use hourly stats for ranges up to 24 hours
|
||||||
const startTime = now.subtract(hours, "hours");
|
const startTime = now.subtract(hours, "hours");
|
||||||
const timestampKey = Math.floor(startTime.valueOf() / (60 * 60 * 1000)) * (60 * 60 * 1000);
|
const timestampKey = Math.floor(startTime.valueOf() / (60 * 60 * 1000)) * (60 * 60 * 1000);
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Using hourly stats from timestamp ${timestampKey} (${dayjs(timestampKey).format()})`);
|
||||||
|
|
||||||
const stats = await R.getAll(`
|
const stats = await R.getAll(`
|
||||||
SELECT * FROM stat_hourly
|
SELECT * FROM stat_hourly
|
||||||
WHERE monitor_id = ? AND timestamp >= ?
|
WHERE monitor_id = ? AND timestamp >= ?
|
||||||
ORDER BY timestamp ASC
|
ORDER BY timestamp ASC
|
||||||
`, [monitorId, timestampKey]);
|
`, [monitorId, timestampKey]);
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Found ${stats.length} hourly stat records`);
|
||||||
|
|
||||||
|
// If no stat data, fall back to raw heartbeat data
|
||||||
|
if (stats.length === 0) {
|
||||||
|
console.log(`[HEARTBEAT-RANGE] No stat data found, falling back to raw heartbeat data`);
|
||||||
|
return null; // This will trigger fallback in router
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to heartbeat-like format
|
// Convert to heartbeat-like format
|
||||||
return stats.map(stat => ({
|
const result = stats.map(stat => ({
|
||||||
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
||||||
up: stat.up,
|
up: stat.up,
|
||||||
down: stat.down,
|
down: stat.down,
|
||||||
ping: stat.ping
|
ping: stat.ping
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Returning ${result.length} converted records`);
|
||||||
|
return result;
|
||||||
} else {
|
} else {
|
||||||
// Use daily stats for ranges over 24 hours
|
// Use daily stats for ranges over 24 hours
|
||||||
const days = Math.ceil(hours / 24);
|
const days = Math.ceil(hours / 24);
|
||||||
const startTime = now.subtract(days, "days");
|
const startTime = now.subtract(days, "days");
|
||||||
const timestampKey = Math.floor(startTime.valueOf() / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000);
|
const timestampKey = Math.floor(startTime.valueOf() / (24 * 60 * 60 * 1000)) * (24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Using daily stats from timestamp ${timestampKey} (${dayjs(timestampKey).format()})`);
|
||||||
|
|
||||||
const stats = await R.getAll(`
|
const stats = await R.getAll(`
|
||||||
SELECT * FROM stat_daily
|
SELECT * FROM stat_daily
|
||||||
WHERE monitor_id = ? AND timestamp >= ?
|
WHERE monitor_id = ? AND timestamp >= ?
|
||||||
ORDER BY timestamp ASC
|
ORDER BY timestamp ASC
|
||||||
`, [monitorId, timestampKey]);
|
`, [monitorId, timestampKey]);
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Found ${stats.length} daily stat records`);
|
||||||
|
|
||||||
|
// If no stat data, fall back to raw heartbeat data
|
||||||
|
if (stats.length === 0) {
|
||||||
|
console.log(`[HEARTBEAT-RANGE] No stat data found, falling back to raw heartbeat data`);
|
||||||
|
return null; // This will trigger fallback in router
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to heartbeat-like format
|
// Convert to heartbeat-like format
|
||||||
return stats.map(stat => ({
|
const result = stats.map(stat => ({
|
||||||
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
time: dayjs(stat.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
status: stat.up > 0 ? 1 : (stat.down > 0 ? 0 : 1), // Simplified status logic
|
||||||
up: stat.up,
|
up: stat.up,
|
||||||
down: stat.down,
|
down: stat.down,
|
||||||
ping: stat.ping
|
ping: stat.ping
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-RANGE] Returning ${result.length} converted records`);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,12 +99,16 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
shortBeatList() {
|
shortBeatList() {
|
||||||
|
console.log(`[HEARTBEAT-BAR] shortBeatList called with range: ${this.heartbeatBarRange}, beatList: ${this.beatList ? this.beatList.length : 'null'} items`);
|
||||||
|
|
||||||
if (!this.beatList) {
|
if (!this.beatList) {
|
||||||
|
console.log(`[HEARTBEAT-BAR] No beatList - returning empty array`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If heartbeat range is configured (not auto), aggregate by time periods
|
// If heartbeat range is configured (not auto), aggregate by time periods
|
||||||
if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") {
|
if (this.heartbeatBarRange && this.heartbeatBarRange !== "auto") {
|
||||||
|
console.log(`[HEARTBEAT-BAR] Using aggregated beat list for range: ${this.heartbeatBarRange}`);
|
||||||
return this.aggregatedBeatList;
|
return this.aggregatedBeatList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,9 +133,92 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
aggregatedBeatList() {
|
aggregatedBeatList() {
|
||||||
// Data is now pre-aggregated by the server using stat tables
|
console.log(`[HEARTBEAT-BAR] aggregatedBeatList called with range: ${this.heartbeatBarRange}, beatList length: ${this.beatList ? this.beatList.length : 'null'}`);
|
||||||
// No client-side processing needed for non-auto ranges
|
|
||||||
return this.beatList || [];
|
if (!this.beatList || this.beatList.length === 0) {
|
||||||
|
console.log(`[HEARTBEAT-BAR] No beatList data`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If data is already aggregated from server (has 'up'/'down' fields), use as-is
|
||||||
|
if (this.beatList[0] && typeof this.beatList[0].up !== 'undefined') {
|
||||||
|
console.log(`[HEARTBEAT-BAR] Using pre-aggregated server data`);
|
||||||
|
return this.beatList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, do client-side aggregation for raw heartbeat data
|
||||||
|
console.log(`[HEARTBEAT-BAR] Performing client-side aggregation`);
|
||||||
|
const now = dayjs();
|
||||||
|
const buckets = [];
|
||||||
|
|
||||||
|
// 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 = startTime.add(i * bucketSize, "hours");
|
||||||
|
let 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
|
||||||
|
} else {
|
||||||
|
// Priority: DOWN (0) > MAINTENANCE (3) > UP (1)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[HEARTBEAT-BAR] Generated ${buckets.length} aggregated buckets`);
|
||||||
|
return buckets;
|
||||||
},
|
},
|
||||||
|
|
||||||
wrapStyle() {
|
wrapStyle() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue