|
|
@@ -0,0 +1,633 @@
|
|
|
+import moment from 'moment';
|
|
|
+import { Pool } from 'mysql';
|
|
|
+
|
|
|
+import defaults from '../../../common/defaults.module';
|
|
|
+import { ServiceConfig, validateParamType } from '../../../common/interfaces/service-config.interface';
|
|
|
+import { HttpCheckData, HttpCheckStatus, ServiceCheckData, ServiceCheckDataEntry } from '../../../common/lib/http-check-data.module';
|
|
|
+import { Logger } from '../../../common/util/logger.class';
|
|
|
+
|
|
|
+import { DatabaseException } from '../lib/database.exception';
|
|
|
+import { ServiceChangedStatus } from '../lib/service-changed-status.enum';
|
|
|
+import { ValidationException } from '../lib/validation-exception.class';
|
|
|
+import { DataProvider } from './data-provider.interface';
|
|
|
+// TODO: import { DBMigration } from './db-migration.class';
|
|
|
+import { MariaDBConnector } from './mariadb-connector.class';
|
|
|
+
|
|
|
+export class MariaDBDatabase implements DataProvider {
|
|
|
+ private db: MariaDBConnector;
|
|
|
+ private _onError: (error: any) => void = err => console.error('[DB.ONERROR]', err);
|
|
|
+
|
|
|
+ constructor(pool: Pool) {
|
|
|
+ this.db = new MariaDBConnector(pool);
|
|
|
+ }
|
|
|
+
|
|
|
+ public set onError(listener: (error: any) => void) {
|
|
|
+ this._onError = listener;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async open(migrate = false) {
|
|
|
+ try {
|
|
|
+ this.db.onError = this._onError.bind(this);
|
|
|
+ await this.db.connect();
|
|
|
+
|
|
|
+ Logger.info('[INFO] Opened MariaDB Connection');
|
|
|
+
|
|
|
+ if (migrate) {
|
|
|
+ //TODO: RUN DB MIGRATIONS
|
|
|
+ // const mig = new DBMigration(this.db);
|
|
|
+ // await mig.update();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ Logger.error('[FATAL] Initializing Database failed:', err);
|
|
|
+ Logger.error('[EXITING]');
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public async getAllServerConfigs(): Promise<Server[]> {
|
|
|
+ const res = await this.db.query(
|
|
|
+ `SELECT
|
|
|
+ Server.*,
|
|
|
+ ServerConfig.Key,
|
|
|
+ ServerConfig.Value
|
|
|
+ FROM Server
|
|
|
+ LEFT OUTER JOIN ServerConfig ON Server.ID = ServerConfig.ServerID
|
|
|
+ ORDER BY Server.Title, ServerConfig.Key`,
|
|
|
+ []
|
|
|
+ );
|
|
|
+
|
|
|
+ return res.reduce((res: Server[], line: any, i) => {
|
|
|
+ const serverID = line['ID'];
|
|
|
+ let server: Server;
|
|
|
+ if (i === 0 || res[res.length - 1].id !== serverID) {
|
|
|
+ server = { id: serverID, title: line['Title'], fqdn: line['FQDN'], config: {} };
|
|
|
+ res.push(server);
|
|
|
+ } else {
|
|
|
+ server = res[res.length - 1];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!!line['Key']) {
|
|
|
+ server.config[line['Key']] = line['Value'];
|
|
|
+ }
|
|
|
+
|
|
|
+ return res;
|
|
|
+ }, [] as Server[]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public async insertServerData(serverID: number, data: ReducedData[]) {
|
|
|
+ if (!data.length) return;
|
|
|
+
|
|
|
+ await this.db.beginTransaction();
|
|
|
+ try {
|
|
|
+ for (const entry of data) {
|
|
|
+ const result = await this.db.query(
|
|
|
+ `INSERT INTO ServerDataEntry(ServerID, Timestamp) VALUES(?, ?);
|
|
|
+ SELECT LAST_INSERT_ID() as ID;`,
|
|
|
+ [serverID, entry.time.getTime()]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!result || result.length < 2) throw new DatabaseException('Unexpected result during insertServerData');
|
|
|
+
|
|
|
+ let entryID = (result[1] as any[])[0]['ID'];
|
|
|
+
|
|
|
+ for (const type of Object.keys(entry).filter(t => !['time', 'hdd'].includes(t))) {
|
|
|
+ for (const key of Object.keys((entry as any)[type])) {
|
|
|
+ await this.db.query('INSERT INTO ServerDataValue(EntryID, Type, Key, Value) VALUES(?, ?, ?, ?);', [
|
|
|
+ entryID,
|
|
|
+ type,
|
|
|
+ key,
|
|
|
+ (entry as any)[type][key]
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (entry.hdd) {
|
|
|
+ for (const mount of Object.keys(entry.hdd)) {
|
|
|
+ for (const key of Object.keys(entry.hdd[mount])) {
|
|
|
+ await this.db.query('INSERT INTO ServerDataValue(EntryID, Type, Key, Value) VALUES(?, ?, ?, ?);', [
|
|
|
+ entryID,
|
|
|
+ `hdd:${mount}`,
|
|
|
+ key,
|
|
|
+ (entry.hdd[mount] as any)[key]
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await this.db.commit();
|
|
|
+ } catch (err) {
|
|
|
+ await this.db.rollback();
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public async getServerDataTypes(serverID: number) {
|
|
|
+ const results: any[] = await this.db.query(
|
|
|
+ `
|
|
|
+ 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 results.reduce((res: Array<ServerDataTypesConfig>, { Type: type }) => {
|
|
|
+ if (!type.startsWith('hdd:')) {
|
|
|
+ res.push({ type });
|
|
|
+ } else {
|
|
|
+ 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>;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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: any[] = await this.db.query(
|
|
|
+ `
|
|
|
+ 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 result.map(r => ({ time: new Date(r.Timegroup), avg: r.avg, peak: r.peak, max: r.max }));
|
|
|
+ }
|
|
|
+
|
|
|
+ public async queryServerStats(serverID: number, type: ServerDataType, from: Date, to: Date): Promise<ReducedValuesPerc> {
|
|
|
+ const select_max = type !== 'cpu';
|
|
|
+ const select_types = select_max ? [type, type, type] : [type, type];
|
|
|
+ const result: any[] = await this.db.query(
|
|
|
+ `
|
|
|
+ SELECT
|
|
|
+ AVG(VALUE_AVG.Value) as 'avg',
|
|
|
+ AVG(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 ?;
|
|
|
+ `,
|
|
|
+ [...select_types, serverID, from.getTime(), to.getTime()]
|
|
|
+ );
|
|
|
+
|
|
|
+ const row = result[0];
|
|
|
+ if (Object.keys(row).includes('max')) {
|
|
|
+ return {
|
|
|
+ avg: ((row['avg'] as number) / (row['max'] as number)) * 100,
|
|
|
+ peak: ((row['peak'] as number) / (row['max'] as number)) * 100
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ avg: row['avg'] as number,
|
|
|
+ peak: row['peak'] as number
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async getHealthCheckConfigs(serverID?: number, type = 'http') {
|
|
|
+ const res: any[] = await this.db.query(
|
|
|
+ `SELECT
|
|
|
+ HealthCheckConfig.*,
|
|
|
+ HealthCheckParams.Type as '_ParamType',
|
|
|
+ HealthCheckParams.Key as '_ParamKey',
|
|
|
+ HealthCheckParams.Value as '_ParamValue'
|
|
|
+ FROM HealthCheckConfig
|
|
|
+ LEFT OUTER JOIN HealthCheckParams ON HealthCheckConfig.ID = HealthCheckParams.ConfigID
|
|
|
+ WHERE HealthCheckConfig.Type = ?
|
|
|
+ ${!!serverID ? 'AND HealthCheckConfig.ServerID = ?' : ''}
|
|
|
+ ORDER BY HealthCheckConfig.Title, _ParamType, _ParamKey`,
|
|
|
+ [type, serverID]
|
|
|
+ );
|
|
|
+
|
|
|
+ return this.configFromResultRows(res);
|
|
|
+ }
|
|
|
+
|
|
|
+ public async getHttpCheckConfigs(serverID?: number) {
|
|
|
+ return (await this.getHealthCheckConfigs(serverID)).map(this.httpCheckConfigFrom);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async getHealthCheckConfigByID(serverID: number, configID: number) {
|
|
|
+ if (!serverID && !configID) return null;
|
|
|
+
|
|
|
+ const res: any[] = await this.db.query(
|
|
|
+ `SELECT
|
|
|
+ HealthCheckConfig.*,
|
|
|
+ HealthCheckParams.Type as '_ParamType',
|
|
|
+ HealthCheckParams.Key as '_ParamKey',
|
|
|
+ HealthCheckParams.Value as '_ParamValue'
|
|
|
+ FROM HealthCheckConfig
|
|
|
+ LEFT OUTER JOIN HealthCheckParams ON HealthCheckConfig.ID = HealthCheckParams.ConfigID
|
|
|
+ WHERE HealthCheckConfig.ID = ?
|
|
|
+ AND HealthCheckConfig.ServerID = ?
|
|
|
+ ORDER BY HealthCheckConfig.Title, _ParamType, _ParamKey`,
|
|
|
+ [configID, serverID]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!res.length) return null;
|
|
|
+
|
|
|
+ const configs = this.configFromResultRows(res);
|
|
|
+
|
|
|
+ return configs[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ public async getHttpCheckConfigByID(serverID: number, configID: number) {
|
|
|
+ return this.httpCheckConfigFrom(await this.getHealthCheckConfigByID(serverID, configID));
|
|
|
+ }
|
|
|
+
|
|
|
+ public async saveHttpCheckConfig(serverID: number, conf: HttpCheckConfig) {
|
|
|
+ const validationErrors = this.validateHttpCheckConfig(conf);
|
|
|
+ if (validationErrors) throw new ValidationException('Validation of HttpCheckConfig object failed', validationErrors);
|
|
|
+
|
|
|
+ conf.serverId = serverID;
|
|
|
+ let status = ServiceChangedStatus.None;
|
|
|
+
|
|
|
+ const oldConf = await this.getHttpCheckConfigByID(serverID, conf.id);
|
|
|
+ await this.db.beginTransaction();
|
|
|
+ try {
|
|
|
+ if (oldConf) {
|
|
|
+ // UPDATE
|
|
|
+ Logger.debug('[DEBUG] Updating HealthCheckConfig', conf.title, `(${oldConf.id})`);
|
|
|
+ if (oldConf.title !== conf.title) {
|
|
|
+ await this.db.query('UPDATE HealthCheckConfig SET Title = ? WHERE ID = ?', [conf.title, oldConf.id]);
|
|
|
+ }
|
|
|
+
|
|
|
+ let updValues: any[][] = [];
|
|
|
+ if (oldConf.url !== conf.url) updValues.push([conf.url, conf.id, 'url']);
|
|
|
+ if (oldConf.interval !== conf.interval) {
|
|
|
+ updValues.push([conf.interval, conf.id, 'interval']);
|
|
|
+ status = ServiceChangedStatus.Rescheduled;
|
|
|
+ }
|
|
|
+ if (oldConf.timeout !== conf.timeout) updValues.push([conf.timeout ?? defaults.serviceChecks.httpTimeout, conf.id, 'timeout']);
|
|
|
+ if (oldConf.active !== conf.active) {
|
|
|
+ updValues.push([conf.active ?? defaults.serviceChecks.active ? 1 : 0, conf.id, 'active']);
|
|
|
+ status = conf.active ?? defaults.serviceChecks.active ? ServiceChangedStatus.Activated : ServiceChangedStatus.Deactivated;
|
|
|
+ }
|
|
|
+ if (oldConf.notify !== conf.notify) updValues.push([conf.notify ?? defaults.serviceChecks.notify ? 1 : 0, conf.id, 'notify']);
|
|
|
+ if (oldConf.notifyThreshold !== conf.notifyThreshold)
|
|
|
+ updValues.push([conf.notifyThreshold ?? defaults.serviceChecks.notifyThreshold, conf.id, 'notifyThreshold']);
|
|
|
+ if (updValues.length) {
|
|
|
+ for (const data of updValues) {
|
|
|
+ await this.db.query(`UPDATE HealthCheckParams SET Value = ? WHERE ConfigID = ? AND Key = ?;`, data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await this.db.query('SELECT * FROM HealthCheckParams WHERE ConfigID = ? and Key = "check";', [conf.id]);
|
|
|
+ updValues = [];
|
|
|
+ const delIDs: number[] = [];
|
|
|
+ res.forEach((row: any, i) => {
|
|
|
+ if (i < conf.checks.length) {
|
|
|
+ updValues.push([JSON.stringify(conf.checks[i]), row['ID']]);
|
|
|
+ } else {
|
|
|
+ delIDs.push(row['ID']);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (delIDs.length) {
|
|
|
+ const delSql = `DELETE FROM HealthCheckParams WHERE ID IN (${delIDs.map(() => '?').join(',')});`;
|
|
|
+ await this.db.query(delSql, delIDs);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (updValues.length) {
|
|
|
+ for (const data of updValues) {
|
|
|
+ await this.db.query('UPDATE HealthCheckParams SET Value = ? WHERE ID = ?;', data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const insValues = conf.checks.filter((c, i) => i > res.length - 1).map(c => [conf.id, 'regexp', 'check', JSON.stringify(c)]);
|
|
|
+ if (insValues.length) {
|
|
|
+ for (const data of insValues) {
|
|
|
+ await this.db.query('INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES(?, ?, ?, ?);', data);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // INSERT
|
|
|
+ Logger.debug('[DEBUG] Inserting new HealthCheckConfig', conf.title);
|
|
|
+ const res = await this.db.query(
|
|
|
+ `INSERT INTO HealthCheckConfig(ServerID, Type, Title) VALUES(?, ?, ?);
|
|
|
+ SELECT LAST_INSERT_ID() as ID;`,
|
|
|
+ [serverID, 'http', conf.title]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!res || res.length < 2) throw new DatabaseException('Unexpected result during saveHttpCheckConfig');
|
|
|
+ conf.id = (res[1] as any[])[0]['ID'];
|
|
|
+
|
|
|
+ if (conf.active ?? defaults.serviceChecks.active) {
|
|
|
+ status = ServiceChangedStatus.Created;
|
|
|
+ }
|
|
|
+
|
|
|
+ const insCheckValues = conf.checks.map(c => [conf.id, 'regexp', 'check', c]);
|
|
|
+ await this.db.query(
|
|
|
+ `INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES
|
|
|
+ (?, ?, ?, ?),
|
|
|
+ (?, ?, ?, ?),
|
|
|
+ (?, ?, ?, ?),
|
|
|
+ (?, ?, ?, ?),
|
|
|
+ (?, ?, ?, ?),
|
|
|
+ (?, ?, ?, ?)${conf.checks.length ? `,${insCheckValues.map(() => '(?, ?, ?, ?)').join(',')}` : ''}`,
|
|
|
+ [
|
|
|
+ ...[conf.id, 'text', 'url', conf.url],
|
|
|
+ ...[conf.id, 'boolean', 'active', conf.active ?? defaults.serviceChecks.active ? 1 : 0],
|
|
|
+ ...[conf.id, 'number', 'interval', conf.interval],
|
|
|
+ ...[conf.id, 'number', 'timeout', conf.timeout ?? defaults.serviceChecks.httpTimeout],
|
|
|
+ ...[conf.id, 'boolean', 'notify', conf.notify ?? defaults.serviceChecks.notify],
|
|
|
+ ...[conf.id, 'number', 'notifyThreshold', conf.notifyThreshold ?? defaults.serviceChecks.notifyThreshold],
|
|
|
+ ...conf.checks.reduce((ret, check) => [...ret, conf.id, 'regexp', 'check', JSON.stringify(check)], [] as any[])
|
|
|
+ ]
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.db.commit();
|
|
|
+ return { status, result: conf };
|
|
|
+ } catch (err) {
|
|
|
+ await this.db.rollback();
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async deleteHealthCheckConfig(serverID: number, confID: number) {
|
|
|
+ const conf = await this.getHealthCheckConfigByID(serverID, confID);
|
|
|
+ if (!conf) return false;
|
|
|
+
|
|
|
+ await this.db.query('DELETE FROM HealthCheckConfig WHERE ID = ?;', [confID]);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ async insertHealthCheckData(confID: number, time: Date, status: HttpCheckStatus, message: string) {
|
|
|
+ const res = await this.db.query(
|
|
|
+ `INSERT INTO HealthCheckDataEntry(ConfigID, Timestamp, Status, Message) VALUES(?, ?, ?, ?);
|
|
|
+ SELECT LAST_INSERT_ID() as ID;`,
|
|
|
+ [confID, time.getTime(), status, message]
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!res || res.length < 2) throw new DatabaseException('Unexpected result during insertHealthCheckData');
|
|
|
+ const id = (res[1] as any[])[0]['ID'];
|
|
|
+
|
|
|
+ return {
|
|
|
+ id,
|
|
|
+ configId: confID,
|
|
|
+ time,
|
|
|
+ status,
|
|
|
+ message
|
|
|
+ } as HttpCheckData;
|
|
|
+ }
|
|
|
+
|
|
|
+ async queryServiceCheckData(serverID: number, confID: number, from: Date, to: Date) {
|
|
|
+ const result = await this.db.query(
|
|
|
+ `
|
|
|
+ SELECT DataEntryChanges.*
|
|
|
+ FROM HealthCheckConfig
|
|
|
+ JOIN (
|
|
|
+ SELECT * FROM (
|
|
|
+ SELECT
|
|
|
+ *
|
|
|
+ FROM HealthCheckDataEntry
|
|
|
+ WHERE ConfigID = ?
|
|
|
+ AND Timestamp BETWEEN ? AND ?
|
|
|
+ ORDER BY ID
|
|
|
+ LIMIT 0, 1
|
|
|
+ ) AS FIRST_STATE
|
|
|
+
|
|
|
+ UNION
|
|
|
+
|
|
|
+ SELECT
|
|
|
+ ID,
|
|
|
+ ConfigID,
|
|
|
+ Timestamp,
|
|
|
+ Status,
|
|
|
+ Message
|
|
|
+ FROM
|
|
|
+ (
|
|
|
+ SELECT
|
|
|
+ HealthCheckDataEntry.*,
|
|
|
+ LAG(Status) OVER (ORDER BY ConfigID, Timestamp) AS previous_state,
|
|
|
+ LAG(Message) OVER (ORDER BY ConfigID, Timestamp) AS previous_msg
|
|
|
+ FROM HealthCheckDataEntry
|
|
|
+ WHERE ConfigID = ?
|
|
|
+ )
|
|
|
+ WHERE Status <> previous_state
|
|
|
+ AND Message <> previous_msg
|
|
|
+
|
|
|
+ UNION
|
|
|
+
|
|
|
+ SELECT * FROM (
|
|
|
+ SELECT
|
|
|
+ *
|
|
|
+ FROM HealthCheckDataEntry
|
|
|
+ WHERE ConfigID = ?
|
|
|
+ AND Timestamp BETWEEN ? AND ?
|
|
|
+ ORDER BY ID DESC
|
|
|
+ LIMIT 0, 1
|
|
|
+ ) AS LAST_STATE
|
|
|
+ ORDER BY ConfigID, Timestamp
|
|
|
+ ) AS DataEntryChanges ON DataEntryChanges.ConfigID = HealthCheckConfig.ID
|
|
|
+ WHERE HealthCheckConfig.ServerID = ?
|
|
|
+ AND DataEntryChanges.Timestamp BETWEEN ? AND ?
|
|
|
+ ORDER BY Timestamp, ID;`,
|
|
|
+ [confID, from.getTime(), to.getTime(), confID, confID, from.getTime(), to.getTime(), serverID, from.getTime(), to.getTime()]
|
|
|
+ );
|
|
|
+
|
|
|
+ const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result);
|
|
|
+
|
|
|
+ const arr: ServiceCheckData[] = [];
|
|
|
+ for (const entry of mapByTimestamp.entries()) {
|
|
|
+ arr.push({
|
|
|
+ time: new Date(entry[0]),
|
|
|
+ data: entry[1]
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async queryServiceCheckLogs(serverID: number, confID: number, from: Date, to: Date) {
|
|
|
+ const result = await this.db.query(
|
|
|
+ `
|
|
|
+ SELECT HealthCheckDataEntry.*
|
|
|
+ FROM HealthCheckConfig
|
|
|
+ JOIN HealthCheckDataEntry ON HealthCheckDataEntry.ConfigID = HealthCheckConfig.ID
|
|
|
+ WHERE HealthCheckConfig.ID = ?
|
|
|
+ AND HealthCheckConfig.ServerID = ?
|
|
|
+ AND HealthCheckDataEntry.Timestamp BETWEEN ? AND ?
|
|
|
+ ORDER BY Timestamp, ConfigID;`,
|
|
|
+ [confID, serverID, from.getTime(), to.getTime()]
|
|
|
+ );
|
|
|
+
|
|
|
+ const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result);
|
|
|
+
|
|
|
+ const arr: ServiceCheckData[] = [];
|
|
|
+ for (const entry of mapByTimestamp.entries()) {
|
|
|
+ arr.push({
|
|
|
+ time: new Date(entry[0]),
|
|
|
+ data: entry[1]
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async getLastErrors(confID: number, threshold: number) {
|
|
|
+ const result = await this.db.query(
|
|
|
+ `SELECT * FROM HealthCheckDataEntry
|
|
|
+ WHERE ConfigID = ?
|
|
|
+ AND Timestamp IN (
|
|
|
+ SELECT Timestamp
|
|
|
+ FROM HealthCheckDataEntry
|
|
|
+ WHERE ConfigID = ?
|
|
|
+ GROUP BY Timestamp
|
|
|
+ ORDER BY Timestamp DESC
|
|
|
+ LIMIT 0, ?
|
|
|
+ )
|
|
|
+ ORDER BY Timestamp DESC, ID DESC`,
|
|
|
+ [confID, confID, threshold]
|
|
|
+ );
|
|
|
+
|
|
|
+ const mapByTimestamp = this.mapServiceCheckDataByTimestamp(result);
|
|
|
+ const errors: ServiceCheckData[] = [];
|
|
|
+ for (const entry of mapByTimestamp.entries()) {
|
|
|
+ const time = entry[0];
|
|
|
+ const data = entry[1];
|
|
|
+
|
|
|
+ const errorData = data.filter(d => d.status !== HttpCheckStatus.OK);
|
|
|
+ if (!errorData.length) break;
|
|
|
+
|
|
|
+ errors.push({
|
|
|
+ time: new Date(time),
|
|
|
+ data: errorData
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return errors;
|
|
|
+ }
|
|
|
+
|
|
|
+ private mapServiceCheckDataByTimestamp(rows: any[]) {
|
|
|
+ return rows.reduce((res: Map<number, ServiceCheckDataEntry[]>, row) => {
|
|
|
+ const time: number = row['Timestamp'];
|
|
|
+ if (!res.has(time)) res.set(time, []);
|
|
|
+ res.get(time)?.push({
|
|
|
+ status: row['Status'] as number,
|
|
|
+ message: row['Message']
|
|
|
+ });
|
|
|
+ return res;
|
|
|
+ }, new Map()) as Map<number, ServiceCheckDataEntry[]>;
|
|
|
+ }
|
|
|
+
|
|
|
+ private configFromResultRows(rows: any[]) {
|
|
|
+ return rows.reduce((res: ServiceConfig[], line, i) => {
|
|
|
+ const configID = line['ID'];
|
|
|
+ let config: ServiceConfig;
|
|
|
+ if (i === 0 || res[res.length - 1].id !== configID) {
|
|
|
+ config = {
|
|
|
+ id: configID,
|
|
|
+ title: line['Title'],
|
|
|
+ type: line['Type'],
|
|
|
+ serverId: line['ServerID'],
|
|
|
+ params: []
|
|
|
+ };
|
|
|
+ res.push(config);
|
|
|
+ } else {
|
|
|
+ config = res[res.length - 1];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!!line['_ParamKey']) {
|
|
|
+ const type = validateParamType(line['_ParamType']);
|
|
|
+ const key = line['_ParamKey'];
|
|
|
+ if (key === 'check') {
|
|
|
+ let checkParam = config.params.find(c => c.key === 'check');
|
|
|
+ if (!checkParam) {
|
|
|
+ config.params.push(
|
|
|
+ (checkParam = {
|
|
|
+ key: 'check',
|
|
|
+ type: 'regexp',
|
|
|
+ value: []
|
|
|
+ })
|
|
|
+ );
|
|
|
+ }
|
|
|
+ (checkParam.value as string[]).push(line['_ParamValue']);
|
|
|
+ } else {
|
|
|
+ config.params.push({
|
|
|
+ type,
|
|
|
+ key,
|
|
|
+ value: type === 'number' ? Number(line['_ParamValue']) : type === 'boolean' ? Boolean(Number(line['_ParamValue'])) : line['_ParamValue']
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return res;
|
|
|
+ }, [] as ServiceConfig[]);
|
|
|
+ }
|
|
|
+
|
|
|
+ private httpCheckConfigFrom(hcConf: ServiceConfig | null): HttpCheckConfig | null {
|
|
|
+ if (!hcConf) return null;
|
|
|
+ const params = {
|
|
|
+ url: hcConf.params?.find(p => p.key === 'url')?.value as string,
|
|
|
+ active: (hcConf.params?.find(p => p.key === 'active')?.value as boolean) ?? defaults.serviceChecks.active,
|
|
|
+ interval: hcConf.params?.find(p => p.key === 'interval')?.value as number,
|
|
|
+ timeout: (hcConf.params?.find(p => p.key === 'timeout')?.value as number) ?? defaults.serviceChecks.httpTimeout,
|
|
|
+ notify: (hcConf.params?.find(p => p.key === 'notify')?.value as boolean) ?? defaults.serviceChecks.notify,
|
|
|
+ notifyThreshold: (hcConf.params?.find(p => p.key === 'notifyThreshold')?.value as number) ?? defaults.serviceChecks.notifyThreshold,
|
|
|
+ checks: hcConf.params?.reduce(
|
|
|
+ (res, p) => (p.key === 'check' && Array.isArray(p.value) ? [...res, ...p.value.map(c => JSON.parse(c))] : res),
|
|
|
+ [] as string[]
|
|
|
+ )
|
|
|
+ };
|
|
|
+ return {
|
|
|
+ id: hcConf.id,
|
|
|
+ title: hcConf.title,
|
|
|
+ type: hcConf.type,
|
|
|
+ serverId: hcConf.serverId,
|
|
|
+ ...params
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private validateHttpCheckConfig(conf: Partial<HttpCheckConfig>): { [key: string]: string } | null {
|
|
|
+ const errors = {} as any;
|
|
|
+ if (!conf) return { null: 'Object was null or undefined' };
|
|
|
+ if (!conf.title?.trim().length) errors['required|title'] = `Field 'title' is required.`;
|
|
|
+ if (!conf.url?.trim().length) errors['required|url'] = `Field 'url' is required.`;
|
|
|
+ if ((!conf.interval && conf.interval !== 0) || Number.isNaN(Number(conf.interval))) errors['required|interval'] = `Field 'interval' is required.`;
|
|
|
+
|
|
|
+ if (!conf.checks || !Array.isArray(conf.checks))
|
|
|
+ errors['required|checks'] = `Field 'checks' is required and must be an array of check expressions.`;
|
|
|
+
|
|
|
+ return Object.keys(errors).length ? errors : null;
|
|
|
+ }
|
|
|
+}
|