diff --git a/src/config/log_appender_xml/engine/LocalLogConfig.xml b/src/config/log_appender_xml/engine/LocalLogConfig.xml index 4775b08d..5ef04916 100644 --- a/src/config/log_appender_xml/engine/LocalLogConfig.xml +++ b/src/config/log_appender_xml/engine/LocalLogConfig.xml @@ -1,5 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -32,6 +59,11 @@ + + + + + diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index 7538bbca..e3862b5b 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -80,12 +80,12 @@ Butler-SOS: # 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? + enable: false # Should summary (counter) of user/log events, and rejected 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? + enable: false # 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 @@ -93,8 +93,10 @@ Butler-SOS: # value: DEV # - name: foo # value: bar - rejectedEventCount: - enable: true # Should rejected events be counted and stored in InfluxDB? + rejectedEventCount: # Rejected events are events that are received from Sense, that are correctly formatted, + # but that are rejected by Butler SOS based on the configuration in this file. + # An example of a rejected event is a performance log event that is filtered out by Butler SOS. + enable: false # 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 @@ -162,65 +164,65 @@ Butler-SOS: scheduler: enable: false # Should log events from the scheduler service be handled? categorise: # Take actions on log events based on their content - enable: true + enable: false rules: # Rules are used to match log events to filters - - description: Find access denied errors - logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. - - WARN - - ERROR - action: categorise # Action to take on matched log events. Possible values are categorise, drop - category: # Category to assign to matched log events. Name/value pairs. - # Will be added to InfluxDB datapoints as tags. - - name: qs_log_category - value: access-denied - filter: # Filter used to match log events. Case sensitive. - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: "Access was denied for User:" - - type: so - value: was denied for User - - description: Find AD issues - logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. - - ERROR - - WARN - action: categorise # Action to take on matched log events. Possible values are categorise, drop - category: # Category to assign to matched log events. Name/value pairs. - # Will be added to InfluxDB datapoints as tags. - - name: qs_log_category - value: user-directory - filter: # Filter used to match log events. Case sensitive. - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Duplicate entity with userId - - description: Qlik Sense service down - logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. - - WARN - action: categorise # Action to take on matched log events. Possible values are categorise, drop - category: # Category to assign to matched log events. Name/value pairs. - # Will be added to InfluxDB datapoints as tags. - - name: qs_log_category - value: qs-service - filter: # Filter used to match log events. Case sensitive. - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Failed to request service alive response from - - type: so # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Unable to connect to the remote server - - description: Reload task failed - logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. - - WARN - - ERROR - action: categorise # Action to take on matched log events. Possible values are categorise, drop - category: # Category to assign to matched log events. Name/value pairs. - # Will be added to InfluxDB datapoints as tags. - - name: qs_log_category - value: reload-failed - filter: # Filter used to match log events. Case sensitive. - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Task finished with state FinishedFail - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Task finished with state Error - - type: ew # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Reload failed in Engine. Check engine or script logs. - - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of - value: Reload sequence was not successful (Result=False, Finished=True, Aborted=False) for engine connection with handle + # - description: Find access denied errors + # logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. + # - WARN + # - ERROR + # action: categorise # Action to take on matched log events. Possible values are categorise, drop + # category: # Category to assign to matched log events. Name/value pairs. + # # Will be added to InfluxDB datapoints as tags. + # - name: qs_log_category + # value: access-denied + # filter: # Filter used to match log events. Case sensitive. + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: "Access was denied for User:" + # - type: so + # value: was denied for User + # - description: Find AD issues + # logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. + # - ERROR + # - WARN + # action: categorise # Action to take on matched log events. Possible values are categorise, drop + # category: # Category to assign to matched log events. Name/value pairs. + # # Will be added to InfluxDB datapoints as tags. + # - name: qs_log_category + # value: user-directory + # filter: # Filter used to match log events. Case sensitive. + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Duplicate entity with userId + # - description: Qlik Sense service down + # logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. + # - WARN + # action: categorise # Action to take on matched log events. Possible values are categorise, drop + # category: # Category to assign to matched log events. Name/value pairs. + # # Will be added to InfluxDB datapoints as tags. + # - name: qs_log_category + # value: qs-service + # filter: # Filter used to match log events. Case sensitive. + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Failed to request service alive response from + # - type: so # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Unable to connect to the remote server + # - description: Reload task failed + # logLevel: # Log events of this Log level will be matched. WARN, ERROR, FATAL. Case insensitive. + # - WARN + # - ERROR + # action: categorise # Action to take on matched log events. Possible values are categorise, drop + # category: # Category to assign to matched log events. Name/value pairs. + # # Will be added to InfluxDB datapoints as tags. + # - name: qs_log_category + # value: reload-failed + # filter: # Filter used to match log events. Case sensitive. + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Task finished with state FinishedFail + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Task finished with state Error + # - type: ew # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Reload failed in Engine. Check engine or script logs. + # - type: sw # Type of filter. sw = starts with, ew = ends with, so = substring of + # value: Reload sequence was not successful (Result=False, Finished=True, Aborted=False) for engine connection with handle ruleDefault: # Default rule to use if no other rules match the log event enable: true category: @@ -246,75 +248,75 @@ Butler-SOS: enable: false # Should all apps be monitored? appExclude: # What apps should be excluded from monitoring? # If both appId and appName are specified, both must match the event's data for it to be considered a match. - - appId: 5b817efe-472d-43ce-8a31-6cce34af7de9 - - appName: Sales forecast - - appId: f42d6b16-8faf-45ca-a783-59f9da47db6e - appName: Inventory analysis + # - appId: 5b817efe-472d-43ce-8a31-6cce34af7de9 + # - appName: Sales forecast + # - appId: f42d6b16-8faf-45ca-a783-59f9da47db6e + # appName: Inventory analysis objectType: allObjectTypes: true # Should all object types be monitored? allObjectTypesExclude: # If allObjectTypes is set to true, the object types in this array are excluded from monitoring. # someObjectTypesInclude (below) is ignored in that case. - - LoadModelList - - - - linechart - - map + # - LoadModelList + # - + # - linechart + # - map someObjectTypesInclude: # What object types should be included in monitoring? # Only applicable if allObjectTypes is set to false. - - LoadModelList - - sheet - - barchart + # - LoadModelList + # - sheet + # - barchart method: allMethods: true # Should all methods be monitored? allMethodsExclude: # If allMethods is set to true, the methods in this array are excluded from monitoring. # someMethodsInclude (below) is ignored in that case. - - Global::OpenApp - - Doc::GetAppLayout - - Doc::CreateSessionObject + # - Global::OpenApp + # - Doc::GetAppLayout + # - Doc::CreateSessionObject someMethodsInclude: # What methods should be included in monitoring? # Only applicable if allMethods is set to false. - - GenericObject::GetLayout - - GenericObject::GetHyperCubeContinuousData + # - GenericObject::GetLayout + # - GenericObject::GetHyperCubeContinuousData appSpecific: - enable: false # Should app specific monitoring be done? + enable: false # Should app specific monitoring be done? app: - include: # What apps should be monitored? # If both appId and appName are specified, both must match the event's data for it to be considered a match. - - appId: d7cf16f9-6a95-462a-9ff1-a6d413326de4 - - appName: Budget 2025 - - appId: 6931136d-c234-4358-a40c-e37153aba7c9 - appName: Sales basket analysis + # - appId: d7cf16f9-6a95-462a-9ff1-a6d413326de4 + # - appName: Budget 2025 + # - appId: 6931136d-c234-4358-a40c-e37153aba7c9 + # appName: Sales basket analysis objectType: - allObjectTypes: true # Should all object types be monitored? + allObjectTypes: true # Should all object types be monitored? allObjectTypesExclude: # If allObjectTypes is set to true, the object types in this array are excluded from monitoring. # someObjectTypesInclude (below) is ignored in that case. - - table - - map + # - table + # - map someObjectTypesInclude: # What object types should be included in monitoring? # Only applicable if allObjectTypes is set to false. - - sheet - - barchart - - linechart - - map + # - sheet + # - barchart + # - linechart + # - map appObject: allAppObjects: true # Should all app objects be monitored? allAppObjectsExclude: # If allAppObjects is set to true, the app objects in this array are excluded from monitoring. # someAppObjectsInclude (below) is ignored in that case. - - objectId: AaBbCc - - objectId: DdEeFf + # - objectId: AaBbCc + # - objectId: DdEeFf someAppObjectsInclude: # What app objects should be included in monitoring? # Only applicable if allAppObjects is set to false. - - objectId: YJEpPT + # - objectId: YJEpPT method: allMethods: true # Should all methods be monitored? allMethodsExclude: # If allMethods is set to true, the methods in this array are excluded from monitoring. # someMethodsInclude (below) is ignored in that case. - - Global::OpenApp - - Doc::GetAppLayout - - Doc::CreateSessionObject + # - Global::OpenApp + # - Doc::GetAppLayout + # - Doc::CreateSessionObject someMethodsInclude: # What methods should be included in monitoring? # Only applicable if allMethods is set to false. - - GenericObject::GetLayout - - GenericObject::GetHyperCubeContinuousData + # - GenericObject::GetLayout + # - GenericObject::GetHyperCubeContinuousData sendToMQTT: enable: false # Should log events be sent as MQTT messages? baseTopic: qliksense/logevent # What topic should log events be forwarded to? diff --git a/src/globals.js b/src/globals.js index f6a4ed80..59d473ba 100755 --- a/src/globals.js +++ b/src/globals.js @@ -333,7 +333,7 @@ class Settings { } // ------------------------------------ - // Track user events and log events + // Track user events and log event counts if (this.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { this.udpEvents = new UdpEvents(this.logger); } else { @@ -410,6 +410,14 @@ class Settings { tagValuesLogEvent.push('windows_user'); tagValuesLogEvent.push('engine_exe_version'); + // Performance log event tags + tagValuesLogEvent.push('method'); + tagValuesLogEvent.push('object_type'); + tagValuesLogEvent.push('proxy_session_id'); + tagValuesLogEvent.push('session_id'); + tagValuesLogEvent.push('event_activity_source'); + tagValuesLogEvent.push('object_id'); + // Check if there are any extra log event tags in the config file if ( this.config.has('Butler-SOS.logEvents.tags') && @@ -615,6 +623,16 @@ class Settings { context: Influx.FieldType.STRING, session_id: Influx.FieldType.STRING, raw_event: Influx.FieldType.STRING, + + // engine performance fields + process_time: Influx.FieldType.FLOAT, + work_time: Influx.FieldType.FLOAT, + lock_time: Influx.FieldType.FLOAT, + validate_time: Influx.FieldType.FLOAT, + traverse_time: Influx.FieldType.FLOAT, + handle: Influx.FieldType.INTEGER, + net_ram: Influx.FieldType.INTEGER, + peak_ram: Influx.FieldType.INTEGER, }, tags: tagValuesLogEvent, }, @@ -740,7 +758,7 @@ class Settings { const dbName = this.config.get('Butler-SOS.influxdbConfig.v1Config.dbName'); if ( - influx && + this.influx && this.config.get('Butler-SOS.influxdbConfig.enable') === true && dbName?.length > 0 ) { diff --git a/src/lib/config-obfuscate.js b/src/lib/config-obfuscate.js index e96665e4..3827a5f2 100644 --- a/src/lib/config-obfuscate.js +++ b/src/lib/config-obfuscate.js @@ -22,11 +22,31 @@ function configObfuscate(config) { accountId: element.accountId.toString().substring(0, 3) + '*'.repeat(10), })); - // Obfuscate Butler-SOS.iuserEvents.udpServerConfig.serverHost, keep first 3 chars, mask the rest with * + // Obfuscate Butler-SOS.userEvents.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.logEvents.enginePerformanceMonitor.monitorFilter.appSpecific.app[].include[], which is an array of objects, each with the following properties: + // - appId: keep first 5 chars, mask the rest with * + // - appName: keep first 5 chars, mask the rest with * + obfuscatedConfig[ + 'Butler-SOS' + ].logEvents.enginePerformanceMonitor.monitorFilter.appSpecific.app.forEach((element) => { + element.include = element.include.map((includeElement) => { + if (includeElement.appId) { + includeElement.appId = includeElement.appId.substring(0, 5) + '*'.repeat(10); + } + + if (includeElement.appName) { + includeElement.appName = + includeElement.appName.substring(0, 5) + '*'.repeat(10); + } + + return includeElement; + }); + }); + // 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[ diff --git a/src/lib/post-to-influxdb.js b/src/lib/post-to-influxdb.js index 3dd63bc4..3937a6ef 100755 --- a/src/lib/post-to-influxdb.js +++ b/src/lib/post-to-influxdb.js @@ -993,16 +993,16 @@ export async function postLogEventToInfluxdb(msg) { }; } else if (msg.source === 'qseow-qix-perf') { tags = { - host: msg.host, - level: msg.level, - source: msg.source, - log_row: msg.log_row, - subsystem: msg.subsystem, - method: msg.method, - object_type: msg.object_type, - proxy_session_id: msg.proxy_session_id, - session_id: msg.session_id, - event_activity_source: msg.event_activity_source, + host: msg.host?.length > 0 ? msg.host : '', + level: msg.level?.length > 0 ? msg.level : '', + source: msg.source?.length > 0 ? msg.source : '', + log_row: msg.log_row?.length > 0 ? msg.log_row : '-1', + subsystem: msg.subsystem?.length > 0 ? msg.subsystem : '', + method: msg.method?.length > 0 ? msg.method : '', + object_type: msg.object_type?.length > 0 ? msg.object_type : '', + proxy_session_id: msg.proxy_session_id?.length > 0 ? msg.proxy_session_id : '-1', + session_id: msg.session_id?.length > 0 ? msg.session_id : '-1', + event_activity_source: msg.event_activity_source?.length > 0 ? msg.event_activity_source : '', }; // Tags that are empty in some cases. Only add if they are non-empty @@ -1321,6 +1321,13 @@ export async function storeEventCountInfluxDB() { const logEvents = await globals.udpEvents.getLogEvents(); const userEvents = await globals.udpEvents.getUserEvents(); + // Debug + globals.logger.debug(`EVENT COUNT INFLUXDB: Log events: ${JSON.stringify(logEvents, null, 2)}`); + + globals.logger.debug( + `EVENT COUNT INFLUXDB: User events: ${JSON.stringify(userEvents, null, 2)}` + ); + // InfluxDB 1.x if (globals.config.get('Butler-SOS.influxdbConfig.version') === 1) { const points = []; @@ -1337,7 +1344,7 @@ export async function storeEventCountInfluxDB() { measurement: measurementName, tags: { event_type: 'log', - event_name: event.eventName, + source: event.source, host: event.host, subsystem: event.subsystem, }, @@ -1371,7 +1378,7 @@ export async function storeEventCountInfluxDB() { measurement: measurementName, tags: { event_type: 'user', - event_name: event.eventName, + source: event.source, host: event.host, subsystem: event.subsystem, }, @@ -1451,7 +1458,7 @@ export async function storeEventCountInfluxDB() { for (const event of logEvents) { const point = new Point(measurementName) .tag('event_type', 'log') - .tag('event_name', event.eventName) + .tag('source', event.source) .tag('host', event.host) .tag('subsystem', event.subsystem) .intField('counter', event.counter); @@ -1480,7 +1487,7 @@ export async function storeEventCountInfluxDB() { for (const event of userEvents) { const point = new Point(measurementName) .tag('event_type', 'user') - .tag('event_name', event.eventName) + .tag('source', event.source) .tag('host', event.host) .tag('subsystem', event.subsystem) .intField('counter', event.counter); @@ -1527,6 +1534,15 @@ export async function storeRejectedEventCountInfluxDB() { // Get array of rejected log events const rejectedLogEvents = await globals.rejectedEvents.getRejectedLogEvents(); + // Debug + globals.logger.debug( + `REJECTED EVENT COUNT INFLUXDB: Rejected log events: ${JSON.stringify( + rejectedLogEvents, + null, + 2 + )}` + ); + // InfluxDB 1.x if (globals.config.get('Butler-SOS.influxdbConfig.version') === 1) { const points = []; @@ -1541,22 +1557,22 @@ export async function storeRejectedEventCountInfluxDB() { // // 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, + if (event.source === 'qseow-qix-perf') { + // For each unique combination of source, appId, appName, .method and objectType, // write the counter and processTime properties to InfluxDB // - // Use eventName, appId,appName, method and objectType as tags + // Use source, appId,appName, method and objectType as tags const tags = { - event_name: event.eventName, + source: event.source, 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; + if (event?.appName?.length > 0) { + tags.app_name = event.appName; tags.app_name_set = 'true'; } else { tags.app_name_set = 'false'; @@ -1598,7 +1614,7 @@ export async function storeRejectedEventCountInfluxDB() { const point = { measurement: measurementName, tags: { - event_name: event.eventName, + source: event.source, }, fields: { counter: event.counter, @@ -1664,13 +1680,13 @@ export async function storeRejectedEventCountInfluxDB() { // // 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, + if (event.source === 'qseow-qix-perf') { + // For each unique combination of source, appId, appName, .method and objectType, // write the counter and processTime properties to InfluxDB // - // Use eventName, appId,appName, method and objectType as tags + // Use source, appId,appName, method and objectType as tags let point = new Point(measurementName) - .tag('event_name', event.eventName) + .tag('source', event.source) .tag('app_id', event.appId) .tag('method', event.method) .tag('object_type', event.objectType) @@ -1706,7 +1722,7 @@ export async function storeRejectedEventCountInfluxDB() { points.push(point); } else { let point = new Point(measurementName) - .tag('event_name', event.eventName) + .tag('source', event.source) .intField('counter', event.counter); points.push(point); diff --git a/src/lib/telemetry.js b/src/lib/telemetry.js index 509f1d4d..25363c2c 100644 --- a/src/lib/telemetry.js +++ b/src/lib/telemetry.js @@ -11,6 +11,8 @@ const callRemoteURL = async function reportTelemetry() { let dockerHealthCheck = false; let uptimeMonitor = false; let uptimeMonitorNewRelic = false; + let eventCountEnable = false; + let rejectedEventCountEnable = false; let userEventsEnable = false; let userEventsMQTTEnable = false; let userEventsInfluxDBEnable = false; @@ -21,6 +23,9 @@ const callRemoteURL = async function reportTelemetry() { let logEventCategoriseEnable = false; let logEventCategoriseRuleCount = 0; let logEventCategoriseRuleDefaultEnable = false; + let logEventEnginePerformanceMonitorEnable = false; + let logEventEnginePerformanceMonitorNameLookupEnable = false; + let logEventEnginePerformanceMonitorTrackRejectedEnable = false; let logEventsMQTTEnable = false; let logEventsInfluxDBEnable = false; let logEventsNewRelicEnable = false; @@ -49,6 +54,14 @@ const callRemoteURL = async function reportTelemetry() { uptimeMonitorNewRelic = true; } + if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { + eventCountEnable = true; + } + + if (globals.config.get('Butler-SOS.qlikSenseEvents.rejectedEventCount.enable') === true) { + rejectedEventCountEnable = true; + } + if (globals.config.get('Butler-SOS.userEvents.enable') === true) { userEventsEnable = true; } @@ -104,6 +117,26 @@ const callRemoteURL = async function reportTelemetry() { logEventCategoriseRuleDefaultEnable = true; } + if (globals.config.get('Butler-SOS.logEvents.enginePerformanceMonitor.enable') === true) { + logEventEnginePerformanceMonitorEnable = true; + } + + if ( + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.appNameLookup.enable' + ) === true + ) { + logEventEnginePerformanceMonitorNameLookupEnable = true; + } + + if ( + globals.config.get( + 'Butler-SOS.logEvents.enginePerformanceMonitor.trackRejectedEvents.enable' + ) === true + ) { + logEventEnginePerformanceMonitorTrackRejectedEnable = true; + } + if (globals.config.get('Butler-SOS.logdb.enable') === true) { logdbEnable = true; } @@ -157,19 +190,30 @@ const callRemoteURL = async function reportTelemetry() { feature_uptimeMonitor: uptimeMonitor, feature_uptimeMonitor_storeNewRelic: uptimeMonitorNewRelic, feature_udpServer: globals.config.get('Butler-SOS.userEvents.enable'), + + feature_eventCount: eventCountEnable, + feature_rejectedEventCount: rejectedEventCountEnable, + feature_userEvents: userEventsEnable, feature_userEventsMQTT: userEventsMQTTEnable, feature_userEventsInfluxdb: userEventsInfluxDBEnable, feature_userEventsNewRelic: userEventsNewRelicEnable, + feature_logEventsProxy: logEventsProxyEnable, feature_logEventsScheduler: logEventsSchedulerEnable, feature_logEventsRepository: logEventsRepositoryEnable, feature_logEventCategorise: logEventCategoriseEnable, feature_logEventCategoriseRuleCount: logEventCategoriseRuleCount, feature_logEventCategoriseRuleDefault: logEventCategoriseRuleDefaultEnable, + feature_logEventEnginePerformanceMonitor: logEventEnginePerformanceMonitorEnable, + feature_logEventEnginePerformanceMonitorNameLookup: + logEventEnginePerformanceMonitorNameLookupEnable, + feature_logEventEnginePerformanceMonitorTrackRejected: + logEventEnginePerformanceMonitorTrackRejectedEnable, feature_logEventsMQTT: logEventsMQTTEnable, feature_logEventsInfluxdb: logEventsInfluxDBEnable, feature_logEventsNewRelic: logEventsNewRelicEnable, + feature_logdb: logdbEnable, feature_mqtt: mqttEnable, feature_newRelic: newRelicEnable, @@ -198,6 +242,8 @@ const callRemoteURL = async function reportTelemetry() { uptimeMonitor, uptimeMonitorNewRelic, udpServer: globals.config.get('Butler-SOS.userEvents.enable'), + eventCount: eventCountEnable, + rejectedEventCount: rejectedEventCountEnable, userEvents: userEventsEnable, userEventsMQTT: userEventsMQTTEnable, userEventsInfluxdb: userEventsInfluxDBEnable, @@ -208,6 +254,12 @@ const callRemoteURL = async function reportTelemetry() { logEventCategorise: logEventCategoriseEnable, logEventCategoriseRuleCount, logEventCategoriseRuleDefault: logEventCategoriseRuleDefaultEnable, + logEventEnginePerformanceMonitor: + logEventEnginePerformanceMonitorEnable, + logEventEnginePerformanceMonitorNameLookup: + logEventEnginePerformanceMonitorNameLookupEnable, + logEventEnginePerformanceMonitorTrackRejected: + logEventEnginePerformanceMonitorTrackRejectedEnable, logEventsMQTT: logEventsMQTTEnable, logEventsInfluxdb: logEventsInfluxDBEnable, logEventsNewRelic: logEventsNewRelicEnable, @@ -246,7 +298,9 @@ const callRemoteURL = async function reportTelemetry() { ); globals.logger.error('❤️ Thank you for supporting Butler SOS by allowing telemetry! ❤️'); globals.logger.error(''); - globals.logger.error(JSON.stringify(err, null, 2)); + if (err.message) { + globals.logger.error(`TELEMETRY: ${err.message}`); + } } }; diff --git a/src/lib/udp-event.js b/src/lib/udp-event.js index d79cd3c6..d5eddfe2 100644 --- a/src/lib/udp-event.js +++ b/src/lib/udp-event.js @@ -10,14 +10,14 @@ export class UdpEvents { // Array of objects with log events // Each object has properties: - // - eventName: string + // - source: string // - subsystem: string // - counter: integer this.logEvents = []; // Array of objects with user events // Each object has properties: - // - eventName: string + // - source: string // - counter: integer this.userEvents = []; @@ -34,12 +34,12 @@ export class UdpEvents { // Add a log event of any type async addLogEvent(event) { // Ensure the passed event is an object with properties: - // - eventName: string + // - source: string // - host: string // - subsystem: string - if (!event.eventName || !event.subsystem || !event.host) { + if (!event.source || !event.subsystem || !event.host) { this.logger.error( - `LOG EVENT TRACKER: Log event object must have properties "eventName", "subsystem" and "host": ${JSON.stringify( + `LOG EVENT TRACKER: Log event object must have properties "source", "subsystem" and "host": ${JSON.stringify( event )}` ); @@ -51,7 +51,7 @@ export class UdpEvents { try { const found = this.logEvents.find((element) => { return ( - element.eventName === event.eventName && + element.source === event.source && element.subsystem === event.subsystem && element.host === event.host ); @@ -68,7 +68,7 @@ export class UdpEvents { ); this.logEvents.push({ - eventName: event.eventName, + source: event.source, host: event.host, subsystem: event.subsystem, counter: 1, @@ -79,15 +79,54 @@ export class UdpEvents { } } + // Clear log events + async clearLogEvents() { + const release = await this.logMutex.acquire(); + + try { + this.logEvents = []; + + this.logger.debug('LOG EVENT TRACKER: Cleared all log events'); + } finally { + release(); + } + } + + // Clear rejected events + async clearRejectedEvents() { + const release = await this.rejectedLogMutex.acquire(); + + try { + this.rejectedLogEvents = []; + + this.logger.debug('REJECTED EVENT: Cleared all rejected events'); + } finally { + release(); + } + } + + // Clear user events + async clearUserEvents() { + const release = await this.userMutex.acquire(); + + try { + this.userEvents = []; + + this.logger.debug('USER EVENT TRACKER: Cleared all user events'); + } finally { + release(); + } + } + // Add a user event async addUserEvent(event) { // Ensure the passed event is an object with properties: - // - eventName: string + // - source: string // - host: string // - subsystem: string - if (!event.eventName || !event.subsystem || !event.host) { + if (!event.source || !event.subsystem || !event.host) { this.logger.error( - `USER EVENT TRACKER: User event object must have properties "eventName", "subsystem" and "host": ${JSON.stringify( + `USER EVENT TRACKER: User event object must have properties "source", "subsystem" and "host": ${JSON.stringify( event )}` ); @@ -99,7 +138,7 @@ export class UdpEvents { try { const found = this.userEvents.find((element) => { return ( - element.eventName === event.eventName && + element.source === event.source && element.subsystem === event.subsystem && element.host === event.host ); @@ -116,7 +155,7 @@ export class UdpEvents { ); this.userEvents.push({ - eventName: event.eventName, + source: event.source, host: event.host, subsystem: event.subsystem, counter: 1, @@ -138,6 +177,16 @@ export class UdpEvents { } } + // Get rejected log events + async getRejectedLogEvents() { + const release = await this.rejectedLogMutex.acquire(); + try { + return this.rejectedLogEvents; + } finally { + release(); + } + } + // Get user events async getUserEvents() { const release = await this.userMutex.acquire(); @@ -154,16 +203,16 @@ export class UdpEvents { // 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 + // - source: string // // Pertformance log events also have these properties: // - appId: string // - method: string) // - objectType: string) // - processTime: float) - if (!event.eventName) { + if (!event.source) { this.logger.error( - `REJECTED EVENT: Log event object must have property "eventName": ${JSON.stringify( + `REJECTED EVENT: Log event object must have property "source": ${JSON.stringify( event )}` ); @@ -172,11 +221,11 @@ export class UdpEvents { const release = await this.rejectedLogMutex.acquire(); // Is this a performance log event? - if (event.eventName === 'qseow-qix-perf') { + if (event.source === 'qseow-qix-perf') { try { const found = this.rejectedLogEvents.find((element) => { return ( - element.eventName === event.eventName && + element.source === event.source && element.appId === event.appId && element.appName === event.appName && element.method === event.method && @@ -196,7 +245,7 @@ export class UdpEvents { ); this.rejectedLogEvents.push({ - eventName: event.eventName, + source: event.source, appId: event.appId, appName: event.appName, method: event.method, @@ -211,7 +260,7 @@ export class UdpEvents { } else { try { const found = this.rejectedLogEvents.find((element) => { - return element.eventName === event.eventName; + return element.source === event.source; }); if (found) { @@ -225,7 +274,7 @@ export class UdpEvents { ); this.rejectedLogEvents.push({ - eventName: event.eventName, + source: event.source, counter: 1, }); } @@ -234,29 +283,6 @@ export class UdpEvents { } } } - - // 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() { @@ -268,13 +294,22 @@ export function setupUdpEventsStorage() { return; } else { // Configure timer for storing event counts to InfluxDB - setInterval(() => { + setInterval(async () => { globals.logger.verbose( 'EVENT COUNTS: Timer for storing event counts to InfluxDB triggered' ); - storeRejectedEventCountInfluxDB(); - storeEventCountInfluxDB(); + // Store log and user event counts + await storeEventCountInfluxDB(); + + // Store rejected event counts + await storeRejectedEventCountInfluxDB(); + + // Clear event counts + globals.logger.debug('clearing event counters'); + await globals.rejectedEvents.clearRejectedEvents(); + await globals.udpEvents.clearLogEvents(); + await globals.udpEvents.clearUserEvents(); }, 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 13c9b010..2e80bfd3 100644 --- a/src/lib/udp_handlers_log_events.js +++ b/src/lib/udp_handlers_log_events.js @@ -132,7 +132,7 @@ export function udpInitLogEventServer() { if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { // Increase counter for log events await globals.udpEvents.addLogEvent({ - eventName: 'Unknown', + source: 'Unknown', host: 'Unknown', subsystem: 'Unknown', }); @@ -149,12 +149,12 @@ export function udpInitLogEventServer() { ); // Increase counter for log events - // Make eventName lower case, also remove leading and trailing / - let eventName = msg[0].toLowerCase().replace('/', ''); - eventName = eventName.replace('/', ''); + // Make source lower case, also remove leading and trailing / + let source = msg[0].toLowerCase().replace('/', ''); + source = source.replace('/', ''); await globals.udpEvents.addLogEvent({ - eventName, + source, host: msg[5], subsystem: msg[6], }); @@ -383,7 +383,6 @@ export function udpInitLogEventServer() { // If the proxy session ID is '0', the event is considered to be non-user activity, for example a scheduled reload. // Otherwise, the event is considered to be the result of an action by a user, for example opening an app, making a selection, etc. let eventActivitySource; - console.log(msg[15] + '---' + msg[8] + ': ' + msg[8]?.length); if (msg[8] === '0') { // Event is the result of an automated process globals.logger.debug( @@ -749,7 +748,7 @@ export function udpInitLogEventServer() { ) { // Increase counter for rejected performance log events await globals.rejectedEvents.addRejectedLogEvent({ - eventName: 'qseow-qix-perf', + source: 'qseow-qix-perf', appId: eventAppId, appName: eventAppName, method: eventMethod, diff --git a/src/lib/udp_handlers_user_activity.js b/src/lib/udp_handlers_user_activity.js index 2471511a..4623e7c2 100644 --- a/src/lib/udp_handlers_user_activity.js +++ b/src/lib/udp_handlers_user_activity.js @@ -82,7 +82,7 @@ export function udpInitUserActivityServer() { if (globals.config.get('Butler-SOS.qlikSenseEvents.eventCount.enable') === true) { // Increase counter for log events await globals.udpEvents.addUserEvent({ - eventName: 'Unknown', + source: 'Unknown', host: 'Unknown', subsystem: 'Unknown', }); @@ -99,12 +99,12 @@ export function udpInitUserActivityServer() { ); // Increase counter for user events - // Make eventName lower case, also remove leading and trailing / - let eventName = msg[0].toLowerCase().replace('/', ''); - eventName = eventName.replace('/', ''); + // Make source lower case, also remove leading and trailing / + let source = msg[0].toLowerCase().replace('/', ''); + source = source.replace('/', ''); await globals.udpEvents.addUserEvent({ - eventName, + source, host: msg[1], subsystem: msg[5], }); diff --git a/static/configvis/index.html b/static/configvis/index.html index 34d71096..9c0850e8 100644 --- a/static/configvis/index.html +++ b/static/configvis/index.html @@ -35,23 +35,30 @@ text-align: right; } #logo, - #pageTitle, + /* #pageTitle, */ #buttons { display: flex; align-items: center; /* Center align the contents of logo and buttons */ } #logo { /* Ensure there's no right margin or padding that could push the title to the right */ + margin-top: 10px; margin-right: 0; padding-right: 0; } - #pageTitle h1 { + #pageTitle h1, #pageTitle h2 { + display: block; /* Display as block element */ text-align: left; /* Align the title to the left */ margin: 0; /* Remove default margin */ padding-left: 20px; /* Add some space between the logo and the title */ - font-size: 32px; /* Adjust font size as needed */ font-family: Arial, sans-serif; /* Use Arial font */ } + #pageTitle h1 { + font-size: 32px; /* Adjust font size as needed */ + } + #pageTitle h2 { + font-size: 20px; /* Adjust font size as needed */ + } #buttons { justify-content: flex-end; } @@ -77,9 +84,8 @@ } body { - padding-top: 100px; /* Add padding to the top of the body to prevent content from being hidden behind the fixed header */ + padding-top: 90px; /* Add padding to the top of the body to prevent content from being hidden behind the fixed header */ } - /* Style the tab */ .tab { overflow: hidden; @@ -140,13 +146,15 @@

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

+

+ Config file docs + here +

@@ -162,7 +170,7 @@

width: 32px; " /> - Download obfuscated YAML + Download YAML