This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Reload alerts for Qlik Sense Cloud

Butler offers a lot of flexibility when it comes to alerts when reloads fai in Qlik Sense Cloud.

Learn how to set up the desired features, the alert layout, formatting and more.

Warning

In general the information related to an app that failed reloading will not be very sensitive.
It’s app name, owner id, tenant id etc.

If this information is considered sensitive in your organization, you should consider the security implications of sending this information to service like Butler via the public Internet. The traffic will be https encrypted, but even so, the information will be sent over the public Internet.

As always, make sure to follow your organization’s security guidelines. Think before you act.

Alert types

These alert types are available:

  • Reload task failure. Send alerts when app reloads fail.

Alert destinations and options

Alerts can be sent to these destinations, with different options available for each destination.
Each destination can be individually enabled/disabled in the config file.

Destination App reload failure Enable/disable alert per app Alerts to app owners Flexible formatting Basic formatting Comment
Email
Slack
MS Teams

How it works

Somehow Butler needs to be notified when a reload task in Qlik Sense Cloud fails.

The only way to do this is currently (2024 October) to use Qlik Cloud’s outgoing webhooks, and have them triggered when the app reload fails.

So, the outbound webhook should call some URL it can reach.
In practice this means a URL on the public Internet.

This could be a Butler provided endpoint, but exposing Butler to the public Internet is not a good idea from a security perspective.

There are various ways to solve this, each described below.
More options for brining Qlik Cloud events to Butler may be added in the future.

Option 1: Azure function forwarding event to MQTT

While this solution may be seen as a bit complex, it does offer some advantages:

  • No need to expose Butler to the public Internet.
  • The http-to-MQTT gateway is a minimal service that can be run as a serverless function in your cloud provider of choice, or on-premise in a de-militarized zone (DMZ). The point is that it’s a very small and simple service that both is easier to deploy and to secure, compared to a full Butler instance.
  • Not having complex services like Butler exposed to the public Internet is a good security practice.
  • The http-to-MQTT gateway can be used for other purposes too, such as sending MQTT messages to Butler when other events occur in your Qlik Cloud environment.
  • The http-to-MQTT gateway can be used to send MQTT messages to other systems too, not just Butler.
  • By exposing several https endpoints, The http-to-MQTT gateway can be used to send MQTT messages to Butler when events occur in other systems, not just Qlik Cloud.
  • By using a serverless function in a cloud provider, the solution scales well and can benefit from the cloud provider’s security features.
  • Low cost. The solution can even be run on a free tier in most cloud providers, and MQTT services are usually very cheap for the message volume in this scenario (one message per failed app reload).
  • Fast. Events typically reach Butler within a few seconds after they occur in Qlik Cloud.

Downsides include:

  • The solution is a bit complex to set up.
  • The solution requires the http-to-MQTT gateway to be up and running at all times.
  • A Internet connected MQTT broker is needed. There are several cloud based MQTT brokers available though.

The solution looks like this:

Sending app reload failure alerts from Qlik Cloud to Butler via an Azure function and MQTT

The webhook in Qlik Cloud is set up to call an Azure function when an app reload completes. The Azure function then sends an MQTT message to Butler.

The webhook is defined like this:

Qlik Cloud webhook definition

The webhook secret can be used in the gateway to verify that the webhook call is coming from an approved Qlik Cloud tenant.

Future options

Various solutions are possible, including:

  • Qlik Cloud supporting other notification mechanisms, such as sending MQTT messages when app reloads fail.
  • Qlik Cloud Application Automations supporting MQTT. The failed app reload could then be captured via an automation, and there forwarded to Butler via MQTT.
  • Using a 3rd party service that runs a webhook-to-MQTT gateway in the cloud.

If the basic assumption is that you want to expose as little attack surface on the Internet as possible, the solution will most likely involve some kind of intermediate service that can be reached by Qlik Cloud, and that can in turn asynchronously forward the event to Butler.

Setting up the http-to-MQTT gateway

An Azure Function App is used in this example, but the same concept can be used with other cloud providers, or on-premise.
In the example below the Azure function is written in Node.js.

