* New StatLog: Replaces various logs, system props, etc. into one class/methods

* Uew StatLog for last callers
* Use new StatLog for +op props
* Use new StatLog for user props such as posts & MCI to access such
* Use StatLog for various new MCI codes for +op
* Misc missing MCI codes
This commit is contained in:
Bryan Ashby 2016-07-27 21:44:27 -06:00
parent d4ce574be3
commit 8787703989
9 changed files with 191 additions and 176 deletions

View File

@ -149,49 +149,58 @@ function initialize(cb) {
function initDatabases(callback) { function initDatabases(callback) {
database.initializeDatabases(callback); database.initializeDatabases(callback);
}, },
function initSystemProperties(callback) { function initStatLog(callback) {
require('./system_property.js').loadSystemProperties(callback); require('./stat_log.js').init(callback);
}, },
function initThemes(callback) { function initThemes(callback) {
// Have to pull in here so it's after Config init // Have to pull in here so it's after Config init
var theme = require('./theme.js'); require('./theme.js').initAvailableThemes(function onThemesInit(err, themeCount) {
theme.initAvailableThemes(function onThemesInit(err, themeCount) {
logger.log.info({ themeCount : themeCount }, 'Themes initialized'); logger.log.info({ themeCount : themeCount }, 'Themes initialized');
callback(err); callback(err);
}); });
}, },
function loadSysOpInformation(callback) { function loadSysOpInformation2(callback) {
// //
// If user 1 has been created, we have a SysOp. Cache some information // Copy over some +op information from the user DB -> system propertys.
// into Config. // * Makes this accessible for MCI codes, easy non-blocking access, etc.
// * We do this every time as the op is free to change this information just
// like any other user
// //
var user = require('./user.js'); // must late load const user = require('./user.js');
user.getUserName(1, function unLoaded(err, sysOpUsername) { async.waterfall(
if(err) { [
callback(null); // non-fatal here function getOpUserName(next) {
} else { return user.getUserName(1, next);
// },
// Load some select properties to cache function getOpProps(opUserName, next) {
// const propLoadOpts = {
var propLoadOpts = { userId : 1,
userId : 1, names : [ 'real_name', 'sex', 'email_address', 'location', 'affiliation' ],
names : [ 'real_name', 'sex', 'email_address' ], };
}; user.loadProperties(propLoadOpts, (err, opProps) => {
return next(err, opUserName, opProps);
});
}
],
(err, opUserName, opProps) => {
const StatLog = require('./stat_log.js');
user.loadProperties(propLoadOpts, function propsLoaded(err, props) { if(err) {
if(!err) { [ 'username', 'real_name', 'sex', 'email_address', 'location', 'affiliation' ].forEach(v => {
conf.config.general.sysOp = { StatLog.setNonPeristentSystemStat(`sysop_${v}`, 'N/A');
username : sysOpUsername, });
properties : props, } else {
}; opProps.username = opUserName;
logger.log.info( { sysOp : conf.config.general.sysOp }, 'System Operator information cached'); _.each(opProps, (v, k) => {
} StatLog.setNonPeristentSystemStat(`sysop_${k}`, v);
callback(null); // any error is again, non-fatal here });
}); }
return callback(null);
} }
}); );
}, },
function readyMessageNetworkSupport(callback) { function readyMessageNetworkSupport(callback) {
require('./msg_network.js').startup(callback); require('./msg_network.js').startup(callback);

View File

@ -100,26 +100,36 @@ function createSystemTables() {
dbs.system.run('PRAGMA foreign_keys = ON;'); dbs.system.run('PRAGMA foreign_keys = ON;');
// Various stat/event logging - see stat_log.js
dbs.system.run( dbs.system.run(
'CREATE TABLE IF NOT EXISTS system_property (' + `CREATE TABLE IF NOT EXISTS system_stat (
' prop_name VARCHAR PRIMARY KEY NOT NULL,' + stat_name VARCHAR PRIMARY KEY NOT NULL,
' prop_value VARCHAR NOT NULL' + stat_value VARCHAR NOT NULL
');' );`
); );
//
// system_log can round log_timestamp for daily, monthly, etc.
// statistics as well as unique entries.
//
/*
dbs.system.run( dbs.system.run(
'CREATE TABLE IF NOT EXISTS system_log (' + `CREATE TABLE IF NOT EXISTS system_event_log (
' log_timestamp DATETIME PRIMARY KEY NOT NULL ( ' + id INTEGER PRIMARY KEY,
' log_name VARCHARNOT NULL,' + timestamp DATETIME NOT NULL,
' log_value VARCHAR NOT NULL,' + log_name VARCHAR NOT NULL,
' UNIQUE(log_timestamp, log_name)' + log_value VARCHAR NOT NULL,
');'
);*/ UNIQUE(timestamp, log_name)
);`
);
dbs.system.run(
`CREATE TABLE IF NOT EXISTS user_event_log (
id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL,
user_id INTEGER NOT NULL,
log_name VARCHAR NOT NULL,
log_value VARCHAR NOT NULL,
UNIQUE(timestamp, user_id, log_name)
);`
);
} }
function createUserTables() { function createUserTables() {

View File

@ -2,11 +2,11 @@
'use strict'; 'use strict';
var Config = require('./config.js').config; var Config = require('./config.js').config;
const StatLog = require('./stat_log.js');
var fs = require('fs'); var fs = require('fs');
var paths = require('path'); var paths = require('path');
var _ = require('lodash'); var _ = require('lodash');
var async = require('async');
var moment = require('moment'); var moment = require('moment');
var iconv = require('iconv-lite'); var iconv = require('iconv-lite');
@ -121,7 +121,7 @@ function DropFile(client, fileType) {
moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate" moment(up.birthdate).format('MM/DD/YY'), // "Caller's Birthdate"
'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)" 'X:\\MAIN\\', // "Path to the MAIN directory (where User File is)"
'X:\\GEN\\', // "Path to the GEN directory" 'X:\\GEN\\', // "Path to the GEN directory"
Config.general.sysOp.username, // "Sysop's Name (name BBS refers to Sysop as)" StatLog.getSystemStat('sysop_username'), // "Sysop's Name (name BBS refers to Sysop as)"
self.client.user.username, // "Alias name" self.client.user.username, // "Alias name"
'00:05', // "Event time (hh:mm)" (note: wat?) '00:05', // "Event time (hh:mm)" (note: wat?)
'Y', // "If its an error correcting connection (Y/N)" 'Y', // "If its an error correcting connection (Y/N)"
@ -143,7 +143,7 @@ function DropFile(client, fileType) {
'0', // "Total Doors Opened" '0', // "Total Doors Opened"
'0', // "Total Messages Left" '0', // "Total Messages Left"
].join('\r\n') + '\r\n', 'cp437'); ].join('\r\n') + '\r\n', 'cp437');
}; };
this.getDoor32Buffer = function() { this.getDoor32Buffer = function() {
@ -178,7 +178,7 @@ function DropFile(client, fileType) {
// //
// Note that usernames are just used for first/last names here // Note that usernames are just used for first/last names here
// //
var opUn = /[^\s]*/.exec(Config.general.sysOp.username)[0]; var opUn = /[^\s]*/.exec(StatLog.getSystemStat('sysop_username'))[0];
var un = /[^\s]*/.exec(self.client.user.username)[0]; var un = /[^\s]*/.exec(self.client.user.username)[0];
var secLevel = self.client.user.getLegacySecurityLevel().toString(); var secLevel = self.client.user.getLegacySecurityLevel().toString();

View File

@ -1,6 +1,7 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
// ENiGMA½
const MenuModule = require('../core/menu_module.js').MenuModule; const MenuModule = require('../core/menu_module.js').MenuModule;
const ViewController = require('../core/view_controller.js').ViewController; const ViewController = require('../core/view_controller.js').ViewController;
const ansi = require('../core/ansi_term.js'); const ansi = require('../core/ansi_term.js');
@ -10,7 +11,10 @@ const getMessageAreaByTag = require('../core/message_area.js').getMessageAreaB
const updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId; const updateMessageAreaLastReadId = require('../core/message_area.js').updateMessageAreaLastReadId;
const getUserIdAndName = require('../core/user.js').getUserIdAndName; const getUserIdAndName = require('../core/user.js').getUserIdAndName;
const cleanControlCodes = require('../core/string_util.js').cleanControlCodes; const cleanControlCodes = require('../core/string_util.js').cleanControlCodes;
const StatLog = require('./stat_log.js');
// deps
const async = require('async'); const async = require('async');
const assert = require('assert'); const assert = require('assert');
const _ = require('lodash'); const _ = require('lodash');
@ -295,6 +299,21 @@ function FullScreenEditorModule(options) {
); );
}; };
this.updateUserStats = function(cb) {
if(Message.isPrivateAreaTag(this.message.areaTag)) {
if(cb) {
return cb(null);
}
}
StatLog.incrementUserStat(
self.client.user,
'post_count',
1,
cb
);
};
this.redrawFooter = function(options, cb) { this.redrawFooter = function(options, cb) {
async.waterfall( async.waterfall(
[ [

View File

@ -2,12 +2,12 @@
'use strict'; 'use strict';
// ENiGMA½ // ENiGMA½
const Config = require('./config.js').config; const Config = require('./config.js').config;
const Log = require('./logger.js').log; const Log = require('./logger.js').log;
const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag; const getMessageAreaByTag = require('./message_area.js').getMessageAreaByTag;
const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag; const getMessageConferenceByTag = require('./message_area.js').getMessageConferenceByTag;
const clientConnections = require('./client_connections.js'); const clientConnections = require('./client_connections.js');
const sysProp = require('./system_property.js'); const StatLog = require('./stat_log.js');
// deps // deps
const packageJson = require('../package.json'); const packageJson = require('../package.json');
@ -34,9 +34,14 @@ function getPredefinedMCIValue(client, code) {
VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; }, VL : function versionLabel() { return 'ENiGMA½ v' + packageJson.version; },
VN : function version() { return packageJson.version; }, VN : function version() { return packageJson.version; },
// :TODO: SysOp username // +op info
// :TODO: SysOp real name SN : function opUserName() { return StatLog.getSystemStat('sysop_username'); },
SR : function opRealName() { return StatLog.getSystemStat('sysop_real_name'); },
SL : function opLocation() { return StatLog.getSystemStat('sysop_location'); },
SA : function opAffils() { return StatLog.getSystemStat('sysop_affiliation'); },
SS : function opSex() { return StatLog.getSystemStat('sysop_sex'); },
SE : function opEmail() { return StatLog.getSystemStat('sysop_email_address'); },
// :TODO: op age, web, ?????
// //
// Current user / session // Current user / session
@ -60,6 +65,16 @@ function getPredefinedMCIValue(client, code) {
MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); }, MS : function accountCreated() { return moment(client.user.properties.account_created).format(client.currentTheme.helpers.getDateFormat()); },
CS : function currentStatus() { return client.currentStatus; }, CS : function currentStatus() { return client.currentStatus; },
PS : function userPostCount() {
const postCount = client.user.properties.post_count || 0;
return postCount.toString();
},
PC : function userPostCallRatio() {
const postCount = client.user.properties.post_count || 0;
const callCount = client.user.properties.login_count;
const ratio = ~~((postCount / callCount) * 100);
return `${ratio}%`;
},
MD : function currentMenuDescription() { MD : function currentMenuDescription() {
return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : ''; return _.has(client, 'currentMenuModule.menuConfig.desc') ? client.currentMenuModule.menuConfig.desc : '';
@ -73,11 +88,14 @@ function getPredefinedMCIValue(client, code) {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag); const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.name : ''; return conf ? conf.name : '';
}, },
ML : function messageAreaDescription() { ML : function messageAreaDescription() {
const area = getMessageAreaByTag(client.user.properties.message_area_tag); const area = getMessageAreaByTag(client.user.properties.message_area_tag);
return area ? area.desc : ''; return area ? area.desc : '';
}, },
CM : function messageConfDescription() {
const conf = getMessageConferenceByTag(client.user.properties.message_conf_tag);
return conf ? conf.desc : '';
},
SH : function termHeight() { return client.term.termHeight.toString(); }, SH : function termHeight() { return client.term.termHeight.toString(); },
SW : function termWidth() { return client.term.termWidth.toString(); }, SW : function termWidth() { return client.term.termWidth.toString(); },
@ -103,7 +121,7 @@ function getPredefinedMCIValue(client, code) {
}, },
OA : function systemArchitecture() { return os.arch(); }, OA : function systemArchitecture() { return os.arch(); },
SC : function systemCpuModel() { SC : function systemCpuModel() {
// //
// Clean up CPU strings a bit for better display // Clean up CPU strings a bit for better display
// //
@ -114,11 +132,11 @@ function getPredefinedMCIValue(client, code) {
// :TODO: MCI for core count, e.g. os.cpus().length // :TODO: MCI for core count, e.g. os.cpus().length
// :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage // :TODO: cpu load average (over N seconds): http://stackoverflow.com/questions/9565912/convert-the-output-of-os-cpus-in-node-js-to-percentage
// :TODO: Node version/info NV : function nodeVersion() { return process.version; },
AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); }, AN : function activeNodes() { return clientConnections.getActiveConnections().length.toString(); },
TC : function totalCalls() { return sysProp.getSystemProperty('login_count').toString(); }, TC : function totalCalls() { return StatLog.getSystemStat('login_count').toString(); },
// //
// Special handling for XY // Special handling for XY

View File

@ -358,28 +358,6 @@ User.prototype.persistAllProperties = function(cb) {
assert(this.userId > 0); assert(this.userId > 0);
this.persistProperties(this.properties, cb); this.persistProperties(this.properties, cb);
/*
var self = this;
var stmt = userDb.prepare(
'REPLACE INTO user_property (user_id, prop_name, prop_value) ' +
'VALUES (?, ?, ?);');
async.each(Object.keys(this.properties), function property(propName, callback) {
stmt.run(self.userId, propName, self.properties[propName], function onRun(err) {
callback(err);
});
}, function complete(err) {
if(err) {
cb(err);
} else {
stmt.finalize(function finalized() {
cb(null);
});
}
});
*/
}; };
User.prototype.setNewAuthCredentials = function(password, cb) { User.prototype.setNewAuthCredentials = function(password, cb) {

View File

@ -1,16 +1,15 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var setClientTheme = require('./theme.js').setClientTheme; // ENiGMA½
var clientConnections = require('./client_connections.js').clientConnections; const setClientTheme = require('./theme.js').setClientTheme;
var userDb = require('./database.js').dbs.user; const clientConnections = require('./client_connections.js').clientConnections;
var sysProp = require('./system_property.js'); const userDb = require('./database.js').dbs.user;
var logger = require('./logger.js'); const StatLog = require('./stat_log.js');
var Config = require('./config.js').config; const logger = require('./logger.js');
var async = require('async'); // deps
var _ = require('lodash'); const async = require('async');
var assert = require('assert');
exports.userLogin = userLogin; exports.userLogin = userLogin;
@ -24,8 +23,8 @@ function userLogin(client, username, password, cb) {
cb(err); cb(err);
} else { } else {
var now = new Date(); const now = new Date();
var user = client.user; const user = client.user;
// //
// Ensure this user is not already logged in. // Ensure this user is not already logged in.
@ -60,46 +59,21 @@ function userLogin(client, username, password, cb) {
async.parallel( async.parallel(
[ [
function setTheme(callback) { function setTheme(callback) {
setClientTheme(client, user.properties.theme_id); setClientTheme(client, user.properties.theme_id);
callback(null); callback(null);
}, },
function updateSystemLoginCount(callback) { function updateSystemLoginCount(callback) {
var sysLoginCount = sysProp.getSystemProperty('login_count') || 0; StatLog.incrementSystemStat('login_count', 1, callback);
sysLoginCount = parseInt(sysLoginCount, 10) + 1;
sysProp.persistSystemProperty('login_count', sysLoginCount, callback);
}, },
function recordLastLogin(callback) { function recordLastLogin(callback) {
user.persistProperty('last_login_timestamp', now.toISOString(), function persisted(err) { StatLog.setUserStat(user, 'last_login_timestamp', StatLog.now, callback);
callback(err);
});
}, },
function updateUserLoginCount(callback) { function updateUserLoginCount(callback) {
if(!user.properties.login_count) { StatLog.incrementUserStat(user, 'login_count', 1, callback);
user.properties.login_count = 1;
} else {
user.properties.login_count++;
}
user.persistProperty('login_count', user.properties.login_count, function persisted(err) {
callback(err);
});
}, },
function recordLoginHistory(callback) { function recordLoginHistory(callback) {
userDb.serialize(function serialized() { StatLog.appendSystemLogEntry('user_login_history', user.userId, 30, callback);
userDb.run(
'INSERT INTO user_login_history (user_id, user_name, timestamp) ' +
'VALUES(?, ?, ?);', [ user.userId, user.username, now.toISOString() ]
);
// keep 30 days of records
userDb.run(
'DELETE FROM user_login_history ' +
'WHERE timestamp <= DATETIME("now", "-30 day");'
);
});
callback(null);
} }
], ],
function complete(err) { function complete(err) {

View File

@ -1,15 +1,17 @@
/* jslint node: true */ /* jslint node: true */
'use strict'; 'use strict';
var MenuModule = require('../core/menu_module.js').MenuModule; // ENiGMA½
var userDb = require('../core/database.js').dbs.user; const MenuModule = require('../core/menu_module.js').MenuModule;
var ViewController = require('../core/view_controller.js').ViewController; const ViewController = require('../core/view_controller.js').ViewController;
var getSystemLoginHistory = require('../core/stats.js').getSystemLoginHistory; const StatLog = require('../core/stat_log.js');
const getUserName = require('../core/user.js').getUserName;
const loadProperties = require('../core/user.js').loadProperties;
var moment = require('moment'); // deps
var async = require('async'); const moment = require('moment');
var assert = require('assert'); const async = require('async');
var _ = require('lodash'); const _ = require('lodash');
/* /*
Available listFormat object members: Available listFormat object members:
@ -41,11 +43,11 @@ function LastCallersModule(options) {
require('util').inherits(LastCallersModule, MenuModule); require('util').inherits(LastCallersModule, MenuModule);
LastCallersModule.prototype.mciReady = function(mciData, cb) { LastCallersModule.prototype.mciReady = function(mciData, cb) {
var self = this; const self = this;
var vc = self.viewControllers.allViews = new ViewController( { client : self.client } ); const vc = self.viewControllers.allViews = new ViewController( { client : self.client } );
var loginHistory; let loginHistory;
var callersView; let callersView;
async.series( async.series(
[ [
@ -53,7 +55,7 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) {
LastCallersModule.super_.prototype.mciReady.call(self, mciData, callback); LastCallersModule.super_.prototype.mciReady.call(self, mciData, callback);
}, },
function loadFromConfig(callback) { function loadFromConfig(callback) {
var loadOpts = { const loadOpts = {
callingMenu : self, callingMenu : self,
mciMap : mciData.menu, mciMap : mciData.menu,
noInput : true, noInput : true,
@ -64,51 +66,53 @@ LastCallersModule.prototype.mciReady = function(mciData, cb) {
function fetchHistory(callback) { function fetchHistory(callback) {
callersView = vc.getView(MciCodeIds.CallerList); callersView = vc.getView(MciCodeIds.CallerList);
getSystemLoginHistory(callersView.dimens.height, function historyRetrieved(err, lh) { StatLog.getSystemLogEntries('user_login_history', 'timestamp_desc', callersView.dimens.height, (err, lh) => {
loginHistory = lh; loginHistory = lh;
callback(err); return callback(err);
}); });
}, },
function fetchUserProperties(callback) { function getUserNamesAndProperties(callback) {
async.each(loginHistory, function entry(histEntry, next) { const getPropOpts = {
userDb.each( names : [ 'location', 'affiliation' ]
'SELECT prop_name, prop_value ' + };
'FROM user_property ' +
'WHERE user_id=? AND (prop_name="location" OR prop_name="affiliation");', const dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
[ histEntry.userId ],
function propRow(err, propEntry) { async.each(
histEntry[propEntry.prop_name] = propEntry.prop_value; loginHistory,
}, (item, next) => {
function complete(err) { item.userId = parseInt(item.log_value);
next(); item.ts = moment(item.timestamp).format(dateTimeFormat);
}
); getUserName(item.userId, (err, userName) => {
}, function complete(err) { item.userName = userName;
callback(err); getPropOpts.userId = item.userId;
});
loadProperties(getPropOpts, (err, props) => {
if(!err) {
item.location = props.location;
item.affiliation = item.affils = props.affiliation;
}
return next();
});
});
},
callback
);
}, },
function populateList(callback) { function populateList(callback) {
var listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affils} - {ts}'; const listFormat = self.menuConfig.config.listFormat || '{userName} - {location} - {affils} - {ts}';
var dateTimeFormat = self.menuConfig.config.dateTimeFormat || 'ddd MMM DD';
callersView.setItems(_.map(loginHistory, function formatCallEntry(ce) { callersView.setItems(_.map(loginHistory, ce => listFormat.format(ce) ) );
return listFormat.format({
userId : ce.userId,
userName : ce.userName,
ts : moment(ce.timestamp).format(dateTimeFormat),
location : ce.location,
affils : ce.affiliation,
});
}));
// :TODO: This is a hack until pipe codes are better implemented // :TODO: This is a hack until pipe codes are better implemented
callersView.focusItems = callersView.items; callersView.focusItems = callersView.items;
callersView.redraw(); callersView.redraw();
callback(null); return callback(null);
} }
], ],
function complete(err) { (err) => {
if(err) { if(err) {
self.client.log.error( { error : err.toString() }, 'Error loading last callers'); self.client.log.error( { error : err.toString() }, 'Error loading last callers');
} }

View File

@ -38,6 +38,9 @@ function AreaPostFSEModule(options) {
}, },
function saveMessage(callback) { function saveMessage(callback) {
return persistMessage(msg, callback); return persistMessage(msg, callback);
},
function updateStats(callback) {
self.updateUserStats(callback);
} }
], ],
function complete(err) { function complete(err) {