Maintenance & Retention Guide
Overview
This guide documents the database maintenance routines, data retention policies, logging architecture, and health monitoring systems used to keep QuranBot running smoothly at scale.
- Automated database cleaners
- 15-day retention policies
- Multi-level logging architecture
- Health check & monitoring
Database Cleaners
Automated cleaners remove stale data to prevent database bloat and maintain performance:
| Cleaner | Target Data | Criteria |
|---|---|---|
controlIds.cleaner.js |
Control message IDs | Removes entries for guilds bot no longer serves or invalid messages |
guildStates.cleaner.js |
Guild state entries | Removes states for left guilds, clears deleted voice channel IDs |
setupGuilds.cleaner.js |
Setup configurations | Validates channel existence, fixes broken IDs, removes invalid categories |
// src/database/firebase/maintenance/cleaners/setupGuilds.cleaner.js
async function cleanSetupGuilds(client) {
try {
const data = await loadSetupGuildsFromFirebase();
if (!data || !Object.keys(data).length) return { cleaned: 0, reason: 'No data' };
const botGuilds = new Set(client.guilds.cache.keys());
const valid = {};
let removed = 0, updated = 0;
for (const [gid, sData] of Object.entries(data)) {
if (!botGuilds.has(gid)) { removed++; continue; }
const guild = client.guilds.cache.get(gid);
if (!guild) { removed++; continue; }
const res = await validateSetupData(gid, sData, guild);
if (!res.valid) { removed++; continue; }
if (res.hasChanges) {
valid[gid] = res.data;
updated++;
} else {
valid[gid] = sData;
}
}
if (removed > 0 || updated > 0) {
await saveSetupGuildsToFirebase(valid);
}
return { cleaned: removed, updated, remaining: Object.keys(valid).length };
} catch (err) {
logger.error('Error Cleaning Setup Guilds', err);
return { cleaned: 0, error: err.message };
}
}
Retention Policies
Guild data is retained for 15 days after bot leaves to allow quick restoration if re-added:
// src/database/firebase/retention/retention.js
const retention_days = 15;
const retention_ms = retention_days * 24 * 60 * 60 * 1000;
async function markGuildAsLeft(guildId) {
const now = Date.now();
const updates = {};
updates[`retention_index/${guildId}/isLeft`] = true;
updates[`retention_index/${guildId}/leftAt`] = now;
await db.ref().update(updates);
logger.db(`Guild ${guildId} marked as left in retention index at ${now}`);
}
async function cleanExpiredLeftData(client) {
const now = Date.now();
const expiredBefore = now - retention_ms;
const snapshot = await db.ref('retention_index').once('value');
const retentionIndex = snapshot.val() || {};
const expiredGuilds = [];
for (const [guildId, data] of Object.entries(retentionIndex)) {
if (data?.isLeft && data?.leftAt && data.leftAt < expiredBefore) {
expiredGuilds.push(guildId);
}
}
// Bulk delete expired data...
}
Logging Architecture
Multi-level logging system with automatic rotation and archiving:
| Log Level | File Pattern | Usage |
|---|---|---|
info |
logs-general-YYYY-MM-DD.log |
Standard operational messages |
warn |
logs-warnings-YYYY-MM-DD.log |
Non-critical issues, fallback activations |
error |
logs-errors-YYYY-MM-DD.log |
Exceptions, failed API calls, voice disconnects |
debug |
logs-general-YYYY-MM-DD.log |
Verbose diagnostic information (dev only) |
voice |
voice-*/logs-general-YYYY-MM-DD.log |
Per-guild voice connection events |
Log Rotation & Archiving
Logs are automatically archived daily at Cairo midnight:
- Location:
storage/logs/archive/ - Format:
logs-general-YYYY-MM-DD.zip - Retention: 60 days (configurable in
logging_config)
Voice Logging System
Dedicated per-guild voice logger in src/logging/voiceLogger.js for granular audio debugging:
async function connection(gid, msg, meta = {}, guildCache = null) {
return this.debug(`[CONN] ${msg}`, meta, gid, guildCache);
}
async function player(gid, msg, meta = {}, guildCache = null) {
return this.debug(`[PLAYER] ${msg}`, meta, gid, guildCache);
}
async function resource(gid, msg, meta = {}, guildCache = null) {
return this.debug(`[RESOURCE] ${msg}`, meta, gid, guildCache);
}
Health Check Endpoint
Lightweight HTTP endpoint on port HH_CH_PORT (default: 3000) for monitoring systems:
const healthServer = http.createServer((request, response) => {
if (request.url === '/health') {
try {
const activeVoiceConnections = getConnectedVoiceCount();
const botuptime = process.uptime();
const currentMemory = process.memoryUsage();
response.writeHead(200, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({
status: 'ok',
voiceConnections: activeVoiceConnections,
uptime: botuptime,
memory: {
rss: currentMemory.rss,
heapTotal: currentMemory.heapTotal,
heapUsed: currentMemory.heapUsed
},
timestamp: new Date(),
}));
} catch (err) {
response.writeHead(500, { 'Content-Type': 'application/json' });
response.end(JSON.stringify({ status: 'error', message: err.message }));
}
} else {
response.writeHead(404);
response.end();
}
});
Backup System
Automated compressed backups sent to Discord channels at configurable intervals:
// src/database/local/localBackup.js
const BACKUP_INTERVAL_MS = parseInt(process.env.BACKUP_INTERVAL_MS);
async function performBackup() {
const snap = await get(ref(db, '/'));
const data = snap.val();
const filename = generateBackupFilename();
const backupPath = pathlra.join(BACKUP_DIR, filename);
await fsPromises.writeFile(backupPath.replace('.gz', '.tmp.json'), JSON.stringify(data, null, 2));
await compressFile(backupPath.replace('.gz', '.tmp.json'), backupPath);
await sendBackupToDiscord(backupPath, filename);
}
function startBackupService() {
setTimeout(() => {
setInterval(performBackup, BACKUP_INTERVAL_MS);
}, 5000);
}
Troubleshooting Maintenance
Logs growing too large
Check:
- Verify
logging_config.max_files_to_keep(default: 10) - Check
storage/logs/archive/for old zip files - Run manual cleanup:
cleanupOldLogs('storage/logs', 60)
Health check returns 500
Check:
- Port
HH_CH_PORTis not in use by another service - Bot client is initialized and connected
getConnectedVoiceCount()isn't throwing on invalid guild state
Firebase data growing unbounded
Cleaners may not be running:
- Verify
databaseCleaner.performMaintenance(client)is scheduled - Check retention interval:
cleanupInterval = 24 * 60 * 60 * 1000(24h) - Run manual cleanup via Discord command or script