wip: #147, #148
This commit is contained in:
Göran Sander
2021-04-14 19:32:53 +02:00
parent 82d065cfef
commit f6119edfd7
11 changed files with 3910 additions and 104 deletions

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ ssl/*
*.tmp
# Logs
log/
logs
*.log
npm-debug.log*

View File

@@ -2,6 +2,18 @@
Releases are [available on Github](https://github.com/ptarmiganlabs/butler-sos/releases).
## 5.6.0
### New features
1. . ([#150](https://github.com/ptarmiganlabs/butler/issues/150))
### Fixes and patches
1. Dependencies updated to stay sharp and secure.
### Changed behavior and/or breaking changes
## 5.5.3
- Dependencies updated to stay sharp and secure.

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="EventSession" type="log4net.Appender.UdpAppender">
<filter type="log4net.Filter.StringMatchFilter">
<param name="stringToMatch" value="Start session for user" />
</filter>
<filter type="log4net.Filter.StringMatchFilter">
<param name="stringToMatch" value="Stop session for user" />
</filter>
<filter type="log4net.Filter.DenyAllFilter" />
<param name="remoteAddress" value="<FQDN or IP of server where Butler is running>" />
<param name="remotePort" value="9997" />
<param name="encoding" value="utf-8" />
<layout type="log4net.Layout.PatternLayout">
<converter>
<param name="name" value="hostname" />
<param name="type" value="Qlik.Sense.Logging.log4net.Layout.Pattern.HostNamePatternConverter" />
</converter>
<param name="conversionpattern" value="/proxy-session/;%hostname;%property{Command};%property{UserDirectory};%property{UserId};%property{Origin};%property{Context};%message" />
</layout>
</appender>
<appender name="EventConnection" type="log4net.Appender.UdpAppender">
<filter type="log4net.Filter.StringMatchFilter">
<param name="stringToMatch" value="connection Opened for session" />
</filter>
<filter type="log4net.Filter.StringMatchFilter">
<param name="stringToMatch" value="connection Closed for session" />
</filter>
<filter type="log4net.Filter.DenyAllFilter" />
<param name="remoteAddress" value="<FQDN or IP of server where Butler is running>" />
<param name="remotePort" value="9997" />
<param name="encoding" value="utf-8" />
<layout type="log4net.Layout.PatternLayout">
<converter>
<param name="name" value="hostname" />
<param name="type" value="Qlik.Sense.Logging.log4net.Layout.Pattern.HostNamePatternConverter" />
</converter>
<param name="conversionpattern" value="/proxy-connection/;%hostname;%property{Command};%property{UserDirectory};%property{UserId};%property{Origin};%property{Context};%message" />
</layout>
</appender>
<logger name="AuditActivity.Proxy">
<appender-ref ref="EventSession" />
<appender-ref ref="EventConnection" />
</logger>
</configuration>

View File

@@ -9,6 +9,8 @@ const sessionMetrics = require('./lib/sessionmetrics');
const appNamesExtract = require('./lib/appnamesextract');
const heartbeat = require('./lib/heartbeat');
const serviceUptime = require('./lib/service_uptime');
const udp = require('./lib/udp_handlers');
const telemetry = require('./lib/telemetry');
globals.initInfluxDB();
@@ -18,7 +20,7 @@ if (globals.config.get('Butler-SOS.uptimeMonitor.enabled') == true) {
mainScript();
function mainScript() {
async function mainScript() {
// Load certificates to use when connecting to healthcheck API
var path = require('path'),
certFile = path.resolve(__dirname, globals.config.get('Butler-SOS.cert.clientCert')),
@@ -54,22 +56,65 @@ function mainScript() {
heartbeat.setupHeartbeatTimer(globals.config, globals.logger);
}
// Set specific log level (if/when needed to override the config file setting)
// Possible values are { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }
// Default is to use log level defined in config file
globals.logger.info('--------------------------------------');
globals.logger.info('Starting Butler SOS');
globals.logger.info(`Log level: ${globals.getLoggingLevel()}`);
globals.logger.info(`App version: ${globals.appVersion}`);
globals.logger.info('--------------------------------------');
try {
// Get host info
globals.hostInfo = await globals.initHostInfo();
globals.logger.debug('CONFIG: Initiated host info data structures');
// Log info about what Qlik Sense certificates are being used
globals.logger.debug(`Client cert: ${certFile}`);
globals.logger.debug(`Client cert key: ${keyFile}`);
globals.logger.debug(`CA cert: ${caFile}`);
// Set specific log level (if/when needed to override the config file setting)
// Possible values are { error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5 }
// Default is to use log level defined in config file
globals.logger.info('--------------------------------------');
globals.logger.info('Starting Butler SOS');
globals.logger.info(`Log level: ${globals.getLoggingLevel()}`);
globals.logger.info(`App version: ${globals.appVersion}`);
globals.logger.info('');
globals.logger.info(`Node version : ${globals.hostInfo.node.nodeVersion}`);
globals.logger.info(`Architecture : ${globals.hostInfo.si.os.arch}`);
globals.logger.info(`Platform : ${globals.hostInfo.si.os.platform}`);
globals.logger.info(`Release : ${globals.hostInfo.si.os.release}`);
globals.logger.info(`Distro : ${globals.hostInfo.si.os.distro}`);
globals.logger.info(`Codename : ${globals.hostInfo.si.os.codename}`);
globals.logger.info(`Virtual : ${globals.hostInfo.si.system.virtual}`);
globals.logger.info(`Processors : ${globals.hostInfo.si.cpu.processors}`);
globals.logger.info(`Physical cores : ${globals.hostInfo.si.cpu.physicalCores}`);
globals.logger.info(`Cores : ${globals.hostInfo.si.cpu.cores}`);
globals.logger.info(`Docker arch. : ${globals.hostInfo.si.cpu.hypervizor}`);
globals.logger.info(`Total memory : ${globals.hostInfo.si.memory.total}`);
globals.logger.info('--------------------------------------');
// Log info about what Qlik Sense certificates are being used
globals.logger.info(`Client cert: ${certFile}`);
globals.logger.info(`Client cert key: ${keyFile}`);
globals.logger.info(`CA cert: ${caFile}`);
// Set up anon usage reports, if enabled
if (
globals.config.has('Butler-SOS.anonTelemetry') == false ||
(globals.config.has('Butler-SOS.anonTelemetry') == true && globals.config.get('Butler-SOS.anonTelemetry') == true)
) {
telemetry.setupAnonUsageReportTimer();
globals.logger.verbose('MAIN: Anonymous telemetry reporting has been set up.');
}
} catch (err) {
globals.logger.error(`CONFIG: Error initiating host info: ${err}`);
}
// ---------------------------------------------------
// Set up UDP handler
if (globals.config.has('Butler-SOS.udpServerConfig.enable') && globals.config.get('Butler-SOS.udpServerConfig.enable')) {
udp.udpInitUserActivityServer();
globals.logger.debug(`MAIN: Server for UDP server: ${globals.udpServer.host}`);
// Start UDP server for user activity events
globals.udpServer.userActivitySocket.bind(
globals.udpServer.portUserActivity,
globals.udpServer.host,
);
}
// ---------------------------------------------------
// Start Docker healthcheck REST server on port set in config file
if (globals.config.get('Butler-SOS.dockerHealthCheck.enabled') == true) {
globals.logger.verbose('MAIN: Starting Docker healthcheck server...');
@@ -77,7 +122,7 @@ function mainScript() {
restServer.listen(globals.config.get('Butler-SOS.dockerHealthCheck.port'), function () {
globals.logger.info('MAIN: Docker healthcheck server now listening');
});
};
}
// Set up extraction of data from log db
if (globals.config.get('Butler-SOS.logdb.enableLogDb') == true) {

View File

@@ -4,6 +4,11 @@ var config = require('config');
const winston = require('winston');
require('winston-daily-rotate-file');
const path = require('path');
var dgram = require('dgram');
const si = require('systeminformation');
const os = require('os');
let crypto = require('crypto');
// const verifyConfig = require('./lib/verifyConfig');
const Influx = require('influx');
@@ -58,6 +63,28 @@ const getLoggingLevel = () => {
}).level;
};
// ------------------------------------
// UDP server connection parameters
var udpServer = {};
try {
udpServer.host = config.has('Butler-SOS.udpServerConfig.serverHost')
? config.get('Butler-SOS.udpServerConfig.serverHost')
: '';
// Prepare to listen on port X for incoming UDP connections regarding user activity events
udpServer.userActivitySocket = dgram.createSocket({
type: 'udp4',
reuseAddr: true,
});
udpServer.portUserActivity = config.has('Butler-SOS.udpServerConfig.portUserActivityEvents')
? config.get('Butler-SOS.udpServerConfig.portUserActivityEvents')
: '';
} catch (err) {
logger.error(`CONFIG: Setting up UDP user activity listener: ${err}`);
}
// ------------------------------------
// Get info on what servers to monitor
const serverList = config.get('Butler-SOS.serversToMonitor.servers');
@@ -72,6 +99,7 @@ const pgPool = new Pool({
// the pool will emit an error on behalf of any idle clients
// it contains if a backend error or network partition happens
// eslint-disable-next-line no-unused-vars
pgPool.on('error', (err, client) => {
logger.error(`CONFIG: Unexpected error on idle client: ${err}`);
// process.exit(-1);
@@ -207,9 +235,7 @@ const influx = new Influx.InfluxDB({
heap_total: Influx.FieldType.FLOAT,
process_memory: Influx.FieldType.FLOAT,
},
tags: [
'butler_sos_instance'
]
tags: ['butler_sos_instance'],
},
],
});
@@ -261,7 +287,7 @@ function initInfluxDB() {
}
})
.catch(err => {
logger.error(`CONFIG: Error getting list of InfuxDB databases! ${err.stack}`);
logger.error(`CONFIG: Error getting list of InfluxDB databases! ${err.stack}`);
});
}
}
@@ -279,7 +305,67 @@ var mqttClient = mqtt.connect({
protocolId: 'MQIsdp',
protocolVersion: 3
});
*/
*/
// Anon telemetry reporting
var hostInfo;
async function initHostInfo() {
try {
const siCPU = await si.cpu(),
siSystem = await si.system(),
siMem = await si.mem(),
siOS = await si.osInfo(),
siDocker = await si.dockerInfo(),
siNetwork = await si.networkInterfaces(),
siNetworkDefault = await si.networkInterfaceDefault();
let defaultNetworkInterface = siNetworkDefault;
let networkInterface = siNetwork.filter(item => {
return item.iface === defaultNetworkInterface;
});
let idSrc = networkInterface[0].mac + networkInterface[0].ip4 + config.get('Butler-SOS.logdb.host') + siSystem.uuid;
let salt = networkInterface[0].mac;
let hash = crypto.createHmac('sha256', salt);
hash.update(idSrc);
let id = hash.digest('hex');
hostInfo = {
id: id,
node: {
nodeVersion: process.version,
versions: process.versions
},
os: {
platform: os.platform(),
release: os.release(),
version: os.version(),
arch: os.arch(),
cpuCores: os.cpus().length,
type: os.type(),
totalmem: os.totalmem(),
},
si: {
cpu: siCPU,
system: siSystem,
memory: {
total: siMem.total,
},
os: siOS,
network: siNetwork,
networkDefault: siNetworkDefault,
docker: siDocker,
},
};
return hostInfo;
} catch (err) {
logger.error(`CONFIG: Getting host info: ${err}`);
}
}
module.exports = {
config,
@@ -292,4 +378,7 @@ module.exports = {
serverList,
initInfluxDB,
appNames,
udpServer,
initHostInfo,
hostInfo,
};

View File

@@ -1,6 +1,6 @@
const globals = require('../globals');
var _ = require('lodash');
const Promise = require('promise');
// const Promise = require('promise');
const sessionAppPrefix = 'SessionApp';

View File

@@ -56,7 +56,7 @@ function getCertificates(options) {
function getSessionStatsFromSense(host, virtualProxy, influxTags) {
// Current user sessions are retrived using this API:
// http://help.qlik.com/en-US/sense-developer/June2019/Subsystems/ProxyServiceAPI/Content/Sense_ProxyServiceAPI/ProxyServiceAPI-Session-Module-API.htm
// https://help.qlik.com/en-US/sense-developer/February2021/Subsystems/ProxyServiceAPI/Content/Sense_ProxyServiceAPI/ProxyServiceAPI-Proxy-API.htm
var options = {};

82
src/lib/telemetry.js Normal file
View File

@@ -0,0 +1,82 @@
/*eslint strict: ["error", "global"]*/
/*eslint no-invalid-this: "error"*/
'use strict';
const axios = require('axios');
var globals = require('../globals');
// const telemetryBaseUrl = 'http://localhost:7071/';
const telemetryBaseUrl = 'https://ptarmiganlabs-telemetry.azurewebsites.net/';
const telemetryUrl = '/api/butlerTelemetry';
var callRemoteURL = async function () {
try {
let body = {
service: 'butler-sos',
serviceVersion: globals.appVersion,
system: {
id: globals.hostInfo.id,
arch: globals.hostInfo.si.os.arch,
platform: globals.hostInfo.si.os.platform,
release: globals.hostInfo.si.os.release,
distro: globals.hostInfo.si.os.distro,
codename: globals.hostInfo.si.os.codename,
virtual: globals.hostInfo.si.system.virtual,
hypervisor: globals.hostInfo.si.os.hypervizor,
nodeVersion: globals.hostInfo.node.nodeVersion
},
enabledFeatures: {
feature: {
heartbeat: globals.config.has('Butler-SOS.heartbeat.enabled') ? globals.config.get('Butler-SOS.heartbeat.enabled') : false,
dockerHealthCheck: globals.config.has('Butler-SOS.dockerHealthCheck.enabled') ? globals.config.get('Butler-SOS.dockerHealthCheck.enabled') : false,
uptimeMonitor: globals.config.has('Butler-SOS.uptimeMonitor.enabled') ? globals.config.get('Butler-SOS.uptimeMonitor.enabled') : false,
uptimeMonitor_storeInInfluxdb: globals.config.has('Butler-SOS.uptimeMonitor.storeInInfluxdb.butlerSOSMemoryUsage') ? globals.config.get('Butler-SOS.uptimeMonitor.storeInInfluxdb.butlerSOSMemoryUsage') : false,
udpServer: globals.config.has('Butler-SOS.udpServerConfig.enable') ? globals.config.get('Butler-SOS.udpServerConfig.enable') : false,
logdb: globals.config.has('Butler-SOS.logdb.enableLogDb') ? globals.config.get('Butler-SOS.logdb.enableLogDb') : false,
mqtt: globals.config.has('Butler-SOS.mqttConfig.enableMQTT') ? globals.config.get('Butler-SOS.mqttConfig.enableMQTT') : false,
influxdb: globals.config.has('Butler-SOS.influxdbConfig.enableInfluxdb') ? globals.config.get('Butler-SOS.influxdbConfig.enableInfluxdb') : false,
appNames: globals.config.has('Butler-SOS.appNames.enableAppNameExtract') ? globals.config.get('Butler-SOS.appNames.enableAppNameExtract') : false,
userSessions: globals.config.has('Butler-SOS.userSessions.enableSessionExtract') ? globals.config.get('Butler-SOS.userSessions.enableSessionExtract') : false,
}
}
};
let axiosConfig = {
url: telemetryUrl,
method: 'post',
baseURL: telemetryBaseUrl,
data: body,
timeout: 5000,
responseType: 'text',
};
await axios.request(axiosConfig);
globals.logger.debug('TELEMETRY: Sent anonymous telemetry. Thanks for contributing to making Butler SOS better!');
} catch (err) {
globals.logger.error('TELEMETRY: Could not send anonymous telemetry.');
globals.logger.error(' While not mandatory the telemetry data greatly helps the Butler SOS developers.');
globals.logger.error(' It provides insights into which features are used most and what hardware/OSs are most used out there.');
globals.logger.error(' This information makes it possible to focus development efforts where they will make most impact and be most valuable.');
globals.logger.error('❤️ Thank you for your supporting Butler SOS by allowing telemetry! ❤️');
}
};
function setupAnonUsageReportTimer(logger, hostInfo) {
try {
setInterval(function () {
callRemoteURL(logger, hostInfo);
// }, 1000*60*60*12); // Report anon usage every 12 hours
}, 1000 * 60 * 60 * 6);
// Do an initial report to the remote URL
callRemoteURL(logger, hostInfo);
} catch (err) {
logger.error(`TELEMETRY: ${err}`);
}
}
module.exports = {
setupAnonUsageReportTimer,
};

65
src/lib/udp_handlers.js Normal file
View File

@@ -0,0 +1,65 @@
/* eslint-disable no-unused-vars */
/* eslint strict: ["error", "global"] */
'use strict';
// Load global variables and functions
var globals = require('../globals');
// --------------------------------------------------------
// Set up UDP server for acting on Sense user activity events
// --------------------------------------------------------
function udpInitUserActivityServer () {
// Handler for UDP server startup event
globals.udpServer.userActivitySocket.on('listening', function (message, remote) {
var address = globals.udpServer.userActivitySocket.address();
globals.logger.info(`USER ACTIVITY: UDP server listening on ${address.address}:${address.port}`);
});
// Handler for UDP messages relating to user activity events
globals.udpServer.userActivitySocket.on('message', async function (message, remote) {
try {
// >> Message parts
// 0: Message type. Possible values are /proxy-connection/, /proxy-session/
// 1: Host
// 2: Command
// 3: User directory
// 4: user ID
// 5: Origin
// 6: Context
// 7: Message. Can contain single quotes and semicolon - handle with care
// >> Parameter 2 (command):
// Start session
// Stop session
// Open connection
// Close connection
var msgTmp1 = message.toString().split(';'),
msg = msgTmp1.slice(0, 7);
globals.logger.verbose(`USER ACTIVITY: ${msg[1]}: ${msg[2]} for user ${msg[3]}/${msg[4]}`);
globals.logger.debug(`USER ACTIVITY details: ${msg}`);
// // Send MQTT messages
// if (msg[2] == 'Start session') {
// globals.mqttClient.publish(globals.config.get('Butler-SOS.mqttConfig.sessionStartTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]);
// } else if (msg[2] == 'Stop session') {
// globals.mqttClient.publish(globals.config.get('Butler-SOS.mqttConfig.sessionStopTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]);
// } else if (msg[2] == 'Open connection') {
// globals.mqttClient.publish(globals.config.get('Butler-SOS.mqttConfig.connectionOpenTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]);
// } else if (msg[2] == 'Close connection') {
// globals.mqttClient.publish(globals.config.get('Butler-SOS.mqttConfig.connectionCloseTopic'), msg[1] + ': ' + msg[3] + '/' + msg[4]);
// }
} catch (err) {
globals.logger.error(`USER ACTIVITY: Error processing user activity event: ${err}`);
}
});
}
module.exports = {
udpInitUserActivityServer
};

3612
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "butler-sos",
"version": "5.5.3",
"version": "5.6.0",
"description": "Butler SenseOps Stats (\"Butler SOS\") is a Node.js service publishing operational Qlik Sense metrics to MQTT and Influxdb.",
"main": "butler-sos.js",
"scripts": {
@@ -27,26 +27,26 @@
"homepage": "https://github.com/ptarmiganlabs/butler-sos#readme",
"dependencies": {
"axios": "^0.21.1",
"config": "^3.3.3",
"eslint-config-google": "^0.14.0",
"influx": "^5.6.3",
"js-yaml": "^3.14.0",
"config": "^3.3.4",
"influx": "^5.7.0",
"js-yaml": "^4.0.0",
"later": "^1.2.0",
"lodash.clonedeep": "^4.5.0",
"moment": "^2.29.1",
"moment-precise-range-plugin": "^1.3.0",
"mqtt": "^4.2.6",
"pg": "^8.5.1",
"promise": "^8.1.0",
"qrs-interact": "^6.2.0",
"qrs-interact": "^6.2.1",
"restify": "^8.5.1",
"systeminformation": "^5.5.0",
"url-join": "^4.0.1",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0"
},
"devDependencies": {
"eslint": "^7.17.0",
"jshint": "2.12.0",
"eslint": "^7.21.0",
"eslint-config-google": "^0.14.0",
"jshint": "^2.12.0",
"prettier": "^2.2.1"
}
}