Note that the code below is a basic example that should be extended before being used in a production environment:

  • Add proper error handling and logging
  • Add better security, using the Qlik Cloud webhook secret to verify that the incoming request is coming from an approved Qlik Cloud tenant.
  • The function does verify that the incoming request has a http header named x-myheader-foo1, but it does not check the value of that header. Room for improvement there.

All in all the function does work, it has been in test use for some months and should serve well as a starting point for your own implementation.

import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
import { connectAsync } from 'mqtt';

export async function qscloudreload(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    context.log(`Http function processed request for url "${request.url}"`);
    context.log(`Request method: ${request.method}`);


    // Get query string parameters
    const query = Object.fromEntries(request.query.entries());
    context.log(`Request query:\n${JSON.stringify(query, null, 2)}`);

    // Ensure there are no query string parameters
    if (Object.keys(query).length > 0) {
        context.log('Too many query string parameters. Expected none.');
        return {
            status: 400,
            body: 'Invalid query string parameters'
        };
    }

    // -----------------------------------------------------
    // Get headers
    const headers = Object.fromEntries(request.headers.entries());
    context.log(`Request headers:\n${JSON.stringify(headers, null, 2)}`);

    // Ensure the correct headers are present
    // The following headers are required:
    // - accept-encoding: gzip
    // - client-ip: <The IP address of the client making the request>
    // - content-length: <The length of the request body>
    // - content-type: application/json
    // - host: <The host name of the function app>
    // -  qlik-signature: <The Qlik Sense Cloud signature of the request>
    // - user-agent: Qlik Webhook
    // - x-forwarded-proto: https
    // - x-forwarded-tlsversion: 1.3
    //
    // Custom https headers (must also be present):
    // - x-myheader-foo1: bar1

    const requiredHeaders = [
        'accept-encoding',
        'client-ip',
        'content-length',
        'content-type',
        'host',
        'qlik-signature',
        'user-agent',
        'x-forwarded-proto',
        'x-forwarded-tlsversion',
        'x-myheader-foo1'
    ];

    for (const header of requiredHeaders) {
        if (!headers[header]) {
            context.log(`Missing required header: ${header}`);
            return {
                status: 400,
                body: `Missing required header`
            };
        }
    }

    // Make sure select headers contain correct values
    // - accept-encoding: gzip
    // - content-type: application/json
    // - user-agent: Qlik Webhook
    // - x-forwarded-proto: https
    // - x-forwarded-tlsversion: 1.2 | 1.3
    if (headers['accept-encoding'] !== 'gzip') {
        context.log(`Invalid header value for accept-encoding: ${headers['accept-encoding']}`);
        return {
            status: 400,
            body: `Invalid header value for accept-encoding`
        };
    }

    if (headers['content-type'] !== 'application/json') {
        context.log(`Invalid header value for content-type: ${headers['content-type']}`);
        return {
            status: 400,
            body: `Invalid header value for content-type`
        };
    }

    if (headers['user-agent'] !== 'Qlik Webhook') {
        context.log(`Invalid header value for user-agent: ${headers['user-agent']}`);
        return {
            status: 400,
            body: `Invalid header value for user-agent`
        };
    }

    if (headers['x-forwarded-proto'] !== 'https') {
        context.log(`Invalid header value for x-forwarded-proto: ${headers['x-forwarded-proto']}`);
        return {
            status: 400,
            body: `Invalid header value for x-forwarded-proto`
        };
    }

    if (headers['x-forwarded-tlsversion'] !== '1.2' && headers['x-forwarded-tlsversion'] !== '1.3') {
        context.log(`Invalid header value for x-forwarded-tlsversion: ${headers['x-forwarded-tlsversion']}`);
        return {
            status: 400,
            body: `Invalid header value for x-forwarded-tlsversion`
        };
    }
    
    // -----------------------------------------------------
    // Get request body
    let body: any = JSON.parse(await request.text());
    let bodyString = JSON.stringify(body, null, 2);
    context.log(`Request body:\n${bodyString}`);

    // Make sure the request body contains the expected properties
    // The following properties are required:
    // - cloudEventsVersion: 0.1
    // - source: com.qlik/engine,
    // - contentType: application/json,
    // - eventId: b0f5c473-5dea-4d7e-a188-5e0b904cde33,
    // - eventTime: 2024-07-27T13:57:27Z,
    // - eventTypeVersion: 1.0.0,
    // - eventType: com.qlik.v1.app.reload.finished,
    // - extensions: <object with the following properties>
    // -   ownerId: <userID of the owner of the Qlik Sense resource that triggered the event>
    // -   tenantId: <tenantID of the Qlik Sense tenant that contains the Qlik Sense resource that triggered the event>
    // -   userId: <userID of the user that triggered the event>
    // data: <object>
    const requiredProperties = [
        'cloudEventsVersion',
        'source',
        'contentType',
        'eventId',
        'eventTime',
        'eventTypeVersion',
        'eventType',
        'extensions',
        'data'
    ];

    for (const property of requiredProperties) {
        if (!body[property]) {
            context.log(`Missing required body property: ${property}`);
            return {
                status: 400,
                body: `Missing required body property`
            };
        }
    }

    // Make sure the extensions object contains the expected properties
    // The following properties are required:
    // - ownerId: <userID of the owner of the Qlik Sense resource that triggered the event>
    // - tenantId: <tenantID of the Qlik Sense tenant that contains the Qlik Sense resource that triggered the event>
    // - userId: <userID of the user that triggered the event>
    const extensions = body.extensions;
    const extensionsProperties = [
        'ownerId',
        'tenantId',
        'userId'
    ];

    for (const property of extensionsProperties) {
        if (!extensions[property]) {
            context.log(`Missing required extensions property in request body: ${property}`);
            return {
                status: 400,
                body: `Missing required extensions property`
            };
        }
    }

    // Make sure select properties contain correct values
    // - cloudEventsVersion: 0.1
    // - contentType: application/json
    if (body.cloudEventsVersion !== '0.1') {
        context.log(`Invalid body value for cloudEventsVersion: ${body.cloudEventsVersion}`);
        return {
            status: 400,
            body: `Invalid body value for cloudEventsVersion`
        };
    } 

    if (body.contentType !== 'application/json') {
        context.log(`Invalid body value for contentType: ${body.contentType}`);
        return {
            status: 400,
            body: `Invalid body value for contentType`
        };
    }

    // -----------------------------------------------------
    // Forward message to MQTT broker
    const brokerHost = 'hostname.of.mqtt.broker';
    const brokerPort = 8765;
    const mqttClient = await connectAsync(`mqtts://${brokerHost}:${brokerPort}`, {
        username: 'my-username',
        password: 'my-password',
    });
    const topic = `qscloud/app/reload/${body?.extensions?.tenantId}`;
    context.log(`Using MQTT topic: ${topic}`);

    context.log('MQTT client connected');
    mqttClient.publish(topic, bodyString, (err) => {
        if (err) {
            context.log(`Error publishing message: ${err}`);
        }
    });

    context.log('Message published');
    await mqttClient.endAsync();
    context.log('MQTT client disconnected');

    // Return a 200 response
    return {
        status: 200,
        // body: `Body received:\n${bodyString}`
        body: `OK. Message received.`
    };

};

