const consoleLogLevel = require('console-log-level');
const Optional = require('optional-js');
const { validateReporterParameters } = require('../validators/inputValidators');
const DEFAULT_REPORTING_INTERVAL_IN_SECONDS = 10;
function prefix() {
return `${new Date().toISOString()}: `;
}
/**
* The abstract reporter that specific implementations can extend to create a Self Reporting Metrics Registry Reporter.
*
* {@link SelfReportingMetricsRegistry}
*
* @example
* const os = require('os');
* const process = require('process');
* const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting');
*
* // Create a self reporting registry with a named anonymous reporter instance;
* const registry = new SelfReportingMetricsRegistry(
* new class ConsoleReporter extends Reporter {
* constructor() {
* super({
* defaultDimensions: {
* hostname: os.hostname(),
* env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset'
* }
* })
* }
*
* _reportMetrics(metrics) {
* metrics.forEach(metric => {
* console.log(JSON.stringify({
* metricName: metric.name,
* dimensions: this._getDimensions(metric),
* data: metric.metricImpl.toJSON()
* }))
* });
* }
* }()
* );
*
* @example
* // Create a regular class that extends Reporter
* class LoggingReporter extends Reporter {
* _reportMetrics(metrics) {
* metrics.forEach(metric => {
* this._log.info(JSON.stringify({
* metricName: metric.name,
* dimensions: this._getDimensions(metric),
* data: metric.metricImpl.toJSON()
* }))
* });
* }
* }
*
* @abstract
*/
class Reporter {
/**
* @param {ReporterOptions} [options] The optional params to supply when creating a reporter.
*/
constructor(options) {
if (this.constructor === Reporter) {
throw new TypeError("Can't instantiate abstract class!");
}
options = options || {};
validateReporterParameters(options);
/**
* Map of intervals to metric keys, this will be used to look up what metrics should be reported at a given interval.
*
* @type {Object.<number, Set<string>>}
* @private
*/
this._intervalToMetric = {};
this._intervals = [];
/**
* Map of default dimensions, that should be sent with every metric.
*
* @type {Dimensions}
* @protected
*/
this._defaultDimensions = options.defaultDimensions || {};
/**
* Loggers to use, defaults to a new console logger if nothing is supplied in options
* @type {Logger}
* @protected
*/
this._log =
options.logger || consoleLogLevel({ name: 'Reporter', level: options.logLevel || 'info', prefix: prefix });
/**
* The default reporting interval, a number in seconds.
* If not overridden via the {@see ReporterOptions}, defaults to 10 seconds.
*
* @type {number}
* @protected
*/
this._defaultReportingIntervalInSeconds =
options.defaultReportingIntervalInSeconds || DEFAULT_REPORTING_INTERVAL_IN_SECONDS;
/**
* Flag to indicate if reporting timers should be unref'd.
* If not overridden via the {@see ReporterOptions}, defaults to false.
*
* @type {boolean}
* @protected
*/
this._unrefTimers = !!options.unrefTimers;
/**
* Flag to indicate if metrics should be reset on each reporting interval.
* If not overridden via the {@see ReporterOptions}, defaults to false.
*
* @type {boolean}
* @protected
*/
this._resetMetricsOnInterval = !!options.resetMetricsOnInterval;
}
/**
* Sets the registry, this must be called before reportMetricOnInterval.
*
* @param {DimensionAwareMetricsRegistry} registry
*/
setRegistry(registry) {
this._registry = registry;
}
/**
* Informs the reporter to report a metric on a given interval in seconds.
*
* @param {string} metricKey The metric key for the metric in the metric registry.
* @param {number} intervalInSeconds The interval in seconds to report the metric on.
*/
reportMetricOnInterval(metricKey, intervalInSeconds) {
intervalInSeconds = intervalInSeconds || this._defaultReportingIntervalInSeconds;
if (!this._registry) {
throw new Error(
'You must call setRegistry(registry) before telling a Reporter to report a metric on an interval.'
);
}
if (Object.prototype.hasOwnProperty.call(this._intervalToMetric, intervalInSeconds)) {
this._intervalToMetric[intervalInSeconds].add(metricKey);
} else {
this._intervalToMetric[intervalInSeconds] = new Set([metricKey]);
this._createIntervalCallback(intervalInSeconds);
setImmediate(() => {
this._reportMetricsWithInterval(intervalInSeconds);
});
}
}
/**
* Creates the timed callback loop for the given interval.
*
* @param {number} intervalInSeconds the interval in seconds for the timeout callback
* @private
*/
_createIntervalCallback(intervalInSeconds) {
this._log.debug(`_createIntervalCallback() called with intervalInSeconds: ${intervalInSeconds}`);
const timer = setInterval(() => {
this._reportMetricsWithInterval(intervalInSeconds);
}, intervalInSeconds * 1000);
if (this._unrefTimers) {
timer.unref();
}
this._intervals.push(timer);
}
/**
* Gathers all the metrics that have been registered to report on the given interval.
*
* @param {number} interval The interval to look up what metrics to report
* @private
*/
_reportMetricsWithInterval(interval) {
this._log.debug(`_reportMetricsWithInterval() called with intervalInSeconds: ${interval}`);
try {
Optional.of(this._intervalToMetric[interval]).ifPresent(metrics => {
const metricsToSend = [];
metrics.forEach(metricKey => {
metricsToSend.push(this._registry.getMetricWrapperByKey(metricKey));
});
this._reportMetrics(metricsToSend);
if (this._resetMetricsOnInterval) {
metricsToSend.forEach(({ name, metricImpl }) => {
if (metricImpl && metricImpl.reset) {
this._log.debug('Resetting metric', name);
metricImpl.reset();
}
});
}
});
} catch (error) {
this._log.error('Failed to send metrics to signal fx', error);
}
}
/**
* This method gets called with an array of {@link MetricWrapper} on an interval, when metrics should be reported.
*
* This is the main method that needs to get implemented when created an aggregator specific reporter.
*
* @param {MetricWrapper[]} metrics The array of metrics to report.
* @protected
* @abstract
*/
_reportMetrics(metrics) {
throw new TypeError('Abstract method _reportMetrics(metrics) must be implemented in implementation class');
}
/**
*
* @param {MetricWrapper} metric The Wrapped Metric Object.
* @return {Dimensions} The left merged default dimensions with the metric specific dimensions
* @protected
*/
_getDimensions(metric) {
return Object.assign({}, this._defaultDimensions, metric.dimensions);
}
/**
* Clears the intervals that are running to report metrics at an interval, and resets the state.
*/
shutdown() {
this._intervals.forEach(interval => clearInterval(interval));
this._intervals = [];
this._intervalToMetric = {};
}
}
/**
* Options for creating a {@link Reporter}
* @interface ReporterOptions
* @typedef ReporterOptions
* @type {Object}
* @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported
* @property {Logger} logger The logger to use, if not supplied a new Buynan logger will be created
* @property {string} logLevel The log level to use with the created console logger if you didn't supply your own logger.
* @property {number} defaultReportingIntervalInSeconds The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds.
* @property {boolean} unrefTimers Indicate if reporting timers should be unref'd, defaults to false.
* @property {boolean} resetMetricsOnInterval Indicate if metrics should be reset on each reporting interval, defaults to false.
*/
module.exports = Reporter;