aboutsummaryrefslogtreecommitdiff
path: root/src/server/DashStats.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/DashStats.ts')
-rw-r--r--src/server/DashStats.ts293
1 files changed, 293 insertions, 0 deletions
diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts
new file mode 100644
index 000000000..8d341db63
--- /dev/null
+++ b/src/server/DashStats.ts
@@ -0,0 +1,293 @@
+import { cyan, magenta } from 'colors';
+import { Response } from 'express';
+import SocketIO from 'socket.io';
+import { timeMap } from './ApiManagers/UserManager';
+import { WebSocket } from './websocket';
+const fs = require('fs');
+
+/**
+ * DashStats focuses on tracking user data for each session.
+ *
+ * This includes time connected, number of operations, and
+ * the rate of their operations
+ */
+export namespace DashStats {
+ export const SAMPLING_INTERVAL = 1000; // in milliseconds (ms) - Time interval to update the frontend.
+ export const RATE_INTERVAL = 10; // in seconds (s) - Used to calculate rate
+
+ const statsCSVFilename = './src/server/stats/userLoginStats.csv';
+ const columns = ['USERNAME', 'ACTION', 'TIME'];
+
+ /**
+ * UserStats holds the stats associated with a particular user.
+ */
+ interface UserStats {
+ socketId: string;
+ username: string;
+ time: string;
+ operations: number;
+ rate: number;
+ }
+
+ /**
+ * UserLastOperations is the queue object for each user
+ * storing their past operations.
+ */
+ interface UserLastOperations {
+ sampleOperations: number; // stores how many operations total are in this rate section (10 sec, for example)
+ lastSampleOperations: number; // stores how many total operations were recorded at the last sample
+ previousOperationsQueue: number[]; // stores the operations to calculate rate.
+ }
+
+ /**
+ * StatsDataBundle represents an object that will be sent to the frontend view
+ * on each websocket update.
+ */
+ interface StatsDataBundle {
+ connectedUsers: UserStats[];
+ }
+
+ /**
+ * CSVStore represents how objects will be stored in the CSV
+ */
+ interface CSVStore {
+ USERNAME: string;
+ ACTION: string;
+ TIME: string;
+ }
+
+ /**
+ * ServerTraffic describes the current traffic going to the backend.
+ */
+ enum ServerTraffic {
+ NOT_BUSY,
+ BUSY,
+ VERY_BUSY
+ }
+
+ // These values can be changed after further testing how many
+ // users correspond to each traffic level in Dash.
+ const BUSY_SERVER_BOUND = 2;
+ const VERY_BUSY_SERVER_BOUND = 3;
+
+ const serverTrafficMessages = [
+ "Not Busy",
+ "Busy",
+ "Very Busy"
+ ]
+
+ // lastUserOperations maps each username to a UserLastOperations
+ // structure
+ export const lastUserOperations = new Map<string, UserLastOperations>();
+
+ /**
+ * handleStats is called when the /stats route is called, providing a JSON
+ * object with relevant stats. In this case, we return the number of
+ * current connections and
+ * @param res Response object from Express
+ */
+ export function handleStats(res: Response) {
+ let current = getCurrentStats();
+ const results: CSVStore[] = [];
+ res.json({
+ currentConnections: current.length,
+ socketMap: current,
+ });
+ }
+
+ /**
+ * getUpdatedStatesBundle() sends an updated copy of the current stats to the
+ * frontend /statsview route via websockets.
+ *
+ * @returns a StatsDataBundle that is sent to the frontend view on each websocket update
+ */
+ export function getUpdatedStatsBundle(): StatsDataBundle {
+ let current = getCurrentStats();
+
+ return {
+ connectedUsers: current,
+ }
+ }
+
+ /**
+ * handleStatsView() is called when the /statsview route is called. This
+ * will use pug to render a frontend view of the current stats
+ *
+ * @param res
+ */
+ export function handleStatsView(res: Response) {
+ let current = getCurrentStats();
+
+ let connectedUsers = current.map((socketPair) => {
+ return socketPair.time + " - " + socketPair.username + " Operations: " + socketPair.operations;
+ })
+
+ let serverTraffic = ServerTraffic.NOT_BUSY;
+ if(current.length < BUSY_SERVER_BOUND) {
+ serverTraffic = ServerTraffic.NOT_BUSY;
+ } else if(current.length >= BUSY_SERVER_BOUND && current.length < VERY_BUSY_SERVER_BOUND) {
+ serverTraffic = ServerTraffic.BUSY;
+ } else {
+ serverTraffic = ServerTraffic.VERY_BUSY;
+ }
+
+ res.render("stats.pug", {
+ title: "Dash Stats",
+ numConnections: connectedUsers.length,
+ serverTraffic: serverTraffic,
+ serverTrafficMessage : serverTrafficMessages[serverTraffic],
+ connectedUsers: connectedUsers
+ });
+ }
+
+ /**
+ * logUserLogin() writes a login event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection
+ */
+ export function logUserLogin(username: string | undefined, socket: SocketIO.Socket) {
+ if (!(username === undefined)) {
+ let currentDate = new Date();
+ console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`));
+
+ let toWrite: CSVStore = {
+ USERNAME : username,
+ ACTION : "loggedIn",
+ TIME : currentDate.toISOString()
+ }
+
+ let statsFile = fs.createWriteStream(statsCSVFilename, { flags: "a"});
+ statsFile.write(convertToCSV(toWrite));
+ statsFile.end();
+ console.log(cyan(convertToCSV(toWrite)));
+ }
+ }
+
+ /**
+ * logUserLogout() writes a logout event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection.
+ */
+ export function logUserLogout(username: string | undefined, socket: SocketIO.Socket) {
+ if (!(username === undefined)) {
+ let currentDate = new Date();
+
+ let statsFile = fs.createWriteStream(statsCSVFilename, { flags: "a"});
+ let toWrite: CSVStore = {
+ USERNAME : username,
+ ACTION : "loggedOut",
+ TIME : currentDate.toISOString()
+ }
+ statsFile.write(convertToCSV(toWrite));
+ statsFile.end();
+ }
+ }
+
+ /**
+ * getLastOperationsOrDefault() is a helper method that will attempt
+ * to query the lastUserOperations map for a specified username. If the
+ * username is not in the map, an empty UserLastOperations object is returned.
+ * @param username
+ * @returns the user's UserLastOperations structure or an empty
+ * UserLastOperations object (All values set to 0) if the username is not found.
+ */
+ function getLastOperationsOrDefault(username: string): UserLastOperations {
+ if(lastUserOperations.get(username) === undefined) {
+ let initializeOperationsQueue = [];
+ for(let i = 0; i < RATE_INTERVAL; i++) {
+ initializeOperationsQueue.push(0);
+ }
+ return {
+ sampleOperations: 0,
+ lastSampleOperations: 0,
+ previousOperationsQueue: initializeOperationsQueue
+ }
+ }
+ return lastUserOperations.get(username)!;
+ }
+
+ /**
+ * updateLastOperations updates a specific user's UserLastOperations information
+ * for the current sampling cycle. The method removes old/outdated counts for
+ * operations from the queue and adds new data for the current sampling
+ * cycle to the queue, updating the total count as it goes.
+ * @param lastOperationData the old UserLastOperations data that must be updated
+ * @param currentOperations the total number of operations measured for this sampling cycle.
+ * @returns the udpated UserLastOperations structure.
+ */
+ function updateLastOperations(lastOperationData: UserLastOperations, currentOperations: number): UserLastOperations {
+ // create a copy of the UserLastOperations to modify
+ let newLastOperationData: UserLastOperations = {
+ sampleOperations: lastOperationData.sampleOperations,
+ lastSampleOperations: lastOperationData.lastSampleOperations,
+ previousOperationsQueue: lastOperationData.previousOperationsQueue.slice()
+ }
+
+ let newSampleOperations = newLastOperationData.sampleOperations;
+ newSampleOperations -= newLastOperationData.previousOperationsQueue.shift()!; // removes and returns the first element of the queue
+ let operationsThisCycle = currentOperations - lastOperationData.lastSampleOperations;
+ newSampleOperations += operationsThisCycle; // add the operations this cycle to find out what our count for the interval should be (e.g operations in the last 10 seconds)
+
+ // update values for the copy object
+ newLastOperationData.sampleOperations = newSampleOperations;
+
+ newLastOperationData.previousOperationsQueue.push(operationsThisCycle);
+ newLastOperationData.lastSampleOperations = currentOperations;
+
+ return newLastOperationData;
+ }
+
+ /**
+ * getUserOperationsOrDefault() is a helper method to get the user's total
+ * operations for the CURRENT sampling interval. The method will return 0
+ * if the username is not in the userOperations map.
+ * @param username the username to search the map for
+ * @returns the total number of operations recorded up to this sampling cycle.
+ */
+ function getUserOperationsOrDefault(username: string): number {
+ return WebSocket.userOperations.get(username) === undefined ? 0 : WebSocket.userOperations.get(username)!
+ }
+
+ /**
+ * getCurrentStats() calculates the total stats for this cycle. In this case,
+ * getCurrentStats() returns an Array of UserStats[] objects describing
+ * the stats for each user
+ * @returns an array of UserStats storing data for each user at the current moment.
+ */
+ function getCurrentStats(): UserStats[] {
+ let socketPairs: UserStats[] = [];
+ for (let [key, value] of WebSocket.socketMap) {
+ let username = value.split(' ')[0];
+ let connectionTime = new Date(timeMap[username]);
+
+ let connectionTimeString = connectionTime.toLocaleDateString() + " " + connectionTime.toLocaleTimeString();
+
+ if (!key.disconnected) {
+ let lastRecordedOperations = getLastOperationsOrDefault(username);
+ let currentUserOperationCount = getUserOperationsOrDefault(username);
+
+ socketPairs.push({
+ socketId: key.id,
+ username: username,
+ time: connectionTimeString.includes("Invalid Date") ? "" : connectionTimeString,
+ operations : WebSocket.userOperations.get(username) ? WebSocket.userOperations.get(username)! : 0,
+ rate: lastRecordedOperations.sampleOperations
+ });
+ lastUserOperations.set(username, updateLastOperations(lastRecordedOperations,currentUserOperationCount));
+ }
+ }
+ return socketPairs;
+ }
+
+ /**
+ * convertToCSV() is a helper method that stringifies a CSVStore object
+ * that can be written to the CSV file later.
+ * @param dataObject the object to stringify
+ * @returns the object as a string.
+ */
+ function convertToCSV(dataObject: CSVStore): string {
+ return `${dataObject.USERNAME},${dataObject.ACTION},${dataObject.TIME}\n`;
+ }
+}