diff --git a/package-lock.json b/package-lock.json index 8def5460..a4600369 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fastify/static": "^7.0.4", "@influxdata/influxdb-client": "^1.35.0", "@influxdata/influxdb-client-apis": "^1.35.0", + "async-mutex": "^0.5.0", "axios": "^1.7.5", "commander": "^12.1.0", "config": "^3.3.12", @@ -1175,6 +1176,15 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/package.json b/package.json index 0b7bb2d7..9ecfe2d4 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@fastify/static": "^7.0.4", "@influxdata/influxdb-client": "^1.35.0", "@influxdata/influxdb-client-apis": "^1.35.0", + "async-mutex": "^0.5.0", "axios": "^1.7.5", "commander": "^12.1.0", "config": "^3.3.12", diff --git a/src/butler-sos.js b/src/butler-sos.js index 70b2aaa5..56028fca 100755 --- a/src/butler-sos.js +++ b/src/butler-sos.js @@ -25,6 +25,7 @@ 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'; +import { setupUdpEventsStorage } from './lib/udp-event.js'; // Suppress experimental warnings // https://stackoverflow.com/questions/55778283/how-to-disable-warnings-when-node-is-launched-via-a-global-shell-script @@ -51,7 +52,6 @@ process.emit = function (name, data, ...args) { }; async function sleep(ms) { - // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -81,7 +81,6 @@ async function mainScript() { // Sleep 5 seconds otherwise to llow globals to be initialised function sleepLocal(ms) { - // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -285,6 +284,12 @@ async function mainScript() { if (globals.config.get('Butler-SOS.configVisualisation.enable') === true) { await setupConfigVisServer(); } + + // Set up rejected user/log events storage, if enabled + if (globals.config.get('Butler-SOS.qlikSenseEvents.rejectedEventCount.enable') === true) { + globals.logger.verbose('MAIN: Rejected events storage enabled'); + await setupUdpEventsStorage(); + } } mainScript(); diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 4d61e47e..b56a5f36 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -77,6 +77,27 @@ Butler-SOS: # insertApiKey: # accountId: + # Shared settings for user and log events (see below) + qlikSenseEvents: # Shared settings for user and log events (see below) + influxdb: + enable: true # Should summary (counter) of user and log events be stored in InfluxDB? + writeFrequency: 20000 # How often (milliseconds) should rejected event count be written to InfluxDB? + eventCount: # Track how many events are received from Sense. + # Some events are valid, some are not. Of the valid events, some are rejected by Butler SOS + # based on the configuration in this file. + enable: true # Should event count be stored in InfluxDB? + influxdb: + measurementName: event_count # Name of the InfluxDB measurement where event count is stored + tags: # Tags are added to the data before it's stored in InfluxDB + # - tag: env + # value: DEV + # - tag: foo + # value: bar + rejectedEventCount: + enable: true # Should rejected events be counted and stored in InfluxDB? + influxdb: + measurementName: rejected_event_count # Name of the InfluxDB measurement where rejected event count is stored + # Track individual users opening/closing apps and starting/stopping sessions. # Requires log appender XML file(s) to be added to Sense server(s). userEvents: @@ -205,10 +226,17 @@ Butler-SOS: category: - name: qs_log_category value: unknown - appPerformanceMonitor: # Detailed app performance data extraction from log events - enable: false # Should app performance data be extracted from log events? + enginePerformanceMonitor: # Detailed app performance data extraction from log events + enable: false # Should app performance data be extracted from log events? appNameLookup: # Should app names be looked up based on app IDs? - enable: true + enable: false + trackRejectedEvents: + enable: false # Should events that are rejected by the app performance monitor be tracked? + tags: # Tags are added to the data before it's stored in InfluxDB + # - tag: env + # value: DEV + # - tag: foo + # value: bar monitorFilter: # What objects should be monitored? Entire apps or just specific object(s) within some specific app(s)? # Two kinds of monitoring can be done: # 1) Monitor all apps, except those listed for exclusion. This is defined in the allApps section. diff --git a/src/globals.js b/src/globals.js index 10977339..a73238d2 100755 --- a/src/globals.js +++ b/src/globals.js @@ -13,9 +13,10 @@ 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 { fileURLToPath } from 'url'; import { getServerTags } from './lib/servertags.js'; -import { fileURLToPath } from 'url'; +import { UdpEvents } from './lib/udp-event.js'; let instance = null; @@ -122,7 +123,6 @@ class Settings { 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); } @@ -131,7 +131,6 @@ class Settings { 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); } @@ -192,24 +191,19 @@ class Settings { // 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}` ); } - // eslint-disable-next-line no-console console.log( `Log file directory: ${upath.join(this.execPath, this.config.get('Butler-SOS.logDirectory'))}` ); - // eslint-disable-next-line no-console console.log(`upath.dirname(process.execPath): ${upath.dirname(process.execPath)}`); - // eslint-disable-next-line no-console console.log(`process.cwd(): ${process.cwd()}`); } @@ -338,6 +332,22 @@ class Settings { this.logger.error(`CONFIG: Setting up UDP log events listener: ${err}`); } + // ------------------------------------ + // Track user events and log events + if (this.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + this.udpEvents = new UdpEvents(this.logger); + } else { + this.udpEvents = null; + } + + // ------------------------------------ + // Track rejected user and log events + if (this.config.get('Butler-SOS.qlikSenseEvents.rejectedEventCount.enable') === true) { + this.rejectedEvents = new UdpEvents(this.logger); + } else { + this.rejectedEvents = null; + } + // ------------------------------------ // Get info on what servers to monitor this.serverList = this.config.get('Butler-SOS.serversToMonitor.servers'); @@ -358,7 +368,6 @@ class Settings { // 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); @@ -710,7 +719,6 @@ class Settings { // Static sleep function static sleep(ms) { - // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/lib/config-file-schema.js b/src/lib/config-file-schema.js index 6604c945..36c77aba 100755 --- a/src/lib/config-file-schema.js +++ b/src/lib/config-file-schema.js @@ -65,6 +65,30 @@ export const confifgFileSchema = { }, ], }, + + qlikSenseEvents: { + influxdb: { + enable: 'boolean', + writeFrequency: 'number', + }, + eventCount: { + enable: 'boolean', + influxdb: { + measurementName: 'string', + "tags?": [ + "tag": 'string', + value: 'string', + ], + }, + }, + rejectedEventCount: { + enable: 'boolean', + influxdb: { + measurementName: 'string', + }, + }, + }, + userEvents: { enable: 'boolean', 'excludeUser?': [ @@ -173,11 +197,20 @@ export const confifgFileSchema = { ], }, }, - appPerformanceMonitor: { + enginePerformanceMonitor: { enable: 'boolean', appNameLookup: { enable: 'boolean', }, + trackRejectedEvents: { + enable: 'boolean', + "tags?": [ + { + tag: 'string', + value: 'string', + }, + ], + }, monitorFilter: { allApps: { enable: 'boolean', diff --git a/src/lib/post-to-influxdb.js b/src/lib/post-to-influxdb.js index 981c8b98..6e036f4a 100755 --- a/src/lib/post-to-influxdb.js +++ b/src/lib/post-to-influxdb.js @@ -1312,3 +1312,416 @@ export async function postLogEventToInfluxdb(msg) { globals.logger.error(`LOG EVENT INFLUXDB 2: Error saving log event to InfluxDB! ${err}`); } } + +// Store event count for all kinds of events in InfluxDB +export async function storeEventCountInfluxDB() { + // Get array of log events + const logEvents = await globals.udpEvents.getLogEvents(); + const userEvents = await globals.udpEvents.getUserEvents(); + + // InfluxDB 1.x + if (globals.config.get('Butler-SOS.influxdbConfig.version') === 1) { + const points = []; + + // Get measurement name to use for event counts + const measurementName = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName' + ); + + // Loop through data in log events and create datapoints. + // Add the created data points to the points array + for (const event of logEvents) { + const point = { + measurement: measurementName, + tags: { + event_type: 'log', + event_name: event.eventName, + host: event.host, + subsystem: event.subsystem, + }, + fields: { + counter: event.counter, + }, + }; + + // Add static tags from config file + if ( + globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !== + null && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags' + ); + for (const item of configTags) { + point.tags[item.tag] = item.value; + } + } + + points.push(point); + } + + // Loop through data in user events and create datapoints. + // Add the created data points to the points array + for (const event of userEvents) { + const point = { + measurement: measurementName, + tags: { + event_type: 'user', + event_name: event.eventName, + host: event.host, + subsystem: event.subsystem, + }, + fields: { + counter: event.counter, + }, + }; + + // Add static tags from config file + if ( + globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !== + null && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags').length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags' + ); + for (const item of configTags) { + point.tags[item.tag] = item.value; + } + } + + points.push(point); + } + + try { + globals.influx.writePoints(points); + } catch (err) { + globals.logger.error(`EVENT COUNT INFLUXDB: Error saving data to InfluxDB v1! ${err}`); + } + } else if (globals.config.get('Butler-SOS.influxdbConfig.version') === 2) { + // Create new write API object + // 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 + + /* 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, + + /* 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 + }; + + // Create new datapoints object + const points = []; + + try { + const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org'); + const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket'); + + const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions); + + // Ensure that the writeApi object was found + if (!writeApi) { + globals.logger.warn( + `EVENT COUNT INFLUXDB: Influxdb write API object not found. Data will not be sent to InfluxDB` + ); + return; + } + + // Get measurement name to use for event counts + const measurementName = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.measurementName' + ); + + // Loop through data in log events and create datapoints. + // Add the created data points to the points array + for (const event of logEvents) { + const point = new Point(measurementName) + .tag('event_type', 'log') + .tag('event_name', event.eventName) + .tag('host', event.host) + .tag('subsystem', event.subsystem) + .intField('counter', event.counter); + + // Add static tags from config file + if ( + globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !== + null && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') + .length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags' + ); + for (const item of configTags) { + point.tag(item.tag, item.value); + } + } + + points.push(point); + } + + // Loop through data in user events and create datapoints. + // Add the created data points to the points array + for (const event of userEvents) { + const point = new Point(measurementName) + .tag('event_type', 'user') + .tag('event_name', event.eventName) + .tag('host', event.host) + .tag('subsystem', event.subsystem) + .intField('counter', event.counter); + + // Add static tags from config file + if ( + globals.config.has('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') !== + null && + globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags') + .length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.qlikSenseEvents.eventCount.influxdb.tags' + ); + for (const item of configTags) { + point.tag(item.tag, item.value); + } + } + + points.push(point); + } + + try { + const res = await writeApi.writePoints(points); + globals.logger.debug(`EVENT COUNT INFLUXDB: Wrote data to InfluxDB v2`); + } catch (err) { + globals.logger.error( + `EVENT COUNT INFLUXDB: Error saving health data to InfluxDB v2! ${err.stack}` + ); + } + + globals.logger.verbose( + 'EVENT COUNT INFLUXDB: Sent Butler SOS event count data to InfluxDB' + ); + } catch (err) { + globals.logger.error(`EVENT COUNT INFLUXDB: Error getting write API: ${err}`); + } + } +} + +// Store rejected event count in InfluxDB +export async function storeRejectedEventCountInfluxDB() { + // Get array of rejected log events + const rejectedLogEvents = await globals.rejectedEvents.getRejectedLogEvents(); + + // InfluxDB 1.x + if (globals.config.get('Butler-SOS.influxdbConfig.version') === 1) { + const points = []; + + // Get measurement name to use for rejected events + const measurementName = globals.config.get( + 'Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName' + ); + + // Loop through data in rejected log events and create datapoints. + // Add the created data points to the points array + // + // Use counter and process_time as fields + for (const event of rejectedLogEvents) { + if (event.eventName === 'qseow-qix-perf') { + // For each unique combination of eventName, appId, appName, .method and objectType, + // write the counter and processTime properties to InfluxDB + // + // Use eventName, appId,appName, method and objectType as tags + + const tags = { + event_name: event.eventName, + app_id: event.appId, + method: event.method, + object_type: event.objectType, + }; + + // Tags that are empty in some cases. Only add if they are non-empty + if (msg?.app_name?.length > 0) { + tags.app_name = msg.app_name; + tags.app_name_set = 'true'; + } else { + tags.app_name_set = 'false'; + } + + // Add static tags from config file + if ( + globals.config.has( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ) && + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ) !== null && + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ).length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ); + for (const item of configTags) { + tags[item.tag] = item.value; + } + } + + const fields = { + counter: event.counter, + process_time: event.processTime, + }; + + const point = { + measurement: measurementName, + tags, + fields, + }; + + points.push(point); + } else { + const point = { + measurement: measurementName, + tags: { + event_name: event.eventName, + }, + fields: { + counter: event.counter, + }, + }; + + points.push(point); + } + } + + try { + globals.influx.writePoints(points); + } catch (err) { + globals.logger.error( + `REJECT LOG EVENT INFLUXDB: Error saving data to InfluxDB v1! ${err}` + ); + } + } else if (globals.config.get('Butler-SOS.influxdbConfig.version') === 2) { + // Create new write API object + // 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 + + /* 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, + + /* 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 + }; + + // Create new datapoints object + const points = []; + + try { + const org = globals.config.get('Butler-SOS.influxdbConfig.v2Config.org'); + const bucketName = globals.config.get('Butler-SOS.influxdbConfig.v2Config.bucket'); + + const writeApi = globals.influx.getWriteApi(org, bucketName, 'ns', writeOptions); + + // Ensure that the writeApi object was found + if (!writeApi) { + globals.logger.warn( + `LOG EVENT INFLUXDB: Influxdb write API object not found. Data will not be sent to InfluxDB` + ); + return; + } + + // Get measurement name to use for rejected events + const measurementName = globals.config.get( + 'Butler-SOS.qlikSenseEvents.rejectedEventCount.influxdb.measurementName' + ); + + // Loop through data in rejected log events and create datapoints. + // Add the created data points to the points array + // + // Use counter and process_time as fields + for (const event of rejectedLogEvents) { + if (event.eventName === 'qseow-qix-perf') { + // For each unique combination of eventName, appId, appName, .method and objectType, + // write the counter and processTime properties to InfluxDB + // + // Use eventName, appId,appName, method and objectType as tags + let point = new Point(measurementName) + .tag('event_name', event.eventName) + .tag('app_id', event.appId) + .tag('method', event.method) + .tag('object_type', event.objectType) + .intField('counter', event.counter) + .floatField('process_time', event.processTime); + + if (event?.appName?.length > 0) { + point.tag('app_name', event.appName).tag('app_name_set', 'true'); + } else { + point.tag('app_name_set', 'false'); + } + + // Add static tags from config file + if ( + globals.config.has( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ) && + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ) !== null && + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ).length > 0 + ) { + const configTags = globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.tags' + ); + for (const item of configTags) { + point.tag(item.tag, item.value); + } + } + + points.push(point); + } else { + let point = new Point(measurementName) + .tag('event_name', event.eventName) + .intField('counter', event.counter); + + points.push(point); + } + } + + // Write to InfluxDB + try { + const res = await writeApi.writePoints(points); + globals.logger.debug(`REJECT LOG EVENT INFLUXDB: Wrote data to InfluxDB v2`); + } catch (err) { + globals.logger.error( + `REJECTED LOG EVENT INFLUXDB: Error saving data to InfluxDB v2! ${err.stack}` + ); + } + } catch (err) { + globals.logger.error(`REJECTED LOG EVENT INFLUXDB: Error getting write API: ${err}`); + } + } +} diff --git a/src/lib/udp-event.js b/src/lib/udp-event.js new file mode 100644 index 00000000..d79cd3c6 --- /dev/null +++ b/src/lib/udp-event.js @@ -0,0 +1,280 @@ +import { Mutex } from 'async-mutex'; + +import globals from '../globals.js'; +import { storeRejectedEventCountInfluxDB, storeEventCountInfluxDB } from './post-to-influxdb.js'; + +// Class for counting rejected events +export class UdpEvents { + constructor(logger) { + this.logger = logger; + + // Array of objects with log events + // Each object has properties: + // - eventName: string + // - subsystem: string + // - counter: integer + this.logEvents = []; + + // Array of objects with user events + // Each object has properties: + // - eventName: string + // - counter: integer + this.userEvents = []; + + // Array of objects with rejected log events + // Each object has a counter and dimension properties to track app IDs, methods and object types + this.rejectedLogEvents = []; + + // Mutexes for synchronizing access to the arrays + this.logMutex = new Mutex(); + this.userMutex = new Mutex(); + this.rejectedLogMutex = new Mutex(); + } + + // Add a log event of any type + async addLogEvent(event) { + // Ensure the passed event is an object with properties: + // - eventName: string + // - host: string + // - subsystem: string + if (!event.eventName || !event.subsystem || !event.host) { + this.logger.error( + `LOG EVENT TRACKER: Log event object must have properties "eventName", "subsystem" and "host": ${JSON.stringify( + event + )}` + ); + return; + } + + const release = await this.logMutex.acquire(); + + try { + const found = this.logEvents.find((element) => { + return ( + element.eventName === event.eventName && + element.subsystem === event.subsystem && + element.host === event.host + ); + }); + + if (found) { + found.counter += 1; + this.logger.debug( + `LOG EVENT TRACKER: Adding another log event: ${JSON.stringify(event)}, new counter value: ${found.counter}` + ); + } else { + this.logger.debug( + `LOG EVENT TRACKER: Adding first log event: ${JSON.stringify(event)}` + ); + + this.logEvents.push({ + eventName: event.eventName, + host: event.host, + subsystem: event.subsystem, + counter: 1, + }); + } + } finally { + release(); + } + } + + // Add a user event + async addUserEvent(event) { + // Ensure the passed event is an object with properties: + // - eventName: string + // - host: string + // - subsystem: string + if (!event.eventName || !event.subsystem || !event.host) { + this.logger.error( + `USER EVENT TRACKER: User event object must have properties "eventName", "subsystem" and "host": ${JSON.stringify( + event + )}` + ); + return; + } + + const release = await this.userMutex.acquire(); + + try { + const found = this.userEvents.find((element) => { + return ( + element.eventName === event.eventName && + element.subsystem === event.subsystem && + element.host === event.host + ); + }); + + if (found) { + found.counter += 1; + this.logger.debug( + `USER EVENT TRACKER: Adding another user event: ${JSON.stringify(event)}, new counter value: ${found.counter}` + ); + } else { + this.logger.debug( + `USER EVENT TRACKER: Adding first user event: ${JSON.stringify(event)}` + ); + + this.userEvents.push({ + eventName: event.eventName, + host: event.host, + subsystem: event.subsystem, + counter: 1, + }); + } + } finally { + release(); + } + } + + // Get log events + async getLogEvents() { + const release = await this.logMutex.acquire(); + + try { + return this.logEvents; + } finally { + release(); + } + } + + // Get user events + async getUserEvents() { + const release = await this.userMutex.acquire(); + + try { + return this.userEvents; + } finally { + release(); + } + } + + // Add rejected log event + // "Rejected log events" are events that are correctly formatted but are rejected + // Butler SOS due to some reason, e.g. matching the exclude filter criteria in the config file. + async addRejectedLogEvent(event) { + // Ensure the passed event is an object with properties: + // - eventName: string + // + // Pertformance log events also have these properties: + // - appId: string + // - method: string) + // - objectType: string) + // - processTime: float) + if (!event.eventName) { + this.logger.error( + `REJECTED EVENT: Log event object must have property "eventName": ${JSON.stringify( + event + )}` + ); + return; + } + + const release = await this.rejectedLogMutex.acquire(); + // Is this a performance log event? + if (event.eventName === 'qseow-qix-perf') { + try { + const found = this.rejectedLogEvents.find((element) => { + return ( + element.eventName === event.eventName && + element.appId === event.appId && + element.appName === event.appName && + element.method === event.method && + element.objectType === event.objectType + ); + }); + + if (found) { + found.counter += 1; + found.processTime += event.processTime; + this.logger.debug( + `REJECTED EVENT: Adding another log event: ${JSON.stringify(event)}, new counter value: ${found.counter}` + ); + } else { + this.logger.debug( + `REJECTED EVENT: Adding first log event: ${JSON.stringify(event)}` + ); + + this.rejectedLogEvents.push({ + eventName: event.eventName, + appId: event.appId, + appName: event.appName, + method: event.method, + objectType: event.objectType, + counter: 1, + processTime: event.processTime, + }); + } + } finally { + release(); + } + } else { + try { + const found = this.rejectedLogEvents.find((element) => { + return element.eventName === event.eventName; + }); + + if (found) { + found.counter += 1; + this.logger.debug( + `REJECTED EVENT: Adding another log event: ${JSON.stringify(event)}, new counter value: ${found.counter}` + ); + } else { + this.logger.debug( + `REJECTED EVENT: Adding first log event: ${JSON.stringify(event)}` + ); + + this.rejectedLogEvents.push({ + eventName: event.eventName, + counter: 1, + }); + } + } finally { + release(); + } + } + } + + // Get rejected log events + async getRejectedLogEvents() { + const release = await this.rejectedLogMutex.acquire(); + try { + return this.rejectedLogEvents; + } finally { + release(); + } + } + + // Clear rejected events + async clearRejectedEvents() { + const releaseLog = await this.rejectedLogMutex.acquire(); + + try { + this.rejectedLogEvents = []; + + this.logger.debug('REJECTED EVENT: Cleared all rejected events'); + } finally { + releaseLog(); + } + } +} + +export function setupUdpEventsStorage() { + // Is storing event counts to InfluxDB enabled? + if (globals.config.get('Butler-SOS.qlikSenseEvents.influxdb.enable') !== true) { + globals.logger.verbose( + 'EVENT COUNTS: Feature is disabled in config file. Skipping setup of timer for storing event counts to InfluxDB' + ); + return; + } else { + // Configure timer for storing event counts to InfluxDB + setInterval(() => { + globals.logger.verbose( + 'EVENT COUNTS: Timer for storing event counts to InfluxDB triggered' + ); + + storeRejectedEventCountInfluxDB(); + storeEventCountInfluxDB(); + }, globals.config.get('Butler-SOS.qlikSenseEvents.influxdb.writeFrequency')); + } +} diff --git a/src/lib/udp_handlers_log_events.js b/src/lib/udp_handlers_log_events.js index e3effd50..d9733a04 100644 --- a/src/lib/udp_handlers_log_events.js +++ b/src/lib/udp_handlers_log_events.js @@ -127,9 +127,39 @@ export function udpInitLogEventServer() { globals.logger.warn( `LOG EVENT: Received message that is not a recognised log event: ${msgShort}` ); + + // Is logging of event counts enabled? + if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + // Increase counter for log events + await globals.udpEvents.addLogEvent({ + eventName: 'Unknown', + host: 'Unknown', + subsystem: 'Unknown', + }); + } + return; } + // Add counter for received log events + // Is logging of event counts enabled? + if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + globals.logger.debug( + `LOG EVENT: Received message that is a recognised log event: ${msg[0]}` + ); + + // Increase counter for log events + // Make eventName lower case, also remove leading and trailing / + let eventName = msg[0].toLowerCase().replace('/', ''); + eventName = eventName.replace('/', ''); + + await globals.udpEvents.addLogEvent({ + eventName, + host: msg[5], + subsystem: msg[6], + }); + } + // Check if any of the log event sources are enabled in the configuration if ( (globals.config.get('Butler-SOS.logEvents.source.engine.enable') === true && @@ -334,8 +364,9 @@ export function udpInitLogEventServer() { // Determine if the message should be handled, based on settings in the config file if ( - globals.config.get('Butler-SOS.logEvents.appPerformanceMonitor.enable') === - false + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.enable' + ) === false ) { globals.logger.debug( 'LOG EVENT: Qix performance monitoring is disabled in the configuration. Skipping event.' @@ -354,7 +385,7 @@ export function udpInitLogEventServer() { // Get the app performance monitor filter configuration from the config file, // so we don't have to read it every time we need some part of it const monitorFilterConfig = globals.config.get( - 'Butler-SOS.logEvents.appPerformanceMonitor.monitorFilter' + 'Butler-SOS.logEvents.enginePerformanceMonitor.monitorFilter' ); let acceptEvent = false; @@ -369,7 +400,7 @@ export function udpInitLogEventServer() { // Should we get app name from the app ID? if ( globals.config.get( - 'Butler-SOS.logEvents.appPerformanceMonitor.appNameLookup.enable' + 'Butler-SOS.logEvents.enginePerformanceMonitor.appNameLookup.enable' ) === true ) { // Get app name from app ID @@ -684,6 +715,27 @@ export function udpInitLogEventServer() { globals.logger.debug( 'LOG EVENT: Qix performance event does not match filters in the configuration. Skipping event.' ); + + // Is logging of rejected performance log events enabled? + if ( + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.enable' + ) === true && + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.enable' + ) === true + ) { + // Increase counter for rejected performance log events + await globals.rejectedEvents.addRejectedLogEvent({ + eventName: 'qseow-qix-perf', + appId: eventAppId, + appName: eventAppName, + method: eventMethod, + objectType: eventObjectType, + processTime: parseFloat(msg[16]), + }); + } + return; } else { acceptEvent = true; diff --git a/src/lib/udp_handlers_user_activity.js b/src/lib/udp_handlers_user_activity.js index 4ceb4d1c..2471511a 100644 --- a/src/lib/udp_handlers_user_activity.js +++ b/src/lib/udp_handlers_user_activity.js @@ -77,9 +77,39 @@ export function udpInitUserActivityServer() { globals.logger.warn( `USER EVENT: Received message that is not a recognised user event: ${msgShort}` ); + + // Is logging of event counts enabled? + if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + // Increase counter for log events + await globals.udpEvents.addUserEvent({ + eventName: 'Unknown', + host: 'Unknown', + subsystem: 'Unknown', + }); + } + return; } + // Add counter for received user events + // Is logging of event counts enabled? + if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + globals.logger.debug( + `USER EVENT: Received message that is a recognised user event: ${msg[0]}` + ); + + // Increase counter for user events + // Make eventName lower case, also remove leading and trailing / + let eventName = msg[0].toLowerCase().replace('/', ''); + eventName = eventName.replace('/', ''); + + await globals.udpEvents.addUserEvent({ + eventName, + host: msg[1], + subsystem: msg[5], + }); + } + // Build object and convert to JSON let msgObj; if (msg[0] === 'qseow-proxy-connection' || msg[0] === 'qseow-proxy-session') {