From bac12589f8e147039830cdac4f906a6061d2959f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20Sander?= Date: Mon, 8 Apr 2024 06:05:12 +0000 Subject: [PATCH] feat: Scheduled removal of unused user Qlik Sense license Implements #1042 --- src/butler.js | 2 +- src/config/production_template.yaml | 30 ++- src/lib/post_to_influxdb.js | 109 +++++++-- src/lib/qliksense_license.js | 326 +++++++++++++++++++++++++++ src/lib/qliksense_license_monitor.js | 82 ------- 5 files changed, 438 insertions(+), 111 deletions(-) create mode 100644 src/lib/qliksense_license.js delete mode 100644 src/lib/qliksense_license_monitor.js diff --git a/src/butler.js b/src/butler.js index 0d6a6727..fd1fa7f8 100644 --- a/src/butler.js +++ b/src/butler.js @@ -29,7 +29,7 @@ const start = async () => { globals.logger.verbose(`START: Globals init done: ${globals.initialised}`); const setupServiceMonitorTimer = (await import('./lib/service_monitor.js')).default; - const { setupQlikSenseLicenseMonitor, setupQlikSenseLicenseRelease } = await import('./lib/qliksense_license_monitor.js'); + const { setupQlikSenseLicenseMonitor, setupQlikSenseLicenseRelease } = await import('./lib/qliksense_license.js'); // The build function creates a new instance of the App class and returns it. const build = (await import('./app.js')).default; diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml index bed8c69b..f5130d88 100644 --- a/src/config/production_template.yaml +++ b/src/config/production_template.yaml @@ -140,12 +140,36 @@ Butler: qlikSenseLicense: licenseMonitor: enable: true - frequency: every 5 minutes + frequency: every 6 hours destination: - influxDb: # Send service alerts to InfluxDB + influxDb: # Store license data in InfluxDB enable: true tag: - static: # Static attributes/dimensions to attach to the data sent to New Relic. + static: # Static attributes/tags to attach to the data sent to InflixDB + - name: foo + value: bar + licenseRelease: + enable: true + frequency: every 6 hours + neverReleaseUsers: + - userDir: 'INTERNAL' + userId: 'sa_repository' + - userDir: 'INTERNAL' + userId: 'sa_api' + - userDir: 'USERDIR' + userId: 'qs_admin_account' + licenseType: # License types to monitor and release + analyzer: + enable: true + releaseThresholdDays: 5 + professional: + enable: true + releaseThresholdDays: 5 + destination: + influxDb: # Store info about released licenses in InfluxDB + enable: true + tag: + static: # Static attributes/tags to attach to the data sent to InflixDB - name: foo value: bar diff --git a/src/lib/post_to_influxdb.js b/src/lib/post_to_influxdb.js index 52481cbb..6aa8273f 100755 --- a/src/lib/post_to_influxdb.js +++ b/src/lib/post_to_influxdb.js @@ -94,17 +94,33 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; + // Get tags from config file + // Stored in array Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag + const configTags = globals.config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag.static'); + + const tags = { + license_type: 'analyzer_access', + butler_instance: instanceTag, + }; + + // Add tags from config file + if (configTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configTags) { + tags[item.name] = item.value; + } + } + // Build InfluxDB datapoint let datapoint = []; // Is there any data for "analyzerAccess" license type? if (qlikSenseLicenseStatus.analyzerAccess.enabled === true) { + tags.license_type = 'analyzer_access'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'analyzer_access', - }, + tags, fields: { allocated: qlikSenseLicenseStatus.analyzerAccess.allocated, available: qlikSenseLicenseStatus.analyzerAccess.available, @@ -118,12 +134,11 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu // Is there any data for "analyzerTimeAccess" license type? if (qlikSenseLicenseStatus.analyzerTimeAccess.enabled === true) { + tags.license_type = 'analyzer_time_access'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'analyzer_time_access', - }, + tags, fields: { allocatedMinutes: qlikSenseLicenseStatus.analyzerTimeAccess.allocatedMinutes, unavailableMinutes: qlikSenseLicenseStatus.analyzerTimeAccess.unavailableMinutes, @@ -134,12 +149,11 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu // Is there any data for "loginAccess" license type? if (qlikSenseLicenseStatus.loginAccess.enabled === true) { + tags.license_type = 'login_access'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'login_access', - }, + tags, fields: { allocatedTokens: qlikSenseLicenseStatus.loginAccess.allocatedTokens, tokenCost: qlikSenseLicenseStatus.loginAccess.tokenCost, @@ -151,12 +165,11 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu // Is there any data for "professionalAccess" license type? if (qlikSenseLicenseStatus.professionalAccess.enabled === true) { + tags.license_type = 'professional_access'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'professional_access', - }, + tags, fields: { allocated: qlikSenseLicenseStatus.professionalAccess.allocated, available: qlikSenseLicenseStatus.professionalAccess.available, @@ -170,12 +183,11 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu // Is there any data for "userAccess" license type? if (qlikSenseLicenseStatus.userAccess.enabled === true) { + tags.license_type = 'user_access'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'user_access', - }, + tags, fields: { allocatedTokens: qlikSenseLicenseStatus.userAccess.allocatedTokens, quarantinedTokens: qlikSenseLicenseStatus.userAccess.quarantinedTokens, @@ -187,12 +199,11 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu // Are tokens available? if (qlikSenseLicenseStatus.tokensEnabled === true) { + tags.license_type = 'tokens_available'; + datapoint.push({ measurement: 'qlik_sense_license', - tags: { - butler_instance: instanceTag, - license_type: 'tokens_available', - }, + tags, fields: { availableTokens: qlikSenseLicenseStatus.availableTokens, totalTokens: qlikSenseLicenseStatus.totalTokens, @@ -209,7 +220,55 @@ export async function postQlikSenseLicenseStatusToInfluxDB(qlikSenseLicenseStatu ); datapoint = null; - globals.logger.info('INFLUXDB QLIK SENSE LICENSE STATUS: Sent Qlik Sense license status data to InfluxDB'); + globals.logger.info('INFLUXDB QLIK SENSE LICENSE STATUS: Sent aggregated Qlik Sense license status to InfluxDB'); +} + +// Function to store info about released Qlik Sense licenses to InfluxDB +export async function postQlikSenseLicenseReleasedToInfluxDB(licenseInfo) { + globals.logger.verbose('INFLUXDB QLIK SENSE LICENSE RELEASE: Sending info on released Qlik Sense license to InfluxDB'); + + const instanceTag = globals.config.has('Butler.influxDb.instanceTag') ? globals.config.get('Butler.influxDb.instanceTag') : ''; + + // Get tags from config file + // Stored in array Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.tag + const configTags = globals.config.get('Butler.qlikSenseLicense.licenseRelease.destination.influxDb.tag.static'); + + const tags = { + license_type: licenseInfo.licenseType, + user: `${licenseInfo.userDir}\\${licenseInfo.userId}`, + butler_instance: instanceTag, + }; + + // Add tags from config file + if (configTags) { + // eslint-disable-next-line no-restricted-syntax + for (const item of configTags) { + tags[item.name] = item.value; + } + } + + // Build InfluxDB datapoint + let datapoint = []; + + // Add data to InfluxDB datapoint + datapoint.push({ + measurement: 'qlik_sense_license_release', + tags, + fields: { + days_since_last_use: licenseInfo.daysSinceLastUse, + }, + }); + + // Write to InfluxDB + const deepClonedDatapoint = _.cloneDeep(datapoint); + await globals.influx.writePoints(deepClonedDatapoint); + + globals.logger.silly( + `INFLUXDB QLIK SENSE LICENSE RELEASE: Influxdb datapoint for released Qlik Sense license: ${JSON.stringify(datapoint, null, 2)}` + ); + + datapoint = null; + globals.logger.debug('INFLUXDB QLIK SENSE LICENSE RELEASE: Sent info on released Qlik Sense license to InfluxDB'); } // Function to store windows service status to InfluxDB diff --git a/src/lib/qliksense_license.js b/src/lib/qliksense_license.js new file mode 100644 index 00000000..50492406 --- /dev/null +++ b/src/lib/qliksense_license.js @@ -0,0 +1,326 @@ +import later from '@breejs/later'; +import QrsInteract from 'qrs-interact'; + +import globals from '../globals.js'; +import { postQlikSenseLicenseStatusToInfluxDB, postQlikSenseLicenseReleasedToInfluxDB } from './post_to_influxdb.js'; + +// Function to check Qlik Sense license status +async function checkQlikSenseLicenseStatus(config, logger) { + try { + // Set up Sense repository service configuration + const configQRS = { + hostname: globals.config.get('Butler.configQRS.host'), + portNumber: 4242, + certificates: { + certFile: globals.configQRS.certPaths.certPath, + keyFile: globals.configQRS.certPaths.keyPath, + }, + }; + + configQRS.headers = { + 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', + }; + const qrsInstance = new QrsInteract(configQRS); + + // Get Qlik Sense license info + const result1 = await qrsInstance.Get(`license/accesstypeoverview`); + + // Is status code 200 or body is empty? + if (result1.statusCode !== 200 || !result1.body) { + logger.error(`QLIKSENSE LICENSE MONITOR: HTTP status code ${result1.statusCode}`); + return; + } + + // Debug log + logger.debug(`QLIKSENSE LICENSE MONITOR: ${JSON.stringify(result1.body)}`); + + // To which destination should we send the license information? + // Check InfluDB first + // If InfluxDB is enabled, post the license status to InfluxDB + if ( + config.has('Butler.influxDb.enable') && + config.get('Butler.influxDb.enable') === true && + config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable') && + config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable') === true + ) { + await postQlikSenseLicenseStatusToInfluxDB(result1.body); + } + } catch (err) { + logger.error(`QLIKSENSE LICENSE MONITOR: ${err}`); + if (err.stack) { + logger.error(`QLIKSENSE LICENSE MONITOR: ${err.stack}`); + } + } +} + +// Function to release professional licenses +async function licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers) { + const result1 = await qrsInstance.Get(`license/professionalaccesstype/full`); + + // Is status code 200 or body is empty? + if (result1.statusCode !== 200 || !result1.body) { + logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result1.statusCode}`); + return false; + } + + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Allocated: ${JSON.stringify(result1.body)}`); + + // Determnine which allocated licenses to release. + // Only release licenses that are NOT quarantined + // Take into account the releaese threshold (days), i.e. days since last use + // Loop over all licenses retrived in previous step, add licenses to be released to releaseProfessional array + const releaseProfessional = []; + // eslint-disable-next-line no-restricted-syntax + for (const license of result1.body) { + if (!license.quarantined) { + // Get days since last use + const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24)); + + // Check if the user is in the neverReleaseUsers array. + // Compare userDir and userId + // If the user is in the neverReleaseUsers array, do not release the license + let doNotRelease = false; + // eslint-disable-next-line no-restricted-syntax + for (const user of neverReleaseUsers) { + if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) { + doNotRelease = true; + break; + } + } + + // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license + if ( + !doNotRelease && + daysSinceLastUse > config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') + ) { + releaseProfessional.push({ + licenseId: license.id, + userDir: license.user.userDirectory, + userId: license.user.userId, + daysSinceLastUse, + }); + } + } + } + + // Release all licenses in the releaseProfessional array + // eslint-disable-next-line no-restricted-syntax + for (const licenseRelease of releaseProfessional) { + logger.info( + `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + ); + + // Release license + // eslint-disable-next-line no-await-in-loop + const result2 = await qrsInstance.Delete(`license/professionalaccesstype/${licenseRelease.licenseId}`); + + // Is status code 204? Error if it's nmt + if (result2.statusCode !== 204) { + logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result2.statusCode}`); + return false; + } + + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: ${JSON.stringify(result2.body)}`); + + // Write info about released license to InfluxDB? + if ( + config.get('Butler.influxDb.enable') === true && + config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') >= 0 + ) { + // eslint-disable-next-line no-await-in-loop + await postQlikSenseLicenseReleasedToInfluxDB({ + licenseType: 'professional_access', + licenseId: licenseRelease.licenseId, + userDir: licenseRelease.userDir, + userId: licenseRelease.userId, + daysSinceLastUse: licenseRelease.daysSinceLastUse, + }); + } + } + return true; +} + +// Function to release analyzer licenses +async function licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers) { + const result3 = await qrsInstance.Get(`license/analyzeraccesstype/full`); + + // Is status code 200 or body is empty? + if (result3.statusCode !== 200 || !result3.body) { + logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result3.statusCode}`); + return; + } + + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Allocated: ${JSON.stringify(result3.body)}`); + + // Determnine which allocated licenses to release. + // Only release licenses that are NOT quarantined + // Take into account the releaese threshold (days), i.e. days since last use + // Loop over all licenses retrived in previous step, add licenses to be released to releaseAnalyzer array + const releaseAnalyzer = []; + // eslint-disable-next-line no-restricted-syntax + for (const license of result3.body) { + if (!license.quarantined) { + // Get days since last use + const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24)); + + // Check if the user is in the neverReleaseUsers array. + // Compare userDir and userId + // If the user is in the neverReleaseUsers array, do not release the license + let doNotRelease = false; + // eslint-disable-next-line no-restricted-syntax + for (const user of neverReleaseUsers) { + if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) { + doNotRelease = true; + break; + } + } + + // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license + if ( + !doNotRelease && + daysSinceLastUse > config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') + ) { + releaseAnalyzer.push({ + licenseId: license.id, + userDir: license.user.userDirectory, + userId: license.user.userId, + daysSinceLastUse, + }); + } + } + } + + // Release all licenses in the releaseAnalyzer array + // eslint-disable-next-line no-restricted-syntax + for (const licenseRelease of releaseAnalyzer) { + logger.info( + `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})` + ); + + // Release license + // eslint-disable-next-line no-await-in-loop + const result4 = await qrsInstance.Delete(`license/analyzeraccesstype/${licenseRelease.licenseId}`); + + // Is status code 204? Error if it's nmt + if (result4.statusCode !== 204) { + logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result4.statusCode}`); + return; + } + + // Debug log + logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: ${JSON.stringify(result4.body)}`); + + // Write info about released license to InfluxDB? + if ( + config.get('Butler.influxDb.enable') === true && + config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') >= 0 + ) { + // eslint-disable-next-line no-await-in-loop + await postQlikSenseLicenseReleasedToInfluxDB({ + licenseType: 'analyzer_access', + licenseId: licenseRelease.licenseId, + userDir: licenseRelease.userDir, + userId: licenseRelease.userId, + daysSinceLastUse: licenseRelease.daysSinceLastUse, + }); + } + } +} + +// Function to release Qlik Sense licenses +async function checkQlikSenseLicenseRelease(config, logger) { + try { + // Set up Sense repository service configuration + const configQRS = { + hostname: globals.config.get('Butler.configQRS.host'), + portNumber: 4242, + certificates: { + certFile: globals.configQRS.certPaths.certPath, + keyFile: globals.configQRS.certPaths.keyPath, + }, + }; + + configQRS.headers = { + 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', + }; + const qrsInstance = new QrsInteract(configQRS); + + // Which user accounts should never be released? + // Get info from config file + const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers'); + + // Release licenses of type "professional" + let res = await licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers); + + // Success? + if (!res) { + return false; + } + + // Release licenses of type "analyzer" + res = await licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers); + // Success? + if (!res) { + return false; + } + + return true + } catch (err) { + logger.error(`QLIKSENSE LICENSE MONITOR: ${err}`); + if (err.stack) { + logger.error(`QLIKSENSE LICENSE MONITOR: ${err.stack}`); + } + return false; + } +} + +// Function to set up the timer used to check Qlik Sense license status +export async function setupQlikSenseLicenseMonitor(config, logger) { + try { + if ( + !config.has('Butler.qlikSenseLicense.licenseMonitor.enable') || + config.get('Butler.qlikSenseLicense.licenseMonitor.enable') === true + ) { + const sched = later.parse.text(config.get('Butler.qlikSenseLicense.licenseMonitor.frequency')); + later.setInterval(() => { + checkQlikSenseLicenseStatus(config, logger, false); + }, sched); + + // Do an initial license check + logger.verbose('Doing initial Qlik Sense license check'); + checkQlikSenseLicenseStatus(config, logger, true); + } + } catch (err) { + logger.error(`QLIKSENSE LICENSE MONITOR INIT: ${err}`); + if (err.stack) { + logger.error(`QLIKSENSE LICENSE MONITOR INIT: ${err.stack}`); + } + } +} + +// Function to set up the timer used to release Qlik Sense licenses +export async function setupQlikSenseLicenseRelease(config, logger) { + try { + if ( + !config.has('Butler.qlikSenseLicense.licenseRelease.enable') || + config.get('Butler.qlikSenseLicense.licenseRelease.enable') === true + ) { + const sched = later.parse.text(config.get('Butler.qlikSenseLicense.licenseRelease.frequency')); + later.setInterval(() => { + checkQlikSenseLicenseRelease(config, logger); + }, sched); + + // Do an initial release + logger.verbose('Doing initial Qlik Sense license check'); + checkQlikSenseLicenseRelease(config, logger); + } + } catch (err) { + logger.error(`QLIKSENSE LICENSE RELEASE INIT: ${err}`); + if (err.stack) { + logger.error(`QLIKSENSE LICENSE RELEASE INIT: ${err.stack}`); + } + } +} diff --git a/src/lib/qliksense_license_monitor.js b/src/lib/qliksense_license_monitor.js deleted file mode 100644 index 83ad365f..00000000 --- a/src/lib/qliksense_license_monitor.js +++ /dev/null @@ -1,82 +0,0 @@ -import later from '@breejs/later'; -import QrsInteract from 'qrs-interact'; - -import globals from '../globals.js'; -import { postQlikSenseLicenseStatusToInfluxDB } from './post_to_influxdb.js'; - -// Function to check Qlik Sense license status -// The isFirsCheck parameter is used to determine if we should send a message to the alert destinations -// Set isFirsCheck default to false -async function checkQlikSenseLicenseStatus(config, logger, initialCheck) { - try { - // Set up Sense repository service configuration - const configQRS = { - hostname: globals.config.get('Butler.configQRS.host'), - portNumber: 4242, - certificates: { - certFile: globals.configQRS.certPaths.certPath, - keyFile: globals.configQRS.certPaths.keyPath, - }, - }; - - configQRS.headers = { - 'X-Qlik-User': 'UserDirectory=Internal; UserId=sa_repository', - }; - const qrsInstance = new QrsInteract(configQRS); - - // Get Qlik Sense license info - const result1 = await qrsInstance.Get(`license/accesstypeoverview`); - - // Is status code 200 or body is empty? - if (result1.statusCode !== 200 || !result1.body) { - logger.error(`QLIKSENSE LICENSE MONITOR: HTTP status code ${result1.statusCode}`); - return; - } - - // Debug log - logger.debug(`QLIKSENSE LICENSE MONITOR: ${JSON.stringify(result1.body)}`); - - // To which destination should we send the license information? - // Check InfluDB first - // If InfluxDB is enabled, post the license status to InfluxDB - if ( - config.has('Butler.influxDb.enable') && - config.get('Butler.influxDb.enable') === true && - config.has('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable') && - config.get('Butler.qlikSenseLicense.licenseMonitor.destination.influxDb.enable') === true - ) { - await postQlikSenseLicenseStatusToInfluxDB(result1.body); - } - } catch (err) { - logger.error(`QLIKSENSE LICENSE MONITOR: ${err}`); - if (err.stack) { - logger.error(`QLIKSENSE LICENSE MONITOR: ${err.stack}`); - } - } -} - -// Function to set up the timer used to check Qlik Sense license status -async function setupQlikSenseLicenseMonitor(config, logger) { - try { - if ( - !config.has('Butler.qlikSenseLicense.licenseMonitor.enable') || - config.get('Butler.qlikSenseLicense.licenseMonitor.enable') === true - ) { - const sched = later.parse.text(config.get('Butler.qlikSenseLicense.licenseMonitor.frequency')); - later.setInterval(() => { - checkQlikSenseLicenseStatus(config, logger, false); - }, sched); - - // Do an initial service status check - logger.verbose('Doing initial Qlik Sense license check'); - checkQlikSenseLicenseStatus(config, logger, true); - } - } catch (err) { - logger.error(`QLIKSENSE LICENSE MONITOR INIT: ${err}`); - if (err.stack) { - logger.error(`QLIKSENSE LICENSE MONITOR INIT: ${err.stack}`); - } - } -} - -export default setupQlikSenseLicenseMonitor;