352 lines
8.4 KiB
JavaScript
Executable File
352 lines
8.4 KiB
JavaScript
Executable File
/**
|
|
* Histogram
|
|
*/
|
|
'use strict';
|
|
|
|
const util = require('util');
|
|
const {
|
|
getLabels,
|
|
hashObject,
|
|
isObject,
|
|
removeLabels,
|
|
nowTimestamp,
|
|
} = require('./util');
|
|
const { validateLabel } = require('./validation');
|
|
const { Metric } = require('./metric');
|
|
const Exemplar = require('./exemplar');
|
|
|
|
class Histogram extends Metric {
|
|
constructor(config) {
|
|
super(config, {
|
|
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
});
|
|
|
|
this.type = 'histogram';
|
|
this.defaultLabels = {};
|
|
this.defaultExemplarLabelSet = {};
|
|
this.enableExemplars = false;
|
|
|
|
for (const label of this.labelNames) {
|
|
if (label === 'le') {
|
|
throw new Error('le is a reserved label keyword');
|
|
}
|
|
}
|
|
|
|
this.upperBounds = this.buckets;
|
|
this.bucketValues = this.upperBounds.reduce((acc, upperBound) => {
|
|
acc[upperBound] = 0;
|
|
return acc;
|
|
}, {});
|
|
|
|
if (config.enableExemplars) {
|
|
this.enableExemplars = true;
|
|
this.bucketExemplars = this.upperBounds.reduce((acc, upperBound) => {
|
|
acc[upperBound] = null;
|
|
return acc;
|
|
}, {});
|
|
Object.freeze(this.bucketExemplars);
|
|
this.observe = this.observeWithExemplar;
|
|
} else {
|
|
this.observe = this.observeWithoutExemplar;
|
|
}
|
|
|
|
Object.freeze(this.bucketValues);
|
|
Object.freeze(this.upperBounds);
|
|
|
|
if (this.labelNames.length === 0) {
|
|
this.hashMap = {
|
|
[hashObject({})]: createBaseValues(
|
|
{},
|
|
this.bucketValues,
|
|
this.bucketExemplars,
|
|
),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Observe a value in histogram
|
|
* @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep
|
|
* @param {Number} value - Value to observe in the histogram
|
|
* @returns {void}
|
|
*/
|
|
observeWithoutExemplar(labels, value) {
|
|
observe.call(this, labels === 0 ? 0 : labels || {})(value);
|
|
}
|
|
|
|
observeWithExemplar({
|
|
labels = this.defaultLabels,
|
|
value,
|
|
exemplarLabels = this.defaultExemplarLabelSet,
|
|
} = {}) {
|
|
observe.call(this, labels === 0 ? 0 : labels || {})(value);
|
|
this.updateExemplar(labels, value, exemplarLabels);
|
|
}
|
|
|
|
updateExemplar(labels, value, exemplarLabels) {
|
|
if (Object.keys(exemplarLabels).length === 0) return;
|
|
const hash = hashObject(labels, this.sortedLabelNames);
|
|
const bound = findBound(this.upperBounds, value);
|
|
const { bucketExemplars } = this.hashMap[hash];
|
|
let exemplar = bucketExemplars[bound];
|
|
if (!isObject(exemplar)) {
|
|
exemplar = new Exemplar();
|
|
bucketExemplars[bound] = exemplar;
|
|
}
|
|
exemplar.validateExemplarLabelSet(exemplarLabels);
|
|
exemplar.labelSet = exemplarLabels;
|
|
exemplar.value = value;
|
|
exemplar.timestamp = nowTimestamp();
|
|
}
|
|
|
|
async get() {
|
|
const data = await this.getForPromString();
|
|
data.values = data.values.map(splayLabels);
|
|
return data;
|
|
}
|
|
|
|
async getForPromString() {
|
|
if (this.collect) {
|
|
const v = this.collect();
|
|
if (v instanceof Promise) await v;
|
|
}
|
|
const data = Object.values(this.hashMap);
|
|
const values = data
|
|
.map(extractBucketValuesForExport(this))
|
|
.reduce(addSumAndCountForExport(this), []);
|
|
|
|
return {
|
|
name: this.name,
|
|
help: this.help,
|
|
type: this.type,
|
|
values,
|
|
aggregator: this.aggregator,
|
|
};
|
|
}
|
|
|
|
reset() {
|
|
this.hashMap = {};
|
|
}
|
|
|
|
/**
|
|
* Initialize the metrics for the given combination of labels to zero
|
|
* @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep
|
|
* @returns {void}
|
|
*/
|
|
zero(labels) {
|
|
const hash = hashObject(labels, this.sortedLabelNames);
|
|
this.hashMap[hash] = createBaseValues(
|
|
labels,
|
|
this.bucketValues,
|
|
this.bucketExemplars,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Start a timer that could be used to logging durations
|
|
* @param {object} labels - Object with labels where key is the label key and value is label value. Can only be one level deep
|
|
* @param {object} exemplarLabels - Object with labels for exemplar where key is the label key and value is label value. Can only be one level deep
|
|
* @returns {function} - Function to invoke when you want to stop the timer and observe the duration in seconds
|
|
* @example
|
|
* var end = histogram.startTimer();
|
|
* makeExpensiveXHRRequest(function(err, res) {
|
|
* const duration = end(); //Observe the duration of expensiveXHRRequest and returns duration in seconds
|
|
* console.log('Duration', duration);
|
|
* });
|
|
*/
|
|
startTimer(labels, exemplarLabels) {
|
|
return this.enableExemplars
|
|
? startTimerWithExemplar.call(this, labels, exemplarLabels)()
|
|
: startTimer.call(this, labels)();
|
|
}
|
|
|
|
labels(...args) {
|
|
const labels = getLabels(this.labelNames, args);
|
|
validateLabel(this.labelNames, labels);
|
|
return {
|
|
observe: observe.call(this, labels),
|
|
startTimer: startTimer.call(this, labels),
|
|
};
|
|
}
|
|
|
|
remove(...args) {
|
|
const labels = getLabels(this.labelNames, args);
|
|
validateLabel(this.labelNames, labels);
|
|
removeLabels.call(this, this.hashMap, labels, this.sortedLabelNames);
|
|
}
|
|
}
|
|
|
|
function startTimer(startLabels) {
|
|
return () => {
|
|
const start = process.hrtime();
|
|
return endLabels => {
|
|
const delta = process.hrtime(start);
|
|
const value = delta[0] + delta[1] / 1e9;
|
|
this.observe(Object.assign({}, startLabels, endLabels), value);
|
|
return value;
|
|
};
|
|
};
|
|
}
|
|
|
|
function startTimerWithExemplar(startLabels, startExemplarLabels) {
|
|
return () => {
|
|
const start = process.hrtime();
|
|
return (endLabels, endExemplarLabels) => {
|
|
const delta = process.hrtime(start);
|
|
const value = delta[0] + delta[1] / 1e9;
|
|
this.observe({
|
|
labels: Object.assign({}, startLabels, endLabels),
|
|
value,
|
|
exemplarLabels: Object.assign(
|
|
{},
|
|
startExemplarLabels,
|
|
endExemplarLabels,
|
|
),
|
|
});
|
|
return value;
|
|
};
|
|
};
|
|
}
|
|
|
|
function setValuePair(labels, value, metricName, exemplar, sharedLabels = {}) {
|
|
return {
|
|
labels,
|
|
sharedLabels,
|
|
value,
|
|
metricName,
|
|
exemplar,
|
|
};
|
|
}
|
|
|
|
function findBound(upperBounds, value) {
|
|
for (let i = 0; i < upperBounds.length; i++) {
|
|
const bound = upperBounds[i];
|
|
if (value <= bound) {
|
|
return bound;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function observe(labels) {
|
|
return value => {
|
|
const labelValuePair = convertLabelsAndValues(labels, value);
|
|
|
|
validateLabel(this.labelNames, labelValuePair.labels);
|
|
if (!Number.isFinite(labelValuePair.value)) {
|
|
throw new TypeError(
|
|
`Value is not a valid number: ${util.format(labelValuePair.value)}`,
|
|
);
|
|
}
|
|
|
|
const hash = hashObject(labelValuePair.labels, this.sortedLabelNames);
|
|
let valueFromMap = this.hashMap[hash];
|
|
if (!valueFromMap) {
|
|
valueFromMap = createBaseValues(
|
|
labelValuePair.labels,
|
|
this.bucketValues,
|
|
this.bucketExemplars,
|
|
);
|
|
}
|
|
|
|
const b = findBound(this.upperBounds, labelValuePair.value);
|
|
|
|
valueFromMap.sum += labelValuePair.value;
|
|
valueFromMap.count += 1;
|
|
|
|
if (Object.prototype.hasOwnProperty.call(valueFromMap.bucketValues, b)) {
|
|
valueFromMap.bucketValues[b] += 1;
|
|
}
|
|
|
|
this.hashMap[hash] = valueFromMap;
|
|
};
|
|
}
|
|
|
|
function createBaseValues(labels, bucketValues, bucketExemplars) {
|
|
const result = {
|
|
labels,
|
|
bucketValues: { ...bucketValues },
|
|
sum: 0,
|
|
count: 0,
|
|
};
|
|
if (bucketExemplars) {
|
|
result.bucketExemplars = { ...bucketExemplars };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function convertLabelsAndValues(labels, value) {
|
|
return isObject(labels)
|
|
? {
|
|
labels,
|
|
value,
|
|
}
|
|
: {
|
|
value: labels,
|
|
labels: {},
|
|
};
|
|
}
|
|
|
|
function extractBucketValuesForExport(histogram) {
|
|
const name = `${histogram.name}_bucket`;
|
|
return bucketData => {
|
|
let acc = 0;
|
|
const buckets = histogram.upperBounds.map(upperBound => {
|
|
acc += bucketData.bucketValues[upperBound];
|
|
return setValuePair(
|
|
{ le: upperBound },
|
|
acc,
|
|
name,
|
|
bucketData.bucketExemplars
|
|
? bucketData.bucketExemplars[upperBound]
|
|
: null,
|
|
bucketData.labels,
|
|
);
|
|
});
|
|
return { buckets, data: bucketData };
|
|
};
|
|
}
|
|
|
|
function addSumAndCountForExport(histogram) {
|
|
return (acc, d) => {
|
|
acc.push(...d.buckets);
|
|
|
|
const infLabel = { le: '+Inf' };
|
|
acc.push(
|
|
setValuePair(
|
|
infLabel,
|
|
d.data.count,
|
|
`${histogram.name}_bucket`,
|
|
d.data.bucketExemplars ? d.data.bucketExemplars['-1'] : null,
|
|
d.data.labels,
|
|
),
|
|
setValuePair(
|
|
{},
|
|
d.data.sum,
|
|
`${histogram.name}_sum`,
|
|
undefined,
|
|
d.data.labels,
|
|
),
|
|
setValuePair(
|
|
{},
|
|
d.data.count,
|
|
`${histogram.name}_count`,
|
|
undefined,
|
|
d.data.labels,
|
|
),
|
|
);
|
|
return acc;
|
|
};
|
|
}
|
|
|
|
function splayLabels(bucket) {
|
|
const { sharedLabels, labels, ...newBucket } = bucket;
|
|
for (const label of Object.keys(sharedLabels)) {
|
|
labels[label] = sharedLabels[label];
|
|
}
|
|
newBucket.labels = labels;
|
|
return newBucket;
|
|
}
|
|
|
|
module.exports = Histogram;
|