From 510a583d7a2e38b0c236c44bdd3c3f950a32cad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Mon, 19 Aug 2024 10:27:11 +0000 Subject: [PATCH 1/8] fix(config): Better config file verification (and exit app when incorrect) Fixes #857 --- src/butler-sos.js | 24 ++++++++++++++++-------- src/lib/config-file-verify.js | 5 +++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/butler-sos.js b/src/butler-sos.js index 95002123..e7113696 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -56,6 +56,22 @@ async function sleep(ms) { } async function mainScript() { + // Verify that the config file has the correct format + // Only do this if the command line option no-config-file-verify is NOT set + let configFileVerify = false + if (globals.options.skipConfigVerification) { + globals.logger.warn('MAIN: Skipping config file verification'); + } else { + configFileVerify = await verifyConfigFile(); + } + // If config file verification failed, the previous function would have returned false. + // In that case, we should exit the script. + if (!configFileVerify) { + globals.logger.error('MAIN: Config file verification failed. Exiting.'); + process.exit(1); + } + + // Sleep 5 seconds to allow inits to complete globals.logger.info('MAIN: Waiting 5 seconds for initializations to complete...'); globals.logger.info('5...'); @@ -75,14 +91,6 @@ async function mainScript() { serviceUptime.serviceUptimeStart(); } - // Verify that the config file has the correct format - // Only do this if the command line option no-config-file-verify is NOT set - if (globals.options.skipConfigVerification) { - globals.logger.warn('MAIN: Skipping config file verification'); - } else { - await verifyConfigFile(); - } - // Load certificates to use when connecting to healthcheck API const certFile = path.resolve(process.cwd(), globals.config.get('Butler-SOS.cert.clientCert')); const keyFile = path.resolve( diff --git a/src/lib/config-file-verify.js b/src/lib/config-file-verify.js index 0a7cd8de..ea47e85b 100755 --- a/src/lib/config-file-verify.js +++ b/src/lib/config-file-verify.js @@ -112,10 +112,11 @@ async function verifyConfigFile() { logger.info(`VERIFY CONFIG FILE: Your config file at ${configFile} is valid, good work!`); - return 'boolean'; + return true; } catch (err) { logger.error(`VERIFY CONFIG FILE: ${err}`); - return []; + + return false; } } From ae4083b33e77303076f6a4b3e780d4e01a115afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 05:17:29 +0000 Subject: [PATCH 2/8] chore: Major update - refactored entire code base from CJS > ESM Implements #859 --- .eslintrc.yml | 14 - eslint.config.js | 37 + package-lock.json | 103 +- package.json | 4 + src/butler-sos.js | 92 +- src/docker-healthcheck.js | 2 +- src/globals.js | 1682 ++++++++++++++----------- src/lib/appnamesextract.js | 25 +- src/lib/config-file-schema.js | 6 +- src/lib/config-file-verify.js | 52 +- src/lib/healthmetrics.js | 47 +- src/lib/heartbeat.js | 10 +- src/lib/log-event-categorise.js | 18 +- src/lib/logdb.js | 12 +- src/lib/post-to-influxdb.js | 22 +- src/lib/post-to-mqtt.js | 20 +- src/lib/post-to-new-relic.js | 24 +- src/lib/prom-client.js | 18 +- src/lib/proxysessionmetrics.js | 38 +- src/lib/serverheaders.js | 8 +- src/lib/servertags.js | 7 +- src/lib/service_uptime.js | 24 +- src/lib/telemetry.js | 11 +- src/lib/udp_handlers_log_events.js | 23 +- src/lib/udp_handlers_user_activity.js | 24 +- 25 files changed, 1253 insertions(+), 1070 deletions(-) delete mode 100644 .eslintrc.yml create mode 100644 eslint.config.js diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 4b247b58..00000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,14 +0,0 @@ -env: - es6: true - es2021: true - node: true -extends: - - airbnb-base - - prettier -parserOptions: - ecmaVersion: 12 - sourceType: module -rules: - prettier/prettier: error -plugins: - - prettier diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..44fc837b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,37 @@ +import prettier from 'eslint-plugin-prettier'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +// export default [...compat.extends("airbnb-base", "prettier"), { +export default [ + ...compat.extends('prettier'), + { + plugins: { + prettier, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + ecmaVersion: 12, + sourceType: 'module', + }, + + rules: { + 'prettier/prettier': 'error', + }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 1262358e..82d34f95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,10 +36,13 @@ "yaml-validator": "^5.0.1" }, "devDependencies": { + "@eslint/js": "^9.9.0", "esbuild": "^0.23.1", "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-formatter-table": "^7.32.1", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.9.0", "prettier": "^3.3.3", "snyk": "^1.1292.4" } @@ -564,6 +567,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.9.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", @@ -726,6 +742,19 @@ "node": ">=8.0.0" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sentry-internal/tracing": { "version": "7.64.0", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.64.0.tgz", @@ -1506,6 +1535,37 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", @@ -1658,6 +1718,13 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2053,9 +2120,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, "license": "MIT", "engines": { @@ -2890,6 +2957,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3332,6 +3412,23 @@ "node": ">=8" } }, + "node_modules/synckit": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/systeminformation": { "version": "5.23.4", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.4.tgz", diff --git a/package.json b/package.json index 5cb03f1e..7a170459 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "url": "https://github.com/ptarmiganlabs/butler-sos/issues" }, "homepage": "https://github.com/ptarmiganlabs/butler-sos#readme", + "type": "module", "dependencies": { "@breejs/later": "^4.2.0", "@influxdata/influxdb-client": "^1.35.0", @@ -56,10 +57,13 @@ "yaml-validator": "^5.0.1" }, "devDependencies": { + "@eslint/js": "^9.9.0", "esbuild": "^0.23.1", "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-formatter-table": "^7.32.1", + "eslint-plugin-prettier": "^5.2.1", + "globals": "^15.9.0", "prettier": "^3.3.3", "snyk": "^1.1292.4" }, diff --git a/src/butler-sos.js b/src/butler-sos.js index e7113696..e1a26383 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -1,30 +1,29 @@ // Add dependencies -const path = require('path'); -const FastifyHealthcheck = require('fastify-healthcheck'); -const Fastify = require('fastify'); +import path from 'path'; +import FastifyHealthcheck from 'fastify-healthcheck'; +import Fastify from 'fastify'; const promServer = Fastify({ logger: false }); const promFastifyMetricsServer = Fastify({ logger: false }); const dockerHealthCheckServer = Fastify({ logger: false }); -const metricsPlugin = require('fastify-metrics'); +import metricsPlugin from 'fastify-metrics'; promServer.server.keepAliveTimeout = 0; promFastifyMetricsServer.register(metricsPlugin, { endpoint: '/metrics' }); // Load code from sub modules -const globals = require('./globals'); -const healthMetrics = require('./lib/healthmetrics'); -const logDb = require('./lib/logdb'); -const proxySessionMetrics = require('./lib/proxysessionmetrics'); -const appNamesExtract = require('./lib/appnamesextract'); -const heartbeat = require('./lib/heartbeat'); -const serviceUptime = require('./lib/service_uptime'); -const udpUserActivity = require('./lib/udp_handlers_user_activity'); -const udpLogEvents = require('./lib/udp_handlers_log_events'); -const telemetry = require('./lib/telemetry'); -const promClient = require('./lib/prom-client'); -const { verifyConfigFile } = require('./lib/config-file-verify'); +import { setupHealthMetricsTimer } from './lib/healthmetrics.js'; +import { setupLogDbTimer } from './lib/logdb.js'; +import { setupUserSessionsTimer } from './lib/proxysessionmetrics.js'; +import { setupAppNamesExtractTimer } from './lib/appnamesextract.js'; +import { setupHeartbeatTimer } from './lib/heartbeat.js'; +import { serviceUptimeStart } from './lib/service_uptime.js'; +import { udpInitUserActivityServer } from './lib/udp_handlers_user_activity.js'; +import { udpInitLogEventServer } from './lib/udp_handlers_log_events.js'; +import { setupAnonUsageReportTimer } from './lib/telemetry.js'; +import { setupPromClient } from './lib/prom-client.js'; +import { verifyConfigFile } from './lib/config-file-verify.js'; // Suppress experimental warnings // https://stackoverflow.com/questions/55778283/how-to-disable-warnings-when-node-is-launched-via-a-global-shell-script @@ -56,9 +55,14 @@ async function sleep(ms) { } async function mainScript() { + // Load globals dynamically/async to ensure singleton pattern works + const settingsObj = (await import('./globals.js')).default; + const globals = await settingsObj.init(); + globals.logger.verbose(`START: Globals init done: ${globals.initialised}`); + // Verify that the config file has the correct format // Only do this if the command line option no-config-file-verify is NOT set - let configFileVerify = false + let configFileVerify = false; if (globals.options.skipConfigVerification) { globals.logger.warn('MAIN: Skipping config file verification'); } else { @@ -71,24 +75,32 @@ async function mainScript() { process.exit(1); } + // Ensure that initialisation of globals is complete + // Sleep 5 seconds otherwise to llow globals to be initialised - // Sleep 5 seconds to allow inits to complete - globals.logger.info('MAIN: Waiting 5 seconds for initializations to complete...'); - globals.logger.info('5...'); - await sleep(1000); - globals.logger.info('4...'); - await sleep(1000); - globals.logger.info('3...'); - await sleep(1000); - globals.logger.info('2...'); - await sleep(1000); - globals.logger.info('1...'); - await sleep(1000); + function sleepLocal(ms) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setTimeout(resolve, ms)); + } - await globals.initInfluxDB(); + if (!globals.initialised) { + globals.logger.info('START: Sleeping 5 seconds to allow globals to be initialised.'); + globals.logger.info('5...'); + await sleepLocal(1000); + globals.logger.info('4...'); + await sleepLocal(1000); + globals.logger.info('3...'); + await sleepLocal(1000); + globals.logger.info('2...'); + await sleepLocal(1000); + globals.logger.info('1...'); + await sleepLocal(1000); + } else { + globals.logger.info('START: Globals initialised - all good.'); + } if (globals.config.get('Butler-SOS.uptimeMonitor.enable') === true) { - serviceUptime.serviceUptimeStart(); + serviceUptimeStart(); } // Load certificates to use when connecting to healthcheck API @@ -101,7 +113,7 @@ async function mainScript() { // Set up heartbeats, if enabled in the config file if (globals.config.get('Butler-SOS.heartbeat.enable') === true) { - heartbeat.setupHeartbeatTimer(globals.config, globals.logger); + setupHeartbeatTimer(globals.config, globals.logger); } try { @@ -140,7 +152,7 @@ async function mainScript() { // Set up anon usage reports, if enabled if (globals.config.get('Butler-SOS.anonTelemetry') === true) { - telemetry.setupAnonUsageReportTimer(); + setupAnonUsageReportTimer(); globals.logger.verbose('MAIN: Anonymous telemetry reporting has been set up.'); globals.logger.verbose( 'MAIN: ❤️ Thank you for supporting Butler SOS by allowing telemetry! ❤️' @@ -152,7 +164,7 @@ async function mainScript() { // Set up UDP handler for user activity/events if (globals.config.get('Butler-SOS.userEvents.enable')) { - udpUserActivity.udpInitUserActivityServer(); + udpInitUserActivityServer(); globals.logger.debug( `MAIN: Server for user activity/events UDP server: ${globals.udpServerUserActivity.host}` @@ -171,7 +183,7 @@ async function mainScript() { globals.config.get('Butler-SOS.logEvents.source.scheduler.enable') || globals.config.get('Butler-SOS.logEvents.source.proxy.enable') ) { - udpLogEvents.udpInitLogEventServer(); + udpInitLogEventServer(); globals.logger.debug( `MAIN: Server for user activity/events UDP server: ${globals.udpServerLogEvents.host}` @@ -224,7 +236,7 @@ async function mainScript() { globals.logger.info( `MAIN: Starting Prometheus Butler SOS endpoint on ${promHost}:${promPort}.` ); - promClient.setupPromClient(promServer, promPort, promHost); + setupPromClient(promServer, promPort, promHost); } catch (err) { globals.logger.error( `MAIN: Error while starting Prometheus Butler SOS endpoint on ${promHost}:${promPort}.` @@ -250,20 +262,20 @@ async function mainScript() { // Set up extraction of data from log db if (globals.config.get('Butler-SOS.logdb.enable') === true) { - logDb.setupLogDbTimer(); + setupLogDbTimer(); } // Set up extraction of sessions data if (globals.config.get('Butler-SOS.userSessions.enableSessionExtract') === true) { - proxySessionMetrics.setupUserSessionsTimer(); + setupUserSessionsTimer(); } // Set up extraction on main metrics data (i.e. the Sense healthcheck API) - healthMetrics.setupHealthMetricsTimer(); + setupHealthMetricsTimer(); // Set up extraction of app IDs and names if (globals.config.get('Butler-SOS.appNames.enableAppNameExtract') === true) { - appNamesExtract.setupAppNamesExtractTimer(); + setupAppNamesExtractTimer(); } } diff --git a/src/docker-healthcheck.js b/src/docker-healthcheck.js index 31376a0a..e604a416 100755 --- a/src/docker-healthcheck.js +++ b/src/docker-healthcheck.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ // Set up REST endpoint for Docker healthchecks -const httpHealth = require('http'); +import httpHealth from 'http'; const optionsHealth = { host: 'localhost', diff --git a/src/globals.js b/src/globals.js index f67ce2a4..257a43c0 100755 --- a/src/globals.js +++ b/src/globals.js @@ -1,843 +1,987 @@ -const path = require('path'); -const dgram = require('dgram'); -const os = require('os'); -const crypto = require('crypto'); - -const mqtt = require('mqtt'); -const fs = require('fs-extra'); -const winston = require('winston'); -require('winston-daily-rotate-file'); -const si = require('systeminformation'); -const { Command, Option } = require('commander'); -const Influx = require('influx'); -const { InfluxDB, HttpError, DEFAULT_WriteOptions } = require('@influxdata/influxdb-client'); -const { OrgsAPI, BucketsAPI } = require('@influxdata/influxdb-client-apis'); - -const { getServerTags } = require('./lib/servertags'); - -const InfluxDB2 = InfluxDB; - -const { Pool } = require('pg'); - -function checkFileExistsSync(filepath) { - let flag = true; - try { - fs.accessSync(filepath, fs.constants.F_OK); - } catch (e) { - flag = false; - } - return flag; -} +import upath from 'path'; +import dgram from 'dgram'; +import os from 'os'; +import crypto from 'crypto'; +import mqtt from 'mqtt'; +import fs from 'fs-extra'; +import winston from 'winston'; +import 'winston-daily-rotate-file'; +import si from 'systeminformation'; +import pg from 'pg'; +import { readFileSync } from 'fs'; +import Influx from 'influx'; +import { Command, Option } from 'commander'; +import { InfluxDB, HttpError, DEFAULT_WriteOptions } from '@influxdata/influxdb-client'; +import { OrgsAPI, BucketsAPI } from '@influxdata/influxdb-client-apis'; + +import { getServerTags } from './lib/servertags.js'; +import { fileURLToPath } from 'url'; + +let instance = null; + +class Settings { + constructor() { + if (!instance) { + instance = this; + } -// Get app version from package.json file -const appVersion = require('../package.json').version; - -// Command line parameters -const program = new Command(); -program - .version(appVersion) - .name('butler-sos') - .description( - 'Butler SenseOps Stats ("Butler-SOS") is a microservice publishing operational Qlik Sense metrics to InfluxDB, Prometheus and New Relic.\nUser events and log events can be forwarded from Sense to Butler SOS and then acted upon there. Events can be stored in InfluxDB and sent to New Relic.\nAdd Grafana for great looking dashboards and you get real-time monitoring of what happens inside a Qlik Sense environment.' - ) - .option('-c, --configfile ', 'path to config file') - .addOption( - new Option('-l, --loglevel ', 'log level').choices([ - 'error', - 'warn', - 'info', - 'verbose', - 'debug', - 'silly', - ]) - ) - .option( - '--new-relic-account-name ', - 'New Relic account name. Used within Butler SOS to differentiate between different target New Relic accounts' - ) - .option('--new-relic-api-key ', 'insert API key to use with New Relic') - .option('--new-relic-account-id ', 'New Relic account ID') - - .option('--skip-config-verification', 'Disable config file verification', false); - -// Parse command line params -program.parse(process.argv); -const options = program.opts(); - -// Is there a config file specified on the command line? -let configFileOption; -let configFileExpanded; -let configFilePath; -let configFileBasename; -let configFileExtension = '.yaml'; -if (options.configfile && options.configfile.length > 0) { - configFileOption = options.configfile; - configFileExpanded = path.resolve(options.configfile); - configFilePath = path.dirname(configFileExpanded); - configFileExtension = path.extname(configFileExpanded); - configFileBasename = path.basename(configFileExpanded, configFileExtension); - - if (configFileExtension.toLowerCase() !== '.yaml') { - // eslint-disable-next-line no-console - console.log('Error: Config file extension must be yaml'); - process.exit(1); - } + // Flag to keep track of initialisation status of globals object + this.initialised = false; - if (checkFileExistsSync(options.configfile)) { - process.env.NODE_CONFIG_DIR = configFilePath; - process.env.NODE_ENV = configFileBasename; - } else { - // eslint-disable-next-line no-console - console.log('Error: Specified config file does not exist'); - process.exit(1); - } -} else { - // Set default values of environment variables controlling config file location and name - if (process.env.NODE_CONFIG_DIR === undefined) { - process.env.NODE_CONFIG_DIR = path.join(process.cwd(), 'config'); + return instance; } - if (process.env.NODE_ENV === undefined) { - process.env.NODE_ENV = 'production'; - } -} + async init() { + // Get app version from package.json file + const filenamePackage = `./package.json`; + let a; + let b; + let c; + // Are we running as a packaged app? + if (process.pkg) { + // Get path to JS file + a = process.pkg.defaultEntrypoint; + + // Strip off the filename + b = upath.dirname(a); + + // Add path to package.json file + c = upath.join(b, filenamePackage); + + // Set base path of the executable + this.appBasePath = upath.join(b); + } else { + // Get path to JS file + a = fileURLToPath(import.meta.url); + + // Strip off the filename + b = upath.dirname(a); + + // Add path to package.json file + c = upath.join(b, '..', filenamePackage); + + // Set base path of the executable + this.appBasePath = upath.join(b, '..'); + } -// Set global variable conttaining the name and full path of the config file -const configFile = path.join( - process.env.NODE_CONFIG_DIR, - `${process.env.NODE_ENV}${configFileExtension}` -); - -// Are we running as standalone app or not? -const isPkg = typeof process.pkg !== 'undefined'; -if (isPkg && configFileOption === undefined) { - // Show help if running as standalone app and mandatory options (e.g. config file) are not specified - program.help({ error: true }); -} + const { version } = JSON.parse(readFileSync(c)); + this.appVersion = version; + + // Make copy of influxdb client + const InfluxDB2 = InfluxDB; + + // Command line parameters + const program = new Command(); + program + .version(this.appVersion) + .name('butler-sos') + .description( + 'Butler SenseOps Stats ("Butler-SOS") is a tool publishing operational Qlik Sense metrics to InfluxDB, Prometheus, New Relic and other destinations.\nUser events and log events can be forwarded from Sense to Butler SOS and then acted upon there. Events can be stored in InfluxDB and sent to New Relic.\nAdd Grafana for great looking dashboards and you get real-time monitoring of what happens inside a Qlik Sense environment.\n\nMore info at https://butler-sos.ptarmiganlabs.com' + ) + .option('-c, --configfile ', 'path to config file') + .addOption( + new Option('-l, --loglevel ', 'log level').choices([ + 'error', + 'warn', + 'info', + 'verbose', + 'debug', + 'silly', + ]) + ) + .option( + '--new-relic-account-name ', + 'New Relic account name. Used within Butler SOS to differentiate between different target New Relic accounts' + ) + .option('--new-relic-api-key ', 'insert API key to use with New Relic') + .option('--new-relic-account-id ', 'New Relic account ID') + + .option('--skip-config-verification', 'Disable config file verification', false); + + // Parse command line params + program.parse(process.argv); + this.options = program.opts(); + + // Utility functions + this.checkFileExistsSync = Settings.checkFileExistsSync; + this.sleep = Settings.sleep; + + // Is there a config file specified on the command line? + let configFileOption; + this.configFile = null; + let configFilePath; + let configFileBasename; + let configFileExtension; + if (this.options.configfile && this.options.configfile.length > 0) { + configFileOption = this.options.configfile; + + // Full path to config file + this.configFile = upath.resolve(this.options.configfile); + configFilePath = upath.dirname(this.configFile); + configFileExtension = upath.extname(this.configFile); + configFileBasename = upath.basename(this.configFile, configFileExtension); + + if (configFileExtension.toLowerCase() !== '.yaml') { + // eslint-disable-next-line no-console + console.log('Error: Config file extension must be yaml'); + process.exit(1); + } -// eslint-disable-next-line import/order -const config = require('config'); + if (this.checkFileExistsSync(this.options.configfile)) { + process.env.NODE_CONFIG_DIR = configFilePath; + process.env.NODE_ENV = configFileBasename; + } else { + // eslint-disable-next-line no-console + console.log('Error: Specified config file does not exist'); + process.exit(1); + } + } else { + // Get value of env variable NODE_ENV + const env = process.env.NODE_ENV; + + // Get path to config file + const filename = fileURLToPath(import.meta.url); + const dirname = upath.dirname(filename); + this.configFile = upath.resolve(dirname, `./config/${env}.yaml`); + } -// Is there a log level file specified on the command line? -if (options.loglevel && options.loglevel.length > 0) { - config['Butler-SOS'].logLevel = options.loglevel; -} + this.config = (await import('config')).default; -// Set up array for storing app ids and names -const appNames = []; - -// Set up logger with timestamps and colors, and optional logging to disk file -const logTransports = []; - -logTransports.push( - new winston.transports.Console({ - name: 'console', - level: config.get('Butler-SOS.logLevel'), - format: winston.format.combine( - winston.format.errors({ stack: true }), - winston.format.timestamp(), - winston.format.colorize(), - winston.format.simple(), - winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`) - ), - }) -); - -if (config.get('Butler-SOS.fileLogging')) { - logTransports.push( - new winston.transports.DailyRotateFile({ - dirname: path.join(process.cwd(), config.get('Butler-SOS.logDirectory')), - filename: 'butler-sos.%DATE%.log', - level: config.get('Butler-SOS.logLevel'), - datePattern: 'YYYY-MM-DD', - maxFiles: '30d', - }) - ); -} + this.execPath = this.isPkg ? upath.dirname(process.execPath) : process.cwd(); -const logger = winston.createLogger({ - transports: logTransports, - format: winston.format.combine( - winston.format.timestamp(), - winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`) - ), -}); - -// Show contents of environment variables controlling config file location and name -logger.debug(`NODE_CONFIG_DIR: ${process.env.NODE_CONFIG_DIR}`); -logger.debug(`NODE_ENV: ${process.env.NODE_ENV}`); - -// Output config file name and path to log -logger.info(`Using config file: ${configFile}`); - -// Function to get current logging level -const getLoggingLevel = () => logTransports.find((transport) => transport.name === 'console').level; - -// Are there New Relic account name(s), API key(s) and account ID(s) specified on the command line? -// There must be the same number of each specified! -// If so, replace any info from the config file with data from command line options -if ( - options?.newRelicAccountName?.length > 0 && - options?.newRelicApiKey?.length > 0 && - options?.newRelicAccountId?.length > 0 && - options?.newRelicAccountName?.length === options?.newRelicApiKey?.length && - options?.newRelicApiKey?.length === options?.newRelicAccountId?.length -) { - config['Butler-SOS'].thirdPartyToolsCredentials.newRelic = []; - - for (let index = 0; index < options.newRelicApiKey.length; index += 1) { - const accountName = options.newRelicAccountName[index]; - const accountId = options.newRelicAccountId[index]; - const insertApiKey = options.newRelicApiKey[index]; - - config['Butler-SOS'].thirdPartyToolsCredentials.newRelic.push({ - accountName, - accountId, - insertApiKey, - }); - } -} else if ( - options?.newRelicAccountName?.length > 0 || - options?.newRelicApiKey?.length > 0 || - options?.newRelicAccountId?.length > 0 -) { - logger.error( - 'Incorrect command line parameters: Number of New Relic account names/IDs/API keys must match.' - ); - process.exit(1); -} + // Are we running as standalone app or not? + this.isPkg = typeof process.pkg !== 'undefined'; + if (this.isPkg && configFileOption === undefined) { + // Show help if running as standalone app and mandatory options (e.g. config file) are not specified + program.help({ error: true }); + } -// ------------------------------------ -// User activity UDP server -const udpServerUserActivity = {}; + // Is there a log level file specified on the command line? + if (this.options.loglevel && this.options.loglevel.length > 0) { + this.config['Butler-SOS'].logLevel = this.options.loglevel; + } -try { - udpServerUserActivity.host = config.get('Butler-SOS.userEvents.udpServerConfig.serverHost'); + // Set up array for storing app ids and names + this.appNames = []; + + // Set up logger with timestamps and colors, and optional logging to disk file + this.logTransports = []; + + this.logTransports.push( + new winston.transports.Console({ + name: 'console', + level: this.config.get('Butler-SOS.logLevel'), + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.timestamp(), + winston.format.colorize(), + winston.format.simple(), + winston.format.printf( + (info) => `${info.timestamp} ${info.level}: ${info.message}` + ) + ), + }) + ); - // Prepare to listen on port X for incoming UDP connections regarding user activity events - udpServerUserActivity.socket = dgram.createSocket({ - type: 'udp4', - reuseAddr: true, - }); + if ( + this.config['Butler-SOS'].logLevel === 'verbose' || + this.config['Butler-SOS'].logLevel === 'debug' || + this.config['Butler-SOS'].logLevel === 'silly' + ) { + // We don't have a logging object yet, so use plain console.log + + // Are we in a packaged app? + if (this.isPkg) { + // eslint-disable-next-line no-console + console.log(`Running in packaged app. Executable path: ${this.execPath}`); + } else { + // eslint-disable-next-line no-console + console.log( + `Running in non-packaged environment. Executable path: ${this.execPath}` + ); + } - udpServerUserActivity.portUserActivity = config.get( - 'Butler-SOS.userEvents.udpServerConfig.portUserActivityEvents' - ); -} catch (err) { - logger.error(`CONFIG: Setting up UDP user activity listener: ${err}`); -} + // eslint-disable-next-line no-console + console.log( + `Log file directory: ${upath.join(this.execPath, this.config.get('Butler-SOS.logDirectory'))}` + ); -// ------------------------------------ -// Log events UDP server -const udpServerLogEvents = {}; + // eslint-disable-next-line no-console + console.log(`upath.dirname(process.execPath): ${upath.dirname(process.execPath)}`); -try { - udpServerLogEvents.host = config.get('Butler-SOS.logEvents.udpServerConfig.serverHost'); + // eslint-disable-next-line no-console + console.log(`process.cwd(): ${process.cwd()}`); + } - // Prepare to listen on port X for incoming UDP connections regarding user activity events - udpServerLogEvents.socket = dgram.createSocket({ - type: 'udp4', - reuseAddr: true, - }); + if (this.config.get('Butler-SOS.fileLogging')) { + this.logTransports.push( + new winston.transports.DailyRotateFile({ + dirname: upath.join(this.execPath, this.config.get('Butler-SOS.logDirectory')), + filename: 'butler-sos.%DATE%.log', + level: this.config.get('Butler-SOS.logLevel'), + datePattern: 'YYYY-MM-DD', + maxFiles: '30d', + }) + ); + } - udpServerLogEvents.port = config.get('Butler-SOS.logEvents.udpServerConfig.portLogEvents'); -} catch (err) { - logger.error(`CONFIG: Setting up UDP log events listener: ${err}`); -} + this.logger = winston.createLogger({ + transports: this.logTransports, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`) + ), + }); -// ------------------------------------ -// Get info on what servers to monitor -const serverList = config.get('Butler-SOS.serversToMonitor.servers'); - -// Only set up connection pool for accessing Qlik Sense log db if that feature is enabled -let pgPool; -if (config.get('Butler-SOS.logdb.enable') === true) { - // Set up connection pool for accessing Qlik Sense log db - pgPool = new Pool({ - host: config.get('Butler-SOS.logdb.host'), - database: 'QLogs', - user: config.get('Butler-SOS.logdb.qlogsReaderUser'), - password: config.get('Butler-SOS.logdb.qlogsReaderPwd'), - port: config.get('Butler-SOS.logdb.port'), - }); - - // the pool will emit an error on behalf of any idle clients - // it contains if a backend error or network partition happens - // eslint-disable-next-line no-unused-vars - pgPool.on('error', (err, client) => { - logger.error(`CONFIG: Unexpected error on idle client: ${err}`); - // process.exit(-1); - }); -} + // Show contents of environment variables controlling config file location and name + this.logger.debug(`NODE_CONFIG_DIR: ${process.env.NODE_CONFIG_DIR}`); + this.logger.debug(`NODE_ENV: ${process.env.NODE_ENV}`); -// Get list of standard and user configurable tags -// ..begin with standard tags -const tagValues = ['host', 'server_name', 'server_description']; - -// ..check if there are any extra tags for this Butler SOS instance that should be sent to InfluxDB -if ( - config.has('Butler-SOS.serversToMonitor.serverTagsDefinition') && - config.get('Butler-SOS.serversToMonitor.serverTagsDefinition') !== null -) { - // Loop over all tags defined for the current server, adding them to the data structure that will later be passed to Influxdb - config.get('Butler-SOS.serversToMonitor.serverTagsDefinition').forEach((entry) => { - logger.debug(`CONFIG: Setting up new Influx database: Found server tag : ${entry}`); - - tagValues.push(entry); - }); -} + // Output config file name and path to log + this.logger.info(`Using config file: ${this.configFile}`); + + // Function to get current logging level + this.getLoggingLevel = () => + this.logTransports.find((transport) => transport.name === 'console').level; + + // Are there New Relic account name(s), API key(s) and account ID(s) specified on the command line? + // There must be the same number of each specified! + // If so, replace any info from the config file with data from command line options + if ( + this.options?.newRelicAccountName?.length > 0 && + this.options?.newRelicApiKey?.length > 0 && + this.options?.newRelicAccountId?.length > 0 && + this.options?.newRelicAccountName?.length === this.options?.newRelicApiKey?.length && + this.options?.newRelicApiKey?.length === this.options?.newRelicAccountId?.length + ) { + this.config['Butler-SOS'].thirdPartyToolsCredentials.newRelic = []; + + for (let index = 0; index < this.options.newRelicApiKey.length; index += 1) { + const accountName = this.options.newRelicAccountName[index]; + const accountId = this.options.newRelicAccountId[index]; + const insertApiKey = this.options.newRelicApiKey[index]; + + this.config['Butler-SOS'].thirdPartyToolsCredentials.newRelic.push({ + accountName, + accountId, + insertApiKey, + }); + } + } else if ( + this.options?.newRelicAccountName?.length > 0 || + this.options?.newRelicApiKey?.length > 0 || + this.options?.newRelicAccountId?.length > 0 + ) { + this.logger.error( + 'Incorrect command line parameters: Number of New Relic account names/IDs/API keys must match.' + ); + process.exit(1); + } -// Add tags for log events -const tagValuesLogEvent = tagValues.slice(); -tagValuesLogEvent.push('level'); -tagValuesLogEvent.push('source'); -tagValuesLogEvent.push('log_row'); -tagValuesLogEvent.push('subsystem'); -tagValuesLogEvent.push('user_full'); -tagValuesLogEvent.push('user_directory'); -tagValuesLogEvent.push('user_id'); -tagValuesLogEvent.push('task_id'); -tagValuesLogEvent.push('task_name'); -tagValuesLogEvent.push('app_id'); -tagValuesLogEvent.push('app_name'); -tagValuesLogEvent.push('result_code'); -tagValuesLogEvent.push('windows_user'); -tagValuesLogEvent.push('engine_exe_version'); - -// Check if there are any extra log event tags in the config file -if (config.has('Butler-SOS.logEvents.tags') && config.get('Butler-SOS.logEvents.tags') !== null) { - config.get('Butler-SOS.logEvents.tags').forEach((entry) => { - logger.debug( - `CONFIG: Setting up new Influx database: Found log event tag in config file: ${JSON.stringify( - entry + // Verbose: Show what New Relic account names/API keys/account IDs have been defined (on command line or in config file) + this.logger.verbose( + `New Relic account names/API keys/account IDs (via command line or config file): ${JSON.stringify( + this.config['Butler-SOS'].thirdPartyToolsCredentials.newRelic, + null, + 2 )}` ); - tagValuesLogEvent.push(entry.tag); - }); -} + // Get certificate file paths for QRS connection + const filename = fileURLToPath(import.meta.url); + const dirname = upath.dirname(filename); + this.certPath = upath.resolve(dirname, this.config.get('Butler-SOS.cert.clientCert')); + this.keyPath = upath.resolve(dirname, this.config.get('Butler-SOS.cert.clientCertKey')); + this.caPath = upath.resolve(dirname, this.config.get('Butler-SOS.cert.clientCertCA')); -// Add tags for log events categories, if enabled and configured -if ( - config.has('Butler-SOS.logEvents.categorise.enable') && - config.get('Butler-SOS.logEvents.categorise.enable') === true && - config.has('Butler-SOS.logEvents.categorise.rules') -) { - // Add tags from Butler-SOS.logEvents.categorise.rules[].category[], where each object has properties 'name' and 'value' - config.get('Butler-SOS.logEvents.categorise.rules').forEach((rule) => { - rule.category.forEach((category) => { - tagValuesLogEvent.push(category.name); - }); - }); - - // Add default rule categories, if enabled - if ( - config.has('Butler-SOS.logEvents.categorise.ruleDefault.enable') && - config.get('Butler-SOS.logEvents.categorise.ruleDefault.enable') === true && - config.has('Butler-SOS.logEvents.categorise.ruleDefault.category') - ) { - config.get('Butler-SOS.logEvents.categorise.ruleDefault.category').forEach((category) => { - tagValuesLogEvent.push(category.name); - }); - } -} + // ------------------------------------ + // User activity UDP server + this.udpServerUserActivity = {}; -// Create InfluxDB tags for data coming from log db -const tagValuesLogEventLogDb = tagValues.slice(); -tagValuesLogEventLogDb.push('source_process'); -tagValuesLogEventLogDb.push('log_level'); - -// Create tags for user sessions -const tagValuesUserProxySessions = tagValues.slice(); -tagValuesUserProxySessions.push('user_session_virtual_proxy'); -tagValuesUserProxySessions.push('user_session_host'); - -// Show Influxdb config -if (config.get('Butler-SOS.influxdbConfig.enable') === true) { - logger.info(`CONFIG: Influxdb enabled: true`); - logger.info(`CONFIG: Influxdb host IP: ${config.get('Butler-SOS.influxdbConfig.host')}`); - logger.info(`CONFIG: Influxdb host port: ${config.get('Butler-SOS.influxdbConfig.port')}`); - logger.info(`CONFIG: Influxdb version: ${config.get('Butler-SOS.influxdbConfig.version')}`); - - // Version specific configs - if (config.get('Butler-SOS.influxdbConfig.version') === 1) { - logger.info( - `CONFIG: Influxdb db name: ${config.get('Butler-SOS.influxdbConfig.v1Config.dbName')}` - ); - logger.info( - `CONFIG: Influxdb retention policy: ${config.get('Butler-SOS.influxdbConfig.v1Config.retentionPolicy.name')}` - ); - } else if (config.get('Butler-SOS.influxdbConfig.version') === 2) { - logger.info( - `CONFIG: Influxdb organisation: ${config.get('Butler-SOS.influxdbConfig.v2Config.org')}` - ); - logger.info( - `CONFIG: Influxdb bucket name: ${config.get('Butler-SOS.influxdbConfig.v2Config.bucket')}` - ); - logger.info( - `CONFIG: Influxdb retention policy duration: ${config.get('Butler-SOS.influxdbConfig.v2Config.retentionDuration')}` - ); - } else { - logger.error( - `CONFIG: Influxdb version ${config.get('Butler-SOS.influxdbConfig.version')} is not supported!` - ); - } -} else { - logger.info(`CONFIG: Influxdb enabled: false`); -} + try { + this.udpServerUserActivity.host = this.config.get( + 'Butler-SOS.userEvents.udpServerConfig.serverHost' + ); + + // Prepare to listen on port X for incoming UDP connections regarding user activity events + this.udpServerUserActivity.socket = dgram.createSocket({ + type: 'udp4', + reuseAddr: true, + }); -// Set up Influxdb client -let influx; -const influxWriteApi = []; -if (config.get('Butler-SOS.influxdbConfig.enable') === true) { - if (config.get('Butler-SOS.influxdbConfig.version') === 1) { - // Set up Influxdb v1 client - influx = new Influx.InfluxDB({ - host: config.get('Butler-SOS.influxdbConfig.host'), - port: config.get('Butler-SOS.influxdbConfig.port'), - database: config.get('Butler-SOS.influxdbConfig.v1Config.dbName'), - username: `${ - config.get('Butler-SOS.influxdbConfig.v1Config.auth.enable') - ? config.get('Butler-SOS.influxdbConfig.v1Config.auth.username') - : '' - }`, - password: `${ - config.get('Butler-SOS.influxdbConfig.v1Config.auth.enable') - ? config.get('Butler-SOS.influxdbConfig.v1Config.auth.password') - : '' - }`, - schema: [ - { - measurement: 'sense_server', - fields: { - version: Influx.FieldType.STRING, - started: Influx.FieldType.STRING, - uptime: Influx.FieldType.STRING, - }, - tags: tagValues, - }, - { - measurement: 'mem', - fields: { - comitted: Influx.FieldType.INTEGER, - allocated: Influx.FieldType.INTEGER, - free: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'apps', - fields: { - active_docs_count: Influx.FieldType.INTEGER, - loaded_docs_count: Influx.FieldType.INTEGER, - in_memory_docs_count: Influx.FieldType.INTEGER, - active_docs: Influx.FieldType.STRING, - active_docs_names: Influx.FieldType.STRING, - active_session_docs_names: Influx.FieldType.STRING, - loaded_docs: Influx.FieldType.STRING, - loaded_docs_names: Influx.FieldType.STRING, - loaded_session_docs_names: Influx.FieldType.STRING, - in_memory_docs: Influx.FieldType.STRING, - in_memory_docs_names: Influx.FieldType.STRING, - in_memory_session_docs_names: Influx.FieldType.STRING, - calls: Influx.FieldType.INTEGER, - selections: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'cpu', - fields: { - total: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'session', - fields: { - active: Influx.FieldType.INTEGER, - total: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'users', - fields: { - active: Influx.FieldType.INTEGER, - total: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'cache', - fields: { - hits: Influx.FieldType.INTEGER, - lookups: Influx.FieldType.INTEGER, - added: Influx.FieldType.INTEGER, - replaced: Influx.FieldType.INTEGER, - bytes_added: Influx.FieldType.INTEGER, - }, - tags: tagValues, - }, - { - measurement: 'log_event_logdb', - fields: { - message: Influx.FieldType.STRING, - }, - tags: tagValuesLogEventLogDb, - }, - { - measurement: 'log_event', - fields: { - message: Influx.FieldType.STRING, - exception_message: Influx.FieldType.STRING, - app_name: Influx.FieldType.STRING, - app_id: Influx.FieldType.STRING, - execution_id: Influx.FieldType.STRING, - command: Influx.FieldType.STRING, - result_code: Influx.FieldType.STRING, - origin: Influx.FieldType.STRING, - context: Influx.FieldType.STRING, - session_id: Influx.FieldType.STRING, - raw_event: Influx.FieldType.STRING, - }, - tags: tagValuesLogEvent, - }, - { - measurement: 'butlersos_memory_usage', - fields: { - heap_used: Influx.FieldType.FLOAT, - heap_total: Influx.FieldType.FLOAT, - external: Influx.FieldType.FLOAT, - process_memory: Influx.FieldType.FLOAT, - }, - tags: ['butler_sos_instance', 'version'], - }, - { - measurement: 'user_session_summary', - fields: { - session_count: Influx.FieldType.INTEGER, - session_user_id_list: Influx.FieldType.STRING, - }, - tags: tagValuesUserProxySessions, - }, - { - measurement: 'user_session_list', - fields: { - session_user_id_list: Influx.FieldType.STRING, - }, - tags: tagValuesUserProxySessions, - }, - // { - // measurement: 'user_events', - // fields: { - // userFull: Influx.FieldType.STRING, - // userId: Influx.FieldType.STRING - // }, - // tags: ['host', 'event_action', 'userFull', 'userDirectory', 'userId', 'origin'] - // }, - ], - }); - } else if (config.get('Butler-SOS.influxdbConfig.version') === 2) { - // Set up Influxdb v2 client - const url = `http://${config.get('Butler-SOS.influxdbConfig.host')}:${config.get( - 'Butler-SOS.influxdbConfig.port' - )}`; - const token = config.get('Butler-SOS.influxdbConfig.v2Config.token'); + this.udpServerUserActivity.portUserActivity = this.config.get( + 'Butler-SOS.userEvents.udpServerConfig.portUserActivityEvents' + ); + } catch (err) { + this.logger.error(`CONFIG: Setting up UDP user activity listener: ${err}`); + } + + // ------------------------------------ + // Log events UDP server + this.udpServerLogEvents = {}; try { - influx = new InfluxDB2({ url, token }); + this.udpServerLogEvents.host = this.config.get( + 'Butler-SOS.logEvents.udpServerConfig.serverHost' + ); + + // Prepare to listen on port X for incoming UDP connections regarding user activity events + this.udpServerLogEvents.socket = dgram.createSocket({ + type: 'udp4', + reuseAddr: true, + }); + + this.udpServerLogEvents.port = this.config.get( + 'Butler-SOS.logEvents.udpServerConfig.portLogEvents' + ); } catch (err) { - logger.error(`INFLUXDB2 INIT: Error creating InfluxDB 2 client: ${err}`); - logger.error(`INFLUXDB2 INIT: Exiting.`); + this.logger.error(`CONFIG: Setting up UDP log events listener: ${err}`); } - } else { - logger.error( - `CONFIG: Influxdb version ${config.get('Butler-SOS.influxdbConfig.version')} is not supported!` - ); - } -} -async function initInfluxDB() { - let enableInfluxdb = false; + // ------------------------------------ + // Get info on what servers to monitor + this.serverList = this.config.get('Butler-SOS.serversToMonitor.servers'); + + // Only set up connection pool for accessing Qlik Sense log db if that feature is enabled + this.pgPool; + if (this.config.get('Butler-SOS.logdb.enable') === true) { + const { Pool } = pg; + + // Set up connection pool for accessing Qlik Sense log db + this.pgPool = new Pool({ + host: this.config.get('Butler-SOS.logdb.host'), + database: 'QLogs', + user: this.config.get('Butler-SOS.logdb.qlogsReaderUser'), + password: this.config.get('Butler-SOS.logdb.qlogsReaderPwd'), + port: this.config.get('Butler-SOS.logdb.port'), + }); + + // the pool will emit an error on behalf of any idle clients + // it contains if a backend error or network partition happens + // eslint-disable-next-line no-unused-vars + this.pgPool.on('error', (err, client) => { + this.logger.error(`CONFIG: Unexpected error on idle client: ${err}`); + // process.exit(-1); + }); + } - // Handle InfluxDB v1 - if (config.get('Butler-SOS.influxdbConfig.version') === 1) { - const dbName = config.get('Butler-SOS.influxdbConfig.v1Config.dbName'); + // Get list of standard and user configurable tags + // ..begin with standard tags + const tagValues = ['host', 'server_name', 'server_description']; + // ..check if there are any extra tags for this Butler SOS instance that should be sent to InfluxDB if ( - influx && - config.get('Butler-SOS.influxdbConfig.enable') === true && - dbName?.length > 0 + this.config.has('Butler-SOS.serversToMonitor.serverTagsDefinition') && + this.config.get('Butler-SOS.serversToMonitor.serverTagsDefinition') !== null ) { - enableInfluxdb = true; + // Loop over all tags defined for the current server, adding them to the data structure that will later be passed to Influxdb + this.config.get('Butler-SOS.serversToMonitor.serverTagsDefinition').forEach((entry) => { + this.logger.debug( + `CONFIG: Setting up new Influx database: Found server tag : ${entry}` + ); + + tagValues.push(entry); + }); } - if (enableInfluxdb) { - try { - const names = await influx.getDatabaseNames(); - if (!names.includes(dbName)) { - try { - const res = await influx.createDatabase(dbName); - logger.info(`CONFIG: Created new InfluxDB v1 database: ${dbName}`); + // Add tags for log events + const tagValuesLogEvent = tagValues.slice(); + tagValuesLogEvent.push('level'); + tagValuesLogEvent.push('source'); + tagValuesLogEvent.push('log_row'); + tagValuesLogEvent.push('subsystem'); + tagValuesLogEvent.push('user_full'); + tagValuesLogEvent.push('user_directory'); + tagValuesLogEvent.push('user_id'); + tagValuesLogEvent.push('task_id'); + tagValuesLogEvent.push('task_name'); + tagValuesLogEvent.push('app_id'); + tagValuesLogEvent.push('app_name'); + tagValuesLogEvent.push('result_code'); + tagValuesLogEvent.push('windows_user'); + tagValuesLogEvent.push('engine_exe_version'); + + // Check if there are any extra log event tags in the config file + if ( + this.config.has('Butler-SOS.logEvents.tags') && + this.config.get('Butler-SOS.logEvents.tags') !== null + ) { + this.config.get('Butler-SOS.logEvents.tags').forEach((entry) => { + this.logger.debug( + `CONFIG: Setting up new Influx database: Found log event tag in config file: ${JSON.stringify( + entry + )}` + ); + + tagValuesLogEvent.push(entry.tag); + }); + } - const newPolicy = config.get( - 'Butler-SOS.influxdbConfig.v1Config.retentionPolicy' - ); + // Add tags for log events categories, if enabled and configured + if ( + this.config.has('Butler-SOS.logEvents.categorise.enable') && + this.config.get('Butler-SOS.logEvents.categorise.enable') === true && + this.config.has('Butler-SOS.logEvents.categorise.rules') + ) { + // Add tags from Butler-SOS.logEvents.categorise.rules[].category[], where each object has properties 'name' and 'value' + this.config.get('Butler-SOS.logEvents.categorise.rules').forEach((rule) => { + rule.category.forEach((category) => { + tagValuesLogEvent.push(category.name); + }); + }); + + // Add default rule categories, if enabled + if ( + this.config.has('Butler-SOS.logEvents.categorise.ruleDefault.enable') && + this.config.get('Butler-SOS.logEvents.categorise.ruleDefault.enable') === true && + this.config.has('Butler-SOS.logEvents.categorise.ruleDefault.category') + ) { + this.config + .get('Butler-SOS.logEvents.categorise.ruleDefault.category') + .forEach((category) => { + tagValuesLogEvent.push(category.name); + }); + } + } + + // Create InfluxDB tags for data coming from log db + const tagValuesLogEventLogDb = tagValues.slice(); + tagValuesLogEventLogDb.push('source_process'); + tagValuesLogEventLogDb.push('log_level'); + + // Create tags for user sessions + const tagValuesUserProxySessions = tagValues.slice(); + tagValuesUserProxySessions.push('user_session_virtual_proxy'); + tagValuesUserProxySessions.push('user_session_host'); + + // Show Influxdb config + if (this.config.get('Butler-SOS.influxdbConfig.enable') === true) { + this.logger.info(`CONFIG: Influxdb enabled: true`); + this.logger.info( + `CONFIG: Influxdb host IP: ${this.config.get('Butler-SOS.influxdbConfig.host')}` + ); + this.logger.info( + `CONFIG: Influxdb host port: ${this.config.get('Butler-SOS.influxdbConfig.port')}` + ); + this.logger.info( + `CONFIG: Influxdb version: ${this.config.get('Butler-SOS.influxdbConfig.version')}` + ); + + // Version specific configs + if (this.config.get('Butler-SOS.influxdbConfig.version') === 1) { + this.logger.info( + `CONFIG: Influxdb db name: ${this.config.get('Butler-SOS.influxdbConfig.v1Config.dbName')}` + ); + this.logger.info( + `CONFIG: Influxdb retention policy: ${this.config.get('Butler-SOS.influxdbConfig.v1Config.retentionPolicy.name')}` + ); + } else if (this.config.get('Butler-SOS.influxdbConfig.version') === 2) { + this.logger.info( + `CONFIG: Influxdb organisation: ${this.config.get('Butler-SOS.influxdbConfig.v2Config.org')}` + ); + this.logger.info( + `CONFIG: Influxdb bucket name: ${this.config.get('Butler-SOS.influxdbConfig.v2Config.bucket')}` + ); + this.logger.info( + `CONFIG: Influxdb retention policy duration: ${this.config.get('Butler-SOS.influxdbConfig.v2Config.retentionDuration')}` + ); + } else { + this.logger.error( + `CONFIG: Influxdb version ${this.config.get('Butler-SOS.influxdbConfig.version')} is not supported!` + ); + } + } else { + this.logger.info(`CONFIG: Influxdb enabled: false`); + } + + this.influxWriteApi = []; + if (this.config.get('Butler-SOS.influxdbConfig.enable') === true) { + if (this.config.get('Butler-SOS.influxdbConfig.version') === 1) { + // Set up Influxdb v1 client + this.influx = new Influx.InfluxDB({ + host: this.config.get('Butler-SOS.influxdbConfig.host'), + port: this.config.get('Butler-SOS.influxdbConfig.port'), + database: this.config.get('Butler-SOS.influxdbConfig.v1Config.dbName'), + username: `${ + this.config.get('Butler-SOS.influxdbConfig.v1Config.auth.enable') + ? this.config.get('Butler-SOS.influxdbConfig.v1Config.auth.username') + : '' + }`, + password: `${ + this.config.get('Butler-SOS.influxdbConfig.v1Config.auth.enable') + ? this.config.get('Butler-SOS.influxdbConfig.v1Config.auth.password') + : '' + }`, + schema: [ + { + measurement: 'sense_server', + fields: { + version: Influx.FieldType.STRING, + started: Influx.FieldType.STRING, + uptime: Influx.FieldType.STRING, + }, + tags: tagValues, + }, + { + measurement: 'mem', + fields: { + comitted: Influx.FieldType.INTEGER, + allocated: Influx.FieldType.INTEGER, + free: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'apps', + fields: { + active_docs_count: Influx.FieldType.INTEGER, + loaded_docs_count: Influx.FieldType.INTEGER, + in_memory_docs_count: Influx.FieldType.INTEGER, + active_docs: Influx.FieldType.STRING, + active_docs_names: Influx.FieldType.STRING, + active_session_docs_names: Influx.FieldType.STRING, + loaded_docs: Influx.FieldType.STRING, + loaded_docs_names: Influx.FieldType.STRING, + loaded_session_docs_names: Influx.FieldType.STRING, + in_memory_docs: Influx.FieldType.STRING, + in_memory_docs_names: Influx.FieldType.STRING, + in_memory_session_docs_names: Influx.FieldType.STRING, + calls: Influx.FieldType.INTEGER, + selections: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'cpu', + fields: { + total: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'session', + fields: { + active: Influx.FieldType.INTEGER, + total: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'users', + fields: { + active: Influx.FieldType.INTEGER, + total: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'cache', + fields: { + hits: Influx.FieldType.INTEGER, + lookups: Influx.FieldType.INTEGER, + added: Influx.FieldType.INTEGER, + replaced: Influx.FieldType.INTEGER, + bytes_added: Influx.FieldType.INTEGER, + }, + tags: tagValues, + }, + { + measurement: 'log_event_logdb', + fields: { + message: Influx.FieldType.STRING, + }, + tags: tagValuesLogEventLogDb, + }, + { + measurement: 'log_event', + fields: { + message: Influx.FieldType.STRING, + exception_message: Influx.FieldType.STRING, + app_name: Influx.FieldType.STRING, + app_id: Influx.FieldType.STRING, + execution_id: Influx.FieldType.STRING, + command: Influx.FieldType.STRING, + result_code: Influx.FieldType.STRING, + origin: Influx.FieldType.STRING, + context: Influx.FieldType.STRING, + session_id: Influx.FieldType.STRING, + raw_event: Influx.FieldType.STRING, + }, + tags: tagValuesLogEvent, + }, + { + measurement: 'butlersos_memory_usage', + fields: { + heap_used: Influx.FieldType.FLOAT, + heap_total: Influx.FieldType.FLOAT, + external: Influx.FieldType.FLOAT, + process_memory: Influx.FieldType.FLOAT, + }, + tags: ['butler_sos_instance', 'version'], + }, + { + measurement: 'user_session_summary', + fields: { + session_count: Influx.FieldType.INTEGER, + session_user_id_list: Influx.FieldType.STRING, + }, + tags: tagValuesUserProxySessions, + }, + { + measurement: 'user_session_list', + fields: { + session_user_id_list: Influx.FieldType.STRING, + }, + tags: tagValuesUserProxySessions, + }, + // { + // measurement: 'user_events', + // fields: { + // userFull: Influx.FieldType.STRING, + // userId: Influx.FieldType.STRING + // }, + // tags: ['host', 'event_action', 'userFull', 'userDirectory', 'userId', 'origin'] + // }, + ], + }); + } else if (this.config.get('Butler-SOS.influxdbConfig.version') === 2) { + // Set up Influxdb v2 client + const url = `http://${this.config.get('Butler-SOS.influxdbConfig.host')}:${this.config.get( + 'Butler-SOS.influxdbConfig.port' + )}`; + const token = this.config.get('Butler-SOS.influxdbConfig.v2Config.token'); + + try { + this.influx = new InfluxDB2({ url, token }); + } catch (err) { + this.logger.error(`INFLUXDB2 INIT: Error creating InfluxDB 2 client: ${err}`); + this.logger.error(`INFLUXDB2 INIT: Exiting.`); + } + } else { + this.logger.error( + `CONFIG: Influxdb version ${this.config.get('Butler-SOS.influxdbConfig.version')} is not supported!` + ); + } + } + + // Now initialise InfluxDB + await this.initInfluxDB(); + + // ------------------------------------ + // Create MQTT client object and connect to MQTT broker + // Only do this if MQTT is enabled + // ------------------------------------ + if (this.config.get('Butler-SOS.mqttConfig.enable') === true) { + this.mqttClient = mqtt.connect({ + port: this.config.get('Butler-SOS.mqttConfig.brokerPort'), + host: this.config.get('Butler-SOS.mqttConfig.brokerHost'), + }); + } + + /* + Following might be needed for conecting to older Mosquitto versions + var mqttClient = mqtt.connect('mqtt://', { + protocolId: 'MQIsdp', + protocolVersion: 3 + }); + */ + + // Anon telemetry reporting + this.hostInfo = await this.initHostInfo(); - // Create new default retention policy + // Indicate that we have finished initialising + this.initialised = true; + + this.logger.verbose('GLOBALS: Init done'); + + // eslint-disable-next-line no-constructor-return + return instance; + } + + // Static function to check if a file exists + static checkFileExistsSync(filepath) { + let flag = true; + try { + fs.accessSync(filepath, fs.constants.F_OK); + } catch (e) { + flag = false; + } + return flag; + } + + // Static sleep function + static sleep(ms) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + // Static function to check if Butler is running in a Docker container + static isRunningInDocker() { + try { + fs.accessSync('/.dockerenv'); + return true; + } catch (_) { + return false; + } + } + + async initInfluxDB() { + let enableInfluxdb = false; + + // Handle InfluxDB v1 + if (this.config.get('Butler-SOS.influxdbConfig.version') === 1) { + const dbName = this.config.get('Butler-SOS.influxdbConfig.v1Config.dbName'); + + if ( + influx && + this.config.get('Butler-SOS.influxdbConfig.enable') === true && + dbName?.length > 0 + ) { + enableInfluxdb = true; + } + + if (enableInfluxdb) { + try { + const names = await this.influx.getDatabaseNames(); + if (!names.includes(dbName)) { + // Create new database try { - const res2 = await influx.createRetentionPolicy(newPolicy.name, { - database: dbName, - duration: newPolicy.duration, - replication: 1, - isDefault: true, - }); + const res = await this.influx.createDatabase(dbName); + this.logger.info(`CONFIG: Created new InfluxDB v1 database: ${dbName}`); - logger.info( - `CONFIG: Created new InfluxDB v1 retention policy: ${newPolicy.name}` + const newPolicy = this.config.get( + 'Butler-SOS.influxdbConfig.v1Config.retentionPolicy' ); + + // Create new default retention policy + try { + const res2 = await this.influx.createRetentionPolicy( + newPolicy.name, + { + database: dbName, + duration: newPolicy.duration, + replication: 1, + isDefault: true, + } + ); + + this.logger.info( + `CONFIG: Created new InfluxDB v1 retention policy: ${newPolicy.name}` + ); + } catch (err) { + this.logger.error( + `CONFIG: Error creating new InfluxDB v1 retention policy "${newPolicy.name}"! ${err.stack}` + ); + } } catch (err) { - logger.error( - `CONFIG: Error creating new InfluxDB v1 retention policy "${newPolicy.name}"! ${err.stack}` + this.logger.error( + `CONFIG: Error creating new InfluxDB v1 database "${dbName}"! ${err.stack}` ); } - } catch (err) { - logger.error( - `CONFIG: Error creating new InfluxDB v1 database "${dbName}"! ${err.stack}` - ); + } else { + this.logger.info(`CONFIG: Found InfluxDB v1 database: ${dbName}`); } - } else { - logger.info(`CONFIG: Found InfluxDB v1 database: ${dbName}`); + } catch (err) { + this.logger.error( + `CONFIG: Error getting list of InfluxDB v1 databases. ${err.stack}` + ); } - } catch (err) { - logger.error(`CONFIG: Error getting list of InfluxDB v1 databases. ${err.stack}`); } - } - } else if (config.get('Butler-SOS.influxdbConfig.version') === 2) { - // Get config - const org = config.get('Butler-SOS.influxdbConfig.v2Config.org'); - const bucketName = config.get('Butler-SOS.influxdbConfig.v2Config.bucket'); - const description = config.get('Butler-SOS.influxdbConfig.v2Config.description'); - const token = config.get('Butler-SOS.influxdbConfig.v2Config.token'); - const retentionDuration = config.get( - 'Butler-SOS.influxdbConfig.v2Config.retentionDuration' - ); - - if ( - influx && - config.get('Butler-SOS.influxdbConfig.enable') === true && - org?.length > 0 && - bucketName?.length > 0 && - token?.length > 0 && - retentionDuration?.length > 0 - ) { - enableInfluxdb = true; - } + } else if (this.config.get('Butler-SOS.influxdbConfig.version') === 2) { + // Get config + const org = this.config.get('Butler-SOS.influxdbConfig.v2Config.org'); + const bucketName = this.config.get('Butler-SOS.influxdbConfig.v2Config.bucket'); + const description = this.config.get('Butler-SOS.influxdbConfig.v2Config.description'); + const token = this.config.get('Butler-SOS.influxdbConfig.v2Config.token'); + const retentionDuration = this.config.get( + 'Butler-SOS.influxdbConfig.v2Config.retentionDuration' + ); + + if ( + this.influx && + this.config.get('Butler-SOS.influxdbConfig.enable') === true && + org?.length > 0 && + bucketName?.length > 0 && + token?.length > 0 && + retentionDuration?.length > 0 + ) { + enableInfluxdb = true; + } - if (enableInfluxdb) { - let orgID; + if (enableInfluxdb) { + let orgID; - try { - // Get organisation by name - const orgsAPI = new OrgsAPI(influx); - const organizations = await orgsAPI.getOrgs({ org }); - if (!organizations || !organizations.orgs || !organizations.orgs.length) { - logger.error(`INFLUXDB2: No organization named "${org}" found!`); + try { + // Get organisation by name + const orgsAPI = new OrgsAPI(this.influx); + const organizations = await orgsAPI.getOrgs({ org }); + if (!organizations || !organizations.orgs || !organizations.orgs.length) { + this.logger.error(`INFLUXDB2: No organization named "${org}" found!`); + } + orgID = organizations.orgs[0].id; + this.logger.info( + `INFLUXDB2: Using organization "${org}" identified by "${orgID}"` + ); + } catch (err) { + this.logger.error(`INFLUXDB2: Error getting organisation: ${err}`); } - orgID = organizations.orgs[0].id; - logger.info(`INFLUXDB2: Using organization "${org}" identified by "${orgID}"`); - } catch (err) { - logger.error(`INFLUXDB2: Error getting organisation: ${err}`); - } - try { - // Get buckets by name - const bucketsAPI = new BucketsAPI(influx); try { - const buckets = await bucketsAPI.getBuckets({ orgID, name: bucketName }); - if (buckets && buckets.buckets && buckets.buckets.length > 0) { - const bucketID = buckets.buckets[0].id; - logger.info( - `INFLUXDB2: Bucket named "${bucketName}" already exists, bucket ID="${bucketID}"` - ); - } - } catch (e) { - if (e instanceof HttpError && e.statusCode === 404) { - // Bucket not found. Let's create it - logger.info( - `INFLUXDB2: Bucket named "${bucketName}" not found, creating it...` - ); + // Get buckets by name + const bucketsAPI = new BucketsAPI(this.influx); + try { + const buckets = await bucketsAPI.getBuckets({ orgID, name: bucketName }); + if (buckets && buckets.buckets && buckets.buckets.length > 0) { + const bucketID = buckets.buckets[0].id; + this.logger.info( + `INFLUXDB2: Bucket named "${bucketName}" already exists, bucket ID="${bucketID}"` + ); + } + } catch (e) { + if (e instanceof HttpError && e.statusCode === 404) { + // Bucket not found. Let's create it + this.logger.info( + `INFLUXDB2: Bucket named "${bucketName}" not found, creating it...` + ); - // creates a bucket, entity properties are specified in the "body" property - const newBucket = await bucketsAPI.postBuckets({ - body: { orgID, name: bucketName, description, rp: retentionDuration }, - }); + // creates a bucket, entity properties are specified in the "body" property + const newBucket = await bucketsAPI.postBuckets({ + body: { + orgID, + name: bucketName, + description, + rp: retentionDuration, + }, + }); - logger.verbose( - `INFLUXDB2: New bucket: ${JSON.stringify( - newBucket, - (key, value) => (key === 'links' ? undefined : value), - 2 - )}` - ); - } else { - throw e; + this.logger.verbose( + `INFLUXDB2: New bucket: ${JSON.stringify( + newBucket, + (key, value) => (key === 'links' ? undefined : value), + 2 + )}` + ); + } else { + throw e; + } } + } catch (err) { + this.logger.error(`INFLUXDB2: Error getting bucket: ${err}`); } - } catch (err) { - logger.error(`INFLUXDB2: Error getting bucket: ${err}`); - } - // Get write API + // Get write API - // Create array of per-server writeAPI objects - // Each object has two properties: host and writeAPI, where host can be used as key later on - serverList.forEach((server) => { - // Get per-server tags - const tags = getServerTags(logger, server); + // Create array of per-server writeAPI objects + // Each object has two properties: host and writeAPI, where host can be used as key later on + this.serverList.forEach((server) => { + // Get per-server tags + const tags = getServerTags(this.logger, server); - // advanced write options - const writeOptions = { - /* the maximum points/lines to send in a single batch to InfluxDB server */ - // batchSize: flushBatchSize + 1, // don't let automatically flush data + // advanced write options + const writeOptions = { + /* the maximum points/lines to send in a single batch to InfluxDB server */ + // batchSize: flushBatchSize + 1, // don't let automatically flush data - /* default tags to add to every point */ - defaultTags: tags, + /* default tags to add to every point */ + defaultTags: tags, - /* maximum time in millis to keep points in an unflushed batch, 0 means don't periodically flush */ - flushInterval: 5000, + /* maximum time in millis to keep points in an unflushed batch, 0 means don't periodically flush */ + flushInterval: 5000, - /* maximum size of the retry buffer - it contains items that could not be sent for the first time */ - // maxBufferLines: 30_000, + /* maximum size of the retry buffer - it contains items that could not be sent for the first time */ + // maxBufferLines: 30_000, - /* the count of internally-scheduled retries upon write failure, the delays between write attempts follow an exponential backoff strategy if there is no Retry-After HTTP header */ - maxRetries: 2, // do not retry writes + /* the count of internally-scheduled retries upon write failure, the delays between write attempts follow an exponential backoff strategy if there is no Retry-After HTTP header */ + maxRetries: 2, // do not retry writes - // ... there are more write options that can be customized, see - // https://influxdata.github.io/influxdb-client-js/influxdb-client.writeoptions.html and - // https://influxdata.github.io/influxdb-client-js/influxdb-client.writeretryoptions.html - }; + // ... there are more write options that can be customized, see + // https://influxdata.github.io/influxdb-client-js/influxdb-client.writeoptions.html and + // https://influxdata.github.io/influxdb-client-js/influxdb-client.writeretryoptions.html + }; - try { - const serverWriteApi = influx.getWriteApi(org, bucketName, 'ns', writeOptions); + try { + const serverWriteApi = this.influx.getWriteApi( + org, + bucketName, + 'ns', + writeOptions + ); - // Save to global variable, using hostNamre as key - influxWriteApi.push({ - serverName: server.serverName, - writeAPI: serverWriteApi, - }); - } catch (err) { - logger.error(`INFLUXDB2: Error getting write API: ${err}`); - } - }); + // Save to global variable, using hostNamre as key + this.influxWriteApi.push({ + serverName: server.serverName, + writeAPI: serverWriteApi, + }); + } catch (err) { + this.logger.error(`INFLUXDB2: Error getting write API: ${err}`); + } + }); + } } } -} -// ------------------------------------ -// Create MQTT client object and connect to MQTT broker -// Only do this if MQTT is enabled -// ------------------------------------ -let mqttClient; - -if (config.get('Butler-SOS.mqttConfig.enable') === true) { - mqttClient = mqtt.connect({ - port: config.get('Butler-SOS.mqttConfig.brokerPort'), - host: config.get('Butler-SOS.mqttConfig.brokerHost'), - }); -} - -/* - Following might be needed for conecting to older Mosquitto versions - var mqttClient = mqtt.connect('mqtt://', { - protocolId: 'MQIsdp', - protocolVersion: 3 - }); -*/ - -// Anon telemetry reporting -let hostInfo; - -async function initHostInfo() { - try { - const siCPU = await si.cpu(); - const siSystem = await si.system(); - const siMem = await si.mem(); - const siOS = await si.osInfo(); - const siDocker = await si.dockerInfo(); - const siNetwork = await si.networkInterfaces(); - const siNetworkDefault = await si.networkInterfaceDefault(); - - const defaultNetworkInterface = siNetworkDefault; - - const networkInterface = siNetwork.filter((item) => item.iface === defaultNetworkInterface); - - const idSrc = - networkInterface[0].mac + - networkInterface[0].ip4 + - config.get('Butler-SOS.logdb.host') + - siSystem.uuid; - const salt = networkInterface[0].mac; - const hash = crypto.createHmac('sha256', salt); - hash.update(idSrc); - - // Get first 50 characters of hash - const id = hash.digest('hex'); - - hostInfo = { - id, - node: { - nodeVersion: process.version, - versions: process.versions, - }, - os: { - platform: os.platform(), - release: os.release(), - version: os.version(), - arch: os.arch(), - cpuCores: os.cpus().length, - type: os.type(), - totalmem: os.totalmem(), - }, - si: { - cpu: siCPU, - system: siSystem, - memory: { - total: siMem.total, + async initHostInfo() { + try { + const siCPU = await si.cpu(); + const siSystem = await si.system(); + const siMem = await si.mem(); + const siOS = await si.osInfo(); + const siDocker = await si.dockerInfo(); + const siNetwork = await si.networkInterfaces(); + const siNetworkDefault = await si.networkInterfaceDefault(); + + const defaultNetworkInterface = siNetworkDefault; + + const networkInterface = siNetwork.filter( + (item) => item.iface === defaultNetworkInterface + ); + + const idSrc = + networkInterface[0].mac + + networkInterface[0].ip4 + + this.config.get('Butler-SOS.logdb.host') + + siSystem.uuid; + const salt = networkInterface[0].mac; + const hash = crypto.createHmac('sha256', salt); + hash.update(idSrc); + + // Get first 50 characters of hash + const id = hash.digest('hex'); + + const hostInfo = { + id, + node: { + nodeVersion: process.version, + versions: process.versions, }, - os: siOS, - network: siNetwork, - networkDefault: siNetworkDefault, - docker: siDocker, - }, - }; - - return hostInfo; - } catch (err) { - logger.error(`CONFIG: Getting host info: ${err}`); - return null; + os: { + platform: os.platform(), + release: os.release(), + version: os.version(), + arch: os.arch(), + cpuCores: os.cpus().length, + type: os.type(), + totalmem: os.totalmem(), + }, + si: { + cpu: siCPU, + system: siSystem, + memory: { + total: siMem.total, + }, + os: siOS, + network: siNetwork, + networkDefault: siNetworkDefault, + docker: siDocker, + }, + }; + + return hostInfo; + } catch (err) { + this.logger.error(`CONFIG: Getting host info: ${err}`); + return null; + } } } -module.exports = { - config, - mqttClient, - logger, - getLoggingLevel, - influx, - influxWriteApi, - pgPool, - appVersion, - serverList, - initInfluxDB, - appNames, - udpServerUserActivity, - udpServerLogEvents, - initHostInfo, - hostInfo, - isPkg, - checkFileExistsSync, - configFile, - options, -}; +export default new Settings(); diff --git a/src/lib/appnamesextract.js b/src/lib/appnamesextract.js index 9aaa47cf..43f70dcb 100755 --- a/src/lib/appnamesextract.js +++ b/src/lib/appnamesextract.js @@ -1,14 +1,9 @@ // Get app names from the Qlik Repository Service (QRS) API +import path from 'path'; +import qrsInteract from 'qrs-interact'; +import clonedeep from 'lodash.clonedeep'; -const path = require('path'); - -const qrsInteract = require('qrs-interact'); -const clonedeep = require('lodash.clonedeep'); -const globals = require('../globals'); - -const certPath = path.resolve(process.cwd(), globals.config.get('Butler-SOS.cert.clientCert')); -const keyPath = path.resolve(process.cwd(), globals.config.get('Butler-SOS.cert.clientCertKey')); -// caPath = path.resolve(process.cwd(), globals.config.get('Butler-SOS.cert.clientCertCA')); +import globals from '../globals.js'; function getAppNames() { globals.logger.verbose(`APP NAMES: Start getting app names from repository db`); @@ -18,8 +13,8 @@ function getAppNames() { hostname: globals.config.get('Butler-SOS.appNames.hostIP'), portNumber: 4242, certificates: { - certFile: certPath, - keyFile: keyPath, + certFile: globals.certPath, + keyFile: globals.keyPath, }, }; @@ -27,7 +22,6 @@ function getAppNames() { 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', }; - // eslint-disable-next-line new-cap const qrsInteractInstance = new qrsInteract(configQRS); const appList = []; @@ -61,7 +55,7 @@ function getAppNames() { } } -function setupAppNamesExtractTimer() { +export function setupAppNamesExtractTimer() { // Configure timer for getting app names data setInterval(() => { globals.logger.verbose('APP NAMES: Event started: Get app names'); @@ -69,8 +63,3 @@ function setupAppNamesExtractTimer() { getAppNames(); }, globals.config.get('Butler-SOS.appNames.extractInterval')); } - -module.exports = { - setupAppNamesExtractTimer, - getAppNames, -}; diff --git a/src/lib/config-file-schema.js b/src/lib/config-file-schema.js index 466f022b..778bc0a9 100755 --- a/src/lib/config-file-schema.js +++ b/src/lib/config-file-schema.js @@ -1,4 +1,4 @@ -const confifgFileSchema = { +export const confifgFileSchema = { 'Butler-SOS': { logLevel: 'string', fileLogging: 'boolean', @@ -409,7 +409,3 @@ const confifgFileSchema = { }, }, }; - -module.exports = { - confifgFileSchema, -}; diff --git a/src/lib/config-file-verify.js b/src/lib/config-file-verify.js index ea47e85b..04267d25 100755 --- a/src/lib/config-file-verify.js +++ b/src/lib/config-file-verify.js @@ -1,9 +1,9 @@ -const { config, configFile, logger } = require('../globals'); -const { confifgFileSchema } = require('./config-file-schema'); +import globals from '../globals.js'; +import { confifgFileSchema } from './config-file-schema.js'; // Function to verify that the config file has the correct format // Use yaml-validator to validate the config file -async function verifyConfigFile() { +export async function verifyConfigFile() { // try { // Dynamically load yaml-validator @@ -12,7 +12,7 @@ async function verifyConfigFile() { // Options for yaml-validator const verifyOptions = { onWarning(error, filepath) { - logger.warn(`${filepath} has error: ${error}`); + globals.logger.warn(`${filepath} has error: ${error}`); }, log: false, structure: confifgFileSchema, @@ -23,21 +23,21 @@ async function verifyConfigFile() { const validator = new YamlValidator(verifyOptions); // File names to validate in array - const files = [configFile]; + const files = [globals.configFile]; // Verify the config file validator.validate(files); // Exit app if there are errors in the config file's structure if (validator.logs.length > 0) { - logger.verbose(`VERIFY CONFIG FILE: Logs length: ${validator.logs.length}`); - logger.verbose(validator.logs); + globals.logger.verbose(`VERIFY CONFIG FILE: Logs length: ${validator.logs.length}`); + globals.logger.verbose(validator.logs); - logger.error(`VERIFY CONFIG FILE: Errors found in config file. Exiting.`); - logger.error( + globals.logger.error(`VERIFY CONFIG FILE: Errors found in config file. Exiting.`); + globals.logger.error( `Tip: Start Butler SOS with --no-config-file-verify option to skip this check and start with provided config file. ` ); - logger.error(`${validator.logs}`); + globals.logger.error(`${validator.logs}`); process.exit(1); } @@ -46,10 +46,10 @@ async function verifyConfigFile() { // If InfluxDB is enabled, check if the version is valid // Valid values: 1 and 2 - if (config.get('Butler-SOS.influxdbConfig.enable') === true) { - const influxdbVersion = config.get('Butler-SOS.influxdbConfig.version'); + if (globals.config.get('Butler-SOS.influxdbConfig.enable') === true) { + const influxdbVersion = globals.config.get('Butler-SOS.influxdbConfig.version'); if (influxdbVersion !== 1 && influxdbVersion !== 2) { - logger.error( + globals.logger.error( `VERIFY CONFIG FILE: Butler-SOS.influxdbConfig.enable (=InfluxDB version) ${influxdbVersion} is invalid. Exiting.` ); process.exit(1); @@ -64,23 +64,23 @@ async function verifyConfigFile() { // If either of the conditions above is false, an error should be logged and Butler SOS should not start. try { // Loop over all defined server tags - const serverTagsDefinition = config.get( + const serverTagsDefinition = globals.config.get( 'Butler-SOS.serversToMonitor.serverTagsDefinition' ); // eslint-disable-next-line no-restricted-syntax for (const tag of serverTagsDefinition) { // Check that all servers have this tag - const servers = config.get('Butler-SOS.serversToMonitor.servers'); + const servers = globals.config.get('Butler-SOS.serversToMonitor.servers'); // eslint-disable-next-line no-restricted-syntax for (const server of servers) { // Check if server.serverTags.tag is defined if (server?.serverTags === null || !server?.serverTags[tag]) { - logger.error( + globals.logger.error( `VERIFY CONFIG FILE: Server tag "${tag}" is not defined for server "${server.serverName}". Exiting.` ); process.exit(1); } else { - logger.verbose( + globals.logger.verbose( `VERIFY CONFIG FILE: Server tag "${tag}" is defined for server "${server.serverName}".` ); } @@ -88,38 +88,36 @@ async function verifyConfigFile() { } // Now ensure that the tags defined for each server are valid and that there are no extra tags there - const servers = config.get('Butler-SOS.serversToMonitor.servers'); + const servers = globals.config.get('Butler-SOS.serversToMonitor.servers'); // eslint-disable-next-line no-restricted-syntax for (const server of servers) { // eslint-disable-next-line no-restricted-syntax for (const tag in server.serverTags) { if (!serverTagsDefinition.includes(tag)) { - logger.error( + globals.logger.error( `VERIFY CONFIG FILE: Server tag "${tag}" for server "${server.serverName}" is not defined in Butler-SOS.serversToMonitor.serverTagsDefinition. Exiting.` ); process.exit(1); } else { - logger.verbose( + globals.logger.verbose( `VERIFY CONFIG FILE: Server tag "${tag}" is defined in Butler-SOS.serversToMonitor.serverTagsDefinition.` ); } } } } catch (err) { - logger.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`); + globals.logger.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`); process.exit(1); } - logger.info(`VERIFY CONFIG FILE: Your config file at ${configFile} is valid, good work!`); + globals.logger.info( + `VERIFY CONFIG FILE: Your config file at ${globals.configFile} is valid, good work!` + ); return true; } catch (err) { - logger.error(`VERIFY CONFIG FILE: ${err}`); + globals.logger.error(`VERIFY CONFIG FILE: ${err}`); return false; } } - -module.exports = { - verifyConfigFile, -}; diff --git a/src/lib/healthmetrics.js b/src/lib/healthmetrics.js index a90e0042..64d65d70 100755 --- a/src/lib/healthmetrics.js +++ b/src/lib/healthmetrics.js @@ -2,17 +2,18 @@ * Get metrics from the Sense health check API */ -const path = require('path'); -const https = require('https'); -const fs = require('fs'); -const axios = require('axios'); -const globals = require('../globals'); -const postToInfluxdb = require('./post-to-influxdb'); -const postToNewRelic = require('./post-to-new-relic'); -const postToMQTT = require('./post-to-mqtt'); -const { getServerTags } = require('./servertags'); -const serverHeaders = require('./serverheaders'); -const prometheus = require('./prom-client'); +import path from 'path'; +import https from 'https'; +import fs from 'fs'; +import axios from 'axios'; + +import globals from '../globals.js'; +import { postHealthMetricsToInfluxdb } from './post-to-influxdb.js'; +import { postHealthMetricsToNewRelic } from './post-to-new-relic.js'; +import { postHealthToMQTT } from './post-to-mqtt.js'; +import { getServerHeaders } from './serverheaders.js'; +import { getServerTags } from './servertags.js'; +import { saveHealthMetricsToPrometheus } from './prom-client.js'; function getCertificates(options) { const certificate = {}; @@ -24,7 +25,7 @@ function getCertificates(options) { return certificate; } -function getHealthStatsFromSense(serverName, host, tags, headers) { +export function getHealthStatsFromSense(serverName, host, tags, headers) { globals.logger.debug(`HEALTH: URL=https://${host}/engine/healthcheck/`); const options = {}; @@ -98,30 +99,25 @@ function getHealthStatsFromSense(serverName, host, tags, headers) { // Post to MQTT if (globals.config.get('Butler-SOS.mqttConfig.enable') === true) { globals.logger.debug('HEALTH: Calling HEALTH metrics MQTT posting method'); - postToMQTT.postHealthToMQTT(host, tags.host, response.data); + postHealthToMQTT(host, tags.host, response.data); } // Post to Influxdb if (globals.config.get('Butler-SOS.influxdbConfig.enable') === true) { globals.logger.debug('HEALTH: Calling HEALTH metrics Influxdb posting method'); - postToInfluxdb.postHealthMetricsToInfluxdb( - serverName, - host, - response.data, - tags - ); + postHealthMetricsToInfluxdb(serverName, host, response.data, tags); } // Post to New Relic if (globals.config.get('Butler-SOS.newRelic.enable') === true) { globals.logger.debug('HEALTH: Calling HEALTH metrics New Relic posting method'); - postToNewRelic.postHealthMetricsToNewRelic(host, response.data, tags); + postHealthMetricsToNewRelic(host, response.data, tags); } // Save latest available data for Prometheus if (globals.config.get('Butler-SOS.prometheus.enable') === true) { globals.logger.debug('HEALTH: Calling HEALTH metrics Prometheus method'); - prometheus.saveHealthMetrics(host, response.data, tags); + saveHealthMetricsToPrometheus(host, response.data, tags); } } }) @@ -130,7 +126,7 @@ function getHealthStatsFromSense(serverName, host, tags, headers) { }); } -function setupHealthMetricsTimer() { +export function setupHealthMetricsTimer() { // Configure timer for getting healthcheck data setInterval(() => { globals.logger.verbose('HEALTH: Event started: Statistics collection'); @@ -152,14 +148,9 @@ function setupHealthMetricsTimer() { // }); // Get per-server headers - const headers = serverHeaders.getServerHeaders(server); + const headers = getServerHeaders(server); getHealthStatsFromSense(server.serverName, server.host, tags, headers); }); }, globals.config.get('Butler-SOS.serversToMonitor.pollingInterval')); } - -module.exports = { - setupHealthMetricsTimer, - getHealthStatsFromSense, -}; diff --git a/src/lib/heartbeat.js b/src/lib/heartbeat.js index 4aa29c08..c1c849a2 100644 --- a/src/lib/heartbeat.js +++ b/src/lib/heartbeat.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ -const later = require('@breejs/later'); -const axios = require('axios'); +import later from '@breejs/later'; +import axios from 'axios'; const callRemoteURL = function callRemoteURL(remoteURL, logger) { axios @@ -15,7 +15,7 @@ const callRemoteURL = function callRemoteURL(remoteURL, logger) { }); }; -function setupHeartbeatTimer(config, logger) { +export function setupHeartbeatTimer(config, logger) { try { logger.debug( `HEARTBEAT: Setting up heartbeat to remote: ${config.get( @@ -34,7 +34,3 @@ function setupHeartbeatTimer(config, logger) { logger.error(`HEARTBEAT: Error ${err}`); } } - -module.exports = { - setupHeartbeatTimer, -}; diff --git a/src/lib/log-event-categorise.js b/src/lib/log-event-categorise.js index 35807de5..0d9e076f 100644 --- a/src/lib/log-event-categorise.js +++ b/src/lib/log-event-categorise.js @@ -1,4 +1,4 @@ -const { config, logger } = require('../globals'); +import globals from '../globals.js'; // Function to categorise log events // @@ -13,7 +13,7 @@ const { config, logger } = require('../globals'); // - name: The name of the category // - value: The value of the category // - actionTaken: The action taken for the log event. Possible values are 'categorised', 'dropped' -function categoriseLogEvent(logLevel, logMessage) { +export function categoriseLogEvent(logLevel, logMessage) { const logEventCategory = []; try { @@ -21,7 +21,7 @@ function categoriseLogEvent(logLevel, logMessage) { // Loop over all rules in the config file // eslint-disable-next-line no-restricted-syntax - for (const rule of config.get('Butler-SOS.logEvents.categorise.rules')) { + for (const rule of globals.config.get('Butler-SOS.logEvents.categorise.rules')) { // Check if the log event matches any of the rule's log levels (which are found in the array 'logLevel' property) // Make the check case insensitive if (rule.logLevel.map((x) => x.toLowerCase()).includes(logLevel.toLowerCase())) { @@ -73,7 +73,7 @@ function categoriseLogEvent(logLevel, logMessage) { // Warn if the filter type is not recognised if (!['sw', 'ew', 'so'].includes(filter.type)) { - logger.warn( + globals.logger.warn( `LOG EVENT CATEGORISATION: Filter type '${filter.type}' is not recognised` ); } @@ -97,22 +97,18 @@ function categoriseLogEvent(logLevel, logMessage) { // If no rule matched, then use default rule (if enabled in the config file) if ( match === false && - config.get('Butler-SOS.logEvents.categorise.ruleDefault.enable') === true + globals.config.get('Butler-SOS.logEvents.categorise.ruleDefault.enable') === true ) { // Deep copy the categories from the default rule to the log event uniqueCategories.push( - ...config.get('Butler-SOS.logEvents.categorise.ruleDefault.category') + ...globals.config.get('Butler-SOS.logEvents.categorise.ruleDefault.category') ); } // Return the log event category and the action taken return { category: uniqueCategories, actionTaken: 'categorised' }; } catch (err) { - logger.error(`LOG EVENT CATEGORISATION: Error processing log event: ${err}`); + globals.logger.error(`LOG EVENT CATEGORISATION: Error processing log event: ${err}`); return null; } } - -module.exports = { - categoriseLogEvent, -}; diff --git a/src/lib/logdb.js b/src/lib/logdb.js index dc38d7ad..95826464 100755 --- a/src/lib/logdb.js +++ b/src/lib/logdb.js @@ -1,9 +1,9 @@ /* eslint-disable no-unused-vars */ -const globals = require('../globals'); -const postToMQTT = require('./post-to-mqtt'); +import globals from '../globals.js'; +import { postLogDbToMQTT } from './post-to-mqtt.js'; -function setupLogDbTimer() { +export function setupLogDbTimer() { // Get query period from config file. const queryPeriod = globals.config.get('Butler-SOS.logdb.queryPeriod'); @@ -155,7 +155,7 @@ function setupLogDbTimer() { // Post to MQTT (if enabled) if (globals.config.get('Butler-SOS.mqttConfig.enable') === true) { globals.logger.silly('LOGDB: Posting log db data to MQTT...'); - postToMQTT.postLogDbToMQTT( + postLogDbToMQTT( row.process_host, row.process_name, row.entry_level, @@ -181,7 +181,3 @@ function setupLogDbTimer() { }); }, globals.config.get('Butler-SOS.logdb.pollingInterval')); } - -module.exports = { - setupLogDbTimer, -}; diff --git a/src/lib/post-to-influxdb.js b/src/lib/post-to-influxdb.js index 1cc9780e..c0cf49a8 100755 --- a/src/lib/post-to-influxdb.js +++ b/src/lib/post-to-influxdb.js @@ -1,9 +1,9 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable no-unused-vars */ -const { Point } = require('@influxdata/influxdb-client'); +import { Point } from '@influxdata/influxdb-client'; -const globals = require('../globals'); +import globals from '../globals.js'; const sessionAppPrefix = 'SessionApp'; @@ -42,7 +42,7 @@ function getFormattedTime(serverStarted) { return `${days} days, ${hours}h ${minutes.substr(-2)}m ${seconds.substr(-2)}s`; } -async function postHealthMetricsToInfluxdb(serverName, host, body, serverTags) { +export async function postHealthMetricsToInfluxdb(serverName, host, body, serverTags) { // Calculate server uptime const formattedTime = getFormattedTime(body.started); @@ -485,7 +485,7 @@ async function postHealthMetricsToInfluxdb(serverName, host, body, serverTags) { } } -async function postProxySessionsToInfluxdb(userSessions) { +export async function postProxySessionsToInfluxdb(userSessions) { globals.logger.debug(`PROXY SESSIONS: User sessions: ${JSON.stringify(userSessions)}`); globals.logger.silly( @@ -562,7 +562,7 @@ async function postProxySessionsToInfluxdb(userSessions) { } } -async function postButlerSOSMemoryUsageToInfluxdb(memory) { +export async function postButlerSOSMemoryUsageToInfluxdb(memory) { globals.logger.debug(`MEMORY USAGE: Memory usage ${JSON.stringify(memory, null, 2)})`); // Get Butler version @@ -678,7 +678,7 @@ async function postButlerSOSMemoryUsageToInfluxdb(memory) { } } -async function postUserEventToInfluxdb(msg) { +export async function postUserEventToInfluxdb(msg) { globals.logger.debug(`USER EVENT INFLUXDB: ${msg})`); // Only write to influuxdb if the global influx object has been initialized @@ -873,7 +873,7 @@ async function postUserEventToInfluxdb(msg) { } } -async function postLogEventToInfluxdb(msg) { +export async function postLogEventToInfluxdb(msg) { globals.logger.debug(`LOG EVENT INFLUXDB: ${msg})`); try { @@ -1258,11 +1258,3 @@ async function postLogEventToInfluxdb(msg) { globals.logger.error(`LOG EVENT INFLUXDB 2: Error saving log event to InfluxDB! ${err}`); } } - -module.exports = { - postHealthMetricsToInfluxdb, - postProxySessionsToInfluxdb, - postButlerSOSMemoryUsageToInfluxdb, - postUserEventToInfluxdb, - postLogEventToInfluxdb, -}; diff --git a/src/lib/post-to-mqtt.js b/src/lib/post-to-mqtt.js index 7560bc5f..fa7bfe09 100755 --- a/src/lib/post-to-mqtt.js +++ b/src/lib/post-to-mqtt.js @@ -1,9 +1,9 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable no-unused-vars */ -const globals = require('../globals'); +import globals from '../globals.js'; -function postLogDbToMQTT(processHost, processName, entryLevel, message, _timestamp) { +export function postLogDbToMQTT(processHost, processName, entryLevel, message, _timestamp) { // Get base MQTT topic const baseTopic = globals.config.get('Butler-SOS.mqttConfig.baseTopic'); @@ -11,7 +11,7 @@ function postLogDbToMQTT(processHost, processName, entryLevel, message, _timesta globals.mqttClient.publish(`${baseTopic + processHost}/${processName}/${entryLevel}`, message); } -function postHealthToMQTT(_host, serverName, body) { +export function postHealthToMQTT(_host, serverName, body) { // Get base MQTT topic const baseTopic = globals.config.get('Butler-SOS.mqttConfig.baseTopic'); @@ -93,7 +93,7 @@ function postHealthToMQTT(_host, serverName, body) { globals.mqttClient.publish(`${baseTopic + serverName}/saturated`, body.saturated.toString()); } -function postUserSessionsToMQTT(host, virtualProxy, body) { +export function postUserSessionsToMQTT(host, virtualProxy, body) { // Get base MQTT topic const baseTopic = globals.config.get('Butler-SOS.mqttConfig.baseTopic'); @@ -101,7 +101,7 @@ function postUserSessionsToMQTT(host, virtualProxy, body) { globals.mqttClient.publish(`${baseTopic + host}/usersession${virtualProxy}`, body); } -function postUserEventToMQTT(msg) { +export function postUserEventToMQTT(msg) { try { // Create payload const payload = { @@ -203,7 +203,7 @@ function postUserEventToMQTT(msg) { } } -function postLogEventToMQTT(msg) { +export function postLogEventToMQTT(msg) { try { // Get MQTT root topic let baseTopic = globals.config.get('Butler-SOS.logEvents.sendToMQTT.baseTopic'); @@ -252,11 +252,3 @@ function postLogEventToMQTT(msg) { globals.logger.error(`LOG EVENT MQTT: Failed posting message to MQTT ${err}.`); } } - -module.exports = { - postLogDbToMQTT, - postHealthToMQTT, - postUserSessionsToMQTT, - postUserEventToMQTT, - postLogEventToMQTT, -}; diff --git a/src/lib/post-to-new-relic.js b/src/lib/post-to-new-relic.js index 168a9b55..335711ac 100755 --- a/src/lib/post-to-new-relic.js +++ b/src/lib/post-to-new-relic.js @@ -1,10 +1,10 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable no-unused-vars */ -const crypto = require('crypto'); -const axios = require('axios'); +import crypto from 'crypto'; +import axios from 'axios'; -const globals = require('../globals'); +import globals from '../globals.js'; // const sessionAppPrefix = 'SessionApp'; @@ -54,7 +54,7 @@ function getFormattedTime(serverStarted) { * @param {*} body * @param {*} tags */ -async function postHealthMetricsToNewRelic(_host, body, tags) { +export async function postHealthMetricsToNewRelic(_host, body, tags) { // Calculate server uptime const formattedTime = getFormattedTime(body.started); @@ -347,7 +347,7 @@ async function postHealthMetricsToNewRelic(_host, body, tags) { * * @param {*} userSessions */ -async function postProxySessionsToNewRelic(userSessions) { +export async function postProxySessionsToNewRelic(userSessions) { globals.logger.debug( `PROXY SESSIONS NEW RELIC: User sessions: ${JSON.stringify(userSessions)}` ); @@ -494,7 +494,7 @@ async function postProxySessionsToNewRelic(userSessions) { } } -async function postButlerSOSUptimeToNewRelic(fields) { +export async function postButlerSOSUptimeToNewRelic(fields) { globals.logger.debug( `MEMORY USAGE NEW RELIC: Memory usage ${JSON.stringify(fields, null, 2)})` ); @@ -651,7 +651,7 @@ async function postButlerSOSUptimeToNewRelic(fields) { } } -async function postUserEventToNewRelic(msg) { +export async function postUserEventToNewRelic(msg) { globals.logger.debug(`USER EVENT NEW RELIC 1: ${JSON.stringify(msg, null, 2)})`); try { @@ -912,7 +912,7 @@ function sendNRLogEventYesNo(sourceService, sourceLogLevel) { * * @param {*} msg */ -async function postLogEventToNewRelic(msg) { +export async function postLogEventToNewRelic(msg) { globals.logger.debug(`LOG EVENT NEW RELIC: ${msg})`); try { @@ -1084,11 +1084,3 @@ async function postLogEventToNewRelic(msg) { globals.logger.error(`LOG EVENT NEW RELIC: Error saving event to New Relic! ${err}`); } } - -module.exports = { - postHealthMetricsToNewRelic, - postProxySessionsToNewRelic, - postButlerSOSUptimeToNewRelic, - postUserEventToNewRelic, - postLogEventToNewRelic, -}; diff --git a/src/lib/prom-client.js b/src/lib/prom-client.js index 7c715910..adb33c6b 100755 --- a/src/lib/prom-client.js +++ b/src/lib/prom-client.js @@ -1,9 +1,9 @@ -const client = require('prom-client'); +import client from 'prom-client'; // import { collectDefaultMetrics, register } from 'prom-client'; // Load global variables and functions -const globals = require('../globals'); -const { getServerTags } = require('./servertags'); +import globals from '../globals.js'; +import { getServerTags } from './servertags.js'; let promLabels = null; @@ -38,7 +38,7 @@ let promMetricUserSessionsTotal = null; // client.collectDefaultMetrics(); -async function setupPromClient(promServer, promPort, promHost) { +export async function setupPromClient(promServer, promPort, promHost) { try { // Create array with all defined server tags that should be used as Prometheus labels globals.serverList.forEach((server) => { @@ -210,7 +210,7 @@ async function setupPromClient(promServer, promPort, promHost) { } } -function saveHealthMetrics(host, data, labels) { +export function saveHealthMetricsToPrometheus(host, data, labels) { try { globals.logger.silly(`PROM: Health metrics (host): ${host}`); globals.logger.silly(`PROM: Health metrics (data): ${JSON.stringify(data)}`); @@ -257,7 +257,7 @@ function saveHealthMetrics(host, data, labels) { } } -function saveUserSessionMetrics(userSessionsData) { +export function saveUserSessionMetricsToPrometheus(userSessionsData) { try { globals.logger.silly(`PROM: Session metrics (host): ${userSessionsData.host}`); globals.logger.silly( @@ -279,9 +279,3 @@ function saveUserSessionMetrics(userSessionsData) { globals.logger.error(`PROM: Error saving health data for Prometheus! ${err.stack}`); } } - -module.exports = { - setupPromClient, - saveHealthMetrics, - saveUserSessionMetrics, -}; diff --git a/src/lib/proxysessionmetrics.js b/src/lib/proxysessionmetrics.js index 8dc0b4b8..57ecb1aa 100755 --- a/src/lib/proxysessionmetrics.js +++ b/src/lib/proxysessionmetrics.js @@ -2,18 +2,18 @@ * Get metrics from Sense repository service */ -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const axios = require('axios'); -const { Point } = require('@influxdata/influxdb-client'); - -const globals = require('../globals'); -const postToInfluxdb = require('./post-to-influxdb'); -const postToNewRelic = require('./post-to-new-relic'); -const postToMQTT = require('./post-to-mqtt'); -const { getServerTags } = require('./servertags'); -const prometheus = require('./prom-client'); +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { Point } from '@influxdata/influxdb-client'; + +import globals from '../globals.js'; +import { postProxySessionsToInfluxdb } from './post-to-influxdb.js'; +import { postProxySessionsToNewRelic } from './post-to-new-relic.js'; +import { postUserSessionsToMQTT } from './post-to-mqtt.js'; +import { getServerTags } from './servertags.js'; +import { saveUserSessionMetricsToPrometheus } from './prom-client.js'; function getCertificates(options) { const certificate = {}; @@ -280,7 +280,7 @@ function getProxySessionStatsFromSense(serverName, host, virtualProxy, influxTag 'PROXY SESSIONS: Calling user sessions MQTT posting method' ); - postToMQTT.postUserSessionsToMQTT( + postUserSessionsToMQTT( host.split(':')[0], // response.request._headers.xvirtualproxy, virtualProxy, @@ -302,7 +302,7 @@ function getProxySessionStatsFromSense(serverName, host, virtualProxy, influxTag 'PROXY SESSIONS: Calling user sessions Influxdb posting method' ); - postToInfluxdb.postProxySessionsToInfluxdb(userProxySessionsData); + postProxySessionsToInfluxdb(userProxySessionsData); } // Post to New Relic @@ -316,13 +316,13 @@ function getProxySessionStatsFromSense(serverName, host, virtualProxy, influxTag 'PROXY SESSIONS: Calling user sessions New Relic posting method' ); - postToNewRelic.postProxySessionsToNewRelic(userProxySessionsData); + postProxySessionsToNewRelic(userProxySessionsData); } // Save latest available data for Prometheus if (globals.config.get('Butler-SOS.prometheus.enable') === true) { globals.logger.debug('HEALTH: Calling SESSIONS metrics Prometheus method'); - prometheus.saveUserSessionMetrics(userProxySessionsData); + saveUserSessionMetricsToPrometheus(userProxySessionsData); } } }) @@ -332,7 +332,7 @@ function getProxySessionStatsFromSense(serverName, host, virtualProxy, influxTag } // Get info on what sessions currently exist -function setupUserSessionsTimer() { +export function setupUserSessionsTimer() { globals.logger.debug( `PROXY SESSIONS: Monitor user sessions for these servers/virtual proxies: ${JSON.stringify( globals.serverList, @@ -366,7 +366,3 @@ function setupUserSessionsTimer() { }); }, globals.config.get('Butler-SOS.userSessions.pollingInterval')); } - -module.exports = { - setupUserSessionsTimer, -}; diff --git a/src/lib/serverheaders.js b/src/lib/serverheaders.js index 3526b588..2c449137 100755 --- a/src/lib/serverheaders.js +++ b/src/lib/serverheaders.js @@ -1,6 +1,6 @@ -const globals = require('../globals'); +import globals from '../globals.js'; -function getServerHeaders(server) { +export function getServerHeaders(server) { try { const headers = {}; @@ -25,7 +25,3 @@ function getServerHeaders(server) { return []; } } - -module.exports = { - getServerHeaders, -}; diff --git a/src/lib/servertags.js b/src/lib/servertags.js index 7d929378..88adbdfc 100755 --- a/src/lib/servertags.js +++ b/src/lib/servertags.js @@ -1,4 +1,5 @@ -function getServerTags(logger, server) { +// Get tag values from the server object +export function getServerTags(logger, server) { try { let tags = { host: server.host.split(':')[0], @@ -34,7 +35,3 @@ function getServerTags(logger, server) { return []; } } - -module.exports = { - getServerTags, -}; diff --git a/src/lib/service_uptime.js b/src/lib/service_uptime.js index f0a5f105..8e5d4b35 100644 --- a/src/lib/service_uptime.js +++ b/src/lib/service_uptime.js @@ -1,18 +1,18 @@ /* eslint-disable no-bitwise */ -const later = require('@breejs/later'); -const luxon = require('luxon'); +import later from '@breejs/later'; +import { Duration } from 'luxon'; -const globals = require('../globals'); -const postToInfluxdb = require('./post-to-influxdb'); -const postToNewRelic = require('./post-to-new-relic'); +import globals from '../globals.js'; +import { postButlerSOSMemoryUsageToInfluxdb } from './post-to-influxdb.js'; +import { postButlerSOSUptimeToNewRelic } from './post-to-new-relic.js'; const fullUnits = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']; -luxon.Duration.prototype.toFull = function convToFull() { +Duration.prototype.toFull = function convToFull() { // return this.shiftTo.apply(this, fullUnits); return this.shiftTo(...fullUnits); // Suggested bt GitHub Copilot }; -function serviceUptimeStart() { +export function serviceUptimeStart() { const uptimeLogLevel = globals.config.get('Butler-SOS.uptimeMonitor.logLevel'); const uptimeInterval = globals.config.get('Butler-SOS.uptimeMonitor.frequency'); @@ -51,7 +51,7 @@ function serviceUptimeStart() { startIterations += 1; const uptimeMilliSec = Date.now() - startTime; - const d = luxon.Duration.fromMillis(uptimeMilliSec).toFull().toObject(); + const d = Duration.fromMillis(uptimeMilliSec).toFull().toObject(); // Round to whole seconds d.seconds = Math.round(d.seconds); const uptimeString = `${d.months} months, ${d.days} days, ${d.hours} hours, ${d.minutes} minutes, ${d.seconds} seconds`; @@ -92,7 +92,7 @@ function serviceUptimeStart() { true && enableInfluxDB === true ) { - postToInfluxdb.postButlerSOSMemoryUsageToInfluxdb({ + postButlerSOSMemoryUsageToInfluxdb({ instanceTag: butlerSosMemoryInfluxTag, heapUsedMByte, heapTotalMByte, @@ -103,7 +103,7 @@ function serviceUptimeStart() { // Send to New Relic if (globals.config.get('Butler-SOS.uptimeMonitor.storeNewRelic.enable') === true) { - postToNewRelic.postButlerSOSUptimeToNewRelic({ + postButlerSOSUptimeToNewRelic({ intervalMillisec, heapUsed, heapTotal, @@ -116,7 +116,3 @@ function serviceUptimeStart() { } }, later.parse.text(uptimeInterval)); } - -module.exports = { - serviceUptimeStart, -}; diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index 0187c6ea..c60f6f0b 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -1,7 +1,6 @@ -const { PostHog } = require('posthog-node'); +import { PostHog } from 'posthog-node'; -const globals = require('../globals'); -const { log } = require('winston'); +import globals from '../globals.js'; // Define variable to hold the PostHog client let posthogClient; @@ -251,7 +250,7 @@ const callRemoteURL = async function reportTelemetry() { } }; -function setupAnonUsageReportTimer(logger, hostInfo) { +export function setupAnonUsageReportTimer(logger, hostInfo) { try { // Setup PostHog client posthogClient = new PostHog('phc_5cmKiX9OubQjsSfOZuaolWaxo2z7WXqd295eB0uOtTb', { @@ -276,7 +275,3 @@ function setupAnonUsageReportTimer(logger, hostInfo) { logger.error(`TELEMETRY: ${err}`); } } - -module.exports = { - setupAnonUsageReportTimer, -}; diff --git a/src/lib/udp_handlers_log_events.js b/src/lib/udp_handlers_log_events.js index a68b9bed..b5b61f30 100644 --- a/src/lib/udp_handlers_log_events.js +++ b/src/lib/udp_handlers_log_events.js @@ -1,17 +1,16 @@ -/* eslint-disable prefer-destructuring */ /* eslint-disable no-unused-vars */ // Load global variables and functions -const globals = require('../globals'); -const postToInfluxdb = require('./post-to-influxdb'); -const postToNewRelic = require('./post-to-new-relic'); -const postToMQTT = require('./post-to-mqtt'); -const { categoriseLogEvent } = require('./log-event-categorise'); +import globals from '../globals.js'; +import { postLogEventToInfluxdb } from './post-to-influxdb.js'; +import { postLogEventToNewRelic } from './post-to-new-relic.js'; +import { postLogEventToMQTT } from './post-to-mqtt.js'; +import { categoriseLogEvent } from './log-event-categorise.js'; // -------------------------------------------------------- // Set up UDP server for acting on Sense log events // -------------------------------------------------------- -function udpInitLogEventServer() { +export function udpInitLogEventServer() { // Handler for UDP server startup event globals.udpServerLogEvents.socket.on('listening', (_message, _remote) => { const address = globals.udpServerLogEvents.socket.address(); @@ -294,7 +293,7 @@ function udpInitLogEventServer() { globals.config.get('Butler-SOS.logEvents.sendToMQTT.enable') ) { globals.logger.debug('LOG EVENT: Calling log event MQTT posting method'); - postToMQTT.postLogEventToMQTT(msgObj); + postLogEventToMQTT(msgObj); } // Post to Influxdb (if enabled) @@ -303,7 +302,7 @@ function udpInitLogEventServer() { globals.config.get('Butler-SOS.logEvents.sendToInfluxdb.enable') ) { globals.logger.debug('LOG EVENT: Calling log event Influxdb posting method'); - postToInfluxdb.postLogEventToInfluxdb(msgObj); + postLogEventToInfluxdb(msgObj); } // Post to New Relic (if enabled) @@ -312,7 +311,7 @@ function udpInitLogEventServer() { globals.config.get('Butler-SOS.logEvents.sendToNewRelic.enable') ) { globals.logger.debug('LOG EVENT: Calling log event New Relic posting method'); - postToNewRelic.postLogEventToNewRelic(msgObj); + postLogEventToNewRelic(msgObj); } } } catch (err) { @@ -320,7 +319,3 @@ function udpInitLogEventServer() { } }); } - -module.exports = { - udpInitLogEventServer, -}; diff --git a/src/lib/udp_handlers_user_activity.js b/src/lib/udp_handlers_user_activity.js index c95ecd04..2456ba31 100644 --- a/src/lib/udp_handlers_user_activity.js +++ b/src/lib/udp_handlers_user_activity.js @@ -1,17 +1,17 @@ /* eslint-disable no-unused-vars */ -const { validate } = require('uuid'); -const parser = require('ua-parser-js'); +import { validate } from 'uuid'; +import parser from 'ua-parser-js'; // Load global variables and functions -const globals = require('../globals'); -const postToInfluxdb = require('./post-to-influxdb'); -const postToNewRelic = require('./post-to-new-relic'); -const postToMQTT = require('./post-to-mqtt'); +import globals from '../globals.js'; +import { postUserEventToInfluxdb } from './post-to-influxdb.js'; +import { postUserEventToNewRelic } from './post-to-new-relic.js'; +import { postUserEventToMQTT } from './post-to-mqtt.js'; // -------------------------------------------------------- // Set up UDP server for acting on Sense user activity events // -------------------------------------------------------- -function udpInitUserActivityServer() { +export function udpInitUserActivityServer() { // Handler for UDP server startup event globals.udpServerUserActivity.socket.on('listening', (_message, _remote) => { const address = globals.udpServerUserActivity.socket.address(); @@ -161,7 +161,7 @@ function udpInitUserActivityServer() { globals.config.get('Butler-SOS.userEvents.sendToMQTT.enable') ) { globals.logger.debug('USER EVENT: Calling user sessions MQTT posting method'); - postToMQTT.postUserEventToMQTT(msgObj); + postUserEventToMQTT(msgObj); } // Post to Influxdb @@ -170,7 +170,7 @@ function udpInitUserActivityServer() { globals.config.get('Butler-SOS.userEvents.sendToInfluxdb.enable') ) { globals.logger.debug('USER EVENT: Calling user sessions Influxdb posting method'); - postToInfluxdb.postUserEventToInfluxdb(msgObj); + postUserEventToInfluxdb(msgObj); } // Post to New Relic @@ -179,14 +179,10 @@ function udpInitUserActivityServer() { globals.config.get('Butler-SOS.userEvents.sendToNewRelic.enable') ) { globals.logger.debug('USER EVENT: Calling user event New Relic posting method'); - postToNewRelic.postUserEventToNewRelic(msgObj); + postUserEventToNewRelic(msgObj); } } catch (err) { globals.logger.error(`USER EVENT: Error processing user activity event: ${err}`); } }); } - -module.exports = { - udpInitUserActivityServer, -}; From bb46e0078f66b41f4d19bc34ba21614e7c56d990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 05:21:09 +0000 Subject: [PATCH 3/8] fix(config): More thorough verification of config file structure Fixes #857 --- src/butler-sos.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/butler-sos.js b/src/butler-sos.js index e1a26383..399c907e 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -68,6 +68,7 @@ async function mainScript() { } else { configFileVerify = await verifyConfigFile(); } + // If config file verification failed, the previous function would have returned false. // In that case, we should exit the script. if (!configFileVerify) { From aff185589dd6a01b457c8db5328ced5386ae0daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 05:28:26 +0000 Subject: [PATCH 4/8] feat(config): Show info at startup whether Docker is used or not Implements #861 --- src/butler-sos.js | 39 ++++++++++++++++++++------------------- src/globals.js | 1 + src/lib/telemetry.js | 4 ++-- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/butler-sos.js b/src/butler-sos.js index 399c907e..03c95634 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -127,28 +127,29 @@ async function mainScript() { // Default is to use log level defined in config file globals.logger.info('--------------------------------------'); globals.logger.info('Starting Butler SOS'); - globals.logger.info(`Log level : ${globals.getLoggingLevel()}`); - globals.logger.info(`App version : ${globals.appVersion}`); - globals.logger.info(`Instance ID : ${globals.hostInfo.id}`); + globals.logger.info(`Log level : ${globals.getLoggingLevel()}`); + globals.logger.info(`App version : ${globals.appVersion}`); + globals.logger.info(`Instance ID : ${globals.hostInfo.id}`); + globals.logger.info(`Running in Docker : ${globals.hostInfo.isRunningInDocker}`); globals.logger.info(''); - globals.logger.info(`Node version : ${globals.hostInfo.node.nodeVersion}`); - globals.logger.info(`Architecture : ${globals.hostInfo.si.os.arch}`); - globals.logger.info(`Platform : ${globals.hostInfo.si.os.platform}`); - globals.logger.info(`Release : ${globals.hostInfo.si.os.release}`); - globals.logger.info(`Distro : ${globals.hostInfo.si.os.distro}`); - globals.logger.info(`Codename : ${globals.hostInfo.si.os.codename}`); - globals.logger.info(`Virtual : ${globals.hostInfo.si.system.virtual}`); - globals.logger.info(`Processors : ${globals.hostInfo.si.cpu.processors}`); - globals.logger.info(`Physical cores : ${globals.hostInfo.si.cpu.physicalCores}`); - globals.logger.info(`Cores : ${globals.hostInfo.si.cpu.cores}`); - globals.logger.info(`Docker arch. : ${globals.hostInfo.si.cpu.hypervizor}`); - globals.logger.info(`Total memory : ${globals.hostInfo.si.memory.total}`); - globals.logger.info(`Standalone app : ${globals.isPkg}`); + globals.logger.info(`Node version : ${globals.hostInfo.node.nodeVersion}`); + globals.logger.info(`Architecture : ${globals.hostInfo.si.os.arch}`); + globals.logger.info(`Platform : ${globals.hostInfo.si.os.platform}`); + globals.logger.info(`Release : ${globals.hostInfo.si.os.release}`); + globals.logger.info(`Distro : ${globals.hostInfo.si.os.distro}`); + globals.logger.info(`Codename : ${globals.hostInfo.si.os.codename}`); + globals.logger.info(`Virtual : ${globals.hostInfo.si.system.virtual}`); + globals.logger.info(`Processors : ${globals.hostInfo.si.cpu.processors}`); + globals.logger.info(`Physical cores : ${globals.hostInfo.si.cpu.physicalCores}`); + globals.logger.info(`Cores : ${globals.hostInfo.si.cpu.cores}`); + globals.logger.info(`Docker arch. : ${globals.hostInfo.si.cpu.hypervizor}`); + globals.logger.info(`Total memory : ${globals.hostInfo.si.memory.total}`); + globals.logger.info(`Standalone app : ${globals.isPkg}`); // Log info about what Qlik Sense certificates are being used - globals.logger.info(`Client cert : ${certFile}`); - globals.logger.info(`Client cert key : ${keyFile}`); - globals.logger.info(`CA cert : ${caFile}`); + globals.logger.info(`Client cert : ${certFile}`); + globals.logger.info(`Client cert key : ${keyFile}`); + globals.logger.info(`CA cert : ${caFile}`); globals.logger.info('--------------------------------------'); // Set up anon usage reports, if enabled diff --git a/src/globals.js b/src/globals.js index 257a43c0..800139c6 100755 --- a/src/globals.js +++ b/src/globals.js @@ -950,6 +950,7 @@ class Settings { const hostInfo = { id, + isRunningInDocker: Settings.isRunningInDocker(), node: { nodeVersion: process.version, versions: process.versions, diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index c60f6f0b..509f1d4d 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -149,7 +149,7 @@ const callRemoteURL = async function reportTelemetry() { system_distro: globals.hostInfo.si.os.distro, system_codename: globals.hostInfo.si.os.codename, system_virtual: globals.hostInfo.si.system.virtual, - system_hypervisor: globals.hostInfo.si.os.hypervizor, + system_isRunningInDocker: globals.hostInfo.isRunningInDocker, system_nodeVersion: globals.hostInfo.node.nodeVersion, feature_heartbeat: heartbeat, @@ -188,7 +188,7 @@ const callRemoteURL = async function reportTelemetry() { distro: globals.hostInfo.si.os.distro, codename: globals.hostInfo.si.os.codename, virtual: globals.hostInfo.si.system.virtual, - hypervisor: globals.hostInfo.si.os.hypervizor, + isRunningInDocker: globals.hostInfo.isRunningInDocker, nodeVersion: globals.hostInfo.node.nodeVersion, }, enabledFeatures: { From bad4667ae6fc649d0b9181fec2cfc93156498e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Mon, 19 Aug 2024 11:54:36 +0000 Subject: [PATCH 5/8] 858 wip --- package-lock.json | 593 ++++++++++++++++- package.json | 4 + src/butler-sos.js | 6 + src/config/production_template.yaml | 7 + src/lib/config-file-schema.js | 6 + src/lib/config-obfuscate.js | 152 +++++ src/lib/config-visualise.js | 138 ++++ src/plugins/README.md | 16 + src/plugins/sensible.js | 14 + src/plugins/support.js | 12 + static/configvis/butler.png | Bin 0 -> 33013 bytes static/configvis/download-solid.svg | 1 + static/configvis/index.html | 245 +++++++ static/configvis/jsontree.js | 927 +++++++++++++++++++++++++++ static/configvis/jsontree.js.css | 330 ++++++++++ static/configvis/jsontree.js.css.map | 1 + static/configvis/jsontree.js.map | 1 + static/configvis/prism.css | 174 +++++ static/configvis/prism.js | 625 ++++++++++++++++++ static/logo.svg | 7 + 20 files changed, 3239 insertions(+), 20 deletions(-) create mode 100644 src/lib/config-obfuscate.js create mode 100644 src/lib/config-visualise.js create mode 100644 src/plugins/README.md create mode 100644 src/plugins/sensible.js create mode 100644 src/plugins/support.js create mode 100644 static/configvis/butler.png create mode 100644 static/configvis/download-solid.svg create mode 100644 static/configvis/index.html create mode 100644 static/configvis/jsontree.js create mode 100644 static/configvis/jsontree.js.css create mode 100644 static/configvis/jsontree.js.css.map create mode 100644 static/configvis/jsontree.js.map create mode 100644 static/configvis/prism.css create mode 100644 static/configvis/prism.js create mode 100644 static/logo.svg diff --git a/package-lock.json b/package-lock.json index 82d34f95..ea80eff9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "license": "MIT", "dependencies": { "@breejs/later": "^4.2.0", + "@fastify/rate-limit": "^9.1.0", + "@fastify/sensible": "^5.6.0", + "@fastify/static": "^7.0.4", "@influxdata/influxdb-client": "^1.35.0", "@influxdata/influxdb-client-apis": "^1.35.0", "axios": "^1.7.4", @@ -19,6 +22,7 @@ "fastify-healthcheck": "^4.4.0", "fastify-metrics": "^11.0.0", "fs-extra": "^11.2.0", + "handlebars": "^4.7.7", "influx": "^5.9.3", "js-yaml": "^4.1.0", "lodash.clonedeep": "^4.5.0", @@ -600,6 +604,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/@fastify/ajv-compiler": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", @@ -648,6 +661,59 @@ "fast-json-stringify": "^5.7.0" } }, + "node_modules/@fastify/rate-limit": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-9.1.0.tgz", + "integrity": "sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "fastify-plugin": "^4.0.0", + "toad-cache": "^3.3.1" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", + "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "2.0.0", + "mime": "^3.0.0" + } + }, + "node_modules/@fastify/sensible": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@fastify/sensible/-/sensible-5.6.0.tgz", + "integrity": "sha512-Vq6Z2ZQy10GDqON+hvLF52K99s9et5gVVxTul5n3SIAf0Kq5QjPRUKkAMT3zPAiiGvoHtS3APa/3uaxfDgCODQ==", + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.1", + "fast-deep-equal": "^3.1.1", + "fastify-plugin": "^4.0.0", + "forwarded": "^0.2.0", + "http-errors": "^2.0.0", + "type-is": "^1.6.18", + "vary": "^1.1.2" + } + }, + "node_modules/@fastify/static": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", + "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, "node_modules/@fastify/under-pressure": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/@fastify/under-pressure/-/under-pressure-8.3.0.tgz", @@ -699,6 +765,82 @@ "@influxdata/influxdb-client": "*" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -742,6 +884,16 @@ "node": ">=8.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -981,7 +1133,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -990,7 +1141,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1063,8 +1213,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -1191,7 +1340,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1307,6 +1455,18 @@ "node": ">= 10.0.0" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -1319,7 +1479,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1375,17 +1534,31 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -1438,6 +1611,12 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2005,6 +2184,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -2063,6 +2258,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2075,6 +2290,30 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -2152,6 +2391,27 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2215,6 +2475,22 @@ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2319,7 +2595,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -2348,8 +2623,22 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } }, "node_modules/js-sdsl": { "version": "4.3.0", @@ -2540,6 +2829,27 @@ "node": ">=10" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2579,6 +2889,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -2641,6 +2960,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", @@ -2731,6 +3056,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2757,11 +3088,26 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pg": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", @@ -3259,11 +3605,16 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3275,11 +3626,22 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -3331,6 +3693,15 @@ "atomic-sleep": "^1.0.0" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -3353,6 +3724,15 @@ "node": "*" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3365,7 +3745,21 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3379,7 +3773,19 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3521,13 +3927,23 @@ } }, "node_modules/toad-cache": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz", - "integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -3553,6 +3969,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -3581,6 +4010,19 @@ "node": "*" } }, + "node_modules/uglify-js": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", + "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "6.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", @@ -3621,11 +4063,19 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3725,6 +4175,12 @@ "node": ">= 6" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, "node_modules/worker-timers": { "version": "7.1.8", "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", @@ -3759,6 +4215,103 @@ "tslib": "^2.6.2" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", diff --git a/package.json b/package.json index 7a170459..e0d6579a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ "type": "module", "dependencies": { "@breejs/later": "^4.2.0", + "@fastify/rate-limit": "^9.1.0", + "@fastify/sensible": "^5.6.0", + "@fastify/static": "^7.0.4", "@influxdata/influxdb-client": "^1.35.0", "@influxdata/influxdb-client-apis": "^1.35.0", "axios": "^1.7.4", @@ -40,6 +43,7 @@ "fastify-healthcheck": "^4.4.0", "fastify-metrics": "^11.0.0", "fs-extra": "^11.2.0", + "handlebars": "^4.7.7", "influx": "^5.9.3", "js-yaml": "^4.1.0", "lodash.clonedeep": "^4.5.0", diff --git a/src/butler-sos.js b/src/butler-sos.js index 03c95634..70b2aaa5 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -24,6 +24,7 @@ import { udpInitLogEventServer } from './lib/udp_handlers_log_events.js'; import { setupAnonUsageReportTimer } from './lib/telemetry.js'; import { setupPromClient } from './lib/prom-client.js'; import { verifyConfigFile } from './lib/config-file-verify.js'; +import { setupConfigVisServer } from './lib/config-visualise.js'; // Suppress experimental warnings // https://stackoverflow.com/questions/55778283/how-to-disable-warnings-when-node-is-launched-via-a-global-shell-script @@ -279,6 +280,11 @@ async function mainScript() { if (globals.config.get('Butler-SOS.appNames.enableAppNameExtract') === true) { setupAppNamesExtractTimer(); } + + // Set up config server, if enabled + if (globals.config.get('Butler-SOS.configVisualisation.enable') === true) { + await setupConfigVisServer(); + } } mainScript(); diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 3f79675b..82e67130 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -10,6 +10,13 @@ Butler-SOS: # More info on whata data is collected: https://butler-sos.ptarmiganlabs.com/docs/about/telemetry/ # Please consider leaving this at true - it really helps future development of Butler SOS! + # Should Butler SOS start a web server that serves an obfuscated view of the Butler SOS config file? + configVisualisation: + enable: false + host: localhost # Hostname or IP address where the web server will listen. Should be localhost in most cases. + port: 3100 # Port where the web server will listen. Change if port 3100 is already in use. + obfuscate: true # Should the config file shown in the web UI be obfuscated? + # Heartbeats can be used to send "I'm alive" messages to some other tool, e.g. an infrastructure monitoring tool # The concept is simple: The remoteURL will be called at the specified frequency. The receiving tool will then know # that Butler SOS is alive. diff --git a/src/lib/config-file-schema.js b/src/lib/config-file-schema.js index 778bc0a9..82203cf9 100755 --- a/src/lib/config-file-schema.js +++ b/src/lib/config-file-schema.js @@ -4,6 +4,12 @@ export const confifgFileSchema = { fileLogging: 'boolean', logDirectory: 'string', anonTelemetry: 'boolean', + configVisualisation: { + enable: 'boolean', + host: 'string', + port: 'number', + obfuscate: 'boolean', + }, heartbeat: { enable: 'boolean', remoteURL: 'string', diff --git a/src/lib/config-obfuscate.js b/src/lib/config-obfuscate.js new file mode 100644 index 00000000..fb482801 --- /dev/null +++ b/src/lib/config-obfuscate.js @@ -0,0 +1,152 @@ +import globals from '../globals.js'; + +function configObfuscate(config) { + try { + const obfuscatedConfig = { ...config }; + + // Keep first 10 chars of remote URL, mask the rest with * + obfuscatedConfig['Butler-SOS'].heartbeat.remoteURL = obfuscatedConfig['Butler-SOS'].heartbeat.remoteURL.substring(0, 10) + '*'.repeat(10); + + // Update entries in the array obfuscatedConfig['Butler-SOS'].thirdPartyToolsCredentials.newRelic + obfuscatedConfig['Butler-SOS'].thirdPartyToolsCredentials.newRelic = obfuscatedConfig['Butler-SOS'].thirdPartyToolsCredentials.newRelic?.map( + (element) => ({ + ...element, + insertApiKey: element.insertApiKey.substring(0, 5) + '*'.repeat(10), + accountId: element.accountId.toString().substring(0, 3) + '*'.repeat(10), + }), + ); + + // Obfuscate Butler-SOS.iuserEvents.udpServerConfig.serverHost, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.udpServerConfig.serverHost = + obfuscatedConfig['Butler-SOS'].userEvents.udpServerConfig.serverHost.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.iuserEvents.sendToMQTT.postTo.everythingTopic.topic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.everythingTopic.topic = + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.everythingTopic.topic.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.iuserEvents.sendToMQTT.postTo.sessionStartTopic.topic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.sessionStartTopic.topic = + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.sessionStartTopic.topic.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.iuserEvents.sendToMQTT.postTo.sessionStopTopic.topic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.sessionStopTopic.topic = + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.sessionStopTopic.topic.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.iuserEvents.sendToMQTT.postTo.connectionOpenTopic.topic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.connectionOpenTopic.topic = + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.connectionOpenTopic.topic.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.iuserEvents.sendToMQTT.postTo.connectionCloseTopic.topic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.connectionCloseTopic.topic = + obfuscatedConfig['Butler-SOS'].userEvents.sendToMQTT.postTo.connectionCloseTopic.topic.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.logEvents.udpServerConfig.serverHost, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].logEvents.udpServerConfig.serverHost = + obfuscatedConfig['Butler-SOS'].logEvents.udpServerConfig.serverHost.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.logEvents.sendToMQTT.baseTopic, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].logEvents.sendToMQTT.baseTopic = + obfuscatedConfig['Butler-SOS'].logEvents.sendToMQTT.baseTopic.substring(0, 10) + '*'.repeat(10); + + // Log db - may not be present in the config in future versions of Butler SOS + if (obfuscatedConfig['Butler-SOS'].logdb) { + // Obfuscate Butler-SOS.logdb.host, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].logdb.host = obfuscatedConfig['Butler-SOS'].logdb.host.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.logdb.qlogsReaderUser, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].logdb.qlogsReaderUser = obfuscatedConfig['Butler-SOS'].logdb.qlogsReaderUser.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.logdb.qlogsReaderPwdd, keep first 0 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].logdb.qlogsReaderPwdd = '*'.repeat(10); + } + + // Obfuscate Butler-SOS.cert.clientCert, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].cert.clientCert = obfuscatedConfig['Butler-SOS'].cert.clientCert.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.cert.clientCertKey, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].cert.clientCertKey = obfuscatedConfig['Butler-SOS'].cert.clientCertKey.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.cert.clientCertCA, keep first 10 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].cert.clientCertCA = obfuscatedConfig['Butler-SOS'].cert.clientCertCA.substring(0, 10) + '*'.repeat(10); + + // Obfuscate Butler-SOS.cert.clientCertPassphrase, keep first 0 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].cert.clientCertPassphrase = '*'.repeat(10); + + // Obfuscate Butler-SOS.mqttConfig.brokerHost, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].mqttConfig.brokerHost = obfuscatedConfig['Butler-SOS'].mqttConfig.brokerHost.substring(0, 3) + '*'.repeat(10); + + + // Obfuscate Butler-SOS.prometheus.host, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].prometheus.host = obfuscatedConfig['Butler-SOS'].prometheus.host.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.host, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.host = obfuscatedConfig['Butler-SOS'].influxdbConfig.host.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.v2Config.org, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.v2Config.org = obfuscatedConfig['Butler-SOS'].influxdbConfig.v2Config.org.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.v2Config.bucket, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.v2Config.bucket = obfuscatedConfig['Butler-SOS'].influxdbConfig.v2Config.bucket.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.v2Config.token, keep first 0 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.v2Config.token = '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.v1Config.auth.username, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.v1Config.auth.username = obfuscatedConfig['Butler-SOS'].influxdbConfig.v1Config.auth.username.substring(0, 3) + '*'.repeat(10); + + // Obfuscate Butler-SOS.influxdbConfig.v1Config.auth.password, keep first 0 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].influxdbConfig.v1Config.auth.password = '*'.repeat(10); + + // Obfuscate Butler-SOS.appNames.hostIP, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].appNames.hostIP = obfuscatedConfig['Butler-SOS'].appNames.hostIP.substring(0, 3) + '*'.repeat(10); + + + + // Obfuscate Butler-SOS.serversToMonitor.servers[].host, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].serversToMonitor.servers = obfuscatedConfig['Butler-SOS'].serversToMonitor.servers?.map((element) => ({ + ...element, + host: element.host.substring(0, 3) + '*'.repeat(10), + })); + + // Obfuscate Butler-SOS.serversToMonitor.servers[].logDbHost, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].serversToMonitor.servers = obfuscatedConfig['Butler-SOS'].serversToMonitor.servers?.map((element) => ({ + ...element, + logDbHost: element.logDbHost.substring(0, 3) + '*'.repeat(10), + })); + + // Obfuscate Butler-SOS.serversToMonitor.servers[].userSessions.host, keep first 3 chars, mask the rest with * + obfuscatedConfig['Butler-SOS'].serversToMonitor.servers = obfuscatedConfig['Butler-SOS'].serversToMonitor.servers?.map((element) => ({ + ...element, + userSessions: element.userSessions.host.substring(0, 3) + '*'.repeat(10), + })); + + // Obfuscate Butler-SOS.serversToMonitor.servers[].headers, keep first 5 chars, mask the rest with * + // Butler-SOS.serversToMonitor.servers[].headers is an object, so we need to obfuscate each key-value pair + // If the array Butler-SOS.serversToMonitor.servers[].headers is empty, no obfuscation should be done + obfuscatedConfig['Butler-SOS'].serversToMonitor.servers = obfuscatedConfig['Butler-SOS'].serversToMonitor.servers?.map((element) => { + const newHeaders = {}; + + // Is elemnt.headers an object with more than 0 key-value pairs? + if (element?.headers && Object.keys(element?.headers)?.length > 0) { + Object.entries(element?.headers).forEach(([key, value]) => { + newHeaders[key] = value.substring(0, 5) + '*'.repeat(10); + }); + } + + return { + ...element, + headers: newHeaders, + }; + }); + + return obfuscatedConfig; + } catch (err) { + globals.logger.error(`CONFIG OBFUSCATE: Error obfuscating config: ${err.message}`); + if (err.stack) { + globals.logger.error(`CONFIG OBFUSCATE: ${err.stack}`); + } + throw err; + } +} + +export default configObfuscate; diff --git a/src/lib/config-visualise.js b/src/lib/config-visualise.js new file mode 100644 index 00000000..2129c1b9 --- /dev/null +++ b/src/lib/config-visualise.js @@ -0,0 +1,138 @@ +import Fastify from 'fastify'; +import FastifyRateLimit from '@fastify/rate-limit'; +import FastifyStatic from '@fastify/static'; +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; +import handlebars from 'handlebars'; + +import globals from '../globals.js'; +import configObfuscate from './config-obfuscate.js'; + +export async function setupConfigVisServer(logger, config) { + try { + // Register rate limit for API + // 0 means no rate limit + + // This code registers the FastifyRateLimit plugin. + // The plugin limits the number of API requests that + // can be made from a given IP address within a given + // time window. + + const configVisServer = Fastify({ logger: true }); + + // Set Fastify log level based on log level in Butler config file + const currLogLevel = globals.getLoggingLevel(); + if (currLogLevel === 'debug' || currLogLevel === 'silly') { + configVisServer.log.level = 'info'; + } else { + configVisServer.log.level = 'silent'; + } + + // 30 requests per minute + await configVisServer.register(FastifyRateLimit, { + max: 300, + timeWindow: '1 minute', + }); + + // Add custom error handler for 429 errors (rate limit exceeded) + configVisServer.setErrorHandler((error, request, reply) => { + if (error.statusCode === 429) { + globals.logger.warn( + `CONFIG VIS: Rate limit exceeded for source IP address ${request.ip}. Method=${request.method}, endpoint=${request.url}`, + ); + } + reply.send(error); + }); + + // This loads all plugins defined in plugins. + // Those should be support plugins that are reused through your application + await configVisServer.register(import('../plugins/sensible.js'), { options: {} }); + await configVisServer.register(import('../plugins/support.js'), { options: {} }); + + // Create absolute path to the html directory + // dirname points to the directory where this file (app.js) is located, taking into account + // if the app is running as a packaged app or as a Node.js app. + globals.logger.verbose(`----------------2: ${globals.appBasePath}`); + + // Get directory contents of dirname + const dirContents = fs.readdirSync(globals.appBasePath); + globals.logger.verbose(`CONFIG VIS: Directory contents of "${globals.appBasePath}": ${dirContents}`); + + + const htmlDir = path.resolve(globals.appBasePath, 'static/configvis'); + globals.logger.info(`CONFIG VIS: Serving static files from ${htmlDir}`); + + await configVisServer.register(FastifyStatic, { + root: htmlDir, + constraints: {}, // optional: default {}. Example: { host: 'example.com' } + redirect: true, // Redirect to trailing '/' when the pathname is a dir + }); + + configVisServer.get('/', async (request, reply) => { + // Obfuscate the config object before sending it to the client + // First get clean copy of the config object + let newConfig = JSON.parse(JSON.stringify(globals.config)); + + if (globals.config.get('Butler-SOS.configVisualisation.obfuscate')) { + // Obfuscate config file before presenting it to the user + // This is done to avoid leaking sensitive information + // to users who should not have access to it. + // The obfuscation is done by replacing parts of the + // config file with masked strings. + newConfig = configObfuscate(newConfig); + } + + // Convert the (potentially obfuscated) config object to YAML format (=string) + const butlerConfigYaml = yaml.dump(newConfig); + + // Read index.html from disk + // dirname points to the directory where this file (app.js) is located, taking into account + // if the app is running as a packaged app or as a Node.js app. + globals.logger.verbose(`----------------3: ${globals.appBasePath}`); + const filePath = path.resolve(globals.appBasePath, 'static/configvis', 'index.html'); + const template = fs.readFileSync(filePath, 'utf8'); + + // Compile handlebars template + const compiledTemplate = handlebars.compile(template); + + // Get config as HTML encoded JSON string + const butlerConfigJsonEncoded = JSON.stringify(newConfig); + + // Render the template + const renderedText = compiledTemplate({ butlerConfigJsonEncoded, butlerConfigYaml }); + + globals.logger.debug(`CONFIG VIS: Rendered text: ${renderedText}`); + + // Send reply as HTML + reply.code(200).header('Content-Type', 'text/html; charset=utf-8').send(renderedText); + }); + + configVisServer.listen( + { + host: globals.config.get('Butler-SOS.configVisualisation.host'), + port: globals.config.get('Butler-SOS.configVisualisation.port'), + }, + (err, address) => { + if (err) { + globals.logger.error(`CONFIG VIS: Could not set up config visualisation server on ${address}`); + globals.logger.error(`CONFIG VIS: ${err.stack}`); + configVisServer.log.error(err); + process.exit(1); + } + globals.logger.info(`CONFIG VIS: Config visualisation server listening on ${address}`); + + configVisServer.ready((err2) => { + if (err2) throw err; + }); + }, + ); + } catch (err) { + globals.logger.error(`CONFIG VIS: Error setting up config visualisation server: ${err.message}`); + if (err.stack) { + globals.logger.error(`CONFIG VIS: ${err.stack}`); + } + throw err; + } +} + diff --git a/src/plugins/README.md b/src/plugins/README.md new file mode 100644 index 00000000..7d8136d6 --- /dev/null +++ b/src/plugins/README.md @@ -0,0 +1,16 @@ +# Plugins Folder + +Plugins define behavior that is common to all the routes in your +application. Authentication, caching, templates, and all the other cross +cutting concerns should be handled by plugins placed in this folder. + +Files in this folder are typically defined through the +[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, +making them non-encapsulated. They can define decorators and set hooks +that will then be used in the rest of your application. + +Check out: + +- [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Plugins-Guide/) +- [Fastify decorators](https://www.fastify.io/docs/latest/Decorators/). +- [Fastify lifecycle](https://www.fastify.io/docs/latest/Lifecycle/). diff --git a/src/plugins/sensible.js b/src/plugins/sensible.js new file mode 100644 index 00000000..a9535b2f --- /dev/null +++ b/src/plugins/sensible.js @@ -0,0 +1,14 @@ +import fp from 'fastify-plugin'; + +/** + * This plugins adds some utilities to handle http errors + * + * @see https://github.com/fastify/fastify-sensible + */ +// eslint-disable-next-line no-unused-vars +export default fp(async (fastify, _opts) => { + // eslint-disable-next-line global-require + await fastify.register(import('@fastify/sensible'), { + errorHandler: false, + }); +}); diff --git a/src/plugins/support.js b/src/plugins/support.js new file mode 100644 index 00000000..373fca62 --- /dev/null +++ b/src/plugins/support.js @@ -0,0 +1,12 @@ +import fp from 'fastify-plugin'; + +// the use of fastify-plugin is required to be able +// to export the decorators to the outer scope + +// eslint-disable-next-line no-unused-vars +export default fp(async (fastify, _opts) => { + // eslint-disable-next-line arrow-body-style + fastify.decorate('someSupport', () => { + return 'hugs'; + }); +}); diff --git a/static/configvis/butler.png b/static/configvis/butler.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecc6944cc5deb31e3251b685d4a36755525529a GIT binary patch literal 33013 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%0A^oTo`$nN%fhPi9 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 cCJL0McbA#IyUp%O_d)OQKI&H&jB=ty_P z8Hm{1{2>@yZ=jS=)t9pD0?jj9_{v5ETq%~apOLyWI}sYMI7P=2h50zg9JNz3rO zlQw#cOz)+lldAQADM0ZkaI(3H?(Sn;j{(De;W08t(GUz&nbqY;8lCQa6wo0Rq|aH# zhZSlPA≧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 i7+e3Z2M0{WWnXkb8v#9Xs`h8VpRA \ No newline at end of file diff --git a/static/configvis/index.html b/static/configvis/index.html new file mode 100644 index 00000000..7aba1a49 --- /dev/null +++ b/static/configvis/index.html @@ -0,0 +1,245 @@ + + + + + Ctrl-Q + + + + + + + + + + + + + + + + +
+