app.http('qscloudreload', {
    methods: ['POST'],
    authLevel: 'anonymous',
    handler: qscloudreload
});

Customizing the alerts

The alerts can be customized in the same ways as for Qlik Sense client-managed. More info at links below.

References

1 - Qlik Cloud reload alerts sent as emails

Description of the various kinds of alert emails Butler can send when an app reload fails in Qlik Cloud.

What’s this?

Butler can send these alert emails:

  • When an app reload fails during execution.

See the Concepts section for additional details and sample alert emails.

Basic vs formatted email alerts

If you want Butler to send email alerts you must provide an email template file.

For some other alert destinations (Slack and Teams) Butler offers a “basic” option. A fixed format alert is then sent by Butler.
This is not the case for email alerts - there you must provide Butler with a template file.

Rate limiting and de-duplication

Butler has rate limiting feature to ensure alert recipients are not spammed with too many alert emails.

The rate limit is configured (in seconds) in the main config file in the Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.rateLimit setting in the config file.

Rate limiting is done based on app reload ID + email address.

Butler also has a de-duplication feature that ensure each email address that has qualified for an alert email only gets ONE email per alert, even if the email address (for example) appears as both an app owner and is specified via an app tag.

Sending test emails to verify correct settings

See the same section for client-managed Qlik Sense.
The commands are identical.

