the-tip-top-backend/node_modules/prom-client/lib/histogram.js

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;