Skip to content

Commit

Permalink
feat(qs-cloud): Reload failed alerts to email
Browse files Browse the repository at this point in the history
Partly implements #1196
  • Loading branch information
Göran Sander committed Sep 26, 2024
1 parent fa35d91 commit 1d4a62d
Show file tree
Hide file tree
Showing 13 changed files with 1,004 additions and 97 deletions.
175 changes: 175 additions & 0 deletions src/config/email_templates/failed-reload-qscloud.handlebars
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<h1>Qlik Sense Cloud app reload failed</h1>
<p>


<table>
<tbody>
<tr>
<td style="padding-right: 20px;">
<strong>App name</strong><br>
{{appName}}
</td>
<td style="padding-left: 20px;">
<strong>App ID</strong><br>
{{appId}}
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>App description</strong><br>
{{appDescription}}
</td>
<td style="padding-left: 20px;">
<strong>Link to app</strong><br>
{{appUrl}}
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>App owner</strong><br>
{{appOwnerName}}
</td>
<td style="padding-left: 20px;">
<strong>App owner email</strong><br>
{{appOwnerEmail}}
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>Tenant ID</strong><br>
{{tenantId}}
</td>
<td style="padding-left: 20px;">
<strong>Tenant comment</strong><br>
{{tenantComment}}
</td>
</tr>

<tr>
<td colspan="2"><hr></td>
</tr>

<tr>
<td style="padding-right: 20px;">
<strong>Reload started</strong><br>
{{executionStartTime.startTimeLocal1}}
</td>
<td style="padding-left: 20px;">
<strong>Reload ended</strong><br>
{{executionStopTime.stopTimeLocal1}}
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>Duration</strong><br>
{{executionDuration.hours}} hours, {{executionDuration.minutes}} minutes, {{executionDuration.seconds}} seconds
</td>
<td style="padding-left: 20px;">
</td>
</tr>

<tr>
<td style="padding-right: 20px;">
<strong>Trigger</strong><br>
{{reloadTrigger}}
</td>
<td style="padding-left: 20px;">
<strong>Reload ID</strong><br>
{{reloadId}}
</td>
</tr>

<tr>
<td colspan="2"><hr></td>
</tr>

<tr></tr>
<td style="padding-right: 20px;">
<strong>Error message</strong><br>
{{errorMessage}}
</td>
<td style="padding-left: 20px;">
<strong>Error code</strong><br>
{{errorCode}}
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>Execution result</strong><br>
{{executionStatusText}}
</td>
<td style="padding-left: 20px;">
<strong></strong><br>
</td>
</tr>
<tr>
<td style="padding-right: 20px;">
<strong>Peak memory bytes</strong><br>
{{peakMemoryBytes}}
</td>
<td style="padding-left: 20px;">
<strong>Failed due to memory constraint</strong><br>
{{endedWithMemoryConstraint}}
</td>
</tr>

<tr>
<td colspan="2"><br></td>
</tr>

<tr>
<td colspan="2">
<a href="{{qlikSenseQMC}}" style="display: inline-block; padding: 10px 20px; font-size: 16px; color: black; background-color: #00b140; text-align: center; text-decoration: none; border-radius: 5px; margin: 5px;">Qlik Sense QMC</a>
<a href="{{qlikSenseHub}}" style="display: inline-block; padding: 10px 20px; font-size: 16px; color: black; background-color: #00b140; text-align: center; text-decoration: none; border-radius: 5px; margin: 5px;">Qlik Sense Hub</a>
<a href="{{appUrl}}" style="display: inline-block; padding: 10px 20px; font-size: 16px; color: black; background-color: #00b140; text-align: center; text-decoration: none; border-radius: 5px; margin: 5px;">Open app</a>
</td>
</tr>

<tr>
<td colspan="2"><br></td>
</tr>

<tr>
<td colspan="2">
<strong>Log message</strong>
</td>
</tr>
<tr>
<td colspan="2">
<pre>{{logMessage}}</pre>
</td>
</tr>

<tr>
<td colspan="2"><hr></td>
</tr>

<tr>
<td colspan="2">
The script log contains {{scriptLogSize}} rows in total. Here are the first ones:
</td>
</tr>
<tr>
<td colspan="2">
<pre>{{scriptLogHead}}</pre>
</td>
</tr>

<tr>
<td colspan="2"><br></td>
</tr>

<tr>
<td colspan="2">
Here are the last {{scriptLogTailCount}} rows:
</td>
</tr>
<tr>
<td colspan="2">
<pre>{{scriptLogTail}}</pre>
</td>
</tr>


</tbody>
</table>
20 changes: 9 additions & 11 deletions src/config/production_template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -984,31 +984,29 @@ Butler:
# Reload failure notifications assume a log appender is configured in Sense AND that the UDP server in Butler is running.
emailNotification:
reloadAppFailure:
enable: false
basicContentOnly: false
enable: false # Enable/disable app reload failed notifications via email
appOwnerAlert:
enable: false # Should app owner get notification email (assuming email address is available in Sense user directory)
enable: false # Should app owner get notification email (assuming email address is available in Sense)?
includeOwner:
includeAll: true # true = Send notification to all app owners except those in exclude list
# false = Send notification to app owners in the include list
user:
- directory: <directory>
userId: <userId>
user: # Array of app owner email addresses that should get notifications
# - email: anna@somecompany.com
# - email: joe@somecompany.com
excludeOwner:
user:
- directory: <directory>
userId: <userId>
# - email: daniel@somecompany.com
rateLimit: 60 # Min seconds between emails for a given taskID. Defaults to 5 minutes.
headScriptLogLines: 15
tailScriptLogLines: 25
priority: high # high/normal/low
subject: '❌ Qlik Sense reload failed: "{{taskName}}"'
bodyFileDirectory: /path/to//email_templates
htmlTemplateFile: failed-reload-qs-cloud
htmlTemplateFile: failed-reload-qscloud
fromAdress: Qlik Sense (no-reply) <qliksense-noreply@ptarmiganlabs.com>
recipients:
- user.joe@company.com
- user.anna@company.com
# - emma@somecompany.com
# - patrick@somecompany.com