Sending alert emails to app owners

Butler can optionally send alert emails to the owner of apps that fail reloading.

Note

App owner notification email can only be sent to app owners that have an email stored in their Qlik Cloud user profile.

If there is no email available for an app owner, he/she will simply not receive any alert emails.

This feature is controlled by the config file properties Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.appOwnerAlert.enable.

If set to true the app owner will be added to the send list of alert emails, in addition to the recipients specied in Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.recipients.

The sections of the config file dealing with app owner notification emails looks like this:

appOwnerAlert:
  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:                    # Array of app owner email addresses that should get notifications
      # - email: anna@somecompany.com
      # - email: joe@somecompany.com
  excludeOwner:
    user:
      # - email: daniel@somecompany.com

It works like this:

  • If appOwnerAlert.enable is set to false no app owner emails will be sent. If it’s set to true the rules below apply.
  • If appOwnerAlert.includeOwner.includeAll is set to true all app owners will get notification emails when apps the own fail/are aborted…
    • … except those app owners listed in the appOwnerAlert.excludeOwner.user array.
    • That array thus provides a way to exclude some app owners (e.g. system accounts) to receive notifcation emails.
  • If appOwnerAlert.includeOwner.includeAll is set to false it’s still possible to add individual app owners to the appOwnerAlert.includeOwner.user array.
    Those users will then receive notification emails for apps they own.

Send alerts only for some apps

Some apps may be more important than others.
I.e. some apps should result in alert emails when they fail reloading, but others not.

Butler controls which app reload failures cause email alerts by looking at a specific app tag.

  • If the config file setting Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.enable is set to false, all failed app reloads will result in alert emails.
  • If that setting is true only some apps will cause alert emails when their reload fails:
    • If an app has the tag specified in Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.alertEnableByTag.tag, an email alert will be sent for that app if it fails reloading.
    • If an app does not have that tag set, no alert will be sent for that app.

Some configuration in Sense is needed to make this work:

  1. Make changes to the config file. Specifically the settings mentioned above needs to be reviewed and (probably) updated.
  2. In Qlik Cloud, tag the apps that should cause alert emails when they fail reloading.
    1. Use the same tag as specified in the config file.

Looks like this in Qlik Sense Cloud:

Tagging apps for reload failed alerts in Qlik Sense Cloud

How it works

The concept is the same for all alert types, see the this page for details.

Settings in config file

---
Butler:
  ...
  ...
  mqttConfig:
    enable: false                                     # Should Qlik Sense events be forwarded as MQTT messages?
    ...
    ...
    qlikSenseCloud:                                                   # MQTT settings for Qlik Sense Cloud integration
      event:                                                          
        mqttForward:                                                  # QS Cloud events forwarded to MQTT topics, which Butler will subscribe to
          enable: false
          broker:                                                     # Settings for MQTT broker to which QS Cloud events are forwarded
            host: mqttbroker.company.com
            port: <port>
            username: <username>
            password: <password>
          topic:
            subscriptionRoot: qscloud/#                     # Topic that Butler will subscribe to
            appReload: qscloud/app/reload
  ...
  ...
  qlikSenseCloud:                   # Settings for Qlik Sense Cloud integration
    enable: false
    event:
      mqtt:                         # Which QS Cloud tenant should Butler receive events from, in the form of MQTT messages?
        tenant:
          id: tenant.region.qlikcloud.com
          tenantUrl: https://tenant.region.qlikcloud.com
          authType: jwt             # Authentication type used to connect to the tenant. Valid options are "jwt"  
          auth:
            jwt:
              token: <JWT token>    # JWT token used to authenticate Butler when connecting to the tenant
          # Qlik Sense Cloud related links used in notification messages
          qlikSenseUrls:
            qmc: <URL to QMC in Qlik Sense Cloud>
            hub: <URL to Hub in Qlik Sense Cloud>
          comment: This is a comment describing the tenant and its settings # Informational only
          alert:
            ...
            ...
            # Settings needed to send email notifications when for example reload tasks fail.
            # Reload failure notifications assume a log appender is configured in Sense AND that the UDP server in Butler is running.
            emailNotification:
              reloadAppFailure:
                enable: false                # Enable/disable app reload failed notifications via email
                alertEnableByTag:
                  enable: false
                  tag: Butler - Send email if app reload fails
                appOwnerAlert:
                  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:                    # Array of app owner email addresses that should get notifications
                      # - email: anna@somecompany.com
                      # - email: joe@somecompany.com
                  excludeOwner:
                    user:
                      # - 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-qscloud
                fromAddress: Qlik Sense (no-reply) <qliksense-noreply@ptarmiganlabs.com>
                recipients:
                  # - emma@somecompany.com
                  # - patrick@somecompany.com
  ...
  ...

