Browse Source

feat: added better module statistics

Kristian Vos 4 years ago
parent
commit
18541b004b

+ 59 - 0
backend/core.js

@@ -9,6 +9,28 @@ class DeferredPromise {
     }
 }
 
+class MovingAverageCalculator {
+    constructor() {
+        this.count = 0;
+        this._mean = 0;
+    }
+
+    update(newValue) {
+        this.count++;
+        const differential = (newValue - this._mean) / this.count;
+        this._mean += differential;
+    }
+
+    get mean() {
+        this.validate();
+        return this._mean;
+    }
+
+    validate() {
+        if (this.count === 0) throw new Error("Mean is undefined");
+    }
+}
+
 class CoreClass {
     constructor(name) {
         this.name = name;
@@ -22,6 +44,9 @@ class CoreClass {
         this.runningJobs = [];
         this.priorities = {};
         this.stage = 0;
+        this.jobStatistics = {};
+
+        this.registerJobs();
     }
 
     setStatus(status) {
@@ -77,6 +102,31 @@ class CoreClass {
         }
     }
 
+    registerJobs() {
+        let props = [];
+        let obj = this;
+        do {
+            props = props.concat(Object.getOwnPropertyNames(obj));
+        } while ((obj = Object.getPrototypeOf(obj)));
+
+        const jobNames = props
+            .sort()
+            .filter(
+                (prop) =>
+                    typeof this[prop] == "function" &&
+                    prop === prop.toUpperCase()
+            );
+
+        jobNames.forEach((jobName) => {
+            this.jobStatistics[jobName] = {
+                successful: 0,
+                failed: 0,
+                total: 0,
+                averageTiming: new MovingAverageCalculator(),
+            };
+        });
+    }
+
     runJob(name, payload, options = {}) {
         let deferredPromise = new DeferredPromise();
         const job = { name, payload, onFinish: deferredPromise };
@@ -97,17 +147,26 @@ class CoreClass {
 
     _runJob(job, cb) {
         this.log("INFO", `Running job ${job.name}`);
+        const startTime = Date.now();
         this.runningJobs.push(job);
         this[job.name](job.payload)
             .then((response) => {
                 this.log("INFO", `Ran job ${job.name} successfully`);
+                this.jobStatistics[job.name].successful++;
                 job.onFinish.resolve(response);
             })
             .catch((error) => {
                 this.log("INFO", `Running job ${job.name} failed`);
+                this.jobStatistics[job.name].failed++;
                 job.onFinish.reject(error);
             })
             .finally(() => {
+                const endTime = Date.now();
+                const executionTime = endTime - startTime;
+                this.jobStatistics[job.name].total++;
+                this.jobStatistics[job.name].averageTiming.update(
+                    executionTime
+                );
                 this.runningJobs.splice(this.runningJobs.indexOf(job), 1);
                 cb();
             });

+ 10 - 0
backend/index.js

@@ -332,4 +332,14 @@ process.stdin.on("data", function(data) {
 
         console.log(moduleManager.modules[parts[1]].runningJobs);
     }
+    if (data.toString().startsWith("stats")) {
+        const parts = data
+            .toString()
+            .substr(0, data.toString().length - 2)
+            .split(" ");
+
+        console.log(moduleManager.modules[parts[1]].jobStatistics);
+    }
 });
+
+module.exports = moduleManager;

+ 5 - 7
backend/logic/actions/hooks/loginRequired.js

@@ -4,8 +4,6 @@ const cache = require("../../cache");
 const utils = require("../../utils");
 // const logger = moduleManager.modules["logger"];
 
-console.log(cache);
-
 module.exports = function(next) {
     return function(session) {
         let args = [];
@@ -13,13 +11,13 @@ module.exports = function(next) {
         let cb = args[args.length - 1];
         async.waterfall(
             [
-                next => {
+                (next) => {
                     cache
                         .runJob("HGET", {
                             table: "sessions",
-                            key: session.sessionId
+                            key: session.sessionId,
                         })
-                        .then(session => next(null, session))
+                        .then((session) => next(null, session))
                         .catch(next);
                 },
                 (session, next) => {
@@ -27,9 +25,9 @@ module.exports = function(next) {
                         return next("Login required.");
                     this.session = session;
                     next();
-                }
+                },
             ],
-            async err => {
+            async (err) => {
                 if (err) {
                     err = await utils.runJob("GET_ERROR", { error: err });
                     console.log(

+ 12 - 11
backend/logic/actions/index.js

@@ -1,14 +1,15 @@
-'use strict';
+"use strict";
 
 module.exports = {
-	apis: require('./apis'),
-	songs: require('./songs'),
-	queueSongs: require('./queueSongs'),
-	stations: require('./stations'),
-	playlists: require('./playlists'),
-	users: require('./users'),
-	activities: require('./activities'),
-	reports: require('./reports'),
-	news: require('./news'),
-	punishments: require('./punishments')
+    apis: require("./apis"),
+    songs: require("./songs"),
+    queueSongs: require("./queueSongs"),
+    stations: require("./stations"),
+    playlists: require("./playlists"),
+    users: require("./users"),
+    activities: require("./activities"),
+    reports: require("./reports"),
+    news: require("./news"),
+    punishments: require("./punishments"),
+    utils: require("./utils"),
 };

+ 93 - 0
backend/logic/actions/utils.js

@@ -0,0 +1,93 @@
+"use strict";
+
+const async = require("async");
+
+const hooks = require("./hooks");
+
+const utils = require("../utils");
+
+module.exports = {
+    getModules: hooks.adminRequired((session, cb) => {
+        async.waterfall(
+            [
+                (next) => {
+                    next(null, utils.moduleManager.modules);
+                },
+
+                (modules, next) => {
+                    console.log(modules, next);
+                    next(
+                        null,
+                        Object.keys(modules).map((moduleName) => {
+                            const module = modules[moduleName];
+                            return {
+                                name: module.name,
+                                status: module.status,
+                                stage: module.stage,
+                                jobsInQueue: module.jobQueue.length(),
+                                jobsInProgress: module.jobQueue.running(),
+                                concurrency: module.jobQueue.concurrency,
+                            };
+                        })
+                    );
+                },
+            ],
+            async (err, modules) => {
+                if (err && err !== true) {
+                    err = await utils.runJob("GET_ERROR", { error: err });
+                    console.log(
+                        "ERROR",
+                        "GET_MODULES",
+                        `User ${session.userId} failed to get modules. '${err}'`
+                    );
+                    cb({ status: "failure", message: err });
+                } else {
+                    console.log(
+                        "SUCCESS",
+                        "GET_MODULES",
+                        `User ${session.userId} has successfully got the modules info.`
+                    );
+                    cb({
+                        status: "success",
+                        message: "Successfully got modules.",
+                        modules,
+                    });
+                }
+            }
+        );
+    }),
+
+    getModule: hooks.adminRequired((session, moduleName, cb) => {
+        async.waterfall(
+            [
+                (next) => {
+                    next(null, utils.moduleManager.modules[moduleName]);
+                },
+            ],
+            async (err, module) => {
+                console.log(module.runningJobs);
+                if (err && err !== true) {
+                    err = await utils.runJob("GET_ERROR", { error: err });
+                    console.log(
+                        "ERROR",
+                        "GET_MODULE",
+                        `User ${session.userId} failed to get module. '${err}'`
+                    );
+                    cb({ status: "failure", message: err });
+                } else {
+                    console.log(
+                        "SUCCESS",
+                        "GET_MODULE",
+                        `User ${session.userId} has successfully got the module info.`
+                    );
+                    cb({
+                        status: "success",
+                        message: "Successfully got module info.",
+                        runningJobs: module.runningJobs,
+                        jobStatistics: module.jobStatistics,
+                    });
+                }
+            }
+        );
+    }),
+};

+ 240 - 0
frontend/components/Admin/NewStatistics.vue

@@ -0,0 +1,240 @@
+<template>
+	<div class="container">
+		<metadata title="Admin | Statistics" />
+		<div class="columns">
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">
+						Average Logs
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<table class="table">
+							<thead>
+								<tr>
+									<th>Name</th>
+									<th>Status</th>
+									<th>Stage</th>
+									<th>Jobs in queue</th>
+									<th>Jobs in progress</th>
+									<th>Concurrency</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr
+									v-for="module_ in modules"
+									:key="module_.name"
+								>
+									<td>
+										<router-link
+											:to="'?moduleName=' + module_.name"
+											>{{ module_.name }}</router-link
+										>
+									</td>
+									<td>{{ module_.status }}</td>
+									<td>{{ module_.stage }}</td>
+									<td>{{ module_.jobsInQueue }}</td>
+									<td>{{ module_.jobsInProgress }}</td>
+									<td>{{ module_.concurrency }}</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</div>
+		<br />
+		<div class="columns" v-if="module_">
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">
+						Average Logs
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<table class="table">
+							<thead>
+								<tr>
+									<th>Name</th>
+									<th>Payload</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr
+									v-for="job in module_.runningJobs"
+									:key="JSON.stringify(job)"
+								>
+									<td>{{ job.name }}</td>
+									<td>
+										{{ JSON.stringify(job.payload) }}
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</div>
+		<br />
+		<div class="columns" v-if="module_">
+			<div
+				class="card column is-10-desktop is-offset-1-desktop is-12-mobile"
+			>
+				<header class="card-header">
+					<p class="card-header-title">
+						Average Logs
+					</p>
+				</header>
+				<div class="card-content">
+					<div class="content">
+						<table class="table">
+							<thead>
+								<tr>
+									<th>Job name</th>
+									<th>Successful</th>
+									<th>Failed</th>
+									<th>Total</th>
+									<th>Average timing</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr
+									v-for="(job,
+									jobName) in module_.jobStatistics"
+									:key="jobName"
+								>
+									<td>{{ jobName }}</td>
+									<td>
+										{{ job.successful }}
+									</td>
+									<td>
+										{{ job.failed }}
+									</td>
+									<td>
+										{{ job.total }}
+									</td>
+									<td>
+										{{ job.averageTiming }}
+									</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import io from "../../io";
+
+export default {
+	components: {},
+	data() {
+		return {
+			modules: [],
+			module_: null
+		};
+	},
+	mounted() {
+		io.getSocket(socket => {
+			this.socket = socket;
+			if (this.socket.connected) this.init();
+			io.onConnect(() => this.init());
+		});
+	},
+	methods: {
+		init() {
+			this.socket.emit("utils.getModules", data => {
+				console.log(data);
+				if (data.status === "success") {
+					this.modules = data.modules;
+				}
+			});
+
+			if (this.$route.query.moduleName) {
+				this.socket.emit(
+					"utils.getModule",
+					this.$route.query.moduleName,
+					data => {
+						console.log(data);
+						if (data.status === "success") {
+							this.module_ = {
+								runningJobs: data.runningJobs,
+								jobStatistics: data.jobStatistics
+							};
+						}
+					}
+				);
+			}
+		},
+		round(number) {
+			return Math.round(number);
+		}
+	}
+};
+</script>
+
+//
+<style lang="scss" scoped>
+@import "styles/global.scss";
+
+.night-mode {
+	.table {
+		color: #ddd;
+		background-color: #222;
+
+		thead tr {
+			background: $night-mode-secondary;
+			td {
+				color: #fff;
+			}
+		}
+
+		tbody tr:hover {
+			background-color: #111 !important;
+		}
+
+		tbody tr:nth-child(even) {
+			background-color: #444;
+		}
+
+		strong {
+			color: #ddd;
+		}
+	}
+
+	.card {
+		background-color: $night-mode-secondary;
+
+		p {
+			color: #ddd;
+		}
+	}
+}
+
+body {
+	font-family: "Roboto", sans-serif;
+}
+
+.user-avatar {
+	display: block;
+	max-width: 50px;
+	margin: 0 auto;
+}
+
+td {
+	vertical-align: middle;
+}
+
+.is-primary:focus {
+	background-color: $primary-color !important;
+}
+</style>

+ 21 - 0
frontend/components/pages/Admin.vue

@@ -66,6 +66,18 @@
 						<span>&nbsp;Statistics</span>
 					</router-link>
 				</li>
+				<li
+					:class="{ 'is-active': currentTab == 'newstatistics' }"
+					@click="showTab('newstatistics')"
+				>
+					<router-link
+						class="tab newstatistics"
+						to="/admin/newstatistics"
+					>
+						<i class="material-icons">show_chart</i>
+						<span>&nbsp;New Statistics</span>
+					</router-link>
+				</li>
 				<li
 					:class="{ 'is-active': currentTab == 'punishments' }"
 					@click="showTab('punishments')"
@@ -88,6 +100,7 @@
 		<news v-if="currentTab == 'news'" />
 		<users v-if="currentTab == 'users'" />
 		<statistics v-if="currentTab == 'statistics'" />
+		<new-statistics v-if="currentTab == 'newstatistics'" />
 		<punishments v-if="currentTab == 'punishments'" />
 	</div>
 </template>
@@ -105,6 +118,7 @@ export default {
 		News: () => import("../Admin/News.vue"),
 		Users: () => import("../Admin/Users.vue"),
 		Statistics: () => import("../Admin/Statistics.vue"),
+		NewStatistics: () => import("../Admin/NewStatistics.vue"),
 		Punishments: () => import("../Admin/Punishments.vue")
 	},
 	data() {
@@ -144,6 +158,9 @@ export default {
 				case "/admin/statistics":
 					this.currentTab = "statistics";
 					break;
+				case "/admin/newstatistics":
+					this.currentTab = "newstatistics";
+					break;
 				case "/admin/punishments":
 					this.currentTab = "punishments";
 					break;
@@ -202,6 +219,10 @@ export default {
 		color: $light-orange;
 		border-color: $light-orange;
 	}
+	.newstatistics {
+		color: $light-orange;
+		border-color: $light-orange;
+	}
 	.punishments {
 		color: $dark-orange;
 		border-color: $dark-orange;