Browse Source

Server: Implementation data querying endpoint(s)

Christian Kahlau 3 years ago
parent
commit
d6971316e0

+ 0 - 0
daemon/src/lib/http-status.exception.ts → common/lib/http-status.exception.ts


+ 5 - 0
common/types/query-response.d.ts

@@ -0,0 +1,5 @@
+type QueryResponse<T> = {
+  start: Date;
+  end: Date;
+  data: T[];
+};

+ 3 - 0
common/types/server-data.d.ts

@@ -0,0 +1,3 @@
+type ServerData = { time: Date; max?: number } & ReducedValuesPerc;
+type ServerDataType = 'cpu' | 'ram' | string;
+type ServerDataTypesConfig = { type: ServerDataType; subtypes?: ServerDataTypesConfig[] };

+ 1 - 1
daemon/src/webserver.class.ts

@@ -1,8 +1,8 @@
 import express, { Express } from 'express';
 
+import { HttpStatusException } from '../../common/lib/http-status.exception';
 import { Logger } from '../../common/util/logger.class';
 
-import { HttpStatusException } from './lib/http-status.exception';
 import { Collector } from './collector.class';
 
 export class Webserver {

+ 1 - 0
server/package.json

@@ -13,6 +13,7 @@
     "axios": "^0.27.2",
     "dotenv": "^16.0.2",
     "express": "^4.18.1",
+    "moment": "^2.29.4",
     "sqlite3": "^5.1.1"
   },
   "devDependencies": {

+ 58 - 36
server/src/database.class.ts

@@ -1,5 +1,6 @@
 import fs from 'fs';
 import fsp from 'fs/promises';
+import moment from 'moment';
 import path from 'path';
 import { Database as SQLiteDB, OPEN_CREATE, OPEN_READWRITE, RunResult } from 'sqlite3';
 
@@ -146,50 +147,71 @@ export class Database {
     }
   }
 
