This commit is contained in:
Göran Sander
2018-02-06 10:36:41 +01:00
parent f1c75aae45
commit 500d1624af
18 changed files with 4547 additions and 818 deletions

0
LICENSE Normal file → Executable file
View File

99
README.md Normal file → Executable file
View File

@@ -7,65 +7,94 @@
# Butler SOS
# Butler SOS v2
Butler SenseOps Stats ("Butler SOS") is a Node.js service publishing operational Qlik Sense Enterprise metrics to MQTT and Influxdb.
It uses the [Sense healthcheck API](http://help.qlik.com/en-US/sense-developer/3.2/Subsystems/EngineAPI/Content/GettingSystemInformation/HealthCheckStatus.htm) to gather operational metrics for the Sense servers specified in the JSON config file.
It uses the [Sense healthcheck API](http://help.qlik.com/en-US/sense-developer/November2017/Subsystems/EngineAPI/Content/GettingSystemInformation/HealthCheckStatus.htm) to gather operational metrics for the Sense servers specified in the YAML config file.
It also pulls warnings and errors from [Sense's Postgres logging database](http://help.qlik.com/en-US/sense/November2017/Subsystems/PlanningQlikSenseDeployments/Content/Deployment/Qlik-Logging-Service.htm), and forwards these to Influx and MQTT.
The most interesting use of Butler SOS is probably to create real-time dashboards, showing operational metrics for a Qlik Sense Enterprise environment:
![Grafana dashboard](img/senseops-1.png "SenseOps dashboard using Grafana")
**Why a separate tool for this?**
Good question. While Qlik Sense ships with a great Operations Monitor application, it is useful for real-time operational monitoring.
It is great for retrospective analysis of what happened in a Qlik Sense environment, but for a real-time view into a Sense environment, something else is needed - enter Butler SOS.
Butler SOS can however also send the data to [MQTT](https://en.wikipedia.org/wiki/MQTT), for use in any MQTT capable tool or system.
The most interesting use of Butler SOS is probably to create real-time dashboards based on the data in the Influx database, showing operational metrics for a Qlik Sense Enterprise environment.
A fully interactive demo dashboard is available [here](https://snapshot.raintank.io/dashboard/snapshot/1hNwAmi50lykKYXr6mswhKmll9myrH20?orgId=2).
Sample screen shots:
![Grafana dashboard](img/SenseOps_dashboard_3.png "SenseOps dashboard showing errors and warnings, using Grafana")
![Grafana dashboard](img/SenseOps_dashboard_4.png "SenseOps dashboard showing Qlik Sense metrics, using Grafana")
Butler SOS can however also send the data to [MQTT](https://en.wikipedia.org/wiki/MQTT), for use in any MQTT enabled tool or system.
## What's new
Updates and new features in v2:
* Close to real-time metrics on warnings and errors appearing in the QLik Sense logs
* Improved posting of data to MQTT
* YAML config files instead of JSON
* New and more comprehensive sample Grafana dashboards
* A [demo dashboard](https://snapshot.raintank.io/dashboard/snapshot/1hNwAmi50lykKYXr6mswhKmll9myrH20?orgId=2) that anyone can try out
## Install and setup
* Clone [the repository](https://github.com/mountaindude/butler-sos) from GitHub to desired location.
* Make sure [Node.js](https://nodejs.org) is installed. Butler-SOS has been tested with Node.js 6.10.0.
* Butler SOS v2 has been developed with Qlik Sense Enterprise November 2017 in mind. In order to use Butler SOS with other Sense versions, some adaptations may be needed.
* Clone [the repository](https://github.com/ptarmiganlabs/butler-sos) from GitHub to desired location.
* Make sure [Node.js](https://nodejs.org) is installed. Butler-SOS has been tested with Node.js 8.9.4.
* Run "npm install" from within the main butler-sos directory to download and install all Node.js dependencies.
* Make a copy of the [config/default_template.json](https://github.com/mountaindude/butler-sos/blob/master/config/default_template.json) configuration file. Edit the file as needed, save it as "default.json" in the ./config directory.
Butler SOS will read its config settings from the default.json file.
* Install [Influxdb](https://docs.influxdata.com/influxdb/v1.2/introduction) (only needed if data is to be stored in Influxdb, of course).
* Install [Mosquitto](https://mosquitto.org) or another MQTT broker (only needed if data is to be forwarded to MQTT).
* Make a copy of the [config/default_template.yaml](https://github.com/ptarmiganlabs/butler-sos/blob/master/config/default_template.yaml) configuration file. Edit the file as needed, save it as "default.yaml" in the ./config directory.
Butler SOS will read its config settings from this file.
* [Export certificates](http://help.qlik.com/en-US/sense/November2017/Subsystems/ManagementConsole/Content/export-certificates.htm) from Qlik Sense QMC, then place them in the ./cert folder under Butler SOS' main folder.
* Install [Influxdb](https://docs.influxdata.com/influxdb/v1.4/introduction) (only needed if data is to be stored in Influxdb, of course).
* Install [Mosquitto](https://mosquitto.org) or another MQTT broker (only needed if data is to be forwarded to MQTT). If you already have an MQTT broker you do not need to install a new one, Butler SOS can use the existing broker.
### Virtual proxies
Butler SOS relies on a [Qlik Sense virtual proxy](http://help.qlik.com/en-US/sense/3.2/Subsystems/ManagementConsole/Content/create-virtual-proxy.htm) to be available for each Sense server that is to be monitored.
Existing virtual proxies or new ones can be used - just make sure authentication etc work, and that the host name in the config file points to the correct virtual proxy of each server.
For example, let's say the config/default.json config file contains
"serversToMonitor": {
"servers": [{
"host": "server1.my.domain/virtualproxyname",
"serverName": "Server 1",
"availableRAM": 32000
}]
### Configuration files
The latst version of Butler SOS introduce several breaking changes to its configuration file:
Butler SOS will then query https://server1.my.domain/virtualproxyname/engine/healthcheck to get operational metrics for the Qlik Sense engine on server1.my.domain.
Make sure that the "virtualproxyname" virtual proxy has authentication suitable to your Qlik Sense setup.
* The configuration file format is now YAML rather than JSON. YAML is a more human readable and compact file format compared to JSON. It also allows comments to be used.
* Virtual proxies are no longer used to get the Sense healthcheck data.
Instead of virtual proxies the main Qlik Sense Engine Service (QES) is called on TCP port 4747 to get the health data of each Sense server that should be monitored.
A consequency of this is that certificates are now used to authenticate with Qlik Sense, rather than the security-by-obscurity that was the most commonly used security solution in the past for Butler SOS.
Please note that the path to these certificates must be properly configured in the config file's Butler-SOS.cert section.
Future versions of Butler SOS may not need these virtual proxies - maybe the needed data can be retrieved straight from the engine. Further work needed to make this happen though.
Pleae refer to the conig/default.yaml for further configuration instructions.
### Postgres log database
The config file allows you to set how often Butler should query the Sense log database for warnings and errors. In order to get real-time (-ish) notifications of warnings and errors, you should set the polling frequency to a reasonably low level. On the other hand, this polling will consume server resources and put some load on the Sense logging database - i.e. you should not set a too low polling frequency...
Experience shows that polling every 15-30 seconds work well and doesn't put too much load on the database.
There is one caveat to be aware of when it comes to the Butler-SOS.logdb.pollingInterval setting:
By default Butler SOS will query the log database for any warnings and errors that have occured during the last 2 minutes. The reason for having such a limit is simply to limit the query load on the Postgres server.
This however also means that you should **not** configure a polling frequency of 2 minutes or more, as such a setting would mean that Butler SOS would not capture all warnings and errors.
If you need a log database polling frequency longer than 2 minutes, you also need to change the SQL query in the butler-sos.js file to a longer time window.
## Usage
Start Influxdb and Mosquitto (or other MQTT broker).
Both Influxdb and Mosquitto should work right after installation - for production use their respective config files should of course be edited as needed, to ensure they work as desired.
Both Influxdb and Mosquitto should work right after installation - for production use their respective config files should be reviewed and edited as needed, with respect to use of https etc.
Starting Influxdb on OSX will look something like this:
Starting Influxdb on OSX will look something like this (for Influx v1.2.3):
![Starting Influxdb](img/influxdb-1.png "Starting Influxdb")
Then start Butler SOS itself from the main butler-sos directory:
"node butler-sos.js".
If the Influxdb database specified in the config file does not exist, it will be created:
If the Influxdb database specified in the config file does not exist, it will be created.
![Starting Butler SOS](img/butler-sos-1.png "Starting Butler SOS")
![Starting Butler SOS](img/butler-sos-cli-1.png "Starting Butler SOS")
Here we see how three servers are queried for data.
Here we see how two servers are queried for data.
The responses are retrived asyncronously as they arrive from the different servers.
Finally, the data is stored to Influxdb and sent as MQTT messages.
@@ -74,7 +103,7 @@ Finally, the data is stored to Influxdb and sent as MQTT messages.
By popular request, here are the commands needed to install Influx and Grafana.
The commands below assume you are using a Mac and have the [Homebrew](https://brew.sh/) package manager installed.
You can also install the software on a Linux server (apt-get install ... on Debian etc). Windows might be possible, but it is usually easier to spin up a small Linux server in a Docker container on your Windows PC, compared to installing the actual software on Windows...
Using Docker containers is actually a great way to play around with software, without clogging down your own computer.
Using Docker containers is actually a great way to play around with software, without clogging down your own computer. Butler SOS is in fact developed using Influx, Grafana and MQTT running in Docker containers.
Install and start Influx:
@@ -93,12 +122,12 @@ Default username/pwd is admin/admin.
## Real-time dashboards using Grafana
Once the data exists in Influxdb it can be visualised using [Grafana](https://grafana.com).
A sample dashboard is included in the Grafana directory - it should work out of the box when imported into your Grafana environment.
A sample dashboard is included in the Grafana directory. Import it into your Grafana environment, then modify it to reflect your server host names, after which it should show real-time metrics for your Sense servers.
Grafana is extremely powerful. Creating automatically updating dashboards for any number of servers is a matter of a few minutes work. Tutorials and docs can be found on their site.
## References
Please see [https://ptarmiganlabs.com](https://ptarmiganlabs.com/blog/2017/04/24/butler-sos-real-time-server-stats-qlik-sense/) and [https://github.com/mountaindude/butler](https://github.com/mountaindude/butler) for more in-depth info on the Butler family of micro services.
Please see [https://ptarmiganlabs.com](https://ptarmiganlabs.com/blog/2017/04/24/butler-sos-real-time-server-stats-qlik-sense/) and [https://github.com/ptarmiganlabs/butler](https://github.com/ptarmiganlabs/butler) for more in-depth info on the Butler family of micro services for Qlik Sense.
At [https://senseops.rocks](https://senseops.rocks) you also find thoughts on using DevOps best practices in the Qlik Sense ecosystem.

557
butler-sos.js Normal file → Executable file
View File

@@ -1,10 +1,27 @@
// Add dependencies
var request = require('request');
var request = require("request");
// Load code from sub modules
var globals = require('./globals');
var globals = require("./globals");
// Load certificates to use when connecting to healthcheck API
var fs = require("fs"),
path = require("path"),
certFile = path.resolve(
__dirname,
globals.config.get("Butler-SOS.cert.clientCert")
),
keyFile = path.resolve(
__dirname,
globals.config.get("Butler-SOS.cert.clientCertKey")
),
caFile = path.resolve(
__dirname,
globals.config.get("Butler-SOS.cert.clientCertCA")
);
// certFile = path.resolve(__dirname, "ssl/client.pem"),
// keyFile = path.resolve(__dirname, "ssl/client_key.pem"),
// caFile = path.resolve(__dirname, "ssl/root.pem");
// Set specific log level (if/when needed)
// Possible values are { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }
@@ -12,222 +29,388 @@ var globals = require('./globals');
// globals.logger.transports.console.level = 'verbose';
// globals.logger.transports.console.level = 'debug';
// Default is to use log level defined in config file
globals.logger.transports.console.level = globals.config.get('Butler-SOS.logLevel');
globals.logger.info('Starting Butler SOS');
globals.logger.info('Log level is: ' + globals.logger.transports.console.level);
globals.logger.transports.console.level = globals.config.get(
"Butler-SOS.logLevel"
);
globals.logger.info("Starting Butler SOS");
globals.logger.info("Log level is: " + globals.logger.transports.console.level);
function postToInfluxdb(host, serverName, body) {
// Calculate server uptime
// Calculate server uptime
var dateTime = Date.now();
var timestamp = Math.floor(dateTime);
var dateTime = Date.now();
var timestamp = Math.floor(dateTime);
var str = body.started;
var year = str.substring(0, 4);
var month = str.substring(4, 6);
var day = str.substring(6, 8);
var hour = str.substring(9, 11);
var minute = str.substring(11, 13);
var second = str.substring(13, 15);
var dateTimeStarted = new Date(year, month - 1, day, hour, minute, second);
var timestampStarted = Math.floor(dateTimeStarted);
var str = body.started;
var year = str.substring(0, 4);
var month = str.substring(4, 6);
var day = str.substring(6, 8);
var hour = str.substring(9, 11);
var minute = str.substring(11, 13);
var second = str.substring(13, 15);
var dateTimeStarted = new Date(year, month - 1, day, hour, minute, second);
var timestampStarted = Math.floor(dateTimeStarted);
var diff = timestamp - timestampStarted;
var diff = timestamp - timestampStarted;
// Create a new JavaScript Date object based on the timestamp
// multiplied by 1000 so that the argument is in milliseconds, not seconds.
var date = new Date(diff);
var days = Math.trunc(diff / (1000 * 60 * 60 * 24));
// Create a new JavaScript Date object based on the timestamp
// multiplied by 1000 so that the argument is in milliseconds, not seconds.
var date = new Date(diff);
// Hours part from the timestamp
var hours = date.getHours();
var days = Math.trunc((diff) / (1000 * 60 * 60 * 24))
// Minutes part from the timestamp
var minutes = "0" + date.getMinutes();
// Hours part from the timestamp
var hours = date.getHours();
// Seconds part from the timestamp
var seconds = "0" + date.getSeconds();
// Minutes part from the timestamp
var minutes = "0" + date.getMinutes();
// Will display time in 10:30:23 format
var formattedTime =
days +
" days, " +
hours +
"h " +
minutes.substr(-2) +
"m " +
seconds.substr(-2) +
"s";
// Seconds part from the timestamp
var seconds = "0" + date.getSeconds();
// Will display time in 10:30:23 format
var formattedTime = days + ' days, ' + hours + 'h ' + minutes.substr(-2) + 'm ' + seconds.substr(-2) + 's';
// Write the whole reading to Influxdb
globals.influx.writePoints([{
measurement: 'sense_server',
tags: {
host: serverName
},
fields: {
version: body.version,
started: body.started,
uptime: formattedTime
}
},
{
measurement: 'mem',
tags: {
host: serverName
},
fields: {
comitted: body.mem.comitted,
allocated: body.mem.allocated,
free: body.mem.free
}
},
{
measurement: 'apps',
tags: {
host: serverName
},
fields: {
active_docs_count: body.apps.active_docs.length,
loaded_docs_count: body.apps.loaded_docs.length,
calls: body.apps.calls,
selections: body.apps.selections
}
},
{
measurement: 'cpu',
tags: {
host: serverName
},
fields: {
total: body.cpu.total
}
},
{
measurement: 'session',
tags: {
host: serverName
},
fields: {
active: body.session.active,
total: body.session.total
}
},
{
measurement: 'users',
tags: {
host: serverName
},
fields: {
active: body.users.active,
total: body.users.total
}
},
{
measurement: 'cache',
tags: {
host: serverName
},
fields: {
hits: body.cache.hits,
lookups: body.cache.lookups,
added: body.cache.added,
replaced: body.cache.replaced,
bytes_added: body.cache.bytes_added
}
}
])
.then(err => {
globals.logger.verbose('Sent data to Influxdb: ' + serverName);
})
.catch(err => {
console.error(`Error saving data to InfluxDB! ${err.stack}`)
})
// Write the whole reading to Influxdb
globals.influx
.writePoints([
{
measurement: "sense_server",
tags: {
host: serverName
},
fields: {
version: body.version,
started: body.started,
uptime: formattedTime
}
},
{
measurement: "mem",
tags: {
host: serverName
},
fields: {
comitted: body.mem.comitted,
allocated: body.mem.allocated,
free: body.mem.free
}
},
{
measurement: "apps",
tags: {
host: serverName
},
fields: {
active_docs_count: body.apps.active_docs.length,
loaded_docs_count: body.apps.loaded_docs.length,
in_memory_docs_count: body.apps.in_memory_docs.length,
calls: body.apps.calls,
selections: body.apps.selections
}
},
{
measurement: "cpu",
tags: {
host: serverName
},
fields: {
total: body.cpu.total
}
},
{
measurement: "session",
tags: {
host: serverName
},
fields: {
active: body.session.active,
total: body.session.total
}
},
{
measurement: "users",
tags: {
host: serverName
},
fields: {
active: body.users.active,
total: body.users.total
}
},
{
measurement: "cache",
tags: {
host: serverName
},
fields: {
hits: body.cache.hits,
lookups: body.cache.lookups,
added: body.cache.added,
replaced: body.cache.replaced,
bytes_added: body.cache.bytes_added
}
}
])
.then(err => {
globals.logger.verbose("Sent health Influxdb: " + serverName);
})
.catch(err => {
console.error(`Error saving health data to InfluxDB! ${err.stack}`);
});
}
function postLogDbToMQTT(
process_host,
process_name,
entry_level,
message,
timestamp
) {
// Get base MQTT topic
var baseTopic = globals.config.get("Butler-SOS.mqttConfig.baseTopic");
function postToMQTT(host, serverName, body) {
// Get base MQTT topic
var baseTopic = globals.config.get('Butler-SOS.mqttConfig.baseTopic');
// Send to MQTT
globals.mqttClient.publish(baseTopic + serverName + '/version', body.version);
globals.mqttClient.publish(baseTopic + serverName + '/started', body.started);
globals.mqttClient.publish(baseTopic + serverName + '/mem/comitted', body.mem.comitted.toString());
globals.mqttClient.publish(baseTopic + serverName + '/mem/allocated', body.mem.allocated.toString());
globals.mqttClient.publish(baseTopic + serverName + '/mem/free', body.mem.free.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cpu/total', body.cpu.total.toString());
globals.mqttClient.publish(baseTopic + serverName + '/session/active', body.session.active.toString());
globals.mqttClient.publish(baseTopic + serverName + '/session/total', body.session.total.toString());
globals.mqttClient.publish(baseTopic + serverName + '/apps/active_docs', body.apps.active_docs.toString());
globals.mqttClient.publish(baseTopic + serverName + '/apps/loaded_docs', body.apps.loaded_docs.toString());
globals.mqttClient.publish(baseTopic + serverName + '/apps/calls', body.apps.calls.toString());
globals.mqttClient.publish(baseTopic + serverName + '/apps/selections', body.apps.selections.toString());
globals.mqttClient.publish(baseTopic + serverName + '/users/active', body.users.active.toString());
globals.mqttClient.publish(baseTopic + serverName + '/users/total', body.users.total.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cache/hits', body.cache.hits.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cache/lookups', body.cache.lookups.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cache/added', body.cache.added.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cache/replaced', body.cache.replaced.toString());
globals.mqttClient.publish(baseTopic + serverName + '/cache/bytes_added', body.cache.bytes_added.toString());
if (body.cache.lookups > 0) {
globals.mqttClient.publish(baseTopic + serverName + '/cache/hit_ratio', Math.floor(body.cache.hits / body.cache.lookups * 100).toString());
}
// Send to MQTT
globals.mqttClient.publish(
baseTopic + process_host + "/" + process_name + "/" + entry_level,
message
);
}
function postHealthToMQTT(host, serverName, body) {
// Get base MQTT topic
var baseTopic = globals.config.get("Butler-SOS.mqttConfig.baseTopic");
// Send to MQTT
globals.mqttClient.publish(baseTopic + serverName + "/version", body.version);
globals.mqttClient.publish(baseTopic + serverName + "/started", body.started);
globals.mqttClient.publish(
baseTopic + serverName + "/mem/comitted",
body.mem.committed.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/mem/allocated",
body.mem.allocated.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/mem/free",
body.mem.free.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cpu/total",
body.cpu.total.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/session/active",
body.session.active.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/session/total",
body.session.total.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/apps/active_docs",
body.apps.active_docs.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/apps/loaded_docs",
body.apps.loaded_docs.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/apps/in_memory_docs",
body.apps.in_memory_docs.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/apps/calls",
body.apps.calls.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/apps/selections",
body.apps.selections.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/users/active",
body.users.active.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/users/total",
body.users.total.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cache/hits",
body.cache.hits.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cache/lookups",
body.cache.lookups.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cache/added",
body.cache.added.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cache/replaced",
body.cache.replaced.toString()
);
globals.mqttClient.publish(
baseTopic + serverName + "/cache/bytes_added",
body.cache.bytes_added.toString()
);
if (body.cache.lookups > 0) {
globals.mqttClient.publish(
baseTopic + serverName + "/cache/hit_ratio",
Math.floor(body.cache.hits / body.cache.lookups * 100).toString()
);
}
}
function getStatsFromSense(host, serverName) {
request({
followAllRedirects: true,
url: 'https://' + host + '/engine/healthcheck/',
headers: {
'Cache-Control': 'no-cache'
},
json: true
}, function (error, response, body) {
globals.logger.log(
"debug",
"URL=" + "https://" + host + "/engine/healthcheck/"
);
// Check for error
if (error) {
return globals.logger.error('Error:', error);
request(
{
followAllRedirects: true,
url: "https://" + host + "/engine/healthcheck/",
headers: {
"Cache-Control": "no-cache"
},
json: true,
cert: fs.readFileSync(certFile),
key: fs.readFileSync(keyFile),
ca: fs.readFileSync(caFile)
},
function(error, response, body) {
// Check for error
if (error) {
return globals.logger.error("Error:", error);
}
if (!error && response.statusCode === 200) {
globals.logger.verbose("Received ok response from " + serverName);
globals.logger.debug(body);
// Post to MQTT (if enabled)
if (globals.config.get("Butler-SOS.mqttConfig.enableMQTT")) {
globals.logger.debug("Calling MQTT posting method");
postHealthToMQTT(host, serverName, body);
}
if (!error && response.statusCode === 200) {
globals.logger.verbose('Received ok response from ' + serverName);
globals.logger.debug(body);
// Post to MQTT (if enabled)
if ( globals.config.get('Butler-SOS.mqttConfig.enableMQTT') ) {
globals.logger.debug('Calling MQTT posting method');
postToMQTT(host, serverName, body);
}
// Post to Influxdb (if enabled)
if ( globals.config.get('Butler-SOS.influxdbConfig.enableInfluxdb') ) {
globals.logger.debug('Calling Influxdb posting method');
postToInfluxdb(host, serverName, body);
}
// Post to Influxdb (if enabled)
if (globals.config.get("Butler-SOS.influxdbConfig.enableInfluxdb")) {
globals.logger.debug("Calling Influxdb posting method");
postToInfluxdb(host, serverName, body);
}
})
}
}
);
}
// Configure timer for getting log data from Postgres
setInterval(function() {
globals.logger.verbose("Event started: Query log db");
// checkout a Postgres client from connection pool
globals.pgPool.connect().then(pgClient => {
return pgClient
.query(
`select
id,
entry_timestamp as timestamp,
entry_level,
process_host,
process_name,
payload
from public.log_entries
where
entry_level in ('WARN', 'ERROR') and
(entry_timestamp > now() - INTERVAL '2 minutes' )
order by
entry_timestamp desc
`
)
.then(res => {
pgClient.release();
globals.logger.debug("Log db query got a response.");
setInterval(function () {
globals.logger.verbose('Event started: Statistics collection');
var rows = res.rows;
rows.forEach(function(row) {
globals.logger.silly("Log db row: " + JSON.stringify(row));
var serverList = globals.config.get('Butler-SOS.serversToMonitor.servers');
serverList.forEach(function (server) {
globals.logger.verbose('Getting stats for server: ' + server.serverName);
// Post to Influxdb (if enabled)
if (globals.config.get("Butler-SOS.influxdbConfig.enableInfluxdb")) {
globals.logger.debug("Posting log db data to Influxdb...");
// Write the whole reading to Influxdb
globals.influx
.writePoints([
{
measurement: "log_entry",
tags: {
host: row.process_host,
source_process: row.process_name,
log_level: row.entry_level
},
fields: {
message: row.payload.Message
},
timestamp: row.timestamp
}
])
.then(err => {
globals.logger.silly("Sent log db event to Influxdb. ");
})
.catch(err => {
console.error(
`Error saving log event to InfluxDB! ${err.stack}`
);
});
}
getStatsFromSense(server.host, server.serverName);
});
// Post to MQTT (if enabled)
if (globals.config.get("Butler-SOS.mqttConfig.enableMQTT")) {
globals.logger.debug("Posting log db data to MQTT...");
postLogDbToMQTT(
row.process_host,
row.process_name,
row.entry_level,
row.payload.Message,
row.timestamp
);
}
});
})
.then(res => {
globals.logger.verbose("Sent log event to Influxdb. ");
})
.catch(e => {
pgClient.release();
globals.logger.error("Log db query error: " + err.stack);
});
});
}, globals.config.get("Butler-SOS.logdb.pollingInterval"));
}, globals.config.get('Butler-SOS.pollingInterval'));
// Configure timer for getting healthcheck data
setInterval(function() {
globals.logger.verbose("Event started: Statistics collection");
var serverList = globals.config.get("Butler-SOS.serversToMonitor.servers");
serverList.forEach(function(server) {
globals.logger.verbose("Getting stats for server: " + server.serverName);
getStatsFromSense(server.host, server.serverName);
});
}, globals.config.get("Butler-SOS.serversToMonitor.pollingInterval"));

View File

@@ -1,29 +0,0 @@
{
"Butler-SOS": {
"logLevel": "verbose",
"mqttConfig": {
"enableMQTT": true,
"brokerIP": "<IP of MQTT server>",
"baseTopic": "butler-sos/"
},
"influxdbConfig": {
"enableInfluxdb": true,
"hostIP": "<IP or FQDN of Influxdb server>",
"dbName": "SenseOps"
},
"pollingInterval": 5000,
"serversToMonitor": {
"servers": [{
"host": "<server1.my.domain>",
"serverName": "<server1>",
"availableRAM": 32000
},
{
"host": "<server2.my.domain>",
"serverName": "<server2>",
"availableRAM": 24000
}
]
}
}
}

45
config/default_template.yaml Executable file
View File

@@ -0,0 +1,45 @@
Butler-SOS:
# Possible log levels are silly, debug, verbose, info, warn, error
logLevel: verbose
# Qlik Sense logging db config parameters
logdb:
# How often (milliseconds) should Postgres log db be queried for warnings and errors?
pollingInterval: 15000
host: <IP or FQDN of Qlik Sense logging db>
port: 4432
qlogsReaderUser: qlogs_reader
qlogsReaderPwd: <pwd>
# Certificates to use when querying Sense for healthcheck data
cert:
clientCert: <path/to/cert/client.pem>
clientCertKey: <path/to/cert/client_key.pem>
clientCertCA: <path/to/cert/root.pem>
# MQTT config parameters
mqttConfig:
enableMQTT: true
brokerIP: <IP of MQTT server>
brokerHost: 1883
# Topic should end with /
baseTopic: butler-sos/
# Influx db config parameters
influxdbConfig:
enableInfluxdb: true
hostIP: <IP or FQDN of Influxdb server>
dbName: SenseOps
serversToMonitor:
# How often (milliseconds) should the healthcheck API be polled?
pollingInterval: 5000
# Sense Servers that should be queried for healthcheck data
servers:
- host: <server1.my.domain>
serverName: <server1>
availableRAM: 32000
- host: <server2.my.domain>
serverName: <server2>
availableRAM: 24000

250
globals.js Normal file → Executable file
View File

@@ -1,126 +1,142 @@
var mqtt = require('mqtt');
var config = require('config');
var winston = require('winston');
const Influx = require('influx');
var mqtt = require("mqtt");
var config = require("config");
var winston = require("winston");
const Influx = require("influx");
const { Pool } = require("pg");
// Set up logger with timestamps and colors
var logger = new(winston.Logger)({
transports: [
new(winston.transports.Console)({
'timestamp': true,
'colorize': true
})
]
var logger = new winston.Logger({
transports: [
new winston.transports.Console({
timestamp: true,
colorize: true
})
]
});
// Set up connection pool for accessing Qlik Sense log db
const pgPool = new Pool({
host: config.get("Butler-SOS.logdb.host"),
database: "QLogs",
user: config.get("Butler-SOS.logdb.qlogsReaderUser"),
password: config.get("Butler-SOS.logdb.qlogsReaderPwd"),
port: config.get("Butler-SOS.logdb.port")
});
// the pool with emit an error on behalf of any idle clients
// it contains if a backend error or network partition happens
pgPool.on("error", (err, client) => {
logger.log("error", "Unexpected error on idle client" + err);
process.exit(-1);
});
// Set up Influxdb client
const influx = new Influx.InfluxDB({
host: config.get('Butler-SOS.influxdbConfig.hostIP'),
database: config.get('Butler-SOS.influxdbConfig.dbName'),
schema: [
{
measurement: 'sense_server',
fields: {
version: Influx.FieldType.STRING,
started: Influx.FieldType.STRING,
uptime: Influx.FieldType.STRING
},
tags: [
'host'
]
},
{
measurement: 'mem',
fields: {
comitted: Influx.FieldType.INTEGER,
allocated: Influx.FieldType.INTEGER,
free: Influx.FieldType.INTEGER
},
tags: [
'host'
]
},
{
measurement: 'apps',
fields: {
active_docs_count: Influx.FieldType.INTEGER,
loaded_docs_count: Influx.FieldType.INTEGER,
calls: Influx.FieldType.INTEGER,
selections: Influx.FieldType.INTEGER
},
tags: [
'host'
]
},
{
measurement: 'cpu',
fields: {
total: Influx.FieldType.INTEGER
},
tags: [
'host'
]
},
{
measurement: 'session',
fields: {
active: Influx.FieldType.INTEGER,
total: Influx.FieldType.INTEGER
},
tags: [
'host'
]
},
{
measurement: 'users',
fields: {
active: Influx.FieldType.INTEGER,
total: Influx.FieldType.INTEGER
},
tags: [
'host'
]
},
{
measurement: 'cache',
fields: {
hits: Influx.FieldType.INTEGER,
lookups: Influx.FieldType.INTEGER,
added: Influx.FieldType.INTEGER,
replaced: Influx.FieldType.INTEGER,
bytes_added: Influx.FieldType.INTEGER
},
tags: [
'host'
]
}
]
})
influx.getDatabaseNames()
.then(names => {
if (!names.includes(config.get('Butler-SOS.influxdbConfig.dbName'))) {
logger.info('Creating Influx database.');
return influx.createDatabase(config.get('Butler-SOS.influxdbConfig.dbName'));
}
})
.then(() => {
logger.info('Connected to Influx database.');
return;
})
.catch(err => {
logger.error(`Error creating Influx database!`);
})
host: config.get("Butler-SOS.influxdbConfig.hostIP"),
database: config.get("Butler-SOS.influxdbConfig.dbName"),
schema: [
{
measurement: "sense_server",
fields: {
version: Influx.FieldType.STRING,
started: Influx.FieldType.STRING,
uptime: Influx.FieldType.STRING
},
tags: ["host"]
},
{
measurement: "mem",
fields: {
comitted: Influx.FieldType.INTEGER,
allocated: Influx.FieldType.INTEGER,
free: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "apps",
fields: {
active_docs_count: Influx.FieldType.INTEGER,
loaded_docs_count: Influx.FieldType.INTEGER,
in_memory_docs_count: Influx.FieldType.INTEGER,
calls: Influx.FieldType.INTEGER,
selections: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "cpu",
fields: {
total: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "session",
fields: {
active: Influx.FieldType.INTEGER,
total: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "users",
fields: {
active: Influx.FieldType.INTEGER,
total: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "cache",
fields: {
hits: Influx.FieldType.INTEGER,
lookups: Influx.FieldType.INTEGER,
added: Influx.FieldType.INTEGER,
replaced: Influx.FieldType.INTEGER,
bytes_added: Influx.FieldType.INTEGER
},
tags: ["host"]
},
{
measurement: "log_event",
fields: {
hits: Influx.FieldType.INTEGER,
lookups: Influx.FieldType.INTEGER,
added: Influx.FieldType.INTEGER,
replaced: Influx.FieldType.INTEGER,
bytes_added: Influx.FieldType.INTEGER
},
tags: ["host"]
}
]
});
influx
.getDatabaseNames()
.then(names => {
if (!names.includes(config.get("Butler-SOS.influxdbConfig.dbName"))) {
logger.info("Creating Influx database.");
return influx.createDatabase(
config.get("Butler-SOS.influxdbConfig.dbName")
);
}
})
.then(() => {
logger.info("Connected to Influx database.");
return;
})
.catch(err => {
logger.error(`Error creating Influx database!`);
});
// ------------------------------------
// Create MQTT client object and connect to MQTT broker
var mqttClient = mqtt.connect('mqtt://' + config.get('Butler-SOS.mqttConfig.brokerIP'));
var mqttClient = mqtt.connect({
port: config.get("Butler-SOS.mqttConfig.brokerPort"),
host: config.get("Butler-SOS.mqttConfig.brokerHost")
});
/*
Following might be needed for conecting to older Mosquitto versions
var mqttClient = mqtt.connect('mqtt://<IP of MQTT server>', {
@@ -129,10 +145,10 @@ var mqttClient = mqtt.connect('mqtt://<IP of MQTT server>', {
});
*/
module.exports = {
config,
mqttClient,
logger,
influx
config,
mqttClient,
logger,
influx,
pgPool
};

View File

@@ -1,444 +0,0 @@
{
"__inputs": [
{
"name": "DS_SENSEOPS",
"label": "senseops",
"description": "",
"type": "datasource",
"pluginId": "influxdb",
"pluginName": "InfluxDB"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "4.1.1"
},
{
"type": "panel",
"id": "graph",
"name": "Graph",
"version": ""
},
{
"type": "datasource",
"id": "influxdb",
"name": "InfluxDB",
"version": "1.0.0"
},
{
"type": "panel",
"id": "singlestat",
"name": "Singlestat",
"version": ""
}
],
"annotations": {
"list": []
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"hideControls": false,
"id": null,
"links": [],
"refresh": "30s",
"rows": [
{
"collapse": false,
"height": 257,
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "${DS_SENSEOPS}",
"fill": 1,
"id": 1,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 5,
"stack": false,
"steppedLine": false,
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "cpu",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"total"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
]
],
"tags": [
{
"key": "host",
"operator": "=~",
"value": "/^$Hosts$/"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "CPU",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": "",
"logBase": 1,
"max": "100",
"min": "0",
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"aliasColors": {},
"bars": false,
"datasource": "${DS_SENSEOPS}",
"fill": 1,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "session",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"total"
],
"type": "field"
},
{
"params": [],
"type": "max"
}
]
],
"tags": [
{
"key": "host",
"operator": "=~",
"value": "/^$Hosts$/"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Sessions",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": "${DS_SENSEOPS}",
"decimals": 0,
"format": "none",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"id": 7,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"span": 3,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": true,
"lineColor": "rgb(31, 120, 193)",
"show": true
},
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"measurement": "apps",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"loaded_docs_count"
],
"type": "field"
},
{
"params": [],
"type": "last"
}
]
],
"tags": [
{
"key": "host",
"operator": "=~",
"value": "/^$Hosts$/"
}
]
}
],
"thresholds": "",
"title": "Loaded apps",
"type": "singlestat",
"valueFontSize": "50%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
}
],
"repeat": "Hosts",
"repeatIteration": null,
"repeatRowId": null,
"showTitle": true,
"title": "$Hosts",
"titleSize": "h6"
}
],
"schemaVersion": 14,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {},
"datasource": "${DS_SENSEOPS}",
"hide": 0,
"includeAll": true,
"label": null,
"multi": true,
"name": "Hosts",
"options": [],
"query": "SHOW TAG VALUES WITH KEY = \"host\"",
"refresh": 1,
"regex": "",
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "browser",
"title": "SenseOps dashboard",
"version": 4
}

2910
grafana/SenseOps dashboard.json Executable file

File diff suppressed because it is too large Load Diff

BIN
img/SenseOps_dashboard_3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
img/SenseOps_dashboard_4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

BIN
img/butler-sos-cli-1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

0
img/butler-sos-small.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

0
img/butler-sos.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

0
img/influxdb-1.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

1017
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

2
package.json Normal file → Executable file
View File

@@ -26,7 +26,9 @@
"dependencies": {
"config": "^1.25.1",
"influx": "^5.0.0-alpha.4",
"js-yaml": "^3.10.0",
"mqtt": "^2.5.0",
"pg": "^7.4.1",
"request": "^2.81.0",
"winston": "^2.3.1"
}