| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747 |
- import moment from 'moment';
- import { MysqlError, 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';
- import { HealthCheckDataProvider } from './health-check-data-provider.interface';
- // TODO: import { DBMigration } from './db-migration.class';
- import { MariaDBConnector } from './mariadb-connector.class';
- export class MariaDBDatabase implements DataProvider, HealthCheckDataProvider {
- private db: MariaDBConnector;
- constructor(pool: Pool) {
- this.db = new MariaDBConnector(pool);
- }
- public async open(migrate = false) {
- try {
- await this.db.connect();
- Logger.debug('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) {
- let entryID = 0;
- try {
- const result = await this.db.query(
- `INSERT INTO \`ServerDataEntry\`(\`ServerID\`, \`Timestamp\`) VALUES(?, ?);
- SELECT LAST_INSERT_ID() as 'ID';`,
- [serverID, entry.time]
- );
- if (!result || result.length < 2) throw new DatabaseException('Unexpected result during insertServerData');
- entryID = (result[1] as any[])[0]['ID'];
- } catch (err: any) {
- if (err.code === 'ER_DUP_ENTRY' && (err as MysqlError).sqlMessage?.includes('UQ_ServerDataEntry_1')) {
- Logger.warn(`[server:${serverID}] Skipping`, err.sqlMessage);
- continue;
- }
- }
- 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(UNIX_TIMESTAMP(\`Timestamp\`) * 1000 / ?) * ? 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, to]
- );
- return result.map(r => ({ time: new Date(r.Timegroup), avg: r.avg, peak: r.peak, max: r.max }));
- }
- public async reduceServerData(intervalSecs: number, to: Date, from?: Date) {
- const select_daterange = from ? [from, to] : [to];
- const result: any[] = await this.db.query(
- `
- SELECT
- MIN(\`ServerDataEntry\`.\`ID\`) AS 'minEntryID',
- MAX(\`ServerDataEntry\`.\`ID\`) AS 'maxEntryID',
- \`ServerDataEntry\`.\`ServerID\` AS 'serverID',
- FROM_UNIXTIME(CEIL(UNIX_TIMESTAMP(\`ServerDataEntry\`.\`Timestamp\`) / ?) * ?) AS 'timegroup',
- \`VALUE_AVG\`.\`Type\` AS 'type',
- MAX(\`VALUE_AVG\`.\`ID\`) AS 'maxAvgID',
- AVG(\`VALUE_AVG\`.\`Value\`) AS 'avg',
- MAX(\`VALUE_PEAK\`.\`ID\`) AS 'maxPeakID',
- MAX(\`VALUE_PEAK\`.\`Value\`) AS 'peak',
- MAX(\`VALUE_MAX\`.\`ID\`) AS 'maxMaxID',
- MAX(\`VALUE_MAX\`.\`Value\`) AS 'max'
- FROM \`ServerDataEntry\`
- LEFT OUTER JOIN \`ServerDataValue\` AS \`VALUE_AVG\` ON \`ServerDataEntry\`.\`ID\` = \`VALUE_AVG\`.\`EntryID\` AND \`VALUE_AVG\`.\`Key\` = 'avg'
- LEFT OUTER JOIN \`ServerDataValue\` AS \`VALUE_PEAK\` ON \`ServerDataEntry\`.\`ID\` = \`VALUE_PEAK\`.\`EntryID\` AND \`VALUE_AVG\`.\`Type\` = \`VALUE_PEAK\`.\`Type\` AND \`VALUE_PEAK\`.\`Key\` = 'peak'
- LEFT OUTER JOIN \`ServerDataValue\` AS \`VALUE_MAX\` ON \`ServerDataEntry\`.\`ID\` = \`VALUE_MAX\`.\`EntryID\` AND \`VALUE_PEAK\`.\`Type\` = \`VALUE_MAX\`.\`Type\` AND \`VALUE_MAX\`.\`Key\` = 'max'
- WHERE ${from ? '`ServerDataEntry`.`Timestamp` BETWEEN ? AND ?' : '`ServerDataEntry`.`Timestamp` < ?'}
- GROUP BY \`serverID\`, \`timegroup\`, \`type\`
- ORDER BY \`serverID\`, \`timegroup\`, \`maxAvgID\`;`,
- [intervalSecs, intervalSecs, ...select_daterange]
- );
- let lastRow: any | undefined = undefined;
- let queries: string[] = [];
- let params: any[] = [];
- const execTransaction = async (row: any) => {
- await this.db.beginTransaction();
- try {
- Logger.debug(
- '[MARIADB] Reducing Server Data:',
- JSON.stringify({
- server: row.serverID,
- time: row.timegroup
- })
- );
- await this.db.query(queries.join('\n'), params);
- await this.db.commit();
- } catch (error) {
- Logger.error('[MARIADB] Reducing Server Data failed:', new String(error).toString());
- await this.db.rollback();
- }
- };
- for (const row of result) {
- const { minEntryID, maxEntryID, serverID, timegroup } = row;
- if (lastRow?.maxEntryID !== maxEntryID) {
- if (queries.length) await execTransaction(row);
- queries = [];
- params = [];
- }
- if (!queries.length) {
- // PARENT ENTRIES
- queries.push(
- 'DELETE FROM `ServerDataEntry` WHERE `ID` != ? AND `ServerID` = ? AND `Timestamp` = ?;', // Avoid ER_DUP_ENTRY on UQ(ServerID, Timestamp):
- 'UPDATE `ServerDataEntry` SET `Timestamp` = ? WHERE ID = ?;'
- );
- params.push(maxEntryID, serverID, timegroup, timegroup, maxEntryID);
- if (maxEntryID - 1 - minEntryID >= 0) {
- queries.push('DELETE FROM `ServerDataEntry` WHERE ServerID = ? AND ID >= ? AND ID <= ?;');
- params.push(serverID, minEntryID, maxEntryID - 1);
- }
- }
- lastRow = row;
- }
- if (queries.length) {
- await execTransaction(lastRow);
- }
- }
- 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, to]
- );
- 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('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('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, 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\` = ?
- ) AS HCDE2
- 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, to, confID, confID, from, to, serverID, from, to]
- );
- 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, to]
- );
- 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 HCDE1.*
- FROM \`HealthCheckDataEntry\` as HCDE1
- INNER JOIN (
- SELECT \`Timestamp\`
- FROM \`HealthCheckDataEntry\`
- WHERE \`ConfigID\` = ?
- GROUP BY \`Timestamp\`
- ORDER BY \`Timestamp\` DESC
- LIMIT 0, ?
- ) AS HCDE2 ON HCDE1.\`Timestamp\` = HCDE2.\`Timestamp\`
- WHERE \`ConfigID\` = ?
- ORDER BY HCDE1.\`Timestamp\` DESC, HCDE1.\`ID\` DESC`,
- [confID, threshold, confID]
- );
- 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;
- }
- public cleanupData() {
- process.nextTick(async () => {
- await this.cleanupHealthCheckData();
- await this.cleanupServerData();
- });
- }
- private async cleanupHealthCheckData() {
- const nowMinus3M = moment().subtract(3, 'months').toDate();
- await this.db.query('DELETE FROM `HealthCheckDataEntry` WHERE Timestamp < ?;', [nowMinus3M]);
- }
- private async cleanupServerData() {
- const nowMinus3M = moment().subtract(3, 'months').toDate();
- const nowMinus1M = moment().subtract(1, 'month').toDate();
- const nowMinus2W = moment().subtract(2, 'weeks').toDate();
- // Reduce Data older than 3Mo to reduced data points like if requested in a 3Mo UI chart
- let intervalSeconds = Math.floor(((365.2422 / 12) * 3 * 24 * 60 * 60) / 100);
- await this.reduceServerData(intervalSeconds, nowMinus3M);
- // Reduce Data older than 1Mo to reduced data points like if requested in a 1Mo UI chart
- intervalSeconds = Math.floor(((365.2422 / 12) * 1 * 24 * 60 * 60) / 100);
- await this.reduceServerData(intervalSeconds, nowMinus1M, nowMinus3M);
- // Reduce Data older than 2We to reduced data points like if requested in a 2We UI chart
- intervalSeconds = Math.floor((14 * 24 * 60 * 60) / 100);
- await this.reduceServerData(intervalSeconds, nowMinus2W, nowMinus1M);
- }
- public async close(): Promise<void> {
- await this.db.close();
- }
- }
|