mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
5ef889addd
Summary: This moves enough server material into core to run a home server. The data engine is not yet incorporated (though in manual testing it works when ported). Test Plan: existing tests pass Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2552
206 lines
7.1 KiB
JavaScript
206 lines
7.1 KiB
JavaScript
const _ = require('underscore');
|
|
const net = require('net');
|
|
const Promise = require('bluebird');
|
|
const log = require('./log');
|
|
const MetricCollector = require('app/common/MetricCollector');
|
|
const metricConfig = require('app/common/metricConfig');
|
|
const shutdown = require('./shutdown');
|
|
const version = require('app/common/version');
|
|
const crypto = require('crypto');
|
|
|
|
// Grist Metrics EC2 instance host and port
|
|
const host = 'metrics.getgrist.com';
|
|
const port = '2023'; // Plain-text port of carbon-aggregator
|
|
|
|
// Global reference to an instance of this class established in the constuctor.
|
|
var globalServerMetrics = null;
|
|
|
|
/**
|
|
* Server-facing class for initializing server metrics collection.
|
|
* Establishes interval attempts to push measured server metrics to the prometheus PushGateway
|
|
* on creation.
|
|
* @param {Object} user - Instance of User.js server class, which contains config settings.
|
|
*/
|
|
function ServerMetrics() {
|
|
MetricCollector.call(this);
|
|
this.socket = null;
|
|
// Randomly generated id to differentiate between metrics from this server and others.
|
|
this.serverId = crypto.randomBytes(8).toString('hex');
|
|
this.serverMetrics = this.initMetricTools(metricConfig.serverMetrics);
|
|
this.clientNames = null;
|
|
this.enabled = false;
|
|
// Produce the prefix string for all metrics.
|
|
// NOTE: If grist-rt is used instead of grist-raw for some metrics, this must be changed.
|
|
let versionStr = version.version.replace(/\W/g, '-');
|
|
let channelStr = version.channel.replace(/\W/g, '-');
|
|
this._prefix = `grist-raw.instance.${channelStr}.${versionStr}`;
|
|
|
|
globalServerMetrics = this;
|
|
|
|
// This will not send metrics when they are disabled since there is a check in pushMetrics.
|
|
shutdown.addCleanupHandler(null, () => this.attemptPush());
|
|
}
|
|
_.extend(ServerMetrics.prototype, MetricCollector.prototype);
|
|
|
|
/**
|
|
* Checks the given preferences object from the user configuration and starts pushing metrics
|
|
* to carbon if metrics are enabled. Otherwise, ends the socket connection if there is one.
|
|
*/
|
|
ServerMetrics.prototype.handlePreferences = function(config) {
|
|
config = config || {};
|
|
this.enabled = config.enableMetrics;
|
|
Promise.resolve(this.enabled && this._connectSocket())
|
|
.then(() => {
|
|
if (this.enabled) {
|
|
this._push = setTimeout(() => this.attemptPush(), metricConfig.SERVER_PUSH_INTERVAL);
|
|
} else if (this.socket) {
|
|
this.socket.end();
|
|
}
|
|
});
|
|
};
|
|
|
|
ServerMetrics.prototype.disable = function() {
|
|
this.enabled = false;
|
|
if (this._push) {
|
|
clearTimeout(this._push);
|
|
this._push = null;
|
|
}
|
|
if (this._collect) {
|
|
clearTimeout(this._collect);
|
|
this._collect = null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a promise for a socket connection to the Carbon metrics collection server.
|
|
* The promise will not fail because of connection errors, rather it will be continuously
|
|
* re-evaluated until it connects. The retry rate is specified in metricConfig.js
|
|
*/
|
|
ServerMetrics.prototype._connectSocket = function() {
|
|
if (!this.enabled) { return Promise.resolve(); }
|
|
var socket = null;
|
|
log.info('Attempting connection to Carbon metrics server');
|
|
return new Promise((resolve, reject) => {
|
|
socket = net.connect({host: host, port: port}, () => {
|
|
log.info('Connected to Carbon metrics server');
|
|
this.socket = socket;
|
|
resolve();
|
|
});
|
|
socket.setEncoding('utf8');
|
|
socket.on('error', err => {
|
|
log.warn('Carbon metrics connection error: %s', err);
|
|
if (this.socket) {
|
|
this.socket.end();
|
|
this.socket = null;
|
|
}
|
|
reject(err);
|
|
});
|
|
})
|
|
.catch(() => {
|
|
return Promise.delay(metricConfig.CONN_RETRY)
|
|
.then(() => this._connectSocket());
|
|
});
|
|
};
|
|
|
|
// Returns a map from metric names (as entered in metricConfig.js) to their metricTools.
|
|
ServerMetrics.prototype.getMetrics = function() {
|
|
return this.serverMetrics;
|
|
};
|
|
|
|
// Pushes ready server and client metrics to the aggregator
|
|
ServerMetrics.prototype.pushMetrics = function(metrics) {
|
|
if (this.enabled) {
|
|
return this._request(metrics.join(""))
|
|
.finally(() => {
|
|
this._push = setTimeout(() => this.attemptPush(), metricConfig.SERVER_PUSH_INTERVAL);
|
|
});
|
|
}
|
|
};
|
|
|
|
ServerMetrics.prototype._request = function(text) {
|
|
return new Promise(resolve => {
|
|
if (!this.enabled) {
|
|
resolve();
|
|
return;
|
|
}
|
|
this.socket.write(text, 'utf8', () => {
|
|
log.info('Pushed metrics to Carbon');
|
|
resolve();
|
|
});
|
|
})
|
|
.catch(() => {
|
|
return this._connectSocket()
|
|
.then(() => this._request(text));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Function exposed to comm interface to provide server with client list of metrics.
|
|
* Used so that ServerMetrics can associate indices to client metric names.
|
|
* @param {Array} metricNames - A list of client metric names in the order in which values will be sent.
|
|
*/
|
|
ServerMetrics.prototype.registerClientMetrics = function(client, metricNames) {
|
|
this.clientNames = metricNames;
|
|
};
|
|
|
|
/**
|
|
* Function exposed to comm interface to allow client metrics to be pushed to this file,
|
|
* so that they may in turn be pushed to Carbon with the server metrics.
|
|
* @param {Array} data - A list of client buckets as defined in ClientMetrics.js's createBucket
|
|
*/
|
|
ServerMetrics.prototype.pushClientMetrics = function(client, data) {
|
|
// Merge ready client bucket metrics into ready server buckets.
|
|
if (!this.clientNames) {
|
|
throw new Error("Client metrics must be registered");
|
|
}
|
|
data.forEach(clientBucket => {
|
|
// Label the bucket with the client id so that clients' metrics do not replace one another
|
|
let clientData = clientBucket.values.map((val, i) => {
|
|
return this._stringifyMetric(this.clientNames[i], client.clientId, val, clientBucket.startTime);
|
|
}).join("");
|
|
this.queueBucket(clientData);
|
|
});
|
|
};
|
|
|
|
ServerMetrics.prototype.get = function(name) {
|
|
this.prepareCompletedBuckets(Date.now());
|
|
return this.serverMetrics[name];
|
|
};
|
|
|
|
/**
|
|
* Creates string bucket with metrics in carbon's text format.
|
|
* For details, see phriction documentation: https://phab.getgrist.com/w/metrics/
|
|
*/
|
|
ServerMetrics.prototype.createBucket = function(bucketStart) {
|
|
var data = [];
|
|
var bucketEnd = bucketStart + metricConfig.BUCKET_SIZE;
|
|
this.forEachBucketMetric(bucketEnd, tool => {
|
|
if (tool.getValue(bucketEnd) !== null) {
|
|
data.push(this._stringifyMetric(tool.getName(), this.serverId, tool.getValue(bucketEnd), bucketStart));
|
|
}
|
|
});
|
|
return data.join("");
|
|
};
|
|
|
|
// Helper to stringify individual metrics for carbon's text format.
|
|
ServerMetrics.prototype._stringifyMetric = function(name, id, val, startTime) {
|
|
// Server/client id is added to name for differentiating inputs to aggregator
|
|
return `${this._prefix}.${name}.${id} ${val} ${startTime/1000}\n`;
|
|
};
|
|
|
|
/**
|
|
* Static get method to retreive server metric recording tools.
|
|
* IMPORTANT: Usage involves the side effect of updating completed buckets and
|
|
* adding them to a ready object. get() results should not be assigned to variables and
|
|
* reused, rather get() should be called each time a metric is needed.
|
|
*/
|
|
ServerMetrics.get = function(name) {
|
|
if (!globalServerMetrics) {
|
|
throw new Error('Must create ServerMetrics instance to access server metrics.');
|
|
}
|
|
return globalServerMetrics.get(name);
|
|
};
|
|
|
|
module.exports = ServerMetrics;
|