const { Reporter } = require('measured-reporting');
const { MetricTypes } = require('measured-core');
const { validateSignalFxClient } = require('../validators/inputValidators');
const { USER_DEFINED } = require('../SignalFxEventCategories');
/**
* A Reporter that reports metrics to Signal Fx
* @extends {Reporter}
*/
class SignalFxMetricsReporter extends Reporter {
/**
* @param {SignalFxClient} signalFxClient The configured signal fx client.
* @param {ReporterOptions} [options] See {@link ReporterOptions}.
*/
constructor(signalFxClient, options) {
options = options || {};
super(options);
validateSignalFxClient(signalFxClient);
this._signalFxClient = signalFxClient;
this._log.debug(`SignalFx Metrics Reporter Created with the following default reporting interval: ${options.defaultReportingIntervalInSeconds}, default dimensions: ${JSON.stringify(options.defaultDimensions, null, 2)}`);
}
/**
* Sends metrics to signal fx, converting name and dimensions and {@link Metric} to data signal fx can ingest
* @param {MetricWrapper[]} metrics The array of metrics to send to signal fx.
* @protected
*/
_reportMetrics(metrics) {
this._log.debug('_reportMetrics() called');
let signalFxDataPointRequest = {};
metrics.forEach(metric => {
if (!metric) {
this._log.warn('Metric was null when it should not have been');
return;
}
signalFxDataPointRequest = this._processMetric(metric, signalFxDataPointRequest);
});
this._log.debug(`Sending data to Signal Fx. Request: ${JSON.stringify(signalFxDataPointRequest)}`);
this._signalFxClient.send(signalFxDataPointRequest).catch(error => {
this._log.error('Failed to send metrics to signal fx error:', error);
});
}
/**
* Method for getting raw signal fx api request values from the Timer Object.
*
* @param {MetricWrapper} metric metric The Wrapped Metric Object.
* @param {any} currentBuiltRequest The signal fx request that is being built.
* @return {any} the currentBuiltRequest The signal fx request that is being built with the given metric in it.
* @protected
*/
_processMetric(metric, currentBuiltRequest) {
const newRequest = Object.assign({}, currentBuiltRequest);
const { name, metricImpl } = metric;
const mergedDimensions = this._getDimensions(metric);
const valuesToProcess = this._getValuesToProcessForType(name, metricImpl);
valuesToProcess.forEach(metricValueTypeWrapper => {
const signalFxDataPointMetric = {
metric: metricValueTypeWrapper.metric,
value: metricValueTypeWrapper.value,
dimensions: mergedDimensions
};
if (Object.prototype.hasOwnProperty.call(newRequest, metricValueTypeWrapper.type)) {
newRequest[metricValueTypeWrapper.type].push(signalFxDataPointMetric);
} else {
newRequest[metricValueTypeWrapper.type] = [signalFxDataPointMetric];
}
});
return newRequest;
}
/**
* Maps Measured Metrics Object JSON outputs to their respective signal fx metrics using logic from
* com.signalfx.codahale.reporter.AggregateMetricSenderSessionWrapper in the java lib to derive naming
*
* @param {string} name The registered metric base name
* @param {Metric} metric The metric.
* @return {MetricValueTypeWrapper[]} an array of MetricValueTypeWrapper that can be used to
* build the SignalFx data point request
* @protected
*/
_getValuesToProcessForType(name, metric) {
const type = metric.getType();
switch (type) {
case MetricTypes.TIMER:
return this._getValuesToProcessForTimer(name, metric);
case MetricTypes.GAUGE:
return this._getValuesToProcessForGauge(name, metric);
case MetricTypes.COUNTER:
return this._getValuesToProcessForCounter(name, metric);
case MetricTypes.HISTOGRAM:
return this._getValuesToProcessForHistogram(name, metric);
case MetricTypes.METER:
this._log.warn(
'Meters are not reported to SignalFx. Meters do not make sense to use with SignalFx because the same values ' +
'can be calculated using simple counters and aggregations within SignalFx itself.'
);
return [];
default:
this._log.error(`Metric Type: ${type} has not been implemented to report to signal fx`);
return [];
}
}
/**
* Maps and Filters values from a Timer to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Timer} timer The Timer
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForTimer(name, timer) {
let valuesToProcess = [];
// only grab histogram data as Meters can be accomplished with signal fx using the count from the histogram
valuesToProcess = valuesToProcess.concat(this._getValuesToProcessForHistogram(name, timer._histogram));
return valuesToProcess;
}
/**
* Maps values from a Gauge to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Gauge} gauge The Gauge
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForGauge(name, gauge) {
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}`,
value: gauge.toJSON(),
type: SIGNAL_FX_GAUGE
});
return valuesToProcess;
}
/**
* Maps values from a Counter to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Counter} counter The data from the measure metric object
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForCounter(name, counter) {
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}.count`,
value: counter.toJSON(),
type: SIGNAL_FX_CUMULATIVE_COUNTER
});
return valuesToProcess;
}
/**
* Maps and Filters values from a Histogram to a set of metrics to report to SigFx.
*
* @param {string} name The registry name
* @param {Histogram} histogram The Histogram
* @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request
* @protected
*/
_getValuesToProcessForHistogram(name, histogram) {
// TODO add full list of histogram metrics but enable filter
const data = histogram.toJSON();
const valuesToProcess = [];
valuesToProcess.push({
metric: `${name}.count`,
value: data.count || 0,
type: SIGNAL_FX_CUMULATIVE_COUNTER
});
valuesToProcess.push({
metric: `${name}.max`,
value: data.max || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.min`,
value: data.min || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.mean`,
value: data.mean || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.p95`,
value: data.p95 || 0,
type: SIGNAL_FX_GAUGE
});
valuesToProcess.push({
metric: `${name}.p99`,
value: data.p99 || 0,
type: SIGNAL_FX_GAUGE
});
return valuesToProcess;
}
/**
* Function exposes the event API of Signal Fx.
* See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details.
*
* @param {string} eventType The event type (name of the event time series).
* @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED.
* @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default
* @param {Object.<string, string>} [properties] a map of extra properties on that event, empty dictionary by default
* @param {number} [timestamp] a timestamp, by default is current time.
*
* @example
* const {
* SignalFxSelfReportingMetricsRegistry,
* SignalFxMetricsReporter,
* SignalFxEventCategories
* } = require('measured-signalfx-reporter');
* const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient));
* registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT);
*/
sendEvent(eventType, category, dimensions, properties, timestamp) {
const body = {
eventType,
category: category || USER_DEFINED,
dimensions: this._getDimensions({ dimensions }),
properties,
timestamp
};
Object.keys(body).forEach(key => body[key] == null && delete body[key]);
return this._signalFxClient.sendEvent(body);
}
}
// const SIGNAL_FX_COUNTER = 'counters';
const SIGNAL_FX_GAUGE = 'gauges';
const SIGNAL_FX_CUMULATIVE_COUNTER = 'cumulative_counters';
module.exports = SignalFxMetricsReporter;
/**
* Wrapper object to wrap metric value and SFX metadata needed to send metric value to SFX data ingestion.
*
* @interface MetricValueTypeWrapper
* @typedef MetricValueTypeWrapper
* @type {Object}
* @property {string} metric The metric name to report to SignalFx
* @property {number} value the value to report to SignalFx
* @property {string} type The mapped SignalFx metric type
*/