From 1912d3e0c515ede34bebfc1d0ec048a17054ee79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Tue, 22 Aug 2023 16:48:01 +0200 Subject: [PATCH] feat(reload-alerts): Make app owner info available in reload failed alerts Implements #728 --- .../email_templates/aborted-reload.handlebars | 12 ++ .../email_templates/failed-reload.handlebars | 12 ++ .../slack_templates/aborted-reload.handlebars | 12 ++ .../slack_templates/failed-reload.handlebars | 12 ++ .../teams_templates/aborted-reload.handlebars | 9 ++ .../teams_templates/failed-reload.handlebars | 9 ++ src/lib/incident_mgmt/new_relic.js | 121 ++++++++++++++++-- src/lib/msteams_notification.js | 15 +++ src/lib/slack_notification.js | 15 +++ src/lib/smtp.js | 16 ++- src/lib/webhook_notification.js | 42 +++++- 11 files changed, 263 insertions(+), 12 deletions(-) diff --git a/src/config/email_templates/aborted-reload.handlebars b/src/config/email_templates/aborted-reload.handlebars index 20f9a000..6d8b349e 100644 --- a/src/config/email_templates/aborted-reload.handlebars +++ b/src/config/email_templates/aborted-reload.handlebars @@ -99,6 +99,18 @@ App ID {{appId}} + + App owner + {{appOwnerName}} + + + App owner user + {{appOwnerUserDirectory}}/{{appOwnerUserId}} + + + App owner email + {{appOwnerEmail}} + diff --git a/src/config/email_templates/failed-reload.handlebars b/src/config/email_templates/failed-reload.handlebars index e499f889..e5aa6ac4 100644 --- a/src/config/email_templates/failed-reload.handlebars +++ b/src/config/email_templates/failed-reload.handlebars @@ -99,6 +99,18 @@ App ID {{appId}} + + App owner + {{appOwnerName}} + + + App owner user + {{appOwnerUserDirectory}}/{{appOwnerUserId}} + + + App owner email + {{appOwnerEmail}} + diff --git a/src/config/slack_templates/aborted-reload.handlebars b/src/config/slack_templates/aborted-reload.handlebars index dd51fcac..69034cde 100644 --- a/src/config/slack_templates/aborted-reload.handlebars +++ b/src/config/slack_templates/aborted-reload.handlebars @@ -26,6 +26,18 @@ { "type": "mrkdwn", "text": "*App ID:*\n{{{appId}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner:*\n{{{appOwnerName}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner email:*\n{{{appOwnerEmail}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner user:*\n{{appOwnerUserDirectory}}/{{appOwnerUserId}}" } ] }, diff --git a/src/config/slack_templates/failed-reload.handlebars b/src/config/slack_templates/failed-reload.handlebars index cd0b6a94..2517011d 100644 --- a/src/config/slack_templates/failed-reload.handlebars +++ b/src/config/slack_templates/failed-reload.handlebars @@ -26,6 +26,18 @@ { "type": "mrkdwn", "text": "*App ID:*\n{{{appId}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner:*\n{{{appOwnerName}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner email:*\n{{{appOwnerEmail}}}" + }, + { + "type": "mrkdwn", + "text": "*App owner user:*\n{{appOwnerUserDirectory}}/{{appOwnerUserId}}" } ] }, diff --git a/src/config/teams_templates/aborted-reload.handlebars b/src/config/teams_templates/aborted-reload.handlebars index 91a82e75..5345ce7d 100644 --- a/src/config/teams_templates/aborted-reload.handlebars +++ b/src/config/teams_templates/aborted-reload.handlebars @@ -18,6 +18,15 @@ },{ "name": "Task ID", "value": "{{taskId}}" + },{ + "name": "App owner", + "value": "{{appOwnerName}}" + },{ + "name": "App owner user", + "value": "{{appOwnerUserDirectory}}/{{appOwnerUserId}}" + },{ + "name": "App owner email", + "value": "{{appOwnerEmail}}" }], "markdown": true }, { diff --git a/src/config/teams_templates/failed-reload.handlebars b/src/config/teams_templates/failed-reload.handlebars index 16ebb7b8..770f3a51 100644 --- a/src/config/teams_templates/failed-reload.handlebars +++ b/src/config/teams_templates/failed-reload.handlebars @@ -18,6 +18,15 @@ },{ "name": "Task ID", "value": "{{taskId}}" + },{ + "name": "App owner", + "value": "{{appOwnerName}}" + },{ + "name": "App owner user", + "value": "{{appOwnerUserDirectory}}/{{appOwnerUserId}}" + },{ + "name": "App owner email", + "value": "{{appOwnerEmail}}" }], "markdown": true }, { diff --git a/src/lib/incident_mgmt/new_relic.js b/src/lib/incident_mgmt/new_relic.js index 0d5efa0a..2351cd9f 100644 --- a/src/lib/incident_mgmt/new_relic.js +++ b/src/lib/incident_mgmt/new_relic.js @@ -132,7 +132,19 @@ function getReloadFailedEventConfig() { return cfg; } catch (err) { - globals.logger.error(`NEWRELIC RELOADFAILEDEVENT: ${err}`); + if (err.message) { + globals.logger.error(`NEWRELIC RELOADFAILEDEVENT message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`NEWRELIC RELOADFAILEDEVENT stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC RELOADFAILEDEVENT: ${JSON.stringify(err, null, 2)}`); + } + return false; } } @@ -196,7 +208,19 @@ function getReloadFailedLogConfig() { return cfg; } catch (err) { - globals.logger.error(`NEWRELIC RELOADFAILEDLOG: ${err}`); + if (err.message) { + globals.logger.error(`NEWRELIC RELOADFAILEDLOG message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`NEWRELIC RELOADFAILEDLOG stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC RELOADFAILEDLOG: ${JSON.stringify(err, null, 2)}`); + } + return false; } } @@ -263,7 +287,19 @@ function getReloadAbortedEventConfig() { return cfg; } catch (err) { - globals.logger.error(`NEWRELIC RELOADABORTEDEVENT: ${err}`); + if (err.message) { + globals.logger.error(`NEWRELIC RELOADABORTEDEVENT message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`NEWRELIC RELOADABORTEDEVENT stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC RELOADABORTEDEVENT: ${JSON.stringify(err, null, 2)}`); + } + return false; } } @@ -327,7 +363,19 @@ function getReloadAbortedLogConfig() { return cfg; } catch (err) { - globals.logger.error(`NEWRELIC RELOADABORTEDLOG: ${err}`); + if (err.message) { + globals.logger.error(`NEWRELIC RELOADABORTEDLOG message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`NEWRELIC RELOADABORTEDLOG stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC RELOADABORTEDLOG: ${JSON.stringify(err, null, 2)}`); + } + return false; } } @@ -404,6 +452,11 @@ async function sendNewRelicEvent(incidentConfig, reloadParams, destNewRelicAccou if (err.stack) { globals.logger.error(`NEWRELIC 1 stack: ${err.stack}`); } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC 1: ${JSON.stringify(err, null, 2)}`); + } } } @@ -532,6 +585,10 @@ async function sendNewRelicLog(incidentConfig, reloadParams, destNewRelicAccount globals.logger.error(`NEWRELIC 2 stack: ${err.stack}`); } + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`NEWRELIC 2: ${JSON.stringify(err, null, 2)}`); + } } } @@ -653,7 +710,19 @@ async function sendReloadTaskFailureEvent(reloadParams) { sendNewRelicEvent(incidentConfig, params, destNewRelicAccounts); return null; } catch (err) { - globals.logger.error(`TASK FAILED NEWRELIC: ${err}`); + if (err.message) { + globals.logger.error(`TASK FAILED NEWRELIC 1 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`TASK FAILED NEWRELIC 1 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`TASK FAILED NEWRELIC 1: ${JSON.stringify(err, null, 2)}`); + } + return null; } }) @@ -781,7 +850,19 @@ async function sendReloadTaskFailureLog(reloadParams) { sendNewRelicLog(incidentConfig, params, destNewRelicAccounts); return null; } catch (err) { - globals.logger.error(`TASK FAILED NEWRELIC: ${err}`); + if (err.message) { + globals.logger.error(`TASK FAILED NEWRELIC 2 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`TASK FAILED NEWRELIC 2 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`TASK FAILED NEWRELIC 2: ${JSON.stringify(err, null, 2)}`); + } + return null; } }) @@ -911,7 +992,19 @@ function sendReloadTaskAbortedEvent(reloadParams) { sendNewRelicEvent(incidentConfig, params, destNewRelicAccounts); return null; } catch (err) { - globals.logger.error(`TASK ABORT NEWRELIC: ${err}`); + if (err.message) { + globals.logger.error(`TASK ABORT NEWRELIC 1 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`TASK ABORT NEWRELIC 1 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`TASK ABORT NEWRELIC 1: ${JSON.stringify(err, null, 2)}`); + } + return null; } }) @@ -1041,7 +1134,19 @@ function sendReloadTaskAbortedLog(reloadParams) { sendNewRelicLog(incidentConfig, params, destNewRelicAccounts); return null; } catch (err) { - globals.logger.error(`TASK ABORT NEWRELIC: ${err}`); + if (err.message) { + globals.logger.error(`TASK ABORT NEWRELIC 2 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`TASK ABORT NEWRELIC 2 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`TASK ABORT NEWRELIC 2: ${JSON.stringify(err, null, 2)}`); + } + return null; } }) diff --git a/src/lib/msteams_notification.js b/src/lib/msteams_notification.js index 8389365b..5184bd44 100644 --- a/src/lib/msteams_notification.js +++ b/src/lib/msteams_notification.js @@ -4,6 +4,7 @@ const handlebars = require('handlebars'); const { RateLimiterMemory } = require('rate-limiter-flexible'); const globals = require('../globals'); +const { getAppOwner } = require('../qrs_util/get_app_owner'); let rateLimiterMemoryFailedReloads; let rateLimiterMemoryAbortedReloads; @@ -433,6 +434,9 @@ function sendReloadTaskFailureNotificationTeams(reloadParams) { return 1; } + // Get app owner + const appOwner = await getAppOwner(reloadParams.appId); + // Get script logs, if enabled in the config file const scriptLogData = reloadParams.scriptLog; @@ -491,6 +495,10 @@ function sendReloadTaskFailureNotificationTeams(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // Check if script log is longer than 3000 characters. Truncate if so. @@ -572,6 +580,9 @@ function sendReloadTaskAbortedNotificationTeams(reloadParams) { return 1; } + // Get app owner + const appOwner = await getAppOwner(reloadParams.appId); + // Get script logs, if enabled in the config file const scriptLogData = reloadParams.scriptLog; @@ -629,6 +640,10 @@ function sendReloadTaskAbortedNotificationTeams(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // Check if script log is longer than 3000 characters. Truncate if so. diff --git a/src/lib/slack_notification.js b/src/lib/slack_notification.js index 97969856..97336ae3 100644 --- a/src/lib/slack_notification.js +++ b/src/lib/slack_notification.js @@ -6,6 +6,7 @@ const { RateLimiterMemory } = require('rate-limiter-flexible'); const globals = require('../globals'); const slackApi = require('./slack_api'); +const { getAppOwner } = require('../qrs_util/get_app_owner'); let rateLimiterMemoryFailedReloads; let rateLimiterMemoryAbortedReloads; @@ -475,6 +476,9 @@ function sendReloadTaskFailureNotificationSlack(reloadParams) { return 1; } + // Get app owner + const appOwner = await getAppOwner(reloadParams.appId); + // Get script logs, if enabled in the config file const scriptLogData = reloadParams.scriptLog; @@ -535,6 +539,10 @@ function sendReloadTaskFailureNotificationSlack(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // Replace all single and dpouble quotes in scriptLogHead and scriptLogTail with escaped dittos @@ -630,6 +638,9 @@ function sendReloadTaskAbortedNotificationSlack(reloadParams) { return 1; } + // Get app owner + const appOwner = await getAppOwner(reloadParams.appId); + // Get script logs, if enabled in the config file const scriptLogData = reloadParams.scriptLog; @@ -688,6 +699,10 @@ function sendReloadTaskAbortedNotificationSlack(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // Check if script log is longer than 3000 characters, which is max for text fields sent to Slack API diff --git a/src/lib/smtp.js b/src/lib/smtp.js index 81ce40a9..3aa5f3c4 100644 --- a/src/lib/smtp.js +++ b/src/lib/smtp.js @@ -420,12 +420,14 @@ async function sendReloadTaskFailureNotificationEmail(reloadParams) { } } + // Get app owner + const appOwner = await qrsUtil.getAppOwner.getAppOwner(reloadParams.appId); + // If enabled in config file: Add app owners (excluding those that don't have an email address!) to list of recipients // 3 Should app owners get alerts? if (globals.config.get('Butler.emailNotification.reloadTaskFailure.appOwnerAlert.enable') === true) { // App owners (at least some of them - maybe all) should get notification email // 3.1 Yes: Should all app owners get alerts? - const appOwner = await qrsUtil.getAppOwner.getAppOwner(reloadParams.appId); let appOwnerSendList = []; // If the current app's owner doesn't have an email address there is nothing to do @@ -549,6 +551,10 @@ async function sendReloadTaskFailureNotificationEmail(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // eslint-disable-next-line no-restricted-syntax @@ -665,12 +671,14 @@ async function sendReloadTaskAbortedNotificationEmail(reloadParams) { } } + // Get app owner + const appOwner = await qrsUtil.getAppOwner.getAppOwner(reloadParams.appId); + // If enabled in config file: Add app owners (excluding those that don't have an email address!) to list of recipients // 3 Should app owners get alerts? if (globals.config.get('Butler.emailNotification.reloadTaskAborted.appOwnerAlert.enable') === true) { // App owners (at least some of them - maybe all) should get notification email // 3.1 Yes: Should all app owners get alerts? - const appOwner = await qrsUtil.getAppOwner.getAppOwner(reloadParams.appId); let appOwnerSendList = []; // If the current app's owner doesn't have an email address there is nothing to do @@ -789,6 +797,10 @@ async function sendReloadTaskAbortedNotificationEmail(reloadParams) { scriptLogHeadCount: scriptLogData.scriptLogHeadCount, qlikSenseQMC: senseUrls.qmcUrl, qlikSenseHub: senseUrls.hubUrl, + appOwnerName: appOwner.userName, + appOwnerUserId: appOwner.userId, + appOwnerUserDirectory: appOwner.directory, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', }; // eslint-disable-next-line no-restricted-syntax diff --git a/src/lib/webhook_notification.js b/src/lib/webhook_notification.js index 06e359f0..e9632668 100644 --- a/src/lib/webhook_notification.js +++ b/src/lib/webhook_notification.js @@ -2,6 +2,7 @@ const { RateLimiterMemory } = require('rate-limiter-flexible'); const axios = require('axios'); const globals = require('../globals'); +const { getAppOwner } = require('../qrs_util/get_app_owner'); let rateLimiterMemoryFailedReloads; let rateLimiterMemoryAbortedReloads; @@ -143,6 +144,9 @@ async function sendOutgoingWebhook(webhookConfig, reloadParams) { try { // webhookConfig.webhooks contains an array of all outgoing webhooks that should be processed + // Get app owner + const appOwner = await getAppOwner(reloadParams.appId); + // eslint-disable-next-line no-restricted-syntax for (const webhook of webhookConfig.webhooks) { globals.logger.debug(`WEBHOOKOUT: Processing webhook ${JSON.stringify(webhook)}`); @@ -179,6 +183,10 @@ async function sendOutgoingWebhook(webhookConfig, reloadParams) { params.append('taskId', reloadParams.taskId); params.append('appName', reloadParams.appName); params.append('appId', reloadParams.appId); + params.append('appOwnerName', appOwner.userName); + params.append('appOwnerUserDirectory', appOwner.directory); + params.append('appOwnerUserId', appOwner.userId); + params.append('appOwnerEmail', appOwner.emails?.length > 0 ? appOwner.emails[0] : ''); params.append('logTimeStamp', reloadParams.logTimeStamp); params.append('logLevel', reloadParams.logLevel); params.append('executionId', reloadParams.executionId); @@ -207,6 +215,10 @@ async function sendOutgoingWebhook(webhookConfig, reloadParams) { taskId: reloadParams.taskId, appName: reloadParams.appName, appId: reloadParams.appId, + appOwnerName: appOwner.userName, + appOwnerUserDirectory: appOwner.directory, + appOwnerUserId: appOwner.userId, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', logTimeStamp: reloadParams.logTimeStamp, logLevel: reloadParams.logLevel, executionId: reloadParams.executionId, @@ -228,6 +240,10 @@ async function sendOutgoingWebhook(webhookConfig, reloadParams) { taskId: reloadParams.taskId, appName: reloadParams.appName, appId: reloadParams.appId, + appOwnerName: appOwner.userName, + appOwnerUserDirectory: appOwner.directory, + appOwnerUserId: appOwner.userId, + appOwnerEmail: appOwner.emails?.length > 0 ? appOwner.emails[0] : '', logTimeStamp: reloadParams.logTimeStamp, logLevel: reloadParams.logLevel, executionId: reloadParams.executionId, @@ -242,7 +258,18 @@ async function sendOutgoingWebhook(webhookConfig, reloadParams) { globals.logger.debug(`WEBHOOKOUT: Webhook response: ${response}`); } } catch (err) { - globals.logger.error(`WEBHOOKOUT: ${JSON.stringify(err, null, 2)}`); + if (err.message) { + globals.logger.error(`WEBHOOKOUT 1 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`WEBHOOKOUT 1 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`WEBHOOKOUT 1: ${JSON.stringify(err, null, 2)}`); + } } } @@ -345,7 +372,18 @@ async function sendOutgoingWebhookServiceMonitor(webhookConfig, serviceParams) { globals.logger.debug(`SERVICE MONITOR SEND WEBHOOK: Webhook response: ${response}`); } } catch (err) { - globals.logger.error(`SERVICE MONITOR SEND WEBHOOK: ${JSON.stringify(err, null, 2)}`); + if (err.message) { + globals.logger.error(`SERVICE MONITOR SEND WEBHOOK 1 message: ${err.message}`); + } + + if (err.stack) { + globals.logger.error(`SERVICE MONITOR SEND WEBHOOK 1 stack: ${err.stack}`); + } + + // If neither message nor stack is available, just log the error object + if (!err.message && !err.stack) { + globals.logger.error(`SERVICE MONITOR SEND WEBHOOK 1: ${JSON.stringify(err, null, 2)}`); + } } }