-  public async getServerData(serverID: number, start: Date, end: Date): Promise<ReducedData[]> {
-    /* FIRST DRAFT - SIMPLY GET ALL DATA POINTS OF ALL TYPES */
-    /* TODO: ONLY GET DATA OF ONE TYPE, REDUCE DOWN TO FEWER DATA POINTS, COMPUTING AVGs & PEAKs */
-
-    const result = await this.stmt(
-      `SELECT
-          ServerDataEntry.*,
-          ServerDataValue.Type,
-          ServerDataValue.Key,
-          ServerDataValue.Value
-        FROM ServerDataEntry
-        JOIN ServerDataValue ON ServerDataEntry.ID = ServerDataValue.EntryID
-        WHERE ServerID = ?
-        AND Timestamp BETWEEN ? AND ?
-        ORDER BY Timestamp, Type, Key;`,
-      [serverID, start.getTime(), end.getTime()]
+  public async getServerDataTypes(serverID: number) {
+    const results = await this.stmt(
+      `
+      SELECT
+        ServerDataValue.Type 
+      FROM ServerDataEntry
+      JOIN ServerDataValue ON ServerDataEntry.ID = ServerDataValue.EntryID
+      WHERE ServerDataEntry.ServerID = ?
+      GROUP BY ServerDataValue.Type
+      ORDER BY ServerDataValue.Type;
+    `,
+      [serverID]
     );
 
-    return result.rows.reduce((res, line, i) => {
-      const timestamp = line['Timestamp'];
-      let entry: ReducedData;
-      if (i === 0 || res[res.length - 1].time.getTime() !== timestamp) {
-        entry = { time: new Date(timestamp) } as ReducedData;
-        res.push(entry);
+    return results.rows.reduce((res: Array<ServerDataTypesConfig>, { Type: type }) => {
+      if (!type.startsWith('hdd:')) {
+        res.push({ type });
       } else {
-        entry = res[res.length - 1];
+        let hdd = res.find(c => c.type === 'hdd');
+        if (!hdd) {
+          hdd = { type: 'hdd', subtypes: [] };
+          res.push(hdd);
+        }
+        hdd.subtypes.push({ type: type.substring(4) });
       }
+      return res;
+    }, []) as Array<ServerDataTypesConfig>;
+  }
 
-      let type: string = line['Type'];
-      if (type.startsWith('hdd:')) {
-        const mount = type.substring(4);
-        if (typeof entry.hdd === 'undefined') entry.hdd = {};
-        if (typeof entry.hdd[mount] === 'undefined') entry.hdd[mount] = {} as ReducedValuesMinMax;
-        entry.hdd[mount][line['Key']] = line['Value'];
-      } else {
-        if (typeof entry[type] === 'undefined') entry[type] = {};
-        entry[type][line['Key']] = line['Value'];
+  public async queryServerData(serverID: number, type: ServerDataType, from: Date, to: Date): Promise<ServerData[]> {
+    const diffMs = moment(to).diff(moment(from));
+    const sectionMs = Math.floor(diffMs / 100);
+    const select_max = type !== 'cpu';
+    const select_types = select_max ? [type, type, type] : [type, type];
+    const result = await this.stmt(
+      `
+      SELECT 
+        CEIL(Timestamp / ?) * ? as 'Timegroup',
+        AVG(VALUE_AVG.Value) as 'avg',
+        MAX(VALUE_PEAK.Value) as 'peak'${
+          select_max
+            ? `,
+        MAX(VALUE_MAX.Value) as 'max'`
+            : ''
+        }
+      FROM ServerDataEntry
+      JOIN ServerDataValue AS VALUE_AVG ON ServerDataEntry.ID = VALUE_AVG.EntryID AND VALUE_AVG.Type = ? AND VALUE_AVG.Key = 'avg'
+      JOIN ServerDataValue AS VALUE_PEAK ON ServerDataEntry.ID = VALUE_PEAK.EntryID AND VALUE_PEAK.Type = ? AND VALUE_PEAK.Key = 'peak'
+      ${
+        select_max
+          ? "JOIN ServerDataValue AS VALUE_MAX ON ServerDataEntry.ID = VALUE_MAX.EntryID AND VALUE_MAX.Type = ? AND VALUE_MAX.Key = 'max'"
+          : ''
       }
+      WHERE ServerDataEntry.ServerID = ?
+      AND ServerDataEntry.Timestamp BETWEEN ? AND ?
+      GROUP BY Timegroup
+      ORDER BY Timegroup;
+    `,
+      [sectionMs, sectionMs, ...select_types, serverID, from.getTime(), to.getTime()]
+    );
 
-      return res;
-    }, [] as ReducedData[]);
+    return result.rows.map(r => ({ time: new Date(r.Timegroup), avg: r.avg, peak: r.peak, max: r.max }));
   }
 
-  private async run(sql: string, params: any): Promise<RunResult> {
+  public async run(sql: string, params: any): Promise<RunResult> {
     return new Promise<RunResult>((res, rej) => {
       this.db.run(sql, params, function (err) {
         if (err) return rej(err);
@@ -198,7 +220,7 @@ export class Database {
     });
   }
 
-  private async stmt(sql: string, params: any[]) {
+  public async stmt(sql: string, params: any[]) {
     return new Promise<{ result: RunResult; rows: any[] }>((res, rej) => {
       const stmt = this.db.prepare(sql);
       stmt.all(params, function (err, rows) {

+ 67 - 0
server/src/webserver.class.ts

@@ -1,5 +1,8 @@
 import express, { Express } from 'express';
+
+import { HttpStatusException } from '../../common/lib/http-status.exception';
 import { Logger } from '../../common/util/logger.class';
+
 import { Database } from './database.class';
 
 export class Webserver {
@@ -8,8 +11,72 @@ export class Webserver {
   constructor(private port: number, private db: Database) {
     this.app = express();
 
+    this.app.get('/server', async (req, res, next) => {
+      try {
+        const serverConfigs = await this.db.getAllServerConfigs();
+        res.send(serverConfigs);
+      } catch (err) {
+        next(err);
+      }
+    });
+
+    this.app.get('/server/:serverID/data/types', async (req, res, next) => {
+      try {
+        const serverID = Number(req.params.serverID);
+
+        if (Number.isNaN(serverID)) {
+          throw new HttpStatusException(`Not a valid server id: ${req.params.serverID}`, 400);
+        }
+
+        const serverDataTypes = await this.db.getServerDataTypes(serverID);
+        res.send(serverDataTypes);
+      } catch (err) {
+        next(err);
+      }
+    });
+
+    this.app.get('/server/:serverID/data', async (req, res, next) => {
+      try {
+        const serverID = Number(req.params.serverID);
+
+        if (Number.isNaN(serverID)) {
+          throw new HttpStatusException(`Not a valid server id: ${req.params.serverID}`, 400);
+        }
+
+        const qStart = (req.query.start || '').toString();
+        const qEnd = (req.query.end || '').toString();
+        const qType = (req.query.type || '').toString();
+
+        if (!qStart || !qEnd || !qType) throw new HttpStatusException("QueryParams 'type', 'start' and 'end' are mandatory.", 400);
+
+        const start = new Date(qStart);
+        const end = new Date(qEnd);
+        if ([start.toString(), end.toString()].includes('Invalid Date')) {
+          throw new HttpStatusException("QueryParams 'start' and 'end' must be parseable dates or unix epoch timestamps (ms).", 400);
+        }
+
+        const data = await this.db.queryServerData(serverID, qType, start, end);
+        res.send({
+          start,
+          end,
+          data
+        } as QueryResponse<ServerData>);
+      } catch (err) {
+        next(err);
+      }
+    });
+
     this.app.use('/', express.static(process.env.STATIC_DIR || 'public'));
 
+    this.app.use((err, req, res, next) => {
+      if (err instanceof HttpStatusException) {
+        res.status(err.statusCode).send(err.message);
+      } else {
+        Logger.error('[ERROR] Webservice ErrorHandler caught:', err);
+        res.status(500).send(JSON.stringify(err));
+      }
+    });
+
     this.app.listen(this.port, () => {
       Logger.info(`[INFO] Monitoring Webserver started at http://localhost:${this.port}`);
     });