# Certificates to use when connecting to Sense. Get these from the Certificate Export in QMC.
cert:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
"type": "section",
"text": {
"type": "mrkdwn",
"text": "The script log contains {{scriptLogSize}} rows in total. Here are the first lines:"
"text": "The script log contains {{scriptLogSize}} rows in total. Here are the first ones:"
}
},
{
Expand Down
42 changes: 13 additions & 29 deletions src/lib/assert/assert_config_file.js
Original file line number Diff line number Diff line change
Expand Up @@ -4367,13 +4367,6 @@ export const configFileStructureAssert = async (config, logger) => {
configFileCorrect = false;
}

if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly')) {
logger.error(
'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.basicContentOnly"',
);
configFileCorrect = false;
}

if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable')) {
logger.error(
'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable"',
Expand All @@ -4393,8 +4386,7 @@ export const configFileStructureAssert = async (config, logger) => {
}

// Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user is an array of objects with the following properties:
// - directory: 'string'
// - userId: 'string'
// - email: 'string'
if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user')) {
const users = config.get(
'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user',
Expand All @@ -4414,15 +4406,9 @@ export const configFileStructureAssert = async (config, logger) => {
);
configFileCorrect = false;
} else {
if (!Object.prototype.hasOwnProperty.call(user, 'directory')) {
logger.error(
`ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`,
);
configFileCorrect = false;
}
if (!Object.prototype.hasOwnProperty.call(user, 'userId')) {
if (!Object.prototype.hasOwnProperty.call(user, 'email')) {
logger.error(
`ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`,
`ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.includeOwner.user[${index}]"`,
);
configFileCorrect = false;
}
Expand All @@ -4438,8 +4424,7 @@ export const configFileStructureAssert = async (config, logger) => {
}

// Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user is an array of objects with the following properties:
// - directory: 'string'
// - userId: 'string'
// - email: 'string'
if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user')) {
const users = config.get(
'Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user',
Expand All @@ -4459,15 +4444,9 @@ export const configFileStructureAssert = async (config, logger) => {
);
configFileCorrect = false;
} else {
if (!Object.prototype.hasOwnProperty.call(user, 'directory')) {
if (!Object.prototype.hasOwnProperty.call(user, 'email')) {
logger.error(
`ASSERT CONFIG: Missing property "directory" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`,
);
configFileCorrect = false;
}
if (!Object.prototype.hasOwnProperty.call(user, 'userId')) {
logger.error(
`ASSERT CONFIG: Missing property "userId" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`,
`ASSERT CONFIG: Missing property "email" in "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.excludeOwner.user[${index}]"`,
);
configFileCorrect = false;
}
Expand Down Expand Up @@ -4531,14 +4510,15 @@ export const configFileStructureAssert = async (config, logger) => {
configFileCorrect = false;
}

if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress')) {
if (!config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress')) {
logger.error(
'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress"',
'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress"',
);
configFileCorrect = false;
}

// Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients is an array of strings
// It is ok for the array to be empty
if (config.has('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients')) {
const recipients = config.get('Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients');

Expand All @@ -4558,6 +4538,10 @@ export const configFileStructureAssert = async (config, logger) => {
}
});
}
} else if (recipients === null) {
logger.warn(
'ASSERT CONFIG: No recipients defined for Qlik Sense cloud alert emails, "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients" is empty.',
);
} else {
logger.error(
'ASSERT CONFIG: Missing config file entry "Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients"',
Expand Down
6 changes: 3 additions & 3 deletions src/lib/config_obfuscate.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ function configObfuscate(config) {
}),
);

// Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress, keep first 5 chars, mask the rest with *
obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress =
obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAdress.substring(0, 5) +
// Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress, keep first 5 chars, mask the rest with *
obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress =
obfuscatedConfig.Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.fromAddress.substring(0, 5) +
'*'.repeat(10);

// Obfuscate Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients
Expand Down
22 changes: 22 additions & 0 deletions src/lib/guid_util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import globals from '../globals.js';

// Function to verify if a string is a valid GUID
// Parameters:
// - guid: string to verify
// Returns:
// - true if guid is valid, false otherwise
export const verifyGuid = (guid) => {
try {
// Construct a new RegExp object matching guids
const guidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/;

if (guidRegExp.test(guid) === true) {
globals.logger.verbose(`GUID VERIFY: GUID is valid: ${guid}`);
return true;
}
globals.logger.warn(`GUID VERIFY: GUID not valid: ${guid}`);
} catch (err) {
globals.logger.error(`GUID VERIFY: Error verifying GUID: ${err}`);
}
return false;
};
Loading

0 comments on commit 1d4a62d

Please sign in to comment.