You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
492 lines
15 KiB
JavaScript
492 lines
15 KiB
JavaScript
/*
|
|
* Copyright DataStax, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const utils = require('./utils');
|
|
const promiseUtils = require('./promise-utils');
|
|
const types = require('./types');
|
|
const requests = require('./requests');
|
|
const { ExecutionOptions } = require('./execution-options');
|
|
const packageInfo = require('../package.json');
|
|
const VersionNumber = require('./types/version-number');
|
|
const { NoAuthProvider } = require('./auth');
|
|
|
|
let kerberosModule;
|
|
|
|
try {
|
|
// eslint-disable-next-line
|
|
kerberosModule = require('kerberos');
|
|
}
|
|
catch (err) {
|
|
// Kerberos is an optional dependency
|
|
}
|
|
|
|
const minDse6Version = new VersionNumber(6, 0, 5);
|
|
const minDse51Version = new VersionNumber(5, 1, 13);
|
|
const dse600Version = new VersionNumber(6, 0, 0);
|
|
const rpc = "CALL InsightsRpc.reportInsight(?)";
|
|
const maxStatusErrorLogs = 5;
|
|
|
|
/**
|
|
* Contains methods and functionality to send events to DSE Insights.
|
|
*/
|
|
class InsightsClient {
|
|
|
|
/**
|
|
* Creates a new instance of the {@link InsightsClient} using the driver {@link Client}.
|
|
* @param {Client} client
|
|
* @param {Object} [options]
|
|
* @param {Number} [options.statusEventDelay]
|
|
* @param {Function} [options.errorCallback]
|
|
*/
|
|
constructor(client, options) {
|
|
this._client = client;
|
|
this._sessionId = types.Uuid.random().toString();
|
|
this._enabled = false;
|
|
this._closed = false;
|
|
this._firstTimeout = null;
|
|
this._recurrentTimeout = null;
|
|
this._statusErrorLogs = 0;
|
|
|
|
options = options || {};
|
|
|
|
this._statusEventDelay = options.statusEventDelay || 300000;
|
|
this._errorCallback = options.errorCallback || utils.noop;
|
|
}
|
|
|
|
/**
|
|
* Initializes the insights client in the background by sending the startup event and scheduling status events at
|
|
* regular intervals.
|
|
* @returns {undefined}
|
|
*/
|
|
init() {
|
|
this._enabled = this._client.options.monitorReporting.enabled && this._dseSupportsInsights();
|
|
if (!this._enabled) {
|
|
return;
|
|
}
|
|
|
|
promiseUtils.toBackground(this._init());
|
|
}
|
|
|
|
async _init() {
|
|
try {
|
|
await this._sendStartupEvent();
|
|
|
|
if (this._closed) {
|
|
// The client was shutdown
|
|
return;
|
|
}
|
|
|
|
// Send the status event the first time with a delay containing some random portion
|
|
// Initial delay should be statusEventDelay - (0 to 10%)
|
|
const firstDelay = Math.floor(this._statusEventDelay - 0.1 * this._statusEventDelay * Math.random());
|
|
// Schedule the first timer
|
|
this._firstTimeout = setTimeout(() => {
|
|
// Send the first status event, the promise will never be rejected
|
|
this._sendStatusEvent();
|
|
// The following status events are sent at regular intervals
|
|
this._recurrentTimeout = setInterval(() => this._sendStatusEvent(), this._statusEventDelay);
|
|
}, firstDelay);
|
|
} catch (err) {
|
|
if (this._closed) {
|
|
// Sending failed because the Client was shutdown
|
|
return;
|
|
}
|
|
// We shouldn't try to recover
|
|
this._client.log('verbose', `Insights startup message could not be sent (${err})`, err);
|
|
this._errorCallback(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends the startup event.
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
async _sendStartupEvent() {
|
|
const message = await this._getStartupMessage();
|
|
const request = new requests.QueryRequest(rpc, [message], ExecutionOptions.empty());
|
|
await this._client.controlConnection.query(request, false);
|
|
}
|
|
|
|
/**
|
|
* Sends the status event.
|
|
* @returns {Promise} A promise that is never rejected.
|
|
* @private
|
|
*/
|
|
async _sendStatusEvent() {
|
|
const request = new requests.QueryRequest(rpc, [ this._getStatusEvent() ], ExecutionOptions.empty());
|
|
|
|
try {
|
|
await this._client.controlConnection.query(request, false);
|
|
} catch (err) {
|
|
if (this._closed) {
|
|
// Sending failed because the Client was shutdown
|
|
return;
|
|
}
|
|
|
|
if (this._statusErrorLogs < maxStatusErrorLogs) {
|
|
this._client.log('warning', `Insights status message could not be sent (${err})`, err);
|
|
this._statusErrorLogs++;
|
|
}
|
|
|
|
this._errorCallback(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the minimum server version for all nodes in the cluster.
|
|
* @private
|
|
*/
|
|
_dseSupportsInsights() {
|
|
if (this._client.hosts.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return this._client.hosts.values().reduce((acc, host) => {
|
|
if (!acc) {
|
|
return acc;
|
|
}
|
|
|
|
const versionArr = host.getDseVersion();
|
|
|
|
if (versionArr.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const version = new VersionNumber(...versionArr);
|
|
|
|
return version.compare(minDse6Version) >= 0 ||
|
|
(version.compare(dse600Version) < 0 && version.compare(minDse51Version) >= 0);
|
|
|
|
}, true);
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<String>} Returns a json string with the startup message.
|
|
* @private
|
|
*/
|
|
async _getStartupMessage() {
|
|
const cc = this._client.controlConnection;
|
|
const options = this._client.options;
|
|
|
|
|
|
const appInfo = await this._getAppInfo(options);
|
|
const message = {
|
|
metadata: {
|
|
name: 'driver.startup',
|
|
insightMappingId: 'v1',
|
|
insightType: 'EVENT',
|
|
timestamp: Date.now(),
|
|
tags: { language: 'nodejs' }
|
|
},
|
|
data: {
|
|
driverName: packageInfo.description,
|
|
driverVersion: packageInfo.version,
|
|
clientId: options.id,
|
|
sessionId: this._sessionId,
|
|
applicationName: appInfo.applicationName,
|
|
applicationVersion: appInfo.applicationVersion,
|
|
applicationNameWasGenerated: appInfo.applicationNameWasGenerated,
|
|
contactPoints: mapToObject(cc.getResolvedContactPoints()),
|
|
dataCenters: this._getDataCenters(),
|
|
initialControlConnection: cc.host ? cc.host.address : undefined,
|
|
protocolVersion: cc.protocolVersion,
|
|
localAddress: cc.getLocalAddress(),
|
|
hostName: os.hostname(),
|
|
executionProfiles: getExecutionProfiles(this._client),
|
|
poolSizeByHostDistance: {
|
|
local: options.pooling.coreConnectionsPerHost[types.distance.local],
|
|
remote: options.pooling.coreConnectionsPerHost[types.distance.remote]
|
|
},
|
|
heartbeatInterval: options.pooling.heartBeatInterval,
|
|
compression: 'NONE',
|
|
reconnectionPolicy: getPolicyInfo(options.policies.reconnection),
|
|
ssl: {
|
|
enabled: !!options.sslOptions,
|
|
certValidation: options.sslOptions ? !!options.sslOptions.rejectUnauthorized : undefined
|
|
},
|
|
authProvider: {
|
|
type: !(options.authProvider instanceof NoAuthProvider) ? getConstructor(options.authProvider) : undefined,
|
|
},
|
|
otherOptions: {
|
|
coalescingThreshold: options.socketOptions.coalescingThreshold,
|
|
},
|
|
platformInfo: {
|
|
os: {
|
|
name: os.platform(),
|
|
version: os.release(),
|
|
arch: os.arch()
|
|
},
|
|
cpus: {
|
|
length: os.cpus().length,
|
|
model: os.cpus()[0].model
|
|
},
|
|
runtime: {
|
|
node: process.versions['node'],
|
|
v8: process.versions['v8'],
|
|
uv: process.versions['uv'],
|
|
openssl: process.versions['openssl'],
|
|
kerberos: kerberosModule ? kerberosModule.version : undefined
|
|
}
|
|
},
|
|
configAntiPatterns: this._getConfigAntiPatterns(),
|
|
periodicStatusInterval: Math.floor(this._statusEventDelay / 1000)
|
|
}
|
|
};
|
|
|
|
return JSON.stringify(message);
|
|
}
|
|
|
|
_getConfigAntiPatterns() {
|
|
const options = this._client.options;
|
|
const result = {};
|
|
|
|
if (options.sslOptions && !options.sslOptions.rejectUnauthorized) {
|
|
result.sslWithoutCertValidation =
|
|
'Client-to-node encryption is enabled but server certificate validation is disabled';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Gets an array of data centers the driver connects to.
|
|
* Whether the driver connects to a certain host is determined by the host distance (local and remote hosts)
|
|
* and the pooling options (whether connection length for remote hosts is greater than 0).
|
|
* @returns {Array}
|
|
* @private
|
|
*/
|
|
_getDataCenters() {
|
|
const remoteConnectionsLength = this._client.options.pooling.coreConnectionsPerHost[types.distance.remote];
|
|
const dataCenters = new Set();
|
|
|
|
this._client.hosts.values().forEach(h => {
|
|
const distance = this._client.profileManager.getDistance(h);
|
|
if (distance === types.distance.local || (distance === types.distance.remote && remoteConnectionsLength > 0)) {
|
|
dataCenters.add(h.datacenter);
|
|
}
|
|
});
|
|
|
|
return Array.from(dataCenters);
|
|
}
|
|
|
|
/**
|
|
* Tries to obtain the application name and version from
|
|
* @param {DseClientOptions} options
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
async _getAppInfo(options) {
|
|
if (typeof options.applicationName === 'string') {
|
|
return Promise.resolve({
|
|
applicationName: options.applicationName,
|
|
applicationVersion: options.applicationVersion,
|
|
applicationNameWasGenerated: false
|
|
});
|
|
}
|
|
|
|
let readPromise = Promise.resolve();
|
|
|
|
if (require.main && require.main.filename) {
|
|
const packageInfoPath = path.dirname(require.main.filename);
|
|
readPromise = this._readPackageInfoFile(packageInfoPath);
|
|
}
|
|
|
|
const text = await readPromise;
|
|
let applicationName = 'Default Node.js Application';
|
|
let applicationVersion;
|
|
|
|
if (text) {
|
|
try {
|
|
const packageInfo = JSON.parse(text);
|
|
if (packageInfo.name) {
|
|
applicationName = packageInfo.name;
|
|
applicationVersion = packageInfo.version;
|
|
}
|
|
}
|
|
catch (err) {
|
|
// The package.json file could not be parsed
|
|
// Use the default name
|
|
}
|
|
}
|
|
|
|
return {
|
|
applicationName,
|
|
applicationVersion,
|
|
applicationNameWasGenerated: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @returns {Promise<string>} A Promise that will never be rejected
|
|
*/
|
|
_readPackageInfoFile(packageInfoPath) {
|
|
return new Promise(resolve => {
|
|
fs.readFile(path.join(packageInfoPath, 'package.json'), 'utf8', (err, data) => {
|
|
// Swallow error
|
|
resolve(data);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {String} Returns a json string with the startup message.
|
|
* @private
|
|
*/
|
|
_getStatusEvent() {
|
|
const cc = this._client.controlConnection;
|
|
const options = this._client.options;
|
|
const state = this._client.getState();
|
|
const connectedNodes = {};
|
|
|
|
state.getConnectedHosts().forEach(h => {
|
|
connectedNodes[h.address] = {
|
|
connections: state.getOpenConnections(h),
|
|
inFlightQueries: state.getInFlightQueries(h)
|
|
};
|
|
});
|
|
|
|
const message = {
|
|
metadata: {
|
|
name: 'driver.status',
|
|
insightMappingId: 'v1',
|
|
insightType: 'EVENT',
|
|
timestamp: Date.now(),
|
|
tags: { language: 'nodejs' }
|
|
},
|
|
data: {
|
|
clientId: options.id,
|
|
sessionId: this._sessionId,
|
|
controlConnection: cc.host ? cc.host.address : undefined,
|
|
connectedNodes
|
|
}
|
|
};
|
|
|
|
return JSON.stringify(message);
|
|
}
|
|
|
|
/**
|
|
* Cleans any timer used internally and sets the client as closed.
|
|
*/
|
|
shutdown() {
|
|
if (!this._enabled) {
|
|
return;
|
|
}
|
|
|
|
this._closed = true;
|
|
|
|
if (this._firstTimeout !== null) {
|
|
clearTimeout(this._firstTimeout);
|
|
}
|
|
|
|
if (this._recurrentTimeout !== null) {
|
|
clearInterval(this._recurrentTimeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = InsightsClient;
|
|
|
|
function mapToObject(map) {
|
|
const result = {};
|
|
map.forEach((value, key) => result[key] = value);
|
|
return result;
|
|
}
|
|
|
|
function getPolicyInfo(policy) {
|
|
if (!policy) {
|
|
return undefined;
|
|
}
|
|
|
|
const options = policy.getOptions && policy.getOptions();
|
|
|
|
return {
|
|
type: policy.constructor.name,
|
|
options: (options instanceof Map) ? mapToObject(options) : utils.emptyObject
|
|
};
|
|
}
|
|
|
|
function getConsistencyString(c) {
|
|
if (typeof c !== 'number') {
|
|
return undefined;
|
|
}
|
|
|
|
return types.consistencyToString[c];
|
|
}
|
|
|
|
function getConstructor(instance) {
|
|
return instance ? instance.constructor.name : undefined;
|
|
}
|
|
|
|
function getExecutionProfiles(client) {
|
|
const executionProfiles = {};
|
|
|
|
const defaultProfile = client.profileManager.getDefault();
|
|
setExecutionProfileProperties(client, executionProfiles, defaultProfile, defaultProfile);
|
|
|
|
client.profileManager.getAll()
|
|
.filter(p => p !== defaultProfile)
|
|
.forEach(profile => setExecutionProfileProperties(client, executionProfiles, profile, defaultProfile));
|
|
|
|
return executionProfiles;
|
|
}
|
|
|
|
function setExecutionProfileProperties(client, parent, profile, defaultProfile) {
|
|
const output = parent[profile.name] = {};
|
|
setExecutionProfileItem(output, profile, defaultProfile, 'readTimeout');
|
|
setExecutionProfileItem(output, profile, defaultProfile, 'loadBalancing', getPolicyInfo);
|
|
setExecutionProfileItem(output, profile, defaultProfile, 'retry', getPolicyInfo);
|
|
setExecutionProfileItem(output, profile, defaultProfile, 'consistency', getConsistencyString);
|
|
setExecutionProfileItem(output, profile, defaultProfile, 'serialConsistency', getConsistencyString);
|
|
|
|
if (profile === defaultProfile) {
|
|
// Speculative execution policy is included in the profiles as some drivers support
|
|
// different spec exec policy per profile, in this case is fixed for all profiles
|
|
output.speculativeExecution = getPolicyInfo(client.options.policies.speculativeExecution);
|
|
}
|
|
|
|
if (profile.graphOptions) {
|
|
output.graphOptions = {};
|
|
const defaultGraphOptions = defaultProfile.graphOptions || utils.emptyObject;
|
|
setExecutionProfileItem(output.graphOptions, profile.graphOptions, defaultGraphOptions, 'language');
|
|
setExecutionProfileItem(output.graphOptions, profile.graphOptions, defaultGraphOptions, 'name');
|
|
setExecutionProfileItem(output.graphOptions, profile.graphOptions, defaultGraphOptions, 'readConsistency',
|
|
getConsistencyString);
|
|
setExecutionProfileItem(output.graphOptions, profile.graphOptions, defaultGraphOptions, 'source');
|
|
setExecutionProfileItem(output.graphOptions, profile.graphOptions, defaultGraphOptions, 'writeConsistency',
|
|
getConsistencyString);
|
|
|
|
if (Object.keys(output.graphOptions).length === 0) {
|
|
// Properties that are undefined will not be included in the JSON
|
|
output.graphOptions = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setExecutionProfileItem(output, profile, defaultProfile, prop, valueGetter) {
|
|
const value = profile[prop];
|
|
valueGetter = valueGetter || (x => x);
|
|
|
|
if ((profile === defaultProfile && value !== undefined) || value !== defaultProfile[prop]) {
|
|
output[prop] = valueGetter(value);
|
|
}
|
|
} |