/* jslint node: true */ 'use strict'; const sysDb = require('./database.js').dbs.system; const { getISOTimestampString } = require('./database.js'); const Errors = require('./enig_error.js'); const SysProps = require('./system_property.js'); const UserProps = require('./user_property'); const Message = require('./message'); const { getActiveConnections, AllConnections } = require('./client_connections'); // deps const _ = require('lodash'); const moment = require('moment'); const SysInfo = require('systeminformation'); /* System Event Log & Stats ------------------------ System & user specific: * Events for generating various statistics, logs such as last callers, etc. * Stats such as counters User specific stats are simply an alternate interface to user properties, while system wide entries are handled on their own. Both are read accessible non-blocking making them easily available for MCI codes for example. */ class StatLog { constructor() { this.systemStats = {}; this.lastSysInfoStatsRefresh = 0; } init(cb) { // // Load previous state/values of |this.systemStats| // const self = this; sysDb.each( `SELECT stat_name, stat_value FROM system_stat;`, (err, row) => { if (row) { self.systemStats[row.stat_name] = row.stat_value; } }, err => { return cb(err); } ); } get KeepDays() { return { Forever: -1, }; } get KeepType() { return { Forever: 'forever', Days: 'days', Max: 'max', Count: 'max', }; } get Order() { return { Timestamp: 'timestamp_asc', TimestampAsc: 'timestamp_asc', TimestampDesc: 'timestamp_desc', Random: 'random', }; } setNonPersistentSystemStat(statName, statValue) { this.systemStats[statName] = statValue; } incrementNonPersistentSystemStat(statName, incrementBy) { incrementBy = incrementBy || 1; let newValue = parseInt(this.systemStats[statName]); if (!isNaN(newValue)) { newValue += incrementBy; } else { newValue = incrementBy; } this.setNonPersistentSystemStat(statName, newValue); return newValue; } setSystemStat(statName, statValue, cb) { // live stats this.systemStats[statName] = statValue; // persisted stats sysDb.run( `REPLACE INTO system_stat (stat_name, stat_value) VALUES (?, ?);`, [statName, statValue], err => { // cb optional - callers may fire & forget if (cb) { return cb(err); } } ); } getSystemStat(statName) { const stat = this.systemStats[statName]; // Some stats are refreshed periodically when they are // being accessed (e.g. "looked at"). This is handled async. this._refreshSystemStat(statName); return stat; } getFriendlySystemStat(statName, defaultValue) { return (this.getSystemStat(statName) || defaultValue).toLocaleString(); } getSystemStatNum(statName) { return parseInt(this.getSystemStat(statName)) || 0; } incrementSystemStat(statName, incrementBy, cb) { const newValue = this.incrementNonPersistentSystemStat(statName, incrementBy); return this.setSystemStat(statName, newValue, cb); } // // User specific stats // These are simply convenience methods to the user's properties // setUserStatWithOptions(user, statName, statValue, options, cb) { // note: cb is optional in PersistUserProperty user.persistProperty(statName, statValue, cb); if (!options.noEvent) { const Events = require('./events.js'); // we need to late load currently Events.emit(Events.getSystemEvents().UserStatSet, { user, statName, statValue, }); } } setUserStat(user, statName, statValue, cb) { return this.setUserStatWithOptions(user, statName, statValue, {}, cb); } getUserStat(user, statName) { return user.getProperty(statName); } getUserStatByClient(client, statName) { const stat = this.getUserStat(client.user, statName); this._refreshUserStat(client, statName); return stat; } getUserStatNum(user, statName) { return parseInt(this.getUserStat(user, statName)) || 0; } getUserStatNumByClient(client, statName, ttlSeconds = 10) { const stat = this.getUserStatNum(client.user, statName); this._refreshUserStat(client, statName, ttlSeconds); return stat; } incrementUserStat(user, statName, incrementBy, cb) { incrementBy = incrementBy || 1; const oldValue = user.getPropertyAsNumber(statName) || 0; const newValue = oldValue + incrementBy; this.setUserStatWithOptions(user, statName, newValue, { noEvent: true }, err => { if (!err) { const Events = require('./events.js'); // we need to late load currently Events.emit(Events.getSystemEvents().UserStatIncrement, { user, statName, oldValue, statIncrementBy: incrementBy, statValue: newValue, }); } if (cb) { return cb(err); } }); } // the time "now" in the ISO format we use and love :) get now() { return getISOTimestampString(); } appendSystemLogEntry(logName, logValue, keep, keepType, cb) { sysDb.run( `INSERT INTO system_event_log (timestamp, log_name, log_value) VALUES (?, ?, ?);`, [this.now, logName, logValue], () => { // // Handle keep // if (-1 === keep) { if (cb) { return cb(null); } return; } switch (keepType) { // keep # of days case 'days': sysDb.run( `DELETE FROM system_event_log WHERE log_name = ? AND timestamp <= DATETIME("now", "-${keep} day");`, [logName], err => { // cb optional - callers may fire & forget if (cb) { return cb(err); } } ); break; case 'count': case 'max': // keep max of N/count sysDb.run( `DELETE FROM system_event_log WHERE id IN( SELECT id FROM system_event_log WHERE log_name = ? ORDER BY id DESC LIMIT -1 OFFSET ${keep} );`, [logName], err => { if (cb) { return cb(err); } } ); break; case 'forever': default: // nop break; } } ); } // // Find System Log entry(s) by |filter|: // // - logName: Name of log (required) // - resultType: 'obj' | 'count' (default='obj') // - limit: Limit returned results // - date: exact date to filter against // - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random' // (default='timestamp') // findSystemLogEntries(filter, cb) { return this._findLogEntries('system_event_log', filter, cb); } getSystemLogEntries(logName, order, limit, cb) { if (!cb && _.isFunction(limit)) { cb = limit; limit = 0; } else { limit = limit || 0; } const filter = { logName, order, limit, }; return this.findSystemLogEntries(filter, cb); } appendUserLogEntry(user, logName, logValue, keepDays, cb) { sysDb.run( `INSERT INTO user_event_log (timestamp, user_id, session_id, log_name, log_value) VALUES (?, ?, ?, ?, ?);`, [this.now, user.userId, user.sessionId, logName, logValue], err => { if (err) { if (cb) { cb(err); } return; } // // Handle keepDays // if (-1 === keepDays) { if (cb) { return cb(null); } return; } sysDb.run( `DELETE FROM user_event_log WHERE user_id = ? AND log_name = ? AND timestamp <= DATETIME("now", "-${keepDays} day");`, [user.userId, logName], err => { // cb optional - callers may fire & forget if (cb) { return cb(err); } } ); } ); } initUserEvents(cb) { const systemEventUserLogInit = require('./sys_event_user_log.js'); systemEventUserLogInit(this); return cb(null); } // // Find User Log entry(s) by |filter|: // // - logName: Name of log (required) // - userId: User ID in which to restrict entries to (missing=all) // - sessionId: Session ID in which to restrict entries to (missing=any) // - resultType: 'obj' | 'count' (default='obj') // - limit: Limit returned results // - date: exact date to filter against // - order: 'timestamp' | 'timestamp_asc' | 'timestamp_desc' | 'random' // (default='timestamp') // findUserLogEntries(filter, cb) { return this._findLogEntries('user_event_log', filter, cb); } _refreshSystemStat(statName) { switch (statName) { case SysProps.SystemLoadStats: case SysProps.SystemMemoryStats: return this._refreshSysInfoStats(); case SysProps.ProcessTrafficStats: return this._refreshProcessTrafficStats(); } } _refreshSysInfoStats() { const now = Math.floor(Date.now() / 1000); if (now < this.lastSysInfoStatsRefresh + 5) { return; } this.lastSysInfoStatsRefresh = now; const basicSysInfo = { mem: 'total, free', currentLoad: 'avgLoad, currentLoad', }; SysInfo.get(basicSysInfo) .then(sysInfo => { const memStats = { totalBytes: sysInfo.mem.total, freeBytes: sysInfo.mem.free, }; this.setNonPersistentSystemStat(SysProps.SystemMemoryStats, memStats); const loadStats = { // Not avail on BSD, yet. average: parseFloat( _.get(sysInfo, 'currentLoad.avgLoad', 0).toFixed(2) ), current: parseFloat( _.get(sysInfo, 'currentLoad.currentLoad', 0).toFixed(2) ), }; this.setNonPersistentSystemStat(SysProps.SystemLoadStats, loadStats); }) .catch(err => { // :TODO: log me }); } _refreshProcessTrafficStats() { const trafficStats = getActiveConnections(AllConnections).reduce( (stats, conn) => { stats.ingress += conn.rawSocket.bytesRead; stats.egress += conn.rawSocket.bytesWritten; return stats; }, { ingress: 0, egress: 0 } ); this.setNonPersistentSystemStat(SysProps.ProcessTrafficStats, trafficStats); } _refreshUserStat(client, statName, ttlSeconds) { switch (statName) { case UserProps.NewPrivateMailCount: this._wrapUserRefreshWithCachedTTL( client, statName, this._refreshUserPrivateMailCount, ttlSeconds ); break; case UserProps.NewAddressedToMessageCount: this._wrapUserRefreshWithCachedTTL( client, statName, this._refreshUserNewAddressedToMessageCount, ttlSeconds ); break; } } _wrapUserRefreshWithCachedTTL(client, statName, updateMethod, ttlSeconds) { client.statLogRefreshCache = client.statLogRefreshCache || new Map(); const now = Math.floor(Date.now() / 1000); const old = client.statLogRefreshCache.get(statName) || 0; if (now < old + ttlSeconds) { return; } updateMethod(client); client.statLogRefreshCache.set(statName, now); } _refreshUserPrivateMailCount(client) { const MsgArea = require('./message_area'); MsgArea.getNewMessageCountInAreaForUser( client.user.userId, Message.WellKnownAreaTags.Private, (err, count) => { if (!err) { client.user.setProperty(UserProps.NewPrivateMailCount, count); } } ); } _refreshUserNewAddressedToMessageCount(client) { const MsgArea = require('./message_area'); MsgArea.getNewMessageCountAddressedToUser(client, (err, count) => { if (!err) { client.user.setProperty(UserProps.NewAddressedToMessageCount, count); } }); } _findLogEntries(logTable, filter, cb) { filter = filter || {}; if (!_.isString(filter.logName)) { return cb(Errors.MissingParam('filter.logName is required')); } filter.resultType = filter.resultType || 'obj'; filter.order = filter.order || 'timestamp'; let sql; if ('count' === filter.resultType) { sql = `SELECT COUNT() AS count FROM ${logTable}`; } else { sql = `SELECT timestamp, log_value FROM ${logTable}`; } sql += ' WHERE log_name = ?'; if (_.isNumber(filter.userId)) { sql += ` AND user_id = ${filter.userId}`; } if (filter.sessionId) { sql += ` AND session_id = ${filter.sessionId}`; } if (filter.date) { filter.date = moment(filter.date); sql += ` AND DATE(timestamp, "localtime") = DATE("${filter.date.format( 'YYYY-MM-DD' )}")`; } if ('count' !== filter.resultType) { switch (filter.order) { case 'timestamp': case 'timestamp_asc': sql += ' ORDER BY timestamp ASC'; break; case 'timestamp_desc': sql += ' ORDER BY timestamp DESC'; break; case 'random': sql += ' ORDER BY RANDOM()'; break; } } if (_.isNumber(filter.limit) && 0 !== filter.limit) { sql += ` LIMIT ${filter.limit}`; } sql += ';'; if ('count' === filter.resultType) { sysDb.get(sql, [filter.logName], (err, row) => { return cb(err, row ? row.count : 0); }); } else { sysDb.all(sql, [filter.logName], (err, rows) => { return cb(err, rows); }); } } } module.exports = new StatLog();