feat: automated backup and production monitoring

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 00:33:35 +00:00
co-authored by Claude Opus 4.6
parent d7d04eb2d7
commit 69601cce3a
4 changed files with 123 additions and 0 deletions
+42
View File
@@ -65,3 +65,45 @@ export function getPublicStats(days: number = 30): Array<{ date: string; metric:
)
return stmt.all(cutoffStr) as Array<{ date: string; metric: string; value: number }>
}
// --- Production monitoring ---
/** Track webhook response latency */
export function trackWebhookLatency(botId: string, latencyMs: number): void {
trackMetricBy('webhook_latency_total', latencyMs)
trackMetric('webhook_calls')
if (latencyMs > 5000) trackMetric('webhook_slow')
}
/** Track payment settlement time */
export function trackPaymentSettlement(latencyMs: number, success: boolean): void {
trackMetricBy('payment_settlement_total', latencyMs)
trackMetric(success ? 'payment_success' : 'payment_failure')
}
/** Track errors */
export function trackError(category: string): void {
trackMetric(`error_${category}`)
}
/** Memory metrics snapshot (call periodically) */
export function snapshotMemory(): void {
const mem = process.memoryUsage()
const rssMB = Math.round(mem.rss / 1024 / 1024)
trackMetricBy('rss_peak_mb', 0) // Initialize if not exists
// Track peak RSS for today
const row = sqlite.prepare(
"SELECT value FROM analytics WHERE date = ? AND metric = 'rss_peak_mb'"
).get(today()) as { value: number } | undefined
if (!row || rssMB > row.value) {
sqlite.prepare(
`INSERT INTO analytics (date, metric, value) VALUES (?, 'rss_peak_mb', ?)
ON CONFLICT(date, metric) DO UPDATE SET value = ?`
).run(today(), rssMB, rssMB)
}
}
/** Start periodic memory snapshots */
export function startMemoryTracking(): void {
setInterval(snapshotMemory, 60_000)
}
+53
View File
@@ -0,0 +1,53 @@
import { copyFileSync, readdirSync, unlinkSync, mkdirSync, existsSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { logger } from '../lib/logger.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
const DB_PATH = process.env.DB_PATH || join(__dirname, '..', '..', 'data', 'botfights.db')
const BACKUP_DIR = join(dirname(DB_PATH), 'backups')
const MAX_BACKUPS = 7
/** Create a timestamped backup of the database */
export function createBackup(): string {
mkdirSync(BACKUP_DIR, { recursive: true })
const timestamp = new Date().toISOString().slice(0, 10)
const backupPath = join(BACKUP_DIR, `botfights-${timestamp}.db`)
copyFileSync(DB_PATH, backupPath)
logger.info('backup', `created backup: ${backupPath}`)
// Rotate: keep only the latest MAX_BACKUPS
rotateBackups()
return backupPath
}
/** Remove old backups beyond MAX_BACKUPS */
function rotateBackups(): void {
if (!existsSync(BACKUP_DIR)) return
const files = readdirSync(BACKUP_DIR)
.filter(f => f.startsWith('botfights-') && f.endsWith('.db'))
.sort()
.reverse()
for (let i = MAX_BACKUPS; i < files.length; i++) {
const path = join(BACKUP_DIR, files[i])
try {
unlinkSync(path)
logger.info('backup', `rotated out: ${files[i]}`)
} catch { /* ok */ }
}
}
/** Start daily backup timer */
export function startDailyBackups(): void {
// Run first backup after 1 minute, then every 24 hours
setTimeout(() => {
createBackup()
setInterval(() => createBackup(), 24 * 60 * 60 * 1000)
}, 60_000)
logger.info('backup', 'daily backups scheduled')
}