YAML configuration

+
{{butlerConfigYaml}}
+
+ +
+

JSON tree view

+
+ Your HTML. +
+
+ + + + diff --git a/static/configvis/jsontree.js b/static/configvis/jsontree.js new file mode 100644 index 00000000..cc09af64 --- /dev/null +++ b/static/configvis/jsontree.js @@ -0,0 +1,927 @@ +'use strict'; + +var Is; + +((e) => { + function t(e) { + return e !== null && e !== void 0 && e.toString() !== ''; + } + e.defined = t; + function n(e) { + return t(e) && typeof e === 'object'; + } + e.definedObject = n; + function r(e) { + return t(e) && typeof e === 'boolean'; + } + e.definedBoolean = r; + function o(e) { + return t(e) && typeof e === 'string'; + } + e.definedString = o; + function l(e) { + return t(e) && typeof e === 'function'; + } + e.definedFunction = l; + function a(e) { + return t(e) && typeof e === 'number'; + } + e.definedNumber = a; + function i(e) { + return n(e) && e instanceof Array; + } + e.definedArray = i; + function s(e) { + return n(e) && e instanceof Date; + } + e.definedDate = s; + function u(e) { + return t(e) && typeof e === 'number' && e % 1 !== 0; + } + e.definedDecimal = u; + function c(e, t = 1) { + return !i(e) || e.length < t; + } + e.invalidOptionArray = c; + function d(e) { + let t = e.length >= 2 && e.length <= 7; + if (t && e[0] === '#') { + t = isNaN(+e.substring(1, e.length - 1)); + } + return t; + } + e.hexColor = d; +})(Is || (Is = {})); + +var Default; + +((e) => { + function t(e, t) { + return typeof e === 'string' ? e : t; + } + e.getAnyString = t; + function n(e, t) { + return Is.definedString(e) ? e : t; + } + e.getString = n; + function r(e, t) { + return Is.definedBoolean(e) ? e : t; + } + e.getBoolean = r; + function o(e, t) { + return Is.definedNumber(e) ? e : t; + } + e.getNumber = o; + function l(e, t) { + return Is.definedFunction(e) ? e : t; + } + e.getFunction = l; + function a(e, t) { + return Is.definedArray(e) ? e : t; + } + e.getArray = a; + function i(e, t) { + return Is.definedObject(e) ? e : t; + } + e.getObject = i; + function s(e, t) { + let n = t; + if (Is.definedString(e)) { + const r = e.toString().split(' '); + if (r.length === 0) { + e = t; + } else { + n = r; + } + } else { + n = a(e, t); + } + return n; + } + e.getStringOrArray = s; + function u(e, t) { + var n; + const r = new RegExp(`^-?\\d+(?:.\\d{0,${t || -1}})?`); + return ((n = e.toString().match(r)) == null ? void 0 : n[0]) || ''; + } + e.getFixedDecimalPlacesValue = u; + function c(e) { + let t; + const n = e.toString().split('('); + const r = n[0].split(' '); + if (r.length === 2) { + t = r[1]; + } else { + t = r[0]; + } + t += '()'; + return t; + } + e.getFunctionName = c; +})(Default || (Default = {})); + +var DomElement; + +((e) => { + function t(e, t, n = '', r = null) { + const o = t.toLowerCase(); + const l = o === 'text'; + let a = l ? document.createTextNode('') : document.createElement(o); + if (Is.defined(n)) { + a.className = n; + } + if (Is.defined(r)) { + e.insertBefore(a, r); + } else { + e.appendChild(a); + } + return a; + } + e.create = t; + function n(e, n, r, o, l = null) { + const a = t(e, n, r, l); + a.innerHTML = o; + return a; + } + e.createWithHTML = n; + function r(e, t) { + e.classList.add(t); + } + e.addClass = r; +})(DomElement || (DomElement = {})); + +var Str; + +((e) => { + function t() { + const e = []; + for (let t = 0; t < 32; t++) { + if (t === 8 || t === 12 || t === 16 || t === 20) { + e.push('-'); + } + const n = Math.floor(Math.random() * 16).toString(16); + e.push(n); + } + return e.join(''); + } + e.newGuid = t; + function n(e, t = 1) { + const n = e.toString(); + let r = n; + if (n.length < t) { + const e = t - n.length + 1; + r = Array(e).join('0') + n; + } + return r; + } + e.padNumber = n; +})(Str || (Str = {})); + +var DateTime; + +((e) => { + function t(e) { + return e.getDay() - 1 < 0 ? 6 : e.getDay() - 1; + } + e.getWeekdayNumber = t; + function n(e, t) { + let n = e.text.thText; + if (t === 31 || t === 21 || t === 1) { + n = e.text.stText; + } else if (t === 22 || t === 2) { + n = e.text.ndText; + } else if (t === 23 || t === 3) { + n = e.text.rdText; + } + return n; + } + e.getDayOrdinal = n; + function r(e, r, o) { + let l = o; + const a = t(r); + l = l.replace('{hh}', Str.padNumber(r.getHours(), 2)); + l = l.replace('{h}', r.getHours().toString()); + l = l.replace('{MM}', Str.padNumber(r.getMinutes(), 2)); + l = l.replace('{M}', r.getMinutes().toString()); + l = l.replace('{ss}', Str.padNumber(r.getSeconds(), 2)); + l = l.replace('{s}', r.getSeconds().toString()); + l = l.replace('{dddd}', e.text.dayNames[a]); + l = l.replace('{ddd}', e.text.dayNamesAbbreviated[a]); + l = l.replace('{dd}', Str.padNumber(r.getDate())); + l = l.replace('{d}', r.getDate().toString()); + l = l.replace('{o}', n(e, r.getDate())); + l = l.replace('{mmmm}', e.text.monthNames[r.getMonth()]); + l = l.replace('{mmm}', e.text.monthNamesAbbreviated[r.getMonth()]); + l = l.replace('{mm}', Str.padNumber(r.getMonth() + 1)); + l = l.replace('{m}', (r.getMonth() + 1).toString()); + l = l.replace('{yyyy}', r.getFullYear().toString()); + l = l.replace('{yyy}', r.getFullYear().toString().substring(1)); + l = l.replace('{yy}', r.getFullYear().toString().substring(2)); + l = l.replace('{y}', Number.parseInt(r.getFullYear().toString().substring(2)).toString()); + return l; + } + e.getCustomFormattedDateText = r; +})(DateTime || (DateTime = {})); + +var Constants; + +((e) => { + e.JSONTREE_JS_ATTRIBUTE_NAME = 'data-jsontree-js'; +})(Constants || (Constants = {})); + +var Binding; + +((e) => { + let t; + ((t) => { + function n(t, n) { + const r = e.Options.get(t); + r._currentView = {}; + r._currentView.element = n; + r._currentView.dataArrayCurrentIndex = 0; + return r; + } + t.getForNewInstance = n; + function r(e) { + let t = Default.getObject(e, {}); + t.data = Default.getObject(t.data, null); + t.showCounts = Default.getBoolean(t.showCounts, true); + t.useZeroIndexingForArrays = Default.getBoolean(t.useZeroIndexingForArrays, true); + t.dateTimeFormat = Default.getString(t.dateTimeFormat, '{dd}{o} {mmmm} {yyyy} {hh}:{MM}:{ss}'); + t.showArrowToggles = Default.getBoolean(t.showArrowToggles, true); + t.showStringQuotes = Default.getBoolean(t.showStringQuotes, true); + t.showAllAsClosed = Default.getBoolean(t.showAllAsClosed, false); + t.sortPropertyNames = Default.getBoolean(t.sortPropertyNames, true); + t.sortPropertyNamesInAlphabeticalOrder = Default.getBoolean(t.sortPropertyNamesInAlphabeticalOrder, true); + t.showCommas = Default.getBoolean(t.showCommas, false); + t.reverseArrayValues = Default.getBoolean(t.reverseArrayValues, false); + t.addArrayIndexPadding = Default.getBoolean(t.addArrayIndexPadding, false); + t.showValueColors = Default.getBoolean(t.showValueColors, true); + t.maximumDecimalPlaces = Default.getNumber(t.maximumDecimalPlaces, 2); + t.maximumStringLength = Default.getNumber(t.maximumStringLength, 0); + t.showStringHexColors = Default.getBoolean(t.showStringHexColors, false); + t.showArrayItemsAsSeparateObjects = Default.getBoolean(t.showArrayItemsAsSeparateObjects, false); + t.copyOnlyCurrentPage = Default.getBoolean(t.copyOnlyCurrentPage, false); + t = o(t); + t = l(t); + t = a(t); + return t; + } + t.get = r; + function o(e) { + e.title = Default.getObject(e.title, {}); + e.title.text = Default.getString(e.title.text, 'JsonTree.js'); + e.title.show = Default.getBoolean(e.title.show, true); + e.title.showTreeControls = Default.getBoolean(e.title.showTreeControls, true); + e.title.showCopyButton = Default.getBoolean(e.title.showCopyButton, true); + return e; + } + function l(e) { + e.ignore = Default.getObject(e.ignore, {}); + e.ignore.nullValues = Default.getBoolean(e.ignore.nullValues, false); + e.ignore.functionValues = Default.getBoolean(e.ignore.functionValues, false); + e.ignore.unknownValues = Default.getBoolean(e.ignore.unknownValues, false); + e.ignore.booleanValues = Default.getBoolean(e.ignore.booleanValues, false); + e.ignore.decimalValues = Default.getBoolean(e.ignore.decimalValues, false); + e.ignore.numberValues = Default.getBoolean(e.ignore.numberValues, false); + e.ignore.stringValues = Default.getBoolean(e.ignore.stringValues, false); + e.ignore.dateValues = Default.getBoolean(e.ignore.dateValues, false); + e.ignore.objectValues = Default.getBoolean(e.ignore.objectValues, false); + e.ignore.arrayValues = Default.getBoolean(e.ignore.arrayValues, false); + return e; + } + function a(e) { + e.events = Default.getObject(e.events, {}); + e.events.onBeforeRender = Default.getFunction(e.events.onBeforeRender, null); + e.events.onRenderComplete = Default.getFunction(e.events.onRenderComplete, null); + e.events.onValueClick = Default.getFunction(e.events.onValueClick, null); + e.events.onRefresh = Default.getFunction(e.events.onRefresh, null); + e.events.onCopyAll = Default.getFunction(e.events.onCopyAll, null); + e.events.onOpenAll = Default.getFunction(e.events.onOpenAll, null); + e.events.onCloseAll = Default.getFunction(e.events.onCloseAll, null); + e.events.onDestroy = Default.getFunction(e.events.onDestroy, null); + e.events.onBooleanRender = Default.getFunction(e.events.onBooleanRender, null); + e.events.onDecimalRender = Default.getFunction(e.events.onDecimalRender, null); + e.events.onNumberRender = Default.getFunction(e.events.onNumberRender, null); + e.events.onStringRender = Default.getFunction(e.events.onStringRender, null); + e.events.onDateRender = Default.getFunction(e.events.onDateRender, null); + e.events.onFunctionRender = Default.getFunction(e.events.onFunctionRender, null); + e.events.onNullRender = Default.getFunction(e.events.onNullRender, null); + e.events.onUnknownRender = Default.getFunction(e.events.onUnknownRender, null); + return e; + } + })((t = e.Options || (e.Options = {}))); +})(Binding || (Binding = {})); + +var Config; + +((e) => { + let t; + ((e) => { + function t(e = null) { + let t = Default.getObject(e, {}); + t.safeMode = Default.getBoolean(t.safeMode, true); + t.domElementTypes = Default.getStringOrArray(t.domElementTypes, ['*']); + t = n(t); + return t; + } + e.get = t; + function n(e) { + e.text = Default.getObject(e.text, {}); + e.text.objectText = Default.getAnyString(e.text.objectText, 'object'); + e.text.arrayText = Default.getAnyString(e.text.arrayText, 'array'); + e.text.closeAllButtonText = Default.getAnyString(e.text.closeAllButtonText, 'Close All'); + e.text.openAllButtonText = Default.getAnyString(e.text.openAllButtonText, 'Open All'); + e.text.copyAllButtonText = Default.getAnyString(e.text.copyAllButtonText, 'Copy All'); + e.text.objectErrorText = Default.getAnyString(e.text.objectErrorText, 'Errors in object: {{error_1}}, {{error_2}}'); + e.text.attributeNotValidErrorText = Default.getAnyString( + e.text.attributeNotValidErrorText, + "The attribute '{{attribute_name}}' is not a valid object.", + ); + e.text.attributeNotSetErrorText = Default.getAnyString( + e.text.attributeNotSetErrorText, + "The attribute '{{attribute_name}}' has not been set correctly.", + ); + e.text.stText = Default.getAnyString(e.text.stText, 'st'); + e.text.ndText = Default.getAnyString(e.text.ndText, 'nd'); + e.text.rdText = Default.getAnyString(e.text.rdText, 'rd'); + e.text.thText = Default.getAnyString(e.text.thText, 'th'); + e.text.ellipsisText = Default.getAnyString(e.text.ellipsisText, '...'); + e.text.closeAllButtonSymbolText = Default.getAnyString(e.text.closeAllButtonSymbolText, '↑'); + e.text.openAllButtonSymbolText = Default.getAnyString(e.text.openAllButtonSymbolText, '↓'); + e.text.copyAllButtonSymbolText = Default.getAnyString(e.text.copyAllButtonSymbolText, '❐'); + e.text.backButtonText = Default.getAnyString(e.text.backButtonText, 'Back'); + e.text.nextButtonText = Default.getAnyString(e.text.nextButtonText, 'Next'); + e.text.backButtonSymbolText = Default.getAnyString(e.text.backButtonSymbolText, '←'); + e.text.nextButtonSymbolText = Default.getAnyString(e.text.nextButtonSymbolText, '→'); + if (Is.invalidOptionArray(e.text.dayNames, 7)) { + e.text.dayNames = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + } + if (Is.invalidOptionArray(e.text.dayNamesAbbreviated, 7)) { + e.text.dayNamesAbbreviated = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + } + if (Is.invalidOptionArray(e.text.monthNames, 12)) { + e.text.monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + } + if (Is.invalidOptionArray(e.text.monthNamesAbbreviated, 12)) { + e.text.monthNamesAbbreviated = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + } + return e; + } + })((t = e.Options || (e.Options = {}))); +})(Config || (Config = {})); + +var Trigger; + +((e) => { + function t(e, ...t) { + let n = null; + if (Is.definedFunction(e)) { + n = e.apply(null, [].slice.call(t, 0)); + } + return n; + } + e.customEvent = t; +})(Trigger || (Trigger = {})); + +(() => { + let _configuration = {}; + let _elements_Data = {}; + function render() { + const e = _configuration.domElementTypes; + const t = e.length; + for (let n = 0; n < t; n++) { + const t = document.getElementsByTagName(e[n]); + const r = [].slice.call(t); + const o = r.length; + for (let e = 0; e < o; e++) { + if (!renderElement(r[e])) { + break; + } + } + } + } + function renderElement(e) { + let t = true; + if (Is.defined(e) && e.hasAttribute(Constants.JSONTREE_JS_ATTRIBUTE_NAME)) { + const n = e.getAttribute(Constants.JSONTREE_JS_ATTRIBUTE_NAME); + if (Is.definedString(n)) { + const r = getObjectFromString(n); + if (r.parsed && Is.definedObject(r.object)) { + renderControl(Binding.Options.getForNewInstance(r.object, e)); + } else { + if (!_configuration.safeMode) { + console.error( + _configuration.text.attributeNotValidErrorText.replace( + '{{attribute_name}}', + Constants.JSONTREE_JS_ATTRIBUTE_NAME, + ), + ); + t = false; + } + } + } else { + if (!_configuration.safeMode) { + console.error( + _configuration.text.attributeNotSetErrorText.replace('{{attribute_name}}', Constants.JSONTREE_JS_ATTRIBUTE_NAME), + ); + t = false; + } + } + } + return t; + } + function renderControl(e) { + Trigger.customEvent(e.events.onBeforeRender, e._currentView.element); + if (!Is.definedString(e._currentView.element.id)) { + e._currentView.element.id = Str.newGuid(); + } + e._currentView.element.className = 'json-tree-js'; + e._currentView.element.removeAttribute(Constants.JSONTREE_JS_ATTRIBUTE_NAME); + if (!_elements_Data.hasOwnProperty(e._currentView.element.id)) { + _elements_Data[e._currentView.element.id] = e; + } + renderControlContainer(e); + Trigger.customEvent(e.events.onRenderComplete, e._currentView.element); + } + function renderControlContainer(e) { + let t = _elements_Data[e._currentView.element.id].data; + e._currentView.element.innerHTML = ''; + renderControlTitleBar(e, t); + if (e.showArrayItemsAsSeparateObjects) { + t = t[e._currentView.dataArrayCurrentIndex]; + } + if (Is.definedObject(t) && !Is.definedArray(t)) { + renderObject(e._currentView.element, e, t, true); + } else if (Is.definedArray(t)) { + renderArray(e._currentView.element, e, t); + } + } + function renderControlTitleBar(e, t) { + if (e.title.show || e.title.showTreeControls || e.title.showCopyButton) { + const n = DomElement.create(e._currentView.element, 'div', 'title-bar'); + const r = DomElement.create(n, 'div', 'controls'); + if (e.title.show) { + DomElement.createWithHTML(n, 'div', 'title', e.title.text, r); + } + if (e.title.showCopyButton) { + const t = DomElement.createWithHTML(r, 'button', 'copy-all', _configuration.text.copyAllButtonSymbolText); + t.title = _configuration.text.copyAllButtonText; + t.onclick = () => { + let t = null; + if (e.copyOnlyCurrentPage && e.showArrayItemsAsSeparateObjects) { + t = JSON.stringify(_elements_Data[e._currentView.element.id].data[e._currentView.dataArrayCurrentIndex], null, 2); + } else { + t = JSON.stringify(_elements_Data[e._currentView.element.id].data, null, 2); + } + navigator.clipboard.writeText(t); + Trigger.customEvent(e.events.onCopyAll, t); + }; + } + if (e.title.showTreeControls) { + const t = DomElement.createWithHTML(r, 'button', 'openAll', _configuration.text.openAllButtonSymbolText); + t.title = _configuration.text.openAllButtonText; + const n = DomElement.createWithHTML(r, 'button', 'closeAll', _configuration.text.closeAllButtonSymbolText); + n.title = _configuration.text.closeAllButtonText; + t.onclick = () => { + openAllNodes(e); + }; + n.onclick = () => { + closeAllNodes(e); + }; + } + if (e.showArrayItemsAsSeparateObjects && Is.definedArray(t) && t.length > 1) { + const n = DomElement.createWithHTML(r, 'button', 'back', _configuration.text.backButtonSymbolText); + n.title = _configuration.text.backButtonText; + if (e._currentView.dataArrayCurrentIndex > 0) { + n.onclick = () => { + e._currentView.dataArrayCurrentIndex--; + renderControlContainer(e); + }; + } else { + n.disabled = true; + } + const o = DomElement.createWithHTML(r, 'button', 'next', _configuration.text.nextButtonSymbolText); + o.title = _configuration.text.nextButtonText; + if (e._currentView.dataArrayCurrentIndex < t.length - 1) { + o.onclick = () => { + e._currentView.dataArrayCurrentIndex++; + renderControlContainer(e); + }; + } else { + o.disabled = true; + } + } else { + e.showArrayItemsAsSeparateObjects = false; + } + } + } + function openAllNodes(e) { + e.showAllAsClosed = false; + renderControlContainer(e); + Trigger.customEvent(e.events.onOpenAll, e._currentView.element); + } + function closeAllNodes(e) { + e.showAllAsClosed = true; + renderControlContainer(e); + Trigger.customEvent(e.events.onCloseAll, e._currentView.element); + } + function renderObject(e, t, n, r = false) { + const o = DomElement.create(e, 'div', 'object-type-title'); + const l = DomElement.create(e, 'div', 'object-type-contents'); + const a = t.showArrowToggles ? DomElement.create(o, 'div', 'down-arrow') : null; + const i = renderObjectValues(a, l, t, n); + const s = DomElement.createWithHTML(o, 'span', t.showValueColors ? 'object' : '', _configuration.text.objectText); + if (r && t.showArrayItemsAsSeparateObjects) { + let e = t.useZeroIndexingForArrays + ? t._currentView.dataArrayCurrentIndex.toString() + : (t._currentView.dataArrayCurrentIndex + 1).toString(); + DomElement.createWithHTML(o, 'span', t.showValueColors ? 'object data-array-index' : 'data-array-index', `[${e}]:`, s); + } + if (t.showCounts && i > 0) { + DomElement.createWithHTML(o, 'span', t.showValueColors ? 'object count' : 'count', `{${i}}`); + } + } + function renderArray(e, t, n) { + const r = DomElement.create(e, 'div', 'object-type-title'); + const o = DomElement.create(e, 'div', 'object-type-contents'); + const l = t.showArrowToggles ? DomElement.create(r, 'div', 'down-arrow') : null; + DomElement.createWithHTML(r, 'span', t.showValueColors ? 'array' : '', _configuration.text.arrayText); + renderArrayValues(l, o, t, n); + if (t.showCounts) { + DomElement.createWithHTML(r, 'span', t.showValueColors ? 'array count' : 'count', `[${n.length}]`); + } + } + function renderObjectValues(e, t, n, r) { + let o = 0; + let l = []; + for (let e in r) { + if (r.hasOwnProperty(e)) { + l.push(e); + } + } + if (n.sortPropertyNames) { + l = l.sort(); + if (!n.sortPropertyNamesInAlphabeticalOrder) { + l = l.reverse(); + } + } + const a = l.length; + for (let e = 0; e < a; e++) { + const i = l[e]; + if (r.hasOwnProperty(i)) { + renderValue(t, n, i, r[i], e === a - 1); + o++; + } + } + addArrowEvent(n, e, t); + return o; + } + function renderArrayValues(e, t, n, r) { + const o = r.length; + if (!n.reverseArrayValues) { + for (let e = 0; e < o; e++) { + renderValue(t, n, getIndexName(n, e, o), r[e], e === o - 1); + } + } else { + for (let e = o; e--; ) { + renderValue(t, n, getIndexName(n, e, o), r[e], e === 0); + } + } + addArrowEvent(n, e, t); + } + function renderValue(e, t, n, r, o) { + const l = DomElement.create(e, 'div', 'object-type-value'); + const a = t.showArrowToggles ? DomElement.create(l, 'div', 'no-arrow') : null; + let i = null; + let s = null; + let u = false; + let c = null; + let d = true; + DomElement.createWithHTML(l, 'span', 'title', n); + DomElement.createWithHTML(l, 'span', 'split', ':'); + if (!Is.defined(r)) { + if (!t.ignore.nullValues) { + i = t.showValueColors ? 'null' : ''; + s = DomElement.createWithHTML(l, 'span', i, 'null'); + d = false; + if (Is.definedFunction(t.events.onNullRender)) { + Trigger.customEvent(t.events.onNullRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedFunction(r)) { + if (!t.ignore.functionValues) { + i = t.showValueColors ? 'function' : ''; + s = DomElement.createWithHTML(l, 'span', i, Default.getFunctionName(r)); + c = 'function'; + if (Is.definedFunction(t.events.onFunctionRender)) { + Trigger.customEvent(t.events.onFunctionRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedBoolean(r)) { + if (!t.ignore.booleanValues) { + i = t.showValueColors ? 'boolean' : ''; + s = DomElement.createWithHTML(l, 'span', i, r); + c = 'boolean'; + if (Is.definedFunction(t.events.onBooleanRender)) { + Trigger.customEvent(t.events.onBooleanRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedDecimal(r)) { + if (!t.ignore.decimalValues) { + const e = Default.getFixedDecimalPlacesValue(r, t.maximumDecimalPlaces); + i = t.showValueColors ? 'decimal' : ''; + s = DomElement.createWithHTML(l, 'span', i, e); + c = 'decimal'; + if (Is.definedFunction(t.events.onDecimalRender)) { + Trigger.customEvent(t.events.onDecimalRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedNumber(r)) { + if (!t.ignore.numberValues) { + i = t.showValueColors ? 'number' : ''; + s = DomElement.createWithHTML(l, 'span', i, r); + c = 'number'; + if (Is.definedFunction(t.events.onNumberRender)) { + Trigger.customEvent(t.events.onNumberRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedString(r)) { + if (!t.ignore.stringValues) { + let e = null; + if (t.showValueColors && t.showStringHexColors && Is.hexColor(r)) { + e = r; + } else { + if (t.maximumStringLength > 0 && r.length > t.maximumStringLength) { + r = r.substring(0, t.maximumStringLength) + _configuration.text.ellipsisText; + } + } + const n = t.showStringQuotes ? `"${r}"` : r; + i = t.showValueColors ? 'string' : ''; + s = DomElement.createWithHTML(l, 'span', i, n); + c = 'string'; + if (Is.definedString(e)) { + s.style.color = e; + } + if (Is.definedFunction(t.events.onStringRender)) { + Trigger.customEvent(t.events.onStringRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedDate(r)) { + if (!t.ignore.dateValues) { + i = t.showValueColors ? 'date' : ''; + s = DomElement.createWithHTML(l, 'span', i, DateTime.getCustomFormattedDateText(_configuration, r, t.dateTimeFormat)); + c = 'date'; + if (Is.definedFunction(t.events.onDateRender)) { + Trigger.customEvent(t.events.onDateRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } else if (Is.definedObject(r) && !Is.definedArray(r)) { + if (!t.ignore.objectValues) { + const e = DomElement.create(l, 'span', t.showValueColors ? 'object' : ''); + const n = DomElement.create(l, 'div', 'object-type-contents'); + const i = renderObjectValues(a, n, t, r); + DomElement.createWithHTML(e, 'span', 'title', _configuration.text.objectText); + if (t.showCounts && i > 0) { + DomElement.createWithHTML(e, 'span', 'count', `{${i}}`); + } + createComma(t, e, o); + c = 'object'; + } else { + u = true; + } + } else if (Is.definedArray(r)) { + if (!t.ignore.arrayValues) { + const e = DomElement.create(l, 'span', t.showValueColors ? 'array' : ''); + const n = DomElement.create(l, 'div', 'object-type-contents'); + DomElement.createWithHTML(e, 'span', 'title', _configuration.text.arrayText); + if (t.showCounts) { + DomElement.createWithHTML(e, 'span', 'count', `[${r.length}]`); + } + createComma(t, e, o); + renderArrayValues(a, n, t, r); + c = 'array'; + } else { + u = true; + } + } else { + if (!t.ignore.unknownValues) { + i = t.showValueColors ? 'unknown' : ''; + s = DomElement.createWithHTML(l, 'span', i, r.toString()); + c = 'unknown'; + if (Is.definedFunction(t.events.onUnknownRender)) { + Trigger.customEvent(t.events.onUnknownRender, s); + } + createComma(t, l, o); + } else { + u = true; + } + } + if (u) { + e.removeChild(l); + } else { + if (Is.defined(s)) { + addValueClickEvent(t, s, r, c, d); + } + } + } + function addValueClickEvent(e, t, n, r, o) { + if (o && Is.definedFunction(e.events.onValueClick)) { + t.onclick = () => { + Trigger.customEvent(e.events.onValueClick, n, r); + }; + } else { + DomElement.addClass(t, 'no-hover'); + } + } + function addArrowEvent(e, t, n) { + if (Is.defined(t)) { + t.onclick = () => { + if (t.className === 'down-arrow') { + n.style.display = 'none'; + t.className = 'right-arrow'; + } else { + n.style.display = 'block'; + t.className = 'down-arrow'; + } + }; + if (e.showAllAsClosed) { + n.style.display = 'none'; + t.className = 'right-arrow'; + } else { + t.className = 'down-arrow'; + } + } + } + function createComma(e, t, n) { + if (e.showCommas && !n) { + DomElement.createWithHTML(t, 'span', 'comma', ','); + } + } + function getIndexName(e, t, n) { + let r = e.useZeroIndexingForArrays ? t.toString() : (t + 1).toString(); + if (!e.addArrayIndexPadding) { + r = Str.padNumber(parseInt(r), n.toString().length); + } + return `[${r}]`; + } + function getObjectFromString(objectString) { + const result = { + parsed: true, + object: null, + }; + try { + if (Is.definedString(objectString)) { + result.object = JSON.parse(objectString); + } + } catch (e1) { + try { + result.object = eval(`(${objectString})`); + if (Is.definedFunction(result.object)) { + result.object = result.object(); + } + } catch (e) { + if (!_configuration.safeMode) { + console.error(_configuration.text.objectErrorText.replace('{{error_1}}', e1.message).replace('{{error_2}}', e.message)); + result.parsed = false; + } + result.object = null; + } + } + return result; + } + function destroyElement(e) { + e._currentView.element.innerHTML = ''; + e._currentView.element.className = ''; + Trigger.customEvent(e.events.onDestroy, e._currentView.element); + } + const _public = { + refresh: function (e) { + if (Is.definedString(e) && _elements_Data.hasOwnProperty(e)) { + const t = _elements_Data[e]; + renderControlContainer(t); + Trigger.customEvent(t.events.onRefresh, t._currentView.element); + } + return _public; + }, + refreshAll: function () { + for (let e in _elements_Data) { + if (_elements_Data.hasOwnProperty(e)) { + const t = _elements_Data[e]; + renderControlContainer(t); + Trigger.customEvent(t.events.onRefresh, t._currentView.element); + } + } + return _public; + }, + render: function (e, t) { + if (Is.definedObject(e) && Is.definedObject(t)) { + renderControl(Binding.Options.getForNewInstance(t, e)); + } + return _public; + }, + renderAll: function () { + render(); + return _public; + }, + openAll: function (e) { + if (Is.definedString(e) && _elements_Data.hasOwnProperty(e)) { + openAllNodes(_elements_Data[e]); + } + return _public; + }, + closeAll: function (e) { + if (Is.definedString(e) && _elements_Data.hasOwnProperty(e)) { + closeAllNodes(_elements_Data[e]); + } + return _public; + }, + destroy: function (e) { + if (Is.definedString(e) && _elements_Data.hasOwnProperty(e)) { + destroyElement(_elements_Data[e]); + delete _elements_Data[e]; + } + return _public; + }, + destroyAll: function () { + for (let e in _elements_Data) { + if (_elements_Data.hasOwnProperty(e)) { + destroyElement(_elements_Data[e]); + } + } + _elements_Data = {}; + return _public; + }, + setConfiguration: function (e) { + if (Is.definedObject(e)) { + let t = false; + const n = _configuration; + for (let r in e) { + if (e.hasOwnProperty(r) && _configuration.hasOwnProperty(r) && n[r] !== e[r]) { + n[r] = e[r]; + t = true; + } + } + if (t) { + _configuration = Config.Options.get(n); + } + } + return _public; + }, + getIds: function () { + const e = []; + for (let t in _elements_Data) { + if (_elements_Data.hasOwnProperty(t)) { + e.push(t); + } + } + return e; + }, + getVersion: function () { + return '2.1.0'; + }, + }; + (() => { + _configuration = Config.Options.get(); + document.addEventListener('DOMContentLoaded', function () { + render(); + }); + if (!Is.defined(window.$jsontree)) { + window.$jsontree = _public; + } + })(); +})(); //# sourceMappingURL=jsontree.js.map diff --git a/static/configvis/jsontree.js.css b/static/configvis/jsontree.js.css new file mode 100644 index 00000000..f3d4d3d4 --- /dev/null +++ b/static/configvis/jsontree.js.css @@ -0,0 +1,330 @@ +/* + * JsonTree.js Library v2.1.0 + * + * Copyright 2024 Bunoon + * Released under the MIT License + */ +:root { + --json-tree-js-default-font: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + --json-tree-js-text-bold-weight: 400; + --json-tree-js-header-bold-weight: 900; + --json-tree-js-title-bold-weight: var(--json-tree-js-header-bold-weight); + --json-tree-js-text-bold-weight-active: var(--json-tree-js-header-bold-weight); + --json-tree-js-color-black: #3b3a3a; + --json-tree-js-color-white: #f5f5f5; + --json-tree-js-color-snow-white: #f5f5f5; + --json-tree-js-color-boolean: #ff0000; + --json-tree-js-color-decimal: #e3c868; + --json-tree-js-color-number: #666bf9; + --json-tree-js-color-string: #78b13f; + --json-tree-js-color-date: #a656f5; + --json-tree-js-color-array: #f28c28; + --json-tree-js-color-object: #c0c0c0; + --json-tree-js-color-null: #bbbbbb; + --json-tree-js-color-function: var(--json-tree-js-color-null); + --json-tree-js-color-unknown: var(--json-tree-js-color-null); + --json-tree-js-container-background-color: #22272e; + --json-tree-js-container-border-color: #454c56; + --json-tree-js-button-background-color: #2d333b; + --json-tree-js-button-border-color: var(--json-tree-js-container-border-color); + --json-tree-js-button-text-color: var(--json-tree-js-color-white); + --json-tree-js-button-background-color-hover: var(--json-tree-js-container-border-color); + --json-tree-js-button-text-color-hover: var(--json-tree-js-color-snow-white); + --json-tree-js-button-background-color-active: #616b79; + --json-tree-js-button-text-color-active: var(--json-tree-js-color-snow-white); + --json-tree-js-border-radius: 0.5rem; + --json-tree-js-border-style-scrollbar: inset 0 0 6px var(--json-tree-js-color-dark-gray); + --json-tree-js-border-size: 0.5px; + --json-tree-js-spacing: 10px; + --json-tree-js-spacing-font-size: 0.85rem; + --json-tree-js-transition: all 0.3s; +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Container + ------------------------------------------------------------------------- +*/ +div.json-tree-js { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + cursor: default; + box-sizing: border-box; + line-height: normal; + font-family: var(--json-tree-js-default-font) !important; + display: inline-block; + position: relative; + border-radius: var(--json-tree-js-border-radius); + background-color: var(--json-tree-js-container-background-color); + color: var(--json-tree-js-color-white); + border: var(--json-tree-js-border-size) solid var(--json-tree-js-container-border-color); + padding: var(--json-tree-js-spacing); + font-size: var(--json-tree-js-spacing-font-size); + font-weight: var(--json-tree-js-text-bold-weight); + width: auto; + overflow: hidden; + margin: 0 !important; +} +div.json-tree-js button { + font-family: var(--heat-js-default-font); +} +div.json-tree-js div.no-click { + pointer-events: none !important; +} +div.json-tree-js * { + box-sizing: border-box; + line-height: normal; +} +div.json-tree-js *::before, +div.json-tree-js *::after { + box-sizing: border-box; + line-height: normal; +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Arrows + ------------------------------------------------------------------------- +*/ +div.json-tree-js div.no-arrow { + display: inline-block; + width: 12px; + height: 8px; + margin-right: calc(var(--json-tree-js-spacing)); +} +div.json-tree-js div.down-arrow, +div.json-tree-js div.right-arrow { + display: inline-block; + width: 0; + height: 0; + margin-right: calc(var(--json-tree-js-spacing)); + cursor: pointer; + transition: var(--json-tree-js-transition); + transition-property: opacity; +} +div.json-tree-js div.down-arrow:hover, +div.json-tree-js div.right-arrow:hover { + opacity: 0.7; +} +div.json-tree-js div.down-arrow { + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 8px solid var(--json-tree-js-color-white); +} +div.json-tree-js div.right-arrow { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 12px solid var(--json-tree-js-color-white); +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Title Bar + ------------------------------------------------------------------------- +*/ +div.json-tree-js div.title-bar { + display: flex; + margin-bottom: var(--json-tree-js-spacing); +} +div.json-tree-js div.title-bar div.title { + text-align: left; + width: auto; + font-weight: var(--json-tree-js-title-bold-weight); + font-size: 1.2rem; +} +div.json-tree-js div.title-bar div.controls { + margin-left: calc(var(--json-tree-js-spacing) * 6); + flex-grow: 1; + text-align: right; +} +@media (min-width: 768px) { + div.json-tree-js div.title-bar div.controls { + margin-left: calc(var(--json-tree-js-spacing) * 12); + } +} +div.json-tree-js div.title-bar div.controls button { + background-color: var(--json-tree-js-button-background-color); + border: var(--json-tree-js-border-size) solid var(--json-tree-js-button-border-color); + color: var(--json-tree-js-button-text-color); + border-radius: var(--json-tree-js-border-radius); + padding-top: 5px; + padding-bottom: 5px; + padding-left: 9px; + padding-right: 9px; + outline: none; + transition: var(--json-tree-js-transition); +} +div.json-tree-js div.title-bar div.controls button:disabled { + color: var(--json-tree-js-button-border-color); +} +div.json-tree-js div.title-bar div.controls button:not(.active):not(:disabled):active { + background: var(--json-tree-js-button-background-color-active) !important; + color: var(--json-tree-js-button-text-color-active) !important; +} +div.json-tree-js div.title-bar div.controls button:not(.active):not(:disabled):hover { + cursor: pointer; + background: var(--json-tree-js-button-background-color-hover); + color: var(--json-tree-js-button-text-color-hover); +} +div.json-tree-js div.title-bar div.controls button { + margin-left: calc(var(--json-tree-js-spacing) / 2) !important; +} +div.json-tree-js div.title-bar div.controls button.back { + margin-left: calc(var(--json-tree-js-spacing) * 2) !important; +} +div.json-tree-js div.title-bar div.controls button.copy-all { + display: none; +} +@media (min-width: 768px) { + div.json-tree-js div.title-bar div.controls button.copy-all { + display: inline-block; + } +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Object Type Title + ------------------------------------------------------------------------- +*/ +div.json-tree-js div.object-type-title { + font-weight: var(--json-tree-js-header-bold-weight); + text-align: left !important; +} +div.json-tree-js div.object-type-title span.array { + color: var(--json-tree-js-color-array); +} +div.json-tree-js div.object-type-title span.object { + color: var(--json-tree-js-color-object); +} +div.json-tree-js div.object-type-title span.count { + margin-left: calc(var(--json-tree-js-spacing) / 2); + font-weight: var(--json-tree-js-text-bold-weight); +} +div.json-tree-js div.object-type-title span.data-array-index { + margin-right: calc(var(--json-tree-js-spacing) / 2); + font-weight: var(--json-tree-js-text-bold-weight); +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Object Type Contents + ------------------------------------------------------------------------- +*/ +div.json-tree-js div.object-type-contents { + margin-top: calc(var(--json-tree-js-spacing) / 2); +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Object Type Contents - Values + ------------------------------------------------------------------------- +*/ +div.json-tree-js div.object-type-contents { + margin-left: calc(var(--json-tree-js-spacing) * 2); + text-align: left !important; +} +div.json-tree-js div.object-type-contents div.object-type-value { + white-space: nowrap; + overflow: hidden; + margin-top: calc(var(--json-tree-js-spacing) / 2); + margin-bottom: calc(var(--json-tree-js-spacing) / 2); +} +div.json-tree-js div.object-type-contents div.object-type-value span.split { + margin-left: calc(var(--json-tree-js-spacing) / 2); + margin-right: calc(var(--json-tree-js-spacing) / 2); +} +div.json-tree-js div.object-type-contents div.object-type-value span.boolean, +div.json-tree-js div.object-type-contents div.object-type-value span.decimal, +div.json-tree-js div.object-type-contents div.object-type-value span.number, +div.json-tree-js div.object-type-contents div.object-type-value span.string, +div.json-tree-js div.object-type-contents div.object-type-value span.date, +div.json-tree-js div.object-type-contents div.object-type-value span.null, +div.json-tree-js div.object-type-contents div.object-type-value span.function, +div.json-tree-js div.object-type-contents div.object-type-value span.unknown { + transition: var(--json-tree-js-transition); + transition-property: opacity; +} +div.json-tree-js div.object-type-contents div.object-type-value span.boolean:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.decimal:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.number:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.string:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.date:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.null:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.function:not(.no-hover):hover, +div.json-tree-js div.object-type-contents div.object-type-value span.unknown:not(.no-hover):hover { + cursor: pointer; + opacity: 0.7; +} +div.json-tree-js div.object-type-contents div.object-type-value span.comma { + color: var(--json-tree-js-color-white); + font-weight: var(--json-tree-js-text-bold-weight); +} +div.json-tree-js div.object-type-contents div.object-type-value span.boolean { + color: var(--json-tree-js-color-boolean); +} +div.json-tree-js div.object-type-contents div.object-type-value span.decimal { + color: var(--json-tree-js-color-decimal); +} +div.json-tree-js div.object-type-contents div.object-type-value span.number { + color: var(--json-tree-js-color-number); +} +div.json-tree-js div.object-type-contents div.object-type-value span.string { + color: var(--json-tree-js-color-string); +} +div.json-tree-js div.object-type-contents div.object-type-value span.date { + color: var(--json-tree-js-color-date); +} +div.json-tree-js div.object-type-contents div.object-type-value span.array { + font-weight: var(--json-tree-js-header-bold-weight); + color: var(--json-tree-js-color-array); +} +div.json-tree-js div.object-type-contents div.object-type-value span.object { + font-weight: var(--json-tree-js-header-bold-weight); + color: var(--json-tree-js-color-object); +} +div.json-tree-js div.object-type-contents div.object-type-value span.null { + color: var(--json-tree-js-color-null); + font-style: italic; +} +div.json-tree-js div.object-type-contents div.object-type-value span.function { + color: var(--json-tree-js-color-function); + font-style: italic; +} +div.json-tree-js div.object-type-contents div.object-type-value span.unknown { + color: var(--json-tree-js-color-unknown); + font-style: italic; +} +div.json-tree-js div.object-type-contents div.object-type-value span.count { + margin-left: calc(var(--json-tree-js-spacing) / 2); + font-weight: var(--json-tree-js-text-bold-weight); +} + +/* + ------------------------------------------------------------------------- + JsonTree.js - Custom Scroll Bar + ------------------------------------------------------------------------- +*/ +.custom-scroll-bars::-webkit-scrollbar { + width: 12px; +} +.custom-scroll-bars::-webkit-scrollbar-track { + -webkit-box-shadow: var(--json-tree-js-border-style-scrollbar); + box-shadow: var(--json-tree-js-border-style-scrollbar); +} +.custom-scroll-bars::-webkit-scrollbar-thumb { + -webkit-box-shadow: var(--json-tree-js-border-style-scrollbar); + box-shadow: var(--json-tree-js-border-style-scrollbar); + background: var(--json-tree-js-color-white); +} +.custom-scroll-bars::-webkit-scrollbar-thumb:hover { + background-color: var(--json-tree-js-color-white); +} +.custom-scroll-bars::-webkit-scrollbar-thumb:active { + background-color: var(--json-tree-js-color-lighter-gray); +} + +/*# sourceMappingURL=jsontree.js.css.map */ diff --git a/static/configvis/jsontree.js.css.map b/static/configvis/jsontree.js.css.map new file mode 100644 index 00000000..782f92e0 --- /dev/null +++ b/static/configvis/jsontree.js.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../src/jsontree.js.scss","../src/sass/_styles.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA;EAEI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAcA;EACA;EACA;EACA;EAGA;EACA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EACA;EAGA;EACA;EACA;EAGA;EACA;EAGA;;;AAIJ;AAAA;AAAA;AAAA;AAAA;AAMA;EC9EI;EACA;EACA;EACA;EACA;EACA;EAUA;EACA;EDiEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;ECxFA;EACA;;AD0FI;EC3FJ;EACA;;;ADkGJ;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;EACA;EACA;EACA;;AAGJ;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;EACI;;AAIR;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;;AAKR;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAEA;EALJ;IAMQ;;;ACjKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AD8IA;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAEA;EAHJ;IAIQ;;;;AAQpB;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;EACA;;AAEA;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;;AAKZ;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;;;AAKR;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQI;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;EACA;;AAIR;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;;;AAOhB;AAAA;AAAA;AAAA;AAAA;AAOI;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI","file":"jsontree.js.css"} \ No newline at end of file diff --git a/static/configvis/jsontree.js.map b/static/configvis/jsontree.js.map new file mode 100644 index 00000000..21e76574 --- /dev/null +++ b/static/configvis/jsontree.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["/Users/williamtroup/Documents/GitHub%20Repos/JsonTree.js/dist/jsontree.js"],"names":["Is","Is2","defined","value","toString","definedObject","object","definedBoolean","definedString","definedFunction","definedNumber","definedArray","Array","definedDate","Date","definedDecimal","invalidOptionArray","array","minimumLength","length","hexColor","valid","isNaN","substring","Default","Default2","getAnyString","defaultValue","getString","getBoolean","getNumber","getFunction","getArray","getObject","getStringOrArray","result2","values","split","getFixedDecimalPlacesValue","decimalPlaces","_a","regExp","RegExp","match","getFunctionName","valueParts","valueNameParts","DomElement","DomElement2","create","container","type","className","beforeNode","nodeType","toLowerCase","isText","document","createTextNode","createElement","insertBefore","appendChild","createWithHTML","html","element","innerHTML","addClass","classList","add","Str","Str2","newGuid","charIndex","push","character","Math","floor","random","join","padNumber","number","numberString","numberResult","arrayLength","DateTime","DateTime2","getWeekdayNumber","date","getDay","getDayOrdinal","configuration","text","thText","stText","ndText","rdText","getCustomFormattedDateText","dateFormat","weekDayNumber","replace","getHours","getMinutes","getSeconds","dayNames","dayNamesAbbreviated","getDate","monthNames","getMonth","monthNamesAbbreviated","getFullYear","Number","parseInt","Constants","Constants2","JSONTREE_JS_ATTRIBUTE_NAME","Binding","Binding2","Options","Options2","getForNewInstance","data","bindingOptions","get","_currentView","dataArrayCurrentIndex","newOptions","options","showCounts","useZeroIndexingForArrays","dateTimeFormat","showArrowToggles","showStringQuotes","showAllAsClosed","sortPropertyNames","sortPropertyNamesInAlphabeticalOrder","showCommas","reverseArrayValues","addArrayIndexPadding","showValueColors","maximumDecimalPlaces","maximumStringLength","showStringHexColors","showArrayItemsAsSeparateObjects","copyOnlyCurrentPage","getTitle","getIgnore","getCustomTriggers","title","show","showTreeControls","showCopyButton","ignore","nullValues","functionValues","unknownValues","booleanValues","decimalValues","numberValues","stringValues","dateValues","objectValues","arrayValues","events","onBeforeRender","onRenderComplete","onValueClick","onRefresh","onCopyAll","onOpenAll","onCloseAll","onDestroy","onBooleanRender","onDecimalRender","onNumberRender","onStringRender","onDateRender","onFunctionRender","onNullRender","onUnknownRender","Config","Config2","newConfiguration","safeMode","domElementTypes","getText","objectText","arrayText","closeAllButtonText","openAllButtonText","copyAllButtonText","objectErrorText","attributeNotValidErrorText","attributeNotSetErrorText","ellipsisText","closeAllButtonSymbolText","openAllButtonSymbolText","copyAllButtonSymbolText","backButtonText","nextButtonText","backButtonSymbolText","nextButtonSymbolText","Trigger","Trigger2","customEvent","triggerFunction","args","apply","slice","call","_configuration","_elements_Data","render","tagTypes","tagTypesLength","tagTypeIndex","domElements","getElementsByTagName","elements","elementsLength","elementIndex","renderElement","hasAttribute","bindingOptionsData","getAttribute","getObjectFromString","parsed","renderControl","console","error","id","removeAttribute","hasOwnProperty","renderControlContainer","renderControlTitleBar","renderObject","renderArray","titleBar","controls","copy","onclick","copyData","JSON","stringify","navigator","clipboard","writeText","openAll","closeAll","openAllNodes","closeAllNodes","back","disabled","next","showPagingIndex","objectTypeTitle","objectTypeContents","arrow","propertyCount","renderObjectValues","titleText","dataArrayIndex","renderArrayValues","properties","key","sort","reverse","propertiesLength","propertyIndex","propertyName","renderValue","addArrowEvent","dataLength","dataIndex1","getIndexName","dataIndex2","name","isLastItem","objectTypeValue","valueClass","valueElement","ignored","addClickEvent","createComma","newValue","color","newStringValue","style","objectTitle","arrayTitle","arrayTypeContents","removeChild","addValueClickEvent","display","index","largestValue","objectString","result","parse","e1","eval","e2","message","destroyElement","_public","refresh","elementId","refreshAll","renderAll","destroy","destroyAll","setConfiguration","configurationHasChanged","newInternalConfiguration","getIds","getVersion","addEventListener","window","$jsontree"],"mappings":"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","sourcesContent":[null]} \ No newline at end of file diff --git a/static/configvis/prism.css b/static/configvis/prism.css new file mode 100644 index 00000000..5d87cd19 --- /dev/null +++ b/static/configvis/prism.css @@ -0,0 +1,174 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-twilight&languages=markup+yaml&plugins=line-numbers */ +code[class*='language-'], +pre[class*='language-'] { + color: #fff; + background: 0 0; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + text-shadow: 0 -0.1em 0.2em #000; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} +:not(pre) > code[class*='language-'], +pre[class*='language-'] { + background: #141414; +} +pre[class*='language-'] { + border-radius: 0.5em; + border: 0.3em solid #545454; + box-shadow: 1px 1px 0.5em #000 inset; + margin: 0.5em 0; + overflow: auto; + padding: 1em; +} +pre[class*='language-']::-moz-selection { + background: #27292a; +} +pre[class*='language-']::selection { + background: #27292a; +} +code[class*='language-'] ::-moz-selection, +code[class*='language-']::-moz-selection, +pre[class*='language-'] ::-moz-selection, +pre[class*='language-']::-moz-selection { + text-shadow: none; + background: hsla(0, 0%, 93%, 0.15); +} +code[class*='language-'] ::selection, +code[class*='language-']::selection, +pre[class*='language-'] ::selection, +pre[class*='language-']::selection { + text-shadow: none; + background: hsla(0, 0%, 93%, 0.15); +} +:not(pre) > code[class*='language-'] { + border-radius: 0.3em; + border: 0.13em solid #545454; + box-shadow: 1px 1px 0.3em -0.1em #000 inset; + padding: 0.15em 0.2em 0.05em; + white-space: normal; +} +.token.cdata, +.token.comment, +.token.doctype, +.token.prolog { + color: #777; +} +.token.punctuation { + opacity: 0.7; +} +.token.namespace { + opacity: 0.7; +} +.token.boolean, +.token.deleted, +.token.number, +.token.tag { + color: #ce6849; +} +.token.builtin, +.token.constant, +.token.keyword, +.token.property, +.token.selector, +.token.symbol { + color: #f9ed99; +} +.language-css .token.string, +.style .token.string, +.token.attr-name, +.token.attr-value, +.token.char, +.token.entity, +.token.inserted, +.token.operator, +.token.string, +.token.url, +.token.variable { + color: #909e6a; +} +.token.atrule { + color: #7385a5; +} +.token.important, +.token.regex { + color: #e8c062; +} +.token.bold, +.token.important { + font-weight: 700; +} +.token.italic { + font-style: italic; +} +.token.entity { + cursor: help; +} +.language-markup .token.attr-name, +.language-markup .token.punctuation, +.language-markup .token.tag { + color: #ac885c; +} +.token { + position: relative; + z-index: 1; +} +.line-highlight.line-highlight { + background: hsla(0, 0%, 33%, 0.25); + background: linear-gradient(to right, hsla(0, 0%, 33%, 0.1) 70%, hsla(0, 0%, 33%, 0)); + border-bottom: 1px dashed #545454; + border-top: 1px dashed #545454; + margin-top: 0.75em; + z-index: 0; +} +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background-color: #8693a6; + color: #f4f1ef; +} +pre[class*='language-'].line-numbers { + position: relative; + padding-left: 3.8em; + counter-reset: linenumber; +} +pre[class*='language-'].line-numbers > code { + position: relative; + white-space: inherit; +} +.line-numbers .line-numbers-rows { + position: absolute; + pointer-events: none; + top: 0; + font-size: 100%; + left: -3.8em; + width: 3em; + letter-spacing: -1px; + border-right: 1px solid #999; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.line-numbers-rows > span { + display: block; + counter-increment: linenumber; +} +.line-numbers-rows > span:before { + content: counter(linenumber); + color: #999; + display: block; + padding-right: 0.8em; + text-align: right; +} diff --git a/static/configvis/prism.js b/static/configvis/prism.js new file mode 100644 index 00000000..4b96b4a7 --- /dev/null +++ b/static/configvis/prism.js @@ -0,0 +1,625 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-twilight&languages=markup+yaml&plugins=line-numbers */ +var _self = + 'undefined' != typeof window ? window : 'undefined' != typeof WorkerGlobalScope && self instanceof WorkerGlobalScope ? self : {}, + Prism = (function (e) { + var n = /(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i, + t = 0, + r = {}, + a = { + manual: e.Prism && e.Prism.manual, + disableWorkerMessageHandler: e.Prism && e.Prism.disableWorkerMessageHandler, + util: { + encode: function e(n) { + return n instanceof i + ? new i(n.type, e(n.content), n.alias) + : Array.isArray(n) + ? n.map(e) + : n + .replace(/&/g, '&') + .replace(/= g.reach); + A += w.value.length, w = w.next + ) { + var E = w.value; + if (n.length > e.length) return; + if (!(E instanceof i)) { + var P, + L = 1; + if (y) { + if (!(P = l(b, A, e, m)) || P.index >= e.length) break; + var S = P.index, + O = P.index + P[0].length, + j = A; + for (j += w.value.length; S >= j; ) j += (w = w.next).value.length; + if (((A = j -= w.value.length), w.value instanceof i)) continue; + for (var C = w; C !== n.tail && (j < O || 'string' == typeof C.value); C = C.next) + L++, (j += C.value.length); + L--, (E = e.slice(A, j)), (P.index -= A); + } else if (!(P = l(b, 0, E, m))) continue; + S = P.index; + var N = P[0], + _ = E.slice(0, S), + M = E.slice(S + N.length), + W = A + E.length; + g && W > g.reach && (g.reach = W); + var z = w.prev; + if ( + (_ && ((z = u(n, z, _)), (A += _.length)), + c(n, z, L), + (w = u(n, z, new i(f, p ? a.tokenize(N, p) : N, k, N))), + M && u(n, w, M), + L > 1) + ) { + var I = { cause: f + ',' + d, reach: W }; + o(e, n, t, w.prev, A, I), g && I.reach > g.reach && (g.reach = I.reach); + } + } + } + } + } + } + function s() { + var e = { value: null, prev: null, next: null }, + n = { value: null, prev: e, next: null }; + (e.next = n), (this.head = e), (this.tail = n), (this.length = 0); + } + function u(e, n, t) { + var r = n.next, + a = { value: t, prev: n, next: r }; + return (n.next = a), (r.prev = a), e.length++, a; + } + function c(e, n, t) { + for (var r = n.next, a = 0; a < t && r !== e.tail; a++) r = r.next; + (n.next = r), (r.prev = n), (e.length -= a); + } + if ( + ((e.Prism = a), + (i.stringify = function e(n, t) { + if ('string' == typeof n) return n; + if (Array.isArray(n)) { + var r = ''; + return ( + n.forEach(function (n) { + r += e(n, t); + }), + r + ); + } + var i = { type: n.type, content: e(n.content, t), tag: 'span', classes: ['token', n.type], attributes: {}, language: t }, + l = n.alias; + l && (Array.isArray(l) ? Array.prototype.push.apply(i.classes, l) : i.classes.push(l)), a.hooks.run('wrap', i); + var o = ''; + for (var s in i.attributes) o += ' ' + s + '="' + (i.attributes[s] || '').replace(/"/g, '"') + '"'; + return '<' + i.tag + ' class="' + i.classes.join(' ') + '"' + o + '>' + i.content + ''; + }), + !e.document) + ) + return e.addEventListener + ? (a.disableWorkerMessageHandler || + e.addEventListener( + 'message', + function (n) { + var t = JSON.parse(n.data), + r = t.language, + i = t.code, + l = t.immediateClose; + e.postMessage(a.highlight(i, a.languages[r], r)), l && e.close(); + }, + !1, + ), + a) + : a; + var g = a.util.currentScript(); + function f() { + a.manual || a.highlightAll(); + } + if ((g && ((a.filename = g.src), g.hasAttribute('data-manual') && (a.manual = !0)), !a.manual)) { + var h = document.readyState; + 'loading' === h || ('interactive' === h && g && g.defer) + ? document.addEventListener('DOMContentLoaded', f) + : window.requestAnimationFrame + ? window.requestAnimationFrame(f) + : window.setTimeout(f, 16); + } + return a; + })(_self); +'undefined' != typeof module && module.exports && (module.exports = Prism), 'undefined' != typeof global && (global.Prism = Prism); +(Prism.languages.markup = { + comment: { pattern: //, greedy: !0 }, + prolog: { pattern: /<\?[\s\S]+?\?>/, greedy: !0 }, + doctype: { + pattern: /"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i, + greedy: !0, + inside: { + 'internal-subset': { pattern: /(^[^\[]*\[)[\s\S]+(?=\]>$)/, lookbehind: !0, greedy: !0, inside: null }, + string: { pattern: /"[^"]*"|'[^']*'/, greedy: !0 }, + punctuation: /^$|[[\]]/, + 'doctype-tag': /^DOCTYPE/i, + name: /[^\s<>'"]+/, + }, + }, + cdata: { pattern: //i, greedy: !0 }, + tag: { + pattern: /<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/, + greedy: !0, + inside: { + tag: { pattern: /^<\/?[^\s>\/]+/, inside: { punctuation: /^<\/?/, namespace: /^[^\s>\/:]+:/ } }, + 'special-attr': [], + 'attr-value': { + pattern: /=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/, + inside: { + punctuation: [ + { pattern: /^=/, alias: 'attr-equals' }, + { pattern: /^(\s*)["']|["']$/, lookbehind: !0 }, + ], + }, + }, + punctuation: /\/?>/, + 'attr-name': { pattern: /[^\s>\/]+/, inside: { namespace: /^[^\s>\/:]+:/ } }, + }, + }, + entity: [{ pattern: /&[\da-z]{1,8};/i, alias: 'named-entity' }, /&#x?[\da-f]{1,8};/i], +}), + (Prism.languages.markup.tag.inside['attr-value'].inside.entity = Prism.languages.markup.entity), + (Prism.languages.markup.doctype.inside['internal-subset'].inside = Prism.languages.markup), + Prism.hooks.add('wrap', function (a) { + 'entity' === a.type && (a.attributes.title = a.content.replace(/&/, '&')); + }), + Object.defineProperty(Prism.languages.markup.tag, 'addInlined', { + value: function (a, e) { + var s = {}; + (s['language-' + e] = { pattern: /(^$)/i, lookbehind: !0, inside: Prism.languages[e] }), + (s.cdata = /^$/i); + var t = { 'included-cdata': { pattern: //i, inside: s } }; + t['language-' + e] = { pattern: /[\s\S]+/, inside: Prism.languages[e] }; + var n = {}; + (n[a] = { + pattern: RegExp( + '(<__[^>]*>)(?:))*\\]\\]>|(?!)'.replace( + /__/g, + function () { + return a; + }, + ), + 'i', + ), + lookbehind: !0, + greedy: !0, + inside: t, + }), + Prism.languages.insertBefore('markup', 'cdata', n); + }, + }), + Object.defineProperty(Prism.languages.markup.tag, 'addAttribute', { + value: function (a, e) { + Prism.languages.markup.tag.inside['special-attr'].push({ + pattern: RegExp('(^|["\'\\s])(?:' + a + ')\\s*=\\s*(?:"[^"]*"|\'[^\']*\'|[^\\s\'">=]+(?=[\\s>]))', 'i'), + lookbehind: !0, + inside: { + 'attr-name': /^[^\s=]+/, + 'attr-value': { + pattern: /=[\s\S]+/, + inside: { + value: { + pattern: /(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/, + lookbehind: !0, + alias: [e, 'language-' + e], + inside: Prism.languages[e], + }, + punctuation: [{ pattern: /^=/, alias: 'attr-equals' }, /"|'/], + }, + }, + }, + }); + }, + }), + (Prism.languages.html = Prism.languages.markup), + (Prism.languages.mathml = Prism.languages.markup), + (Prism.languages.svg = Prism.languages.markup), + (Prism.languages.xml = Prism.languages.extend('markup', {})), + (Prism.languages.ssml = Prism.languages.xml), + (Prism.languages.atom = Prism.languages.xml), + (Prism.languages.rss = Prism.languages.xml); +!(function (e) { + var n = /[*&][^\s[\]{},]+/, + r = /!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/, + t = '(?:' + r.source + '(?:[ \t]+' + n.source + ')?|' + n.source + '(?:[ \t]+' + r.source + ')?)', + a = + '(?:[^\\s\\x00-\\x08\\x0e-\\x1f!"#%&\'*,\\-:>?@[\\]`{|}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*'.replace( + //g, + function () { + return '[^\\s\\x00-\\x08\\x0e-\\x1f,[\\]{}\\x7f-\\x84\\x86-\\x9f\\ud800-\\udfff\\ufffe\\uffff]'; + }, + ), + d = '"(?:[^"\\\\\r\n]|\\\\.)*"|\'(?:[^\'\\\\\r\n]|\\\\.)*\''; + function o(e, n) { + n = (n || '').replace(/m/g, '') + 'm'; + var r = '([:\\-,[{]\\s*(?:\\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\\]|\\}|(?:[\r\n]\\s*)?#))' + .replace(/<>/g, function () { + return t; + }) + .replace(/<>/g, function () { + return e; + }); + return RegExp(r, n); + } + (e.languages.yaml = { + scalar: { + pattern: RegExp( + '([\\-:]\\s*(?:\\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\\S[^\r\n]*(?:\\2[^\r\n]+)*)'.replace( + /<>/g, + function () { + return t; + }, + ), + ), + lookbehind: !0, + alias: 'string', + }, + comment: /#.*/, + key: { + pattern: RegExp( + '((?:^|[:\\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\\s*:\\s)' + .replace(/<>/g, function () { + return t; + }) + .replace(/<>/g, function () { + return '(?:' + a + '|' + d + ')'; + }), + ), + lookbehind: !0, + greedy: !0, + alias: 'atrule', + }, + directive: { pattern: /(^[ \t]*)%.+/m, lookbehind: !0, alias: 'important' }, + datetime: { + pattern: o( + '\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?(?:[ \t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?))?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?', + ), + lookbehind: !0, + alias: 'number', + }, + boolean: { pattern: o('false|true', 'i'), lookbehind: !0, alias: 'important' }, + null: { pattern: o('null|~', 'i'), lookbehind: !0, alias: 'important' }, + string: { pattern: o(d), lookbehind: !0, greedy: !0 }, + number: { + pattern: o('[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)', 'i'), + lookbehind: !0, + }, + tag: r, + important: n, + punctuation: /---|[:[\]{}\-,|>?]|\.\.\./, + }), + (e.languages.yml = e.languages.yaml); +})(Prism); +!(function () { + if ('undefined' != typeof Prism && 'undefined' != typeof document) { + var e = 'line-numbers', + n = /\n(?!$)/g, + t = (Prism.plugins.lineNumbers = { + getLine: function (n, t) { + if ('PRE' === n.tagName && n.classList.contains(e)) { + var i = n.querySelector('.line-numbers-rows'); + if (i) { + var r = parseInt(n.getAttribute('data-start'), 10) || 1, + s = r + (i.children.length - 1); + t < r && (t = r), t > s && (t = s); + var l = t - r; + return i.children[l]; + } + } + }, + resize: function (e) { + r([e]); + }, + assumeViewportIndependence: !0, + }), + i = void 0; + window.addEventListener('resize', function () { + (t.assumeViewportIndependence && i === window.innerWidth) || + ((i = window.innerWidth), r(Array.prototype.slice.call(document.querySelectorAll('pre.line-numbers')))); + }), + Prism.hooks.add('complete', function (t) { + if (t.code) { + var i = t.element, + s = i.parentNode; + if (s && /pre/i.test(s.nodeName) && !i.querySelector('.line-numbers-rows') && Prism.util.isActive(i, e)) { + i.classList.remove(e), s.classList.add(e); + var l, + o = t.code.match(n), + a = o ? o.length + 1 : 1, + u = new Array(a + 1).join(''); + (l = document.createElement('span')).setAttribute('aria-hidden', 'true'), + (l.className = 'line-numbers-rows'), + (l.innerHTML = u), + s.hasAttribute('data-start') && + (s.style.counterReset = 'linenumber ' + (parseInt(s.getAttribute('data-start'), 10) - 1)), + t.element.appendChild(l), + r([s]), + Prism.hooks.run('line-numbers', t); + } + } + }), + Prism.hooks.add('line-numbers', function (e) { + (e.plugins = e.plugins || {}), (e.plugins.lineNumbers = !0); + }); + } + function r(e) { + if ( + 0 != + (e = e.filter(function (e) { + var n, + t = ((n = e), n ? (window.getComputedStyle ? getComputedStyle(n) : n.currentStyle || null) : null)['white-space']; + return 'pre-wrap' === t || 'pre-line' === t; + })).length + ) { + var t = e + .map(function (e) { + var t = e.querySelector('code'), + i = e.querySelector('.line-numbers-rows'); + if (t && i) { + var r = e.querySelector('.line-numbers-sizer'), + s = t.textContent.split(n); + r || (((r = document.createElement('span')).className = 'line-numbers-sizer'), t.appendChild(r)), + (r.innerHTML = '0'), + (r.style.display = 'block'); + var l = r.getBoundingClientRect().height; + return (r.innerHTML = ''), { element: e, lines: s, lineHeights: [], oneLinerHeight: l, sizer: r }; + } + }) + .filter(Boolean); + t.forEach(function (e) { + var n = e.sizer, + t = e.lines, + i = e.lineHeights, + r = e.oneLinerHeight; + (i[t.length - 1] = void 0), + t.forEach(function (e, t) { + if (e && e.length > 1) { + var s = n.appendChild(document.createElement('span')); + (s.style.display = 'block'), (s.textContent = e); + } else i[t] = r; + }); + }), + t.forEach(function (e) { + for (var n = e.sizer, t = e.lineHeights, i = 0, r = 0; r < t.length; r++) + void 0 === t[r] && (t[r] = n.children[i++].getBoundingClientRect().height); + }), + t.forEach(function (e) { + var n = e.sizer, + t = e.element.querySelector('.line-numbers-rows'); + (n.style.display = 'none'), + (n.innerHTML = ''), + e.lineHeights.forEach(function (e, n) { + t.children[n].style.height = e + 'px'; + }); + }); + } + } +})(); diff --git a/static/logo.svg b/static/logo.svg new file mode 100644 index 00000000..2e809b42 --- /dev/null +++ b/static/logo.svg @@ -0,0 +1,7 @@ + + + + + + Butler + From 2b067c8e4cdb858506c88710cb33200f39fec1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 07:49:02 +0000 Subject: [PATCH 6/8] feat(config): Visualise obfuscated config file in Butler SOS hosted web page Implements #858 --- static/configvis/butler-sos.png | Bin 0 -> 29659 bytes static/configvis/butler.png | Bin 33013 -> 0 bytes static/configvis/index.html | 8 ++++---- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 static/configvis/butler-sos.png delete mode 100644 static/configvis/butler.png diff --git a/static/configvis/butler-sos.png b/static/configvis/butler-sos.png new file mode 100644 index 0000000000000000000000000000000000000000..52f6957035aca6d09072e5933dc60539d018944d GIT binary patch literal 29659 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 AEdT%j literal 0 HcmV?d00001 diff --git a/static/configvis/butler.png b/static/configvis/butler.png deleted file mode 100644 index 5ecc6944cc5deb31e3251b685d4a36755525529a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33013 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%0A^oTo`$nN%fhPi9 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 cCJL0McbA#IyUp%O_d)OQKI&H&jB=ty_P z8Hm{1{2>@yZ=jS=)t9pD0?jj9_{v5ETq%~apOLyWI}sYMI7P=2h50zg9JNz3rO zlQw#cOz)+lldAQADM0ZkaI(3H?(Sn;j{(De;W08t(GUz&nbqY;8lCQa6wo0Rq|aH# zhZSlPA≧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 i7+e3Z2M0{WWnXkb8v#9Xs`h8VpRA

- Current Butler configuration (docs - here) + Current Butler SOS configuration (docs + here)

From c2b1579371bb2044d8dca593d0fc44abc757b962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 08:08:38 +0000 Subject: [PATCH 7/8] fix(new-relic): Allow empty static uptime attributes w/o errors Fixes #863 --- src/lib/post-to-new-relic.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/post-to-new-relic.js b/src/lib/post-to-new-relic.js index 335711ac..000f5c27 100755 --- a/src/lib/post-to-new-relic.js +++ b/src/lib/post-to-new-relic.js @@ -505,12 +505,13 @@ export async function postButlerSOSUptimeToNewRelic(fields) { const attributes = {}; const ts = new Date().getTime(); // Timestamp in millisec - // Add static fields to attributes - if (globals.config.has('Butler-SOS.uptimeMonitor.storeNewRelic.attribute.static')) { + // Add static fields to attributes if they exist + if (globals.config.has('Butler-SOS.uptimeMonitor.storeNewRelic.attribute.static') && globals.config.get('Butler-SOS.uptimeMonitor.storeNewRelic.attribute.static') !== null) { const staticAttributes = globals.config.get( 'Butler-SOS.uptimeMonitor.storeNewRelic.attribute.static' ); + // staticAttributes is an array of objects. Null // eslint-disable-next-line no-restricted-syntax for (const item of staticAttributes) { attributes[item.name] = item.value; From d9372084cbf51eb7a5519d9c2d66d05b3cee5347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 20 Aug 2024 08:18:26 +0000 Subject: [PATCH 8/8] fix(new-relic): Better error messages when there is no New Relic config, but NR features are enabled Fixes #863 --- src/lib/post-to-new-relic.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/lib/post-to-new-relic.js b/src/lib/post-to-new-relic.js index 000f5c27..1cd2d703 100755 --- a/src/lib/post-to-new-relic.js +++ b/src/lib/post-to-new-relic.js @@ -291,7 +291,11 @@ export async function postHealthMetricsToNewRelic(_host, body, tags) { // Send data to all New Relic accounts that are enabled for this metric/event // // Get New Relic accounts - const nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + let nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + if (nrAccounts === null) { + nrAccounts = []; + } + globals.logger.debug( `HEALTH METRICS NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}` ); @@ -438,7 +442,10 @@ export async function postProxySessionsToNewRelic(userSessions) { // Send data to all New Relic accounts that are enabled for this metric/event // // Get New Relic accounts - const nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + let nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + if (nrAccounts === null) { + nrAccounts = []; + } globals.logger.debug( `PROXY SESSIONS NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}` ); @@ -602,7 +609,10 @@ export async function postButlerSOSUptimeToNewRelic(fields) { // Send data to all New Relic accounts that are enabled for this metric/event // // Get New Relic accounts - const nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + let nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + if (nrAccounts === null) { + nrAccounts = []; + } globals.logger.debug( `UPTIME NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}` ); @@ -748,7 +758,10 @@ export async function postUserEventToNewRelic(msg) { // Send data to all New Relic accounts that are enabled for this metric/event // // Get New Relic accounts - const nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + let nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + if (nrAccounts === null) { + nrAccounts = []; + } globals.logger.debug( `USER EVENT NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}` ); @@ -1027,7 +1040,10 @@ export async function postLogEventToNewRelic(msg) { // Send data to all New Relic accounts that are enabled for this metric/event // // Get New Relic accounts - const nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + let nrAccounts = globals.config.get('Butler-SOS.thirdPartyToolsCredentials.newRelic'); + if (nrAccounts === null) { + nrAccounts = []; + } globals.logger.debug( `LOG EVENT NEW RELIC: Complete New Relic config=${JSON.stringify(nrAccounts)}` );