Templates: Configuring email appearance

Alert emails use standard HTML formatting. Inline CSS can be used (if so desired) for fine tuning the visual look of the alert email.

Butler’s process for sending alert emails is

  1. Figure out which email body template file should be used. This is determine by two set of fields in the main config file:
    1. For reload failure emails these config file properties are used: Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.bodyFileDirectory and Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.htmlTemplateFile. A .handlebars extension is assumed.
  2. Email subjects are specified in the config property Butler.qlikSenseCloud.event.mqtt.tenant.alert.emailNotification.reloadAppFailure.subject.
  3. Process the body template, replacing template fields with actual values.
  4. Process the email subject template, replacing template fields with actual values.
  5. Send the email.

A couple of sample template files are found in the src/config/email_templates directory of the GitHub repository.

Tip

You can use template fields in email subjects too!

It is also possible to define custom links in the config file, and use them in email templates.
This is described here: Custom links in alerts.

Template fields reference

A complete list of template fields - including descriptions - is available in the Reference section.

2 - Reload alerts via Slack

Description of how app reload failed alerts can be sent as Slack messages.

What’s this?

Butler can send two kinds of alert messages via Slack:

  • When an app fails during reload.

See the Concepts section for additional details.

A complete reference to the config file format is found here.

Basic vs formatted Slack alerts

Slack alerts come in two forms:

  • Customizable formatting using a template concept. A standard template that will fit most use cases is included with Butler. Using this option the first and last parts of the script log can be included in the message, making it possible to tell from the Slack message what caused the reload to fail.
    You can also add buttons to the message that can be used to open any URL you want, or open the app that failed reloading.
  • A fixed, more basic format that is built into Butler. No template file needed, but also less detailed.

Which option to go for depends on whether you want just a notification that something went wrong, or if you want as much detail as possible in the Slack message. In most cases the customizable formatting is the best option.

Sample message with custom formatting

An “app reload failed” Slack message using the custom formatting option could look like this:

Reload failed alert Slack message

Here’s how to set this up:

  1. Create an incoming webhook in Slack, take note of its URL (you will need it in step 2 below).

  2. Edit the Slack section of the config file, i.e. the settings in Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.

    The messageType property should be set to formatted.
    The basicMsgTemplate property is not used with formatted messages and can thus be left empty,

  3. Edit the template file if/as needed, the file is specified in Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.templateFile. It uses the Handlebars templating engine, to which Butler provides template fields with actual values.

    The available template fields are described here.

    Sample template files are included in the release Zip file, and are also available in the GitHub repository’s src/config/slack_templates directory.

  4. Restart Butler if it’s already running.

Sample message with basic formatting

A “reload task failed” Slack message with basic formatting could look like this:

Reload failed alert Slack message

To set it up:

  1. Create an incoming webhook in Slack if you don’t already have one, take note of its URL (you will need it in step 2 below).

  2. Edit the Slack section of the config file, i.e. in Butler.qlikSenseCloud.event.mqtt.tenant.alert.slackNotification.reloadAppFailure.

    The messageType property should be set to basic.
    The basicMsgTemplate property is the message that will be sent via Slack. Template fields can be used.

  3. Restart Butler if it’s already running.

Customizing Slack messages

When using the formatted Slack alerts you have full freedom to create the alert you need.
Behind the scenes Slack messages are constructed from blocks defined in a JSON object. Each block can then contain either plain text, Markdown, images, buttons etc.

The Slack documentation is the best place for learning how to customize messages.

When it comes to Butler, it uses the Handlebars templating engine to render the template files into Slack JSON objects that are then sent to Slack via their APIs.

