v2.0
109
README.md
Normal file → Executable 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:
|
||||
|
||||

|
||||
**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:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### Configuration files
|
||||
The latst version of Butler SOS introduce several breaking changes to its configuration file:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
For example, let's say the config/default.json config file contains
|
||||
|
||||
"serversToMonitor": {
|
||||
"servers": [{
|
||||
"host": "server1.my.domain/virtualproxyname",
|
||||
"serverName": "Server 1",
|
||||
"availableRAM": 32000
|
||||
}]
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
## 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):
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
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:
|
||||
|
||||
@@ -86,19 +115,19 @@ Install and start Grafana
|
||||
brew install grafana
|
||||
brew services start grafana
|
||||
|
||||
Connect to Grafana by visiting http://localhost:3000
|
||||
Connect to Grafana by visiting http://localhost:3000
|
||||
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
@@ -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"));
|
||||
|
||||
@@ -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
@@ -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
|
||||
252
globals.js
Normal file → Executable 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
|
||||
};
|
||||
|
||||
@@ -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
BIN
img/SenseOps_dashboard_3.png
Executable file
|
After Width: | Height: | Size: 336 KiB |
BIN
img/SenseOps_dashboard_4.png
Executable file
|
After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 313 KiB |
BIN
img/butler-sos-cli-1.png
Executable file
|
After Width: | Height: | Size: 387 KiB |
0
img/butler-sos-small.png
Normal file → Executable file
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
0
img/butler-sos.png
Normal file → Executable file
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
0
img/influxdb-1.png
Normal file → Executable file
|
Before Width: | Height: | Size: 479 KiB After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 226 KiB |
1017
package-lock.json
generated
Executable file
2
package.json
Normal file → Executable 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"
|
||||
}
|
||||
|
||||