UNPKG

@stoplight/moleculer

Version:

Fast & powerful microservices framework for Node.JS

418 lines (366 loc) 8.92 kB
/* * moleculer * Copyright (c) 2019 MoleculerJS (https://github.com/moleculerjs/moleculer) * MIT Licensed */ "use strict"; const BaseMetric = require("./base"); const _ = require("lodash"); const METRIC = require("../constants"); const MetricRate = require("../rates"); const { isPlainObject } = require("../../utils"); const sortAscending = (a, b) => a - b; const setProp = (o, k, v) => { o[k] = v; return o; }; /** * Histogram metric class. * * @class HistogramMetric * @extends {BaseMetric} */ class HistogramMetric extends BaseMetric { /** * Creates an instance of HistogramMetric. * @param {Object} opts * @param {MetricRegistry} registry * @memberof HistogramMetric */ constructor(opts, registry) { super(opts, registry); this.type = METRIC.TYPE_HISTOGRAM; // Create buckets if (isPlainObject(opts.linearBuckets)) { this.buckets = HistogramMetric.generateLinearBuckets( opts.linearBuckets.start, opts.linearBuckets.width, opts.linearBuckets.count ); } else if (isPlainObject(opts.exponentialBuckets)) { this.buckets = HistogramMetric.generateExponentialBuckets( opts.exponentialBuckets.start, opts.exponentialBuckets.factor, opts.exponentialBuckets.count ); } else if (Array.isArray(opts.buckets)) { this.buckets = Array.from(opts.buckets); } else if (opts.buckets === true) { this.buckets = this.registry.opts.defaultBuckets; } if (this.buckets) { this.buckets.sort(sortAscending); } // Create quantiles if (Array.isArray(opts.quantiles)) { this.quantiles = Array.from(opts.quantiles); } else if (opts.quantiles === true) { this.quantiles = this.registry.opts.defaultQuantiles; } if (this.quantiles) { this.quantiles.sort(sortAscending); this.maxAgeSeconds = opts.maxAgeSeconds || this.registry.opts.defaultMaxAgeSeconds; // 1 minute this.ageBuckets = opts.ageBuckets || this.registry.opts.defaultAgeBuckets; // 10 secs per bucket } this.rate = opts.rate; } /** * Observe a value. * * @param {Number} value * @param {Object?} labels * @param {Number?} timestamp * @returns * @memberof HistogramMetric */ observe(value, labels, timestamp) { const hash = this.hashingLabels(labels); let item = this.values.get(hash); if (!item) { item = this.resetItem({ labels: _.pick(labels, this.labelNames) }); if (this.rate) item.rate = new MetricRate(this, item, 1); this.values.set(hash, item); } item.timestamp = timestamp == null ? Date.now() : timestamp; item.sum += value; item.count++; item.lastValue = value; if (item.bucketValues) { const len = this.buckets.length; for (let i = 0; i < len; i++) { if (value <= this.buckets[i]) { item.bucketValues[this.buckets[i]] += 1; } } } if (item.quantileValues) { item.quantileValues.add(value); } if (item.rate) item.rate.update(item.count); this.changed(value, labels, timestamp); return item; } /** * Create new bucket values based on options. * * @returns {Object} * @memberof HistogramMetric */ createBucketValues() { return this.buckets.reduce((a, bound) => setProp(a, bound, 0), {}); } /** * Generate a snapshot * * @returns {Array<Object>} * @memberof HistogramMetric */ generateSnapshot() { return Array.from(this.values.keys()).map(key => this.generateItemSnapshot(this.values.get(key), key) ); } /** * Generate a snapshot for an item * * @param {Object} item * @param {String} key * @returns {Object} * @memberof HistogramMetric */ generateItemSnapshot(item, key) { const snapshot = { key, labels: item.labels, count: item.count, sum: item.sum, lastValue: item.lastValue, timestamp: item.timestamp }; if (this.buckets) snapshot.buckets = this.buckets.reduce( (a, b) => setProp(a, b, item.bucketValues[b]), {} ); if (this.quantiles) Object.assign(snapshot, item.quantileValues.snapshot()); if (item.rate) snapshot.rate = item.rate.rate; return snapshot; } /** * Reset value of item. * * @param {Object} item * @param {Number?} timestamp */ resetItem(item, timestamp) { item.timestamp = timestamp == null ? Date.now() : timestamp; item.sum = 0; item.count = 0; item.lastValue = null; if (this.buckets) { item.bucketValues = this.createBucketValues(); } if (this.quantiles) { item.quantileValues = new TimeWindowQuantiles( this, this.quantiles, this.maxAgeSeconds, this.ageBuckets ); } return item; } /** * Reset item by labels. * * @param {Object} labels * @param {Number?} timestamp * @returns * @memberof HistogramMetric */ reset(labels, timestamp) { const hash = this.hashingLabels(labels); const item = this.values.get(hash); if (item) { this.resetItem(item, timestamp); this.changed(null, labels, timestamp); } } /** * Reset all items. * * @param {Number?} timestamp * @memberof HistogramMetric */ resetAll(timestamp) { this.values.forEach(item => this.resetItem(item, timestamp)); this.changed(); } /** * Generate linear buckets * * @static * @param {Number} start * @param {Number} width * @param {Number} count * @returns {Array<Number>} * @memberof HistogramMetric */ static generateLinearBuckets(start, width, count) { const buckets = []; for (let i = 0; i < count; i++) buckets.push(start + i * width); return buckets; } /** * Generate exponential buckets * * @static * @param {Number} start * @param {Number} factor * @param {Number} count * @returns {Array<Number>} * @memberof HistogramMetric */ static generateExponentialBuckets(start, factor, count) { const buckets = []; for (let i = 0; i < count; i++) buckets[i] = start * Math.pow(factor, i); return buckets; } } /** * Timewindow class for quantiles. * * @class TimeWindowQuantiles */ class TimeWindowQuantiles { /** * Creates an instance of TimeWindowQuantiles. * @param {BaseMetric} metric * @param {Array<Number>} quantiles * @param {Number} maxAgeSeconds * @param {Number} ageBuckets * @memberof TimeWindowQuantiles */ constructor(metric, quantiles, maxAgeSeconds, ageBuckets) { this.metric = metric; this.quantiles = Array.from(quantiles); this.maxAgeSeconds = maxAgeSeconds; this.ageBuckets = ageBuckets; this.ringBuckets = []; for (let i = 0; i < ageBuckets; i++) { this.ringBuckets.push(new Bucket()); } this.dirty = true; this.currentBucket = -1; this.rotate(); this.lastSnapshot = null; this.setDirty(); } /** * Set dirty flag. * * @memberof TimeWindowQuantiles */ setDirty() { this.dirty = true; this.metric.setDirty(); } /** * Clear dirty flag. * * @memberof TimeWindowQuantiles */ clearDirty() { this.dirty = false; } /** * Rotate the ring buckets. * * @memberof TimeWindowQuantiles */ rotate() { this.currentBucket = (this.currentBucket + 1) % this.ageBuckets; this.ringBuckets[this.currentBucket].clear(); this.setDirty(); setTimeout(() => this.rotate(), (this.maxAgeSeconds / this.ageBuckets) * 1000).unref(); } /** * Add a new value to the current bucket. * * @param {Number} value * @memberof TimeWindowQuantiles */ add(value) { this.setDirty(); this.ringBuckets[this.currentBucket].add(value); } /** * Generate a snapshot from buckets and calculate min, max, mean, quantiles, variance & StdDev. * * @returns {Object} * @memberof TimeWindowQuantiles */ snapshot() { if (!this.dirty && this.lastSnapshot) return this.lastSnapshot; const samples = this.ringBuckets.reduce((a, b) => a.concat(b.samples), []); samples.sort(sortAscending); const mean = samples.length ? samples.reduce((a, b) => a + b, 0) / samples.length : null; const variance = samples.length > 1 ? samples.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / (samples.length - 1) : null; const stdDev = variance ? Math.sqrt(variance) : null; this.lastSnapshot = { min: samples.length ? samples[0] : null, mean, variance, stdDev, max: samples.length ? samples[samples.length - 1] : null, quantiles: this.quantiles.reduce( (a, q) => setProp(a, q, samples[Math.ceil(q * samples.length) - 1]), {} ) }; this.clearDirty(); return this.lastSnapshot; } } /** * Bucket class * * @class Bucket */ class Bucket { /** * Creates an instance of Bucket. * @memberof Bucket */ constructor() { this.count = 0; this.samples = []; } /** * Add value to the bucket. * * @param {Number} value * @memberof Bucket */ add(value) { this.samples.push(value); this.count++; } /** * Clear bucket. * * @memberof Bucket */ clear() { this.count = 0; this.samples.length = 0; } } HistogramMetric.Bucket = Bucket; HistogramMetric.TimeWindowQuantiles = TimeWindowQuantiles; module.exports = HistogramMetric;