feat: P0 backup system (WAL+snapshot+restore+verify), monitoring stack, admin health API
This commit is contained in:
91
memento-note/app/api/admin/health/route.ts
Normal file
91
memento-note/app/api/admin/health/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getPrisma } from '@/lib/prisma'
|
||||
import { redis } from '@/lib/redis'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const start = Date.now()
|
||||
const checks: Record<string, { status: string; latency?: string; error?: string; [k: string]: unknown }> = {}
|
||||
|
||||
// Database check
|
||||
try {
|
||||
const dbStart = Date.now()
|
||||
const prisma = getPrisma()
|
||||
const [noteCount, notebookCount, userCount] = await Promise.all([
|
||||
prisma.note.count(),
|
||||
prisma.notebook.count(),
|
||||
prisma.user.count(),
|
||||
])
|
||||
checks.database = {
|
||||
status: 'healthy',
|
||||
latency: `${Date.now() - dbStart}ms`,
|
||||
notes: noteCount,
|
||||
notebooks: notebookCount,
|
||||
users: userCount,
|
||||
}
|
||||
} catch (e) {
|
||||
checks.database = { status: 'unhealthy', error: e instanceof Error ? e.message : 'Unknown error' }
|
||||
}
|
||||
|
||||
// Redis check
|
||||
try {
|
||||
const redisStart = Date.now()
|
||||
await redis.ping()
|
||||
const info = await redis.info('memory')
|
||||
const dbSize = await redis.dbsize()
|
||||
const memMatch = info.match(/used_memory_human:(\S+)/)
|
||||
checks.redis = {
|
||||
status: 'healthy',
|
||||
latency: `${Date.now() - redisStart}ms`,
|
||||
keys: dbSize,
|
||||
memory: memMatch ? memMatch[1] : 'unknown',
|
||||
}
|
||||
} catch (e) {
|
||||
checks.redis = { status: 'unhealthy', error: e instanceof Error ? e.message : 'Unknown error' }
|
||||
}
|
||||
|
||||
// AI providers check
|
||||
try {
|
||||
const { getAIProvider, getChatProvider } = await import('@/lib/ai/providers/registry')
|
||||
const embeddingProvider = getAIProvider()
|
||||
const chatProvider = getChatProvider()
|
||||
checks.ai = {
|
||||
status: 'configured',
|
||||
embedding: { provider: embeddingProvider },
|
||||
chat: { provider: chatProvider },
|
||||
}
|
||||
} catch (e) {
|
||||
checks.ai = { status: 'unhealthy', error: e instanceof Error ? e.message : 'Unknown error' }
|
||||
}
|
||||
|
||||
// Disk check
|
||||
try {
|
||||
const { execSync } = await import('child_process')
|
||||
const diskInfo = execSync("df -h /opt/memento | awk 'NR==2{print $2,$3,$4,$5}'").toString().trim()
|
||||
const [total, used, available, percent] = diskInfo.split(/\s+/)
|
||||
checks.storage = {
|
||||
status: parseInt(percent) > 90 ? 'warning' : 'healthy',
|
||||
total,
|
||||
used,
|
||||
available,
|
||||
usagePercent: percent,
|
||||
}
|
||||
} catch {
|
||||
checks.storage = { status: 'unknown' }
|
||||
}
|
||||
|
||||
const allHealthy = Object.values(checks).every(c => c.status === 'healthy' || c.status === 'configured')
|
||||
const hasDegraded = Object.values(checks).some(c => c.status === 'warning')
|
||||
|
||||
return NextResponse.json({
|
||||
status: allHealthy ? (hasDegraded ? 'degraded' : 'healthy') : 'unhealthy',
|
||||
uptime: process.uptime(),
|
||||
version: process.env.npm_package_version || '0.2.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
responseTime: `${Date.now() - start}ms`,
|
||||
components: checks,
|
||||
}, {
|
||||
status: allHealthy ? 200 : 503,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user