@stoplight/moleculer
Version:
Fast & powerful microservices framework for Node.JS
404 lines (343 loc) • 9.2 kB
JavaScript
/*
* moleculer
* Copyright (c) 2020 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/
"use strict";
const _ = require("lodash");
const { match, isFunction, isPlainObject, isString } = require("../utils");
const METRIC = require("./constants");
const Types = require("./types");
const Reporters = require("./reporters");
const { registerCommonMetrics, updateCommonMetrics } = require("./commons");
const METRIC_NAME_REGEXP = /^[a-zA-Z_][a-zA-Z0-9-_:.]*$/;
const METRIC_LABEL_REGEXP = /^[a-zA-Z_][a-zA-Z0-9-_.]*$/;
/**
* Metric Registry class
*/
class MetricRegistry {
/**
* Creates an instance of MetricRegistry.
*
* @param {ServiceBroker} broker
* @param {Object} opts
* @memberof MetricRegistry
*/
constructor(broker, opts) {
this.broker = broker;
this.logger = broker.getLogger("metrics");
this.dirty = true;
if (opts === true || opts === false) opts = { enabled: opts };
this.opts = _.defaultsDeep({}, opts, {
enabled: true,
collectProcessMetrics: process.env.NODE_ENV !== "test",
collectInterval: 5,
reporter: false,
defaultBuckets: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000], // in milliseconds
defaultQuantiles: [0.5, 0.9, 0.95, 0.99, 0.999], // percentage
defaultMaxAgeSeconds: 60,
defaultAgeBuckets: 10,
defaultAggregator: "sum"
});
this.store = new Map();
if (this.opts.enabled) this.logger.info("Metrics: Enabled");
}
/**
* Initialize Registry.
*/
init() {
if (this.opts.enabled) {
// Create Reporter instances
if (this.opts.reporter) {
const reporters = Array.isArray(this.opts.reporter)
? this.opts.reporter
: [this.opts.reporter];
this.reporter = _.compact(reporters).map(r => {
const reporter = Reporters.resolve(r);
reporter.init(this);
return reporter;
});
const reporterNames = this.reporter.map(reporter =>
this.broker.getConstructorName(reporter)
);
this.logger.info(
`Metric reporter${reporterNames.length > 1 ? "s" : ""}: ${reporterNames.join(
", "
)}`
);
}
// Start colllect timer
if (this.opts.collectProcessMetrics) {
this.collectTimer = setInterval(() => {
updateCommonMetrics.call(this);
}, this.opts.collectInterval * 1000);
this.collectTimer.unref();
registerCommonMetrics.call(this);
updateCommonMetrics.call(this);
}
}
}
/**
* Stop Metric Registry
*/
stop() {
if (this.collectTimer) {
clearInterval(this.collectTimer);
}
if (this.reporter) {
return this.broker.Promise.all(this.reporter.map(r => r.stop()));
}
}
/**
* Check metric is enabled?
*
* @returns
* @memberof MetricRegistry
*/
isEnabled() {
return this.opts.enabled;
}
/**
* Register a new metric.
*
* @param {Object} opts
* @returns {BaseMetric}
* @memberof MetricRegistry
*/
register(opts) {
if (!isPlainObject(opts)) throw new Error("Wrong argument. Must be an Object.");
if (!opts.type) throw new Error("The metric 'type' property is mandatory.");
if (!opts.name) throw new Error("The metric 'name' property is mandatory.");
if (!METRIC_NAME_REGEXP.test(opts.name))
throw new Error("The metric 'name' is not valid: " + opts.name);
if (Array.isArray(opts.labelNames)) {
opts.labelNames.forEach(name => {
if (!METRIC_LABEL_REGEXP.test(name))
throw new Error(`The '${opts.name}' metric label name is not valid: ${name}`);
});
}
const MetricClass = Types.resolve(opts.type);
if (!this.opts.enabled) return null;
const item = new MetricClass(opts, this);
this.store.set(opts.name, item);
return item;
}
/**
* Check a metric by name.
*
* @param {String} name
* @returns {Boolean}
* @memberof MetricRegistry
*/
hasMetric(name) {
return this.store.has(name);
}
/**
* Get metric by name
*
* @param {String} name
* @returns {BaseMetric}
* @memberof MetricRegistry
*/
getMetric(name) {
const item = this.store.get(name);
if (!item) return null;
return item;
}
/**
* Increment a metric value.
*
* @param {String} name
* @param {Object?} labels
* @param {number} [value=1]
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
increment(name, labels, value = 1, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
if (!isFunction(item.increment))
throw new Error(
"Invalid metric type. Incrementing works only with counter & gauge metric types."
);
return item.increment(labels, value, timestamp);
}
/**
* Decrement a metric value.
*
* @param {String} name
* @param {Object?} labels
* @param {number} [value=1]
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
decrement(name, labels, value = 1, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
if (!isFunction(item.decrement))
throw new Error("Invalid metric type. Decrementing works only with gauge metric type.");
return item.decrement(labels, value, timestamp);
}
/**
* Set a metric value.
*
* @param {String} name
* @param {*} value
* @param {Object?} labels
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
set(name, value, labels, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
if (!isFunction(item.set))
throw new Error(
"Invalid metric type. Value setting works only with counter, gauge & info metric types."
);
return item.set(value, labels, timestamp);
}
/**
* Observe a metric.
*
* @param {String} name
* @param {Number} value
* @param {Object?} labels
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
observe(name, value, labels, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
if (!isFunction(item.observe))
throw new Error(
"Invalid metric type. Observing works only with histogram metric type."
);
return item.observe(value, labels, timestamp);
}
/**
* Reset metric values.
*
* @param {String} name
* @param {Object?} labels
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
reset(name, labels, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
item.reset(labels, timestamp);
}
/**
* Reset metric all values.
*
* @param {String} name
* @param {Number?} timestamp
* @returns
* @memberof MetricRegistry
*/
resetAll(name, timestamp) {
if (!this.opts.enabled) return null;
const item = this.getMetric(name);
item.resetAll(timestamp);
}
/**
* Start a timer & observe the elapsed time.
*
* @param {String} name
* @param {Object?} labels
* @param {Number?} timestamp
* @returns {Function} `end`˙function.
* @memberof MetricRegistry
*/
timer(name, labels, timestamp) {
let item;
if (name && this.opts.enabled) {
item = this.getMetric(name);
if (!isFunction(item.observe) && !isFunction(item.set)) {
/* istanbul ignore next */
throw new Error(
"Invalid metric type. Timing works only with histogram or gauge metric types"
);
}
}
const start = process.hrtime();
return () => {
const delta = process.hrtime(start);
const duration = (delta[0] + delta[1] / 1e9) * 1000;
if (item) {
if (item.type == METRIC.TYPE_HISTOGRAM) item.observe(duration, labels, timestamp);
else if (item.type == METRIC.TYPE_GAUGE) item.set(duration, labels, timestamp);
}
return duration;
};
}
/**
* Some metric has been changed.
*
* @param {BaseMetric} metric
* @param {any} value
* @param {Object} labels
* @param {Number?} timestamp
*
* @memberof MetricRegistry
*/
changed(metric, value, labels, timestamp) {
this.dirty = true;
if (Array.isArray(this.reporter))
this.reporter.forEach(reporter =>
reporter.metricChanged(metric, value, labels, timestamp)
);
}
/**
* List all registered metrics with labels & values.
*
* @param {Object?} opts
* @param {String|Array<String>|null} opts.types
* @param {String|Array<String>|null} opts.includes
* @param {String|Array<String>|null} opts.excludes
*/
list(opts) {
const res = [];
opts = opts || {};
const types =
opts.types != null ? (isString(opts.types) ? [opts.types] : opts.types) : null;
const includes =
opts.includes != null
? isString(opts.includes)
? [opts.includes]
: opts.includes
: null;
const excludes =
opts.excludes != null
? isString(opts.excludes)
? [opts.excludes]
: opts.excludes
: null;
this.store.forEach(metric => {
if (types && !types.some(type => metric.type == type)) return;
if (includes && !includes.some(pattern => match(metric.name, pattern))) return;
if (excludes && !excludes.every(pattern => !match(metric.name, pattern))) return;
res.push(metric.toObject());
});
return res;
}
/**
* Pluralize metric units.
*
* @param {String} unit
* @returns {String}
*/
pluralizeUnit(unit) {
switch (unit) {
case METRIC.UNIT_GHZ:
return unit;
}
return unit + "s";
}
}
module.exports = MetricRegistry;