A few things to keep in mind when creating custom Slack messages:

  • The handlebars syntax itself must be correct. If incorrect no Slack JSON object will be created. And no Slack messages sent.
  • The handlebars template must result in a JSON object that adheres to Slack’s API specifications.
    If the JSON syntax is somehow invaid the Slack API will return errors and no messages sent. JSON can be pretty sensitive to details, there should for example not be any trailing commas in properly formatted JSON objects.

Some useful links to Slacks’s documentation:

It is also possible to define custom links in the config file, and use them in Slack templates.
This is described here: Custom links in alerts.

How it works

The concept is the same for all alert types, see the this page for details.

Settings in config file

---
Butler:
  ...
  ...
  mqttConfig:
    enable: false                                     # Should Qlik Sense events be forwarded as MQTT messages?
    ...
    ...
    qlikSenseCloud:                                                   # MQTT settings for Qlik Sense Cloud integration
      event:                                                          
        mqttForward:                                                  # QS Cloud events forwarded to MQTT topics, which Butler will subscribe to
          enable: false
          broker:                                                     # Settings for MQTT broker to which QS Cloud events are forwarded
            host: mqttbroker.company.com
            port: <port>
            username: <username>
            password: <password>
          topic:
            subscriptionRoot: qscloud/#                     # Topic that Butler will subscribe to
            appReload: qscloud/app/reload
  ...
  ...
  qlikSenseCloud:                   # Settings for Qlik Sense Cloud integration
    enable: false
    event:
      mqtt:                         # Which QS Cloud tenant should Butler receive events from, in the form of MQTT messages?
        tenant:
          id: tenant.region.qlikcloud.com
          tenantUrl: https://tenant.region.qlikcloud.com
          authType: jwt             # Authentication type used to connect to the tenant. Valid options are "jwt"  
          auth:
            jwt:
              token: <JWT token>    # JWT token used to authenticate Butler when connecting to the tenant
          # Qlik Sense Cloud related links used in notification messages
          qlikSenseUrls:
            qmc: <URL to QMC in Qlik Sense Cloud>
            hub: <URL to Hub in Qlik Sense Cloud>
          comment: This is a comment describing the tenant and its settings # Informational only
          alert:
            ...
            ...
            # Settings for notifications and messages sent to Slack
            slackNotification:
              reloadAppFailure:
                enable: false
                alertEnableByTag:
                  enable: false
                  tag: Butler - Send Slack alert if app reload fails
                basicContentOnly: false
                webhookURL: <URL to Slack webhook>
                channel: sense-task-failure     # Slack channel to which app reload failure notifications are sent
                messageType: formatted          # formatted / basic. Formatted means that template file below will be used to create the message.
                basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"'      # Only needed if message type = basic
                rateLimit: 60                   # Min seconds between emails for a given appId/recipient combo. Defaults to 5 minutes.
                headScriptLogLines: 10
                tailScriptLogLines: 20
                templateFile: /path/to/slack_templates/failed-reload-qscloud.handlebars
                fromUser: Qlik Sense
                iconEmoji: ':ghost:'
  ...
  ...

3 - Reload alerts via Microsoft Teams

Description of how reload alerts can be sent as Microsoft Teams messages.

What’s this?

Butler can send two kinds of alert messages via Teams:

  • When an app fails during reload.

See the Concepts section for additional details.

A complete reference to the config file format is found here.

Basic vs formatted Teams alerts

Teams alerts come in two forms:

  • Customizable formatting using a template concept. A standard template that will fit most use cases is included with Butler. With this option the first and last parts of the script log can be included in the message, allowing you to tell from the Teams message what caused the reload to fail.
    You can also add buttons to the message that can be used to open any URL you want, or open the app that failed reloading.
  • A fixed, more basic format that is built into Butler. No template file needed, but also less detailed.

Which option to go for depends on whether you want just a notification that something went wrong, or if you want as much detail as possible in the Teams message. In most cases the customizable formatting is the best option.

Sample message with custom formatting

An “app reload failed” Teams message using the custom formatting option could look like this:

Reload failed alert Teams message

Here’s how to set it up:

  1. Create a workflow in Teams, take note of its URL (you will need it in step 2 below). More information on how to create a Teams workflow in the Concepts section.

  2. Edit the Teams section of the config file, i.e. the settings in Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.

    The messageType property should be set to formatted.
    The basicMsgTemplate property is not used with formatted messages and can thus be left empty,

  3. Edit the template file if/as needed, the file is specified in Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.templateFile.It uses the Handlebars templating engine, to which Butler provides template fields with actual values.

    The available template fields are described here.

    Sample template files are included in the release Zip file, and are also available in the GitHub repository’s src/config/teams_templates directory.

  4. Restart Butler if it’s already running.

Sample message with basic formatting

A “reload task failed” Teams message with basic formatting could look like this:

Reload failed alert Teams message

To set it up:

  1. Create a workflow in Teams if you don’t already have one, take note of its URL (you will need it in step 2 below).

  2. Edit the Teams section of the config file i.e. Butler.qlikSenseCloud.event.mqtt.tenant.alert.teamsNotification.reloadAppFailure.

    The messageType property should be set to basic.
    The basicMsgTemplate property is the message that will be sent via Teams. Template fields can be used.

  3. Restart Butler if it’s already running.

Customizing Teams messages

When using the formatted Teams alerts you have full freedom to create the alert you need.
Behind the scenes Teams messages are constructed as “Adaptive Cards”, which is standardised JSON format that Teams understands.

More information on Adaptive Cards can be found here, here and here.

When it comes to Butler, it uses the Handlebars templating engine to render a template file into an adaptive card JSON object that is then sent to the workflow webhook.

A few things to keep in mind when creating custom Teams messages:

  • The handlebars syntax itself must be correct. If incorrect no Teams JSON object will be created. And no Teams message sent.
  • The handlebars template must result in a JSON object that adheres to Teams’s specifications for JSON payloads.
    If the JSON syntax is somehow invaid the Teams API will return errors and no messages sent. JSON can be pretty sensitive to details, there should for example not be any trailing commas in properly formatted JSON objects.

It is also possible to define custom links in the config file, and use them in Teams templates.
This is described here: Custom links in alerts.

How it works

The concept is the same for all alert types, see the this page for details.

Settings in config file

---
Butler:
  ...
  ...
  mqttConfig:
    enable: false                                     # Should Qlik Sense events be forwarded as MQTT messages?
    ...
    ...
    qlikSenseCloud:                                                   # MQTT settings for Qlik Sense Cloud integration
      event:                                                          
        mqttForward:                                                  # QS Cloud events forwarded to MQTT topics, which Butler will subscribe to
          enable: false
          broker:                                                     # Settings for MQTT broker to which QS Cloud events are forwarded
            host: mqttbroker.company.com
            port: <port>
            username: <username>
            password: <password>
          topic:
            subscriptionRoot: qscloud/#                     # Topic that Butler will subscribe to
            appReload: qscloud/app/reload
  ...
  ...
  qlikSenseCloud:                   # Settings for Qlik Sense Cloud integration
    enable: false
    event:
      mqtt:                         # Which QS Cloud tenant should Butler receive events from, in the form of MQTT messages?
        tenant:
          id: tenant.region.qlikcloud.com
          tenantUrl: https://tenant.region.qlikcloud.com
          authType: jwt             # Authentication type used to connect to the tenant. Valid options are "jwt"  
          auth:
            jwt:
              token: <JWT token>    # JWT token used to authenticate Butler when connecting to the tenant
          # Qlik Sense Cloud related links used in notification messages
          qlikSenseUrls:
            qmc: <URL to QMC in Qlik Sense Cloud>
            hub: <URL to Hub in Qlik Sense Cloud>
          comment: This is a comment describing the tenant and its settings # Informational only
          alert:
            # Settings for notifications and messages sent to MS Teams
            teamsNotification:
              reloadAppFailure:
                enable: false
                alertEnableByTag:
                  enable: false
                  tag: Butler - Send Teams alert if app reload fails
                basicContentOnly: false
                webhookURL: <URL to MS Teams webhook>
                messageType: formatted     # formatted / basic
                basicMsgTemplate: 'Qlik Sense Cloud app reload failed: "{{appName}}"'      # Only needed if message type = basic
                rateLimit: 15              # Min seconds between emails for a given appId. Defaults to 5 minutes.
                headScriptLogLines: 15
                tailScriptLogLines: 15
                templateFile: /path/to/teams_templates/failed-reload-qscloud-workflow.handlebars
  ...
  ...