← back to Dear Bubbe Admin
backend/services/MonitoringService.js
544 lines
const fs = require('fs')
const path = require('path')
const axios = require('axios')
const moment = require('moment')
const os = require('os')
const User = require('../models/User')
const Session = require('../models/Session')
const Conversation = require('../models/Conversation')
class MonitoringService {
constructor(io, logger, alertService) {
this.io = io
this.logger = logger
this.alertService = alertService
this.currentStats = {
activeUsers: 0,
activeSessions: 0,
totalConversations: 0,
alertsToday: 0,
systemHealth: 'healthy',
lastUpdate: new Date()
}
this.systemMetrics = {}
this.isMonitoring = false
// Track real-time user activity
this.activeUsers = new Set()
this.activeSessions = new Map()
this.realtimeStats = {
messagesPerMinute: 0,
avgResponseTime: 0,
errorRate: 0
}
this.logger.info('MonitoringService initialized')
}
startMonitoring() {
if (this.isMonitoring) return
this.isMonitoring = true
this.logger.info('Starting real-time monitoring')
// Monitor every 30 seconds
this.monitoringInterval = setInterval(() => {
this.collectSystemMetrics()
this.updateUserStats()
this.broadcastStats()
}, 30000)
// Monitor Dear Bubbe API health every minute
this.healthCheckInterval = setInterval(() => {
this.checkBubbeHealth()
}, 60000)
// Generate detailed reports every 5 minutes
this.reportInterval = setInterval(() => {
this.generateDetailedReport()
}, 5 * 60000)
// Initial collection
this.collectSystemMetrics()
this.updateUserStats()
}
stopMonitoring() {
this.isMonitoring = false
if (this.monitoringInterval) clearInterval(this.monitoringInterval)
if (this.healthCheckInterval) clearInterval(this.healthCheckInterval)
if (this.reportInterval) clearInterval(this.reportInterval)
this.logger.info('Monitoring stopped')
}
async collectSystemMetrics() {
try {
// System metrics
const totalMem = os.totalmem()
const freeMem = os.freemem()
const usedMem = totalMem - freeMem
const memUsagePercent = (usedMem / totalMem) * 100
const loadAvg = os.loadavg()
const cpuCount = os.cpus().length
const cpuUsage = (loadAvg[0] / cpuCount) * 100
// Process metrics
const processMemory = process.memoryUsage()
const uptime = process.uptime()
this.systemMetrics = {
timestamp: new Date(),
system: {
platform: os.platform(),
arch: os.arch(),
hostname: os.hostname(),
uptime: os.uptime(),
totalMemory: totalMem,
freeMemory: freeMem,
memoryUsage: memUsagePercent,
cpuUsage: Math.min(cpuUsage, 100),
loadAverage: loadAvg
},
process: {
pid: process.pid,
uptime: uptime,
memory: processMemory,
memoryUsageMB: Math.round(processMemory.rss / 1024 / 1024),
version: process.version
}
}
// Check for high resource usage
if (memUsagePercent > 90) {
this.createSystemAlert('high_memory_usage', {
usage: memUsagePercent,
threshold: 90
})
}
if (cpuUsage > 80) {
this.createSystemAlert('high_cpu_usage', {
usage: cpuUsage,
threshold: 80
})
}
} catch (error) {
this.logger.error('Failed to collect system metrics:', error)
}
}
async updateUserStats() {
try {
const now = moment()
const today = now.startOf('day')
// Active users (last 5 minutes)
const activeUsers = await Session.countDocuments({
lastActivity: { $gte: moment().subtract(5, 'minutes').toDate() }
})
// Active sessions (last 30 minutes)
const activeSessions = await Session.countDocuments({
lastActivity: { $gte: moment().subtract(30, 'minutes').toDate() },
ended: { $ne: true }
})
// Total conversations today
const conversationsToday = await Conversation.countDocuments({
timestamp: { $gte: today.toDate() }
})
// Alerts today
const alertsToday = await this.alertService.Alert?.countDocuments?.({
timestamp: { $gte: today.toDate() }
}) || 0
// User registrations today
const registrationsToday = await User.countDocuments({
createdAt: { $gte: today.toDate() }
})
// Update current stats
this.currentStats = {
activeUsers,
activeSessions,
totalConversations: conversationsToday,
alertsToday,
registrationsToday,
systemHealth: this.calculateSystemHealth(),
lastUpdate: new Date()
}
} catch (error) {
this.logger.error('Failed to update user stats:', error)
}
}
calculateSystemHealth() {
const { system } = this.systemMetrics
if (!system) return 'unknown'
const memUsage = system.memoryUsage || 0
const cpuUsage = system.cpuUsage || 0
if (memUsage > 90 || cpuUsage > 90) return 'critical'
if (memUsage > 70 || cpuUsage > 70) return 'warning'
return 'healthy'
}
async checkBubbeHealth() {
try {
const startTime = Date.now()
const response = await axios.get('http://localhost:3011/api/health', {
timeout: 10000
})
const responseTime = Date.now() - startTime
if (response.status === 200) {
this.logger.debug('Dear Bubbe API health check passed')
this.realtimeStats.avgResponseTime = responseTime
} else {
this.createSystemAlert('api_health_failed', {
status: response.status,
responseTime
})
}
} catch (error) {
this.logger.error('Dear Bubbe API health check failed:', error.message)
this.createSystemAlert('api_unreachable', {
error: error.message,
timestamp: new Date()
})
}
}
async generateDetailedReport() {
try {
// Get hourly stats for the last 24 hours
const hourlyStats = await this.getHourlyStats()
// Get top active users
const topUsers = await this.getTopActiveUsers()
// Get conversation topics breakdown
const topicBreakdown = await this.getConversationTopics()
// Get geographic distribution
const geoStats = await this.getGeographicStats()
// Alert trends
const alertTrends = await this.alertService.getAlertStats('24h')
const report = {
timestamp: new Date(),
period: 'last_24_hours',
systemMetrics: this.systemMetrics,
userActivity: {
hourlyStats,
topUsers,
topicBreakdown,
geoStats
},
alertTrends,
realtimeStats: this.realtimeStats
}
// Broadcast to admin clients
this.io.to('admin').emit('detailed_report', report)
// Save report to file for historical analysis
this.saveReportToFile(report)
} catch (error) {
this.logger.error('Failed to generate detailed report:', error)
}
}
async getHourlyStats() {
try {
const last24Hours = moment().subtract(24, 'hours')
const hourlyConversations = await Conversation.aggregate([
{
$match: {
timestamp: { $gte: last24Hours.toDate() }
}
},
{
$group: {
_id: {
year: { $year: '$timestamp' },
month: { $month: '$timestamp' },
day: { $dayOfMonth: '$timestamp' },
hour: { $hour: '$timestamp' }
},
count: { $sum: 1 },
uniqueUsers: { $addToSet: '$userId' }
}
},
{
$addFields: {
uniqueUserCount: { $size: '$uniqueUsers' }
}
},
{
$sort: { '_id.year': 1, '_id.month': 1, '_id.day': 1, '_id.hour': 1 }
}
])
return hourlyConversations
} catch (error) {
this.logger.error('Failed to get hourly stats:', error)
return []
}
}
async getTopActiveUsers(limit = 20) {
try {
const last24Hours = moment().subtract(24, 'hours')
const topUsers = await Conversation.aggregate([
{
$match: {
timestamp: { $gte: last24Hours.toDate() }
}
},
{
$group: {
_id: '$userId',
messageCount: { $sum: 1 },
lastActivity: { $max: '$timestamp' },
topics: { $addToSet: '$topics' }
}
},
{
$sort: { messageCount: -1 }
},
{
$limit: limit
}
])
return topUsers
} catch (error) {
this.logger.error('Failed to get top active users:', error)
return []
}
}
async getConversationTopics() {
try {
const last24Hours = moment().subtract(24, 'hours')
const topics = await Conversation.aggregate([
{
$match: {
timestamp: { $gte: last24Hours.toDate() }
}
},
{
$unwind: '$topics'
},
{
$group: {
_id: '$topics',
count: { $sum: 1 }
}
},
{
$sort: { count: -1 }
},
{
$limit: 10
}
])
return topics
} catch (error) {
this.logger.error('Failed to get conversation topics:', error)
return []
}
}
async getGeographicStats() {
try {
const last24Hours = moment().subtract(24, 'hours')
const geoStats = await Session.aggregate([
{
$match: {
startTime: { $gte: last24Hours.toDate() }
}
},
{
$group: {
_id: {
country: '$location.country',
state: '$location.state'
},
sessions: { $sum: 1 },
uniqueUsers: { $addToSet: '$userId' }
}
},
{
$addFields: {
userCount: { $size: '$uniqueUsers' }
}
},
{
$sort: { sessions: -1 }
}
])
return geoStats
} catch (error) {
this.logger.error('Failed to get geographic stats:', error)
return []
}
}
saveReportToFile(report) {
try {
const reportsDir = path.join(__dirname, '../reports')
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true })
}
const filename = `report_${moment().format('YYYY-MM-DD_HH-mm-ss')}.json`
const filepath = path.join(reportsDir, filename)
fs.writeFileSync(filepath, JSON.stringify(report, null, 2))
// Clean up old reports (keep only last 30 days)
this.cleanupOldReports(reportsDir)
} catch (error) {
this.logger.error('Failed to save report to file:', error)
}
}
cleanupOldReports(reportsDir) {
try {
const files = fs.readdirSync(reportsDir)
const cutoff = moment().subtract(30, 'days')
files.forEach(file => {
const filepath = path.join(reportsDir, file)
const stats = fs.statSync(filepath)
if (moment(stats.birthtime).isBefore(cutoff)) {
fs.unlinkSync(filepath)
this.logger.info(`Deleted old report: ${file}`)
}
})
} catch (error) {
this.logger.error('Failed to cleanup old reports:', error)
}
}
async cleanupOldLogs() {
try {
const logsDir = path.join(__dirname, '../logs')
if (!fs.existsSync(logsDir)) return
const files = fs.readdirSync(logsDir)
const cutoff = moment().subtract(7, 'days') // Keep logs for 7 days
files.forEach(file => {
const filepath = path.join(logsDir, file)
const stats = fs.statSync(filepath)
if (moment(stats.birthtime).isBefore(cutoff)) {
fs.unlinkSync(filepath)
this.logger.info(`Deleted old log: ${file}`)
}
})
} catch (error) {
this.logger.error('Failed to cleanup old logs:', error)
}
}
createSystemAlert(type, details) {
const alert = {
type: 'system',
subtype: type,
severity: this.getAlertSeverity(type),
timestamp: new Date(),
details,
systemMetrics: this.systemMetrics
}
// Emit to admin dashboard
this.io.to('admin').emit('system_alert', alert)
this.logger.warn(`System alert: ${type}`, details)
}
getAlertSeverity(type) {
const severityMap = {
'high_memory_usage': 'high',
'high_cpu_usage': 'high',
'api_health_failed': 'medium',
'api_unreachable': 'critical'
}
return severityMap[type] || 'medium'
}
broadcastStats() {
this.io.to('admin').emit('stats', {
...this.currentStats,
systemMetrics: this.systemMetrics,
realtimeStats: this.realtimeStats
})
}
getCurrentStats() {
return {
...this.currentStats,
systemMetrics: this.systemMetrics,
realtimeStats: this.realtimeStats
}
}
trackUserActivity(userId, sessionId, activity) {
this.activeUsers.add(userId)
if (!this.activeSessions.has(sessionId)) {
this.activeSessions.set(sessionId, {
userId,
startTime: new Date(),
lastActivity: new Date(),
messageCount: 0
})
}
const session = this.activeSessions.get(sessionId)
session.lastActivity = new Date()
session.messageCount++
// Clean up inactive sessions (older than 30 minutes)
const cutoff = moment().subtract(30, 'minutes')
for (const [sid, sessionData] of this.activeSessions.entries()) {
if (moment(sessionData.lastActivity).isBefore(cutoff)) {
this.activeSessions.delete(sid)
this.activeUsers.delete(sessionData.userId)
}
}
// Update real-time stats
this.currentStats.activeUsers = this.activeUsers.size
this.currentStats.activeSessions = this.activeSessions.size
}
async generateRealtimeStats() {
// This method is called every 5 minutes to update real-time statistics
await this.updateUserStats()
this.broadcastStats()
}
}
module.exports = MonitoringService