mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Provide the basic version of Summary that does not report quantiles in the prometheus library. Useful for geting averages of observed values.
1809 lines
50 KiB
JavaScript
1809 lines
50 KiB
JavaScript
/**
|
|
* Asynchronous Prometheus client
|
|
*
|
|
* Prometheus client for exposing metrics to a Prometheus server based on
|
|
* (possibly) asynchronous callbacks. This library was developed as a
|
|
* replacement for prom-client in Clusterio due to the lack of callbacks on
|
|
* metric collections in prom-client.
|
|
*
|
|
* The ordinary use case of instrumenting a code base should be covered by
|
|
* {@link module:lib/prometheus.Counter}, {@link
|
|
* module:lib/prometheus.Gauge}, {@link module:lib/prometheus.Histogram} and
|
|
* {@link module:lib/prometheus.exposition}. See documentation for each of
|
|
* the listed interfaces for more information.
|
|
*
|
|
* @example
|
|
* const { Counter, exposition } = require("@clusterio/lib/prometheus");
|
|
*
|
|
* // Collectors are by default registered to the default collector regitry.
|
|
* const totalRequests = new Counter(
|
|
* "app_request_count_total",
|
|
* "Total requests handled.",
|
|
* );
|
|
*
|
|
* function handleRequest(...) {
|
|
* // Do stuff
|
|
* totalRequests.inc();
|
|
* }
|
|
*
|
|
* // Code handling the /metrics HTTP request, here shown for express
|
|
* // but any http framework may be used
|
|
* const app = require("express")();
|
|
* async function getMetrics(req, res) {
|
|
* // By default exposition uses the default collector registry
|
|
* let text = await exposition();
|
|
* res.set("Content-Type", exposition.contentType);
|
|
* res.send(text);
|
|
* }
|
|
* app.get("/metrics", (req, res, next) => getMetrics(req, res).catch(next));
|
|
* app.listen(9100);
|
|
* @module lib/prometheus
|
|
*/
|
|
"use strict";
|
|
|
|
/**
|
|
* Result from collecting a {@link module:lib/prometheus.Collector}
|
|
* @typedef {Object} CollectorResult
|
|
* @property {module:lib/prometheus.Metric} metric -
|
|
* Metric collected.
|
|
* @property {Map<string, Map<string, number>>} samples -
|
|
* Mapping of metric suffix to mapping of label keys to values
|
|
* collected. For normal metrics the first level contains a single
|
|
* entry under the empty string as key. If the metric does not have
|
|
* labels the second level also contains a single entry under the empty
|
|
* string as a key.
|
|
*
|
|
* @example <caption>Simple Metric</caption>
|
|
* let result = {
|
|
* metric: new Metric("count", "simple_total_count", "A simple counter"),
|
|
* samples: new Map([
|
|
* ["", new Map([
|
|
* ["", 123],
|
|
* ])],
|
|
* ]),
|
|
* }
|
|
*
|
|
* @example <caption>Labeled Metric</caption>
|
|
* let result = {
|
|
* metric: new Metric(
|
|
* "count", "labeled_total_count", "A labeled counter", ["a", "b"]
|
|
* ),
|
|
* samples: new Map([
|
|
* ["", new Map([
|
|
* ['a="1",b="3"', 123],
|
|
* ['a="2",b="1"', 34],
|
|
* ['a="2",b="9"', 7],
|
|
* ])],
|
|
* ]),
|
|
* }
|
|
*
|
|
* @example <caption>Histogram Metric</caption>
|
|
* let result = {
|
|
* metric: new Metric(
|
|
* "histogram", "histogram_size", "A histogram of sizes"
|
|
* ),
|
|
* samples: new Map([
|
|
* ["_bucket", new Map([
|
|
* ['le="1"', 1],
|
|
* ['le="5"', 4],
|
|
* ['le="+Inf"', 5],
|
|
* ])],
|
|
* ["_sum", new Map([
|
|
* ["", 48],
|
|
* ])],
|
|
* ["_count", new Map([
|
|
* ["", 5],
|
|
* ])],
|
|
* ]),
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Represents a collectable metric
|
|
*
|
|
* Used in implementing collectors in order to validate the name, help text
|
|
* and labels attached to the collector.
|
|
* @static
|
|
*/
|
|
class Metric {
|
|
/**
|
|
* @param {string} type -
|
|
* Metric type, should be one of `counter`, `gauge`, `histogram`,
|
|
* `summary` or `untyped`,
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for the metric.
|
|
* @param {Array<string>} labels -
|
|
* @param {Array<string>} _reserved_labels -
|
|
*/
|
|
constructor(type, name, help, labels = [], _reserved_labels = []) {
|
|
if (typeof name !== "string") {
|
|
throw new Error("Expected name to be a string");
|
|
}
|
|
|
|
if (typeof help !== "string") {
|
|
throw new Error("Expected help to be a string");
|
|
}
|
|
|
|
if (!/^[a-zA-Z_:][a-zA-Z0-9_:]*$/.test(name)) {
|
|
throw new Error(`Invalid name '${name}'`);
|
|
}
|
|
|
|
for (let label of labels) {
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(label)) {
|
|
throw new Error(`Invalid label '${label}'`);
|
|
} else if (_reserved_labels.includes(label)) {
|
|
throw new Error(`Reserved label '${label}'`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Name of the metric
|
|
* @type {string}
|
|
*/
|
|
this.name = name;
|
|
|
|
/**
|
|
* Help text for the metric
|
|
* @type {string}
|
|
*/
|
|
this.help = help;
|
|
|
|
/**
|
|
* Metric type, should be one of `counter`, `gauge`, `histogram`,
|
|
* `summary` or `untyped`.
|
|
* @type {string}
|
|
*/
|
|
this.type = type;
|
|
|
|
/**
|
|
* Labels for this metric.
|
|
* @type {Array<string>}
|
|
*/
|
|
this.labels = labels;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The default registry which collectors are regististered to.
|
|
* @type {module:lib/prometheus.CollectorRegistry}
|
|
* @static
|
|
*/
|
|
let defaultRegistry;
|
|
|
|
/**
|
|
* Base class for all collectors
|
|
*
|
|
* This servers mostly as a conceptual base for all collectors, the only
|
|
* feature implemented here is registring to the default registry on
|
|
* construction. If you want to implement a custom collector you most
|
|
* likely want to base it on {@link module:lib/prometheus.LabeledCollector}
|
|
* instead.
|
|
*
|
|
* @static
|
|
*/
|
|
class Collector {
|
|
/**
|
|
* Create collector
|
|
*
|
|
* @param {boolean=} register -
|
|
* Whether to register this collector to the default registry.
|
|
*/
|
|
constructor(register = true) {
|
|
if (register) {
|
|
defaultRegistry.register(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve metric data from this collctor.
|
|
*
|
|
* Called by {@link module:lib/prometheus.CollectorRegistry} to gather
|
|
* the metric data this Collector exports.
|
|
*
|
|
* @returns {*} Async iterator of {@link CollectorResult}.
|
|
*/
|
|
async* collect() { }
|
|
}
|
|
|
|
const labelEscapesChars = /[\\\n\"]/;
|
|
/**
|
|
* Escapes a label value in accordance with the text exposition format
|
|
*
|
|
* @param {string} value - Value to escape.
|
|
* @returns {string} escaped value with \, ", and newline escaped.
|
|
* @private
|
|
*/
|
|
function escapeLabelValue(value) {
|
|
if (!labelEscapesChars.test(value)) {
|
|
return value;
|
|
}
|
|
return value
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/"/g, "\\\"")
|
|
;
|
|
}
|
|
|
|
/**
|
|
* Convert label expression to unique string
|
|
*
|
|
* @param {Array<(string|Object<string, string>)>} labels -
|
|
* label values to compute key for.
|
|
* @param {Array<string>} metricLabels - labels defined for the metric.
|
|
* @returns {string} computed key
|
|
* @private
|
|
*/
|
|
function labelsToKey(labels, metricLabels) {
|
|
let items = [];
|
|
if (labels.length === 1 && typeof labels[0] === "object") {
|
|
labels = labels[0];
|
|
for (let name of metricLabels) {
|
|
if (!Object.hasOwnProperty.call(labels, name)) {
|
|
throw new Error(`Missing label '${name}'`);
|
|
}
|
|
if (typeof labels[name] !== "string") {
|
|
throw new Error(
|
|
`Expected value for label '${name}' to be a string`
|
|
);
|
|
}
|
|
items.push(`${name}="${escapeLabelValue(labels[name])}"`);
|
|
}
|
|
|
|
for (let name of Object.keys(labels)) {
|
|
if (!metricLabels.includes(name)) {
|
|
throw new Error(`Extra label '${name}'`);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
if (metricLabels.length > labels.length) {
|
|
throw new Error(`Missing label '${metricLabels[labels.length]}'`);
|
|
}
|
|
|
|
if (labels.length > metricLabels.length) {
|
|
throw new Error("Extra positional label");
|
|
}
|
|
|
|
for (let i=0; i < metricLabels.length; ++i) {
|
|
if (typeof labels[i] !== "string") {
|
|
throw new Error(
|
|
`Expected value for label '${metricLabels[i]}' to be a string`
|
|
);
|
|
}
|
|
items.push(`${metricLabels[i]}="${escapeLabelValue(labels[i])}"`);
|
|
}
|
|
}
|
|
|
|
if (items.length === 1) {
|
|
return items[0];
|
|
}
|
|
|
|
return items.join(",");
|
|
}
|
|
|
|
function keyToLabels(key) {
|
|
let labels = new Map();
|
|
|
|
if (key !== "") {
|
|
for (let pair of key.split(",")) {
|
|
let [name, value] = pair.split("=", 2);
|
|
labels.set(name, value
|
|
.slice(1, -1)
|
|
.replace(/\\"/g, "\"")
|
|
.replace(/\\n/g, "\n")
|
|
.replace(/\\\\/g, "\\")
|
|
);
|
|
}
|
|
}
|
|
|
|
return labels;
|
|
}
|
|
|
|
function removeMatchingLabels(mapping, labels) {
|
|
for (let key of mapping.keys()) {
|
|
let candidate = keyToLabels(key);
|
|
let hasLabels = Object.entries(labels).every(([name, value]) => (
|
|
candidate.get(name) === value
|
|
));
|
|
if (hasLabels) {
|
|
mapping.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for implementing labeled collectors
|
|
*
|
|
* Provides the scaffolding for creating labled collectors where each label
|
|
* set is a child to the collector that can be retrieved with the .labels()
|
|
* method.
|
|
*
|
|
* @extends module:lib/prometheus.Collector
|
|
* @static
|
|
*/
|
|
class LabeledCollector extends Collector {
|
|
/**
|
|
* Create optionally labeled collector
|
|
*
|
|
* @param {string} type - Type for metric.
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object} options - options for collector.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {Array<string>=} options._reservedLabels -
|
|
* Labels which may not be used. Is passed to Metric constructor.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called before the metric is
|
|
* collected. The collector being collected is passed as the
|
|
* argument.
|
|
* @param {
|
|
* function(module:lib/prometheus.LabeledCollector,string)
|
|
* } childClass -
|
|
* Constructor taking instance of collector and label key as
|
|
* arguments and returns a child instance.
|
|
*/
|
|
constructor(type, name, help, options, childClass) {
|
|
let labels = [];
|
|
let reservedLabels = [];
|
|
let register = true;
|
|
let callback = null;
|
|
for (let [key, value] of Object.entries(options)) {
|
|
if (key === "labels") {
|
|
labels = value;
|
|
} else if (key === "_reservedLabels") {
|
|
reservedLabels = value;
|
|
} else if (key === "register") {
|
|
register = value;
|
|
} else if (key === "callback") {
|
|
callback = value;
|
|
} else {
|
|
throw new Error(`Unrecognized option '${key}'`);
|
|
}
|
|
}
|
|
|
|
// Make sure we don't register to the default registry if metric throws.
|
|
let metric = new Metric(type, name, help, labels, reservedLabels);
|
|
|
|
super(register);
|
|
|
|
this.callback = callback;
|
|
this.metric = metric;
|
|
this._children = new Map();
|
|
this._childClass = childClass;
|
|
}
|
|
|
|
/**
|
|
* Access child collector with the given labels.
|
|
*
|
|
* Creates a labeled child collector for this collector. The child
|
|
* supports the methods of Collector for modifying and querying the
|
|
* value of the metric.
|
|
*
|
|
* Once created, a value is initialized for the label values used and
|
|
* exported from the collector until it's removed explicitly with
|
|
* the .remove() or .removeAll() methods.
|
|
*
|
|
* @param {...string|Object<string,string>} labels -
|
|
* A string value passed for each label defined on the metric in the
|
|
* same order as the labels option given to the collector, or an
|
|
* object mapping label name to label value.
|
|
* @returns {*} Child collector for the given labels.
|
|
*/
|
|
labels(...labels) {
|
|
let key = labelsToKey(labels, this.metric.labels);
|
|
let child = this._children.get(key);
|
|
if (child === undefined) {
|
|
child = new this._childClass(this, key);
|
|
this._children.set(key, child);
|
|
}
|
|
return child;
|
|
}
|
|
|
|
/**
|
|
* Remove child collector and data for a given label set
|
|
*
|
|
* Remove the child collector and the value it stores from the collector
|
|
* itself. This will remove the entry exported for the given labels.
|
|
*
|
|
* @param {...string|Object<string,string>} labels -
|
|
* A string value passed for each label defined on the metric in the
|
|
* same order as the labels option given to the collector, or an
|
|
* object mapping label name to label value.
|
|
* @returns {string} key for labels to remove (for use in subclasses).
|
|
*/
|
|
remove(...labels) {
|
|
let key = labelsToKey(labels, this.metric.labels);
|
|
if (key === "") {
|
|
throw new Error("labels cannot be empty");
|
|
}
|
|
this._children.delete(key);
|
|
return key;
|
|
}
|
|
|
|
/**
|
|
* Remove child collectors matching a partial label set
|
|
*
|
|
* Removes all child collecters which has labels matching the ones given
|
|
* in labels parameter, Unline .remove this can be a partial set of
|
|
* labels and all child collectors with their stored values that shares
|
|
* this set of labels will be removed.
|
|
*
|
|
* @param {Object<string,string>} labels -
|
|
* Object mapping with label name to label values that should be
|
|
* matches.
|
|
*/
|
|
removeAll(labels) {
|
|
if (!Object.keys(labels).length) {
|
|
throw new Error("labels cannot be empty");
|
|
}
|
|
|
|
removeMatchingLabels(this._children, labels);
|
|
}
|
|
|
|
/**
|
|
* Clear all labeles on this collector
|
|
*
|
|
* Clears all child collectors from this collector along with their
|
|
* stored values. This effectively removes all the exported values.
|
|
*/
|
|
clear() {
|
|
if (!this.metric.labels.length) {
|
|
throw new Error("Cannot clear unlabeled metric");
|
|
}
|
|
this._children = new Map();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for implementing single value per label collectors
|
|
*
|
|
* @extends module:lib/prometheus.LabeledCollector
|
|
* @static
|
|
*/
|
|
class ValueCollector extends LabeledCollector {
|
|
/**
|
|
* Create optionally labeled value collector
|
|
*
|
|
* @param {string} type - Type for metric.
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object} options - options for collector.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {Array<string>=} options._reservedLabels -
|
|
* Labels which may not be used. Is passed to Metric constructor.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called before the collector value
|
|
* is collected. The collector being collected is passed as the
|
|
* argument.
|
|
* @param {
|
|
* function(
|
|
* new:module:lib/prometheus.ValueCollectorChild,
|
|
* module:lib/prometheus.LabeledCollector,
|
|
* string
|
|
* )
|
|
* } childClass -
|
|
* Constructor taking instance of collector and label key as
|
|
* arguments and returns a child instance.
|
|
*/
|
|
constructor(type, name, help, options, childClass) {
|
|
super(type, name, help, options, childClass);
|
|
|
|
this._values = new Map();
|
|
this._defaultChild = this.metric.labels.length ? null : this.labels({});
|
|
}
|
|
|
|
async* collect() {
|
|
if (this.callback) { await this.callback(this); }
|
|
yield { metric: this.metric, samples: new Map([["", this._values]]) };
|
|
}
|
|
|
|
/**
|
|
* Get the current value for this collector.
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @returns {number} value stored.
|
|
*/
|
|
get() {
|
|
return this._defaultChild.get();
|
|
}
|
|
|
|
remove(...labels) {
|
|
let key = super.remove(...labels);
|
|
this._values.delete(key);
|
|
}
|
|
|
|
removeAll(labels) {
|
|
super.removeAll(labels);
|
|
removeMatchingLabels(this._values, labels);
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this._values = new Map();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Child collector representing the value of a single label set
|
|
*/
|
|
class ValueCollectorChild {
|
|
constructor(collector, key) {
|
|
this._values = collector._values;
|
|
this._key = key;
|
|
|
|
this._values.set(key, 0);
|
|
}
|
|
|
|
/**
|
|
* Returns the current value of label set
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @returns {number} value stored.
|
|
*/
|
|
get() {
|
|
return this._values.get(this._key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Child counter holding the value for a single label set.
|
|
* @extends module:lib/prometheus~ValueCollectorChild
|
|
*/
|
|
class CounterChild extends ValueCollectorChild {
|
|
/**
|
|
* Increment counter for label set
|
|
* @param {number} value - Positive number to increment by.
|
|
*/
|
|
inc(value = 1) {
|
|
// Note: Inverted to also catch NaN
|
|
if (!(value >= 0)) {
|
|
throw new Error("Expected value to be a positive number");
|
|
}
|
|
|
|
this._values.set(this._key, this._values.get(this._key) + value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic increasing counter
|
|
*
|
|
* Stores and exports an optionally labeled counter metric. This is one of
|
|
* the two basic building blocks for instrumenting code and represents a
|
|
* metric that can only increase in value, such as the total number of
|
|
* requests processed or total time spent processing requests.
|
|
*
|
|
* Counters should be created at module load time and referenced in the
|
|
* functions that increment them, for example:
|
|
*
|
|
* ```js
|
|
* const totalRequests = new Counter(
|
|
* "app_request_count_total",
|
|
* "Total requests handled.",
|
|
* );
|
|
*
|
|
* function handleRequest(...) {
|
|
* // Do stuff
|
|
* totalRequests.inc();
|
|
* }
|
|
* ```
|
|
*
|
|
* The `totalRequests` counter will register with the default registry and
|
|
* provided exposition is set up (see {@link
|
|
* module:lib/prometheus.exposition}) the counter will be exported to
|
|
* Prometheus starting out with a value of 0. And that is all there is to
|
|
* it.
|
|
*
|
|
* It is sometimes useful however to divide a metric up into diffrent
|
|
* sections, for example to have a different count for each endpoint handled
|
|
* or one count for successfull requests and one for requests resulting in
|
|
* an error. For this Prometheus provides labels, and to use them pass the
|
|
* labels option as the third argument to Counter:
|
|
*
|
|
* ```js
|
|
* const totalRequests = new Counter(
|
|
* "app_request_count_total",
|
|
* "Total requests handled.",
|
|
* { labels: ["endpoint", "status"] },
|
|
* );
|
|
*
|
|
* function handleRequest(endpoint, ...) {
|
|
* let status = "ok";
|
|
* try {
|
|
* // Do stuff
|
|
* } catch (err) {
|
|
* status = "err";
|
|
* } finally {
|
|
* totalRequests.labels(endpoint, status).inc();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* When using labels the counter no longer gets a default value and it's no
|
|
* longer possible to use the `.inc()` method on the counter itself, the
|
|
* `.labels()` method has to be invoked with the values for the labels
|
|
* defined. `.labels()` returns a child counter for the label values given
|
|
* to it and this child can be operated on and cached for performance if
|
|
* that is critical.
|
|
*
|
|
* The lack of a default value may cause issues with aggregating and
|
|
* querying the counter values in Prometheus. It is therefore recommended
|
|
* where possible to initialize all possible combinations of label values
|
|
* that will be used. This is done by calling `.labels()` for each
|
|
* combination, for example:
|
|
*
|
|
* ```js
|
|
* for (let endpoint of allEndpoints) {
|
|
* for (let status of ["ok", "err"]) {
|
|
* totalRequests.labels(endpoint, status);
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that every combination of label values used creates a new time
|
|
* series to be stored and processed. You should carefully evaluate which
|
|
* labels you actually need as resource usage for a metric increases
|
|
* exponentially with the number of labels used.
|
|
*
|
|
* @extends module:lib/prometheus.ValueCollector
|
|
* @static
|
|
*/
|
|
class Counter extends ValueCollector {
|
|
/**
|
|
* Create optionally labeled counter
|
|
*
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object=} options - options for collector.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called when the metric is
|
|
* collected. The collector being collected is passed as the
|
|
* argument.
|
|
*/
|
|
constructor(name, help, options = {}) {
|
|
super("counter", name, help, options, CounterChild);
|
|
}
|
|
|
|
/**
|
|
* Increment counter value
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - Positive number to increment by.
|
|
*/
|
|
inc(value = 1) {
|
|
this._defaultChild.inc(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Child gauge holding the value for a single label set.
|
|
* @extends module:lib/prometheus~ValueCollectorChild
|
|
*/
|
|
class GaugeChild extends ValueCollectorChild {
|
|
/**
|
|
* Increment gague for label set
|
|
*
|
|
* @param {number} value - number to increment by.
|
|
*/
|
|
inc(value = 1) {
|
|
this._values.set(this._key, this._values.get(this._key) + value);
|
|
}
|
|
|
|
/**
|
|
* Decrement gague for label set
|
|
*
|
|
* @param {number} value - number to decrease by.
|
|
*/
|
|
dec(value = 1) {
|
|
this._values.set(this._key, this._values.get(this._key) - value);
|
|
}
|
|
|
|
/**
|
|
* Set gague for label set
|
|
*
|
|
* @param {number} value - number to set gauge to.
|
|
*/
|
|
set(value) {
|
|
this._values.set(this._key, value);
|
|
}
|
|
|
|
/**
|
|
* Set to current Unix epoch time in seconds for label set
|
|
*/
|
|
setToCurrentTime() {
|
|
this._values.set(this._key, Date.now() / 1000);
|
|
}
|
|
|
|
/**
|
|
* Start a timer for setting a duration for label set
|
|
*
|
|
* @returns {function()}
|
|
* function that when called will set the guage to the duration in
|
|
* seconds from when the timer was started
|
|
*/
|
|
startTimer() {
|
|
let start = process.hrtime.bigint();
|
|
return () => {
|
|
let end = process.hrtime.bigint();
|
|
this.set(Number(end - start) / 1e9);
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic value metric
|
|
*
|
|
* Stores and exports an optionally labeled value metric. This is one of
|
|
* the two basic building blocks for instrumenting code and represents a
|
|
* value that can both increase and decrease over time, such as the number
|
|
* of requests in-flight or the number of users in a database.
|
|
*
|
|
* Gauges should be created at module load time and referenced in the
|
|
* functions that modify them, for example:
|
|
*
|
|
* ```js
|
|
* const activeRequests = new Gauge(
|
|
* "app_active_request_count",
|
|
* "Number of requests in-flight.",
|
|
* );
|
|
*
|
|
* async function handleRequest(...) {
|
|
* activeRequests.inc();
|
|
* try {
|
|
* // Do async stuff
|
|
* } finally {
|
|
* activeRequests.dec();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* The `activeRequests` gauge will register with the default registry and
|
|
* provided exposition is set up (see {@link
|
|
* module:lib/prometheus.exposition}) the gauge will be exported to
|
|
* Prometheus starting out with a value of 0.
|
|
*
|
|
* Sometimes keeping track of the value measured is impractical or
|
|
* prohibitly expensive. In those cases you can update the value
|
|
* of the collector as it's being collected for export with a callback
|
|
* function passed as one of the options.
|
|
*
|
|
* ```js
|
|
* const userCount = new Gauge(
|
|
* "app_user_count",
|
|
* "Number of users in the app.",
|
|
* {
|
|
* callback: async function() {
|
|
* // Make sure this request can not take a long time to
|
|
* // complete as that will cause the metrics gathering
|
|
* // to time out.
|
|
* userCount.set(await someApi.getUserCount());
|
|
* },
|
|
* },
|
|
* );
|
|
* ```
|
|
*
|
|
* Keep in mind the callbacks are executed one by one at the time the
|
|
* collectors are collected for the exposition given the Prometheus. The
|
|
* default timeout in Prometheus for a collection job is 10 seconds. This
|
|
* means the callback completion time should be in the order of milliseconds
|
|
* to avoid having the time of many collectors add up to too much.
|
|
*
|
|
* Like with the Counter the Gauge also supports labels. When using labels
|
|
* the methods for changing the value of the counter is no longer usable
|
|
* directly on the counter itself, instead a child counter with label values
|
|
* set has to be retrieved with the `.labels()` method. For example:
|
|
*
|
|
* ```js
|
|
* const userCount = new Gauge(
|
|
* "app_user_count",
|
|
* "Number of users in the app",
|
|
* {
|
|
* labels: ["role"],
|
|
* callback: async function() {
|
|
* userCount.labels("system").set(await someApi.getSystemUserCount());
|
|
* userCount.labels("admin").set(await someApi.getAdminUserCount());
|
|
* userCount.labels("normal").set(await someApi.getNormalUserCount());
|
|
* }
|
|
* },
|
|
* );
|
|
* ```
|
|
*
|
|
* When using labels the gauge no longer gets a default value, and this may
|
|
* cause issues with aggregating and querying the gauge values in
|
|
* Prometheus. This is not an issue with the example shown above as all
|
|
* label combinations used are given a value when the gauge is collected.
|
|
* But if these labeled values were calculated through some other means
|
|
* dynamically there may be cases where values for timeseries that are
|
|
* occasionally used are missing. When dynamically setting labels it is
|
|
* recommended where possible to initialize all possible combinations of
|
|
* label values that will be used. This is done by calling `.labels()` for
|
|
* each combination, for example:
|
|
*
|
|
* ```js
|
|
* for (let role of ["system", "admin", "normal") {
|
|
* userCount.labels(role);
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that every combination of label values used creates a new time
|
|
* series to be stored and processed. You should carefully evaluate which
|
|
* labels you actually need as resource usage for a metric increases
|
|
* exponentially with the number of labels used.
|
|
*
|
|
* @extends module:lib/prometheus.ValueCollector
|
|
* @static
|
|
*/
|
|
class Gauge extends ValueCollector {
|
|
/**
|
|
* Create optionally labeled gauge
|
|
*
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object=} options - options for collector.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called when the metric is
|
|
* collected. The collector being collected is passed as the
|
|
* argument.
|
|
*/
|
|
constructor(name, help, options = {}) {
|
|
super("gauge", name, help, options, GaugeChild);
|
|
}
|
|
|
|
/**
|
|
* Increment gague value
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - number to increment by.
|
|
*/
|
|
inc(value = 1) {
|
|
this._defaultChild.inc(value);
|
|
}
|
|
|
|
/**
|
|
* Decrement gague value
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - number to decrease by.
|
|
*/
|
|
dec(value = 1) {
|
|
this._defaultChild.dec(value);
|
|
}
|
|
|
|
/**
|
|
* Set gague value
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - number to set gauge to.
|
|
*/
|
|
set(value) {
|
|
this._defaultChild.set(value);
|
|
}
|
|
|
|
/**
|
|
* Set to current Unix epoch time in seconds
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
*/
|
|
setToCurrentTime() {
|
|
this._defaultChild.setToCurrentTime();
|
|
}
|
|
|
|
/**
|
|
* Start a timer for setting a duration
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @returns {function()}
|
|
* function that when called will set the guage to the duration in
|
|
* seconds from when the timer was started
|
|
*/
|
|
startTimer() {
|
|
return this._defaultChild.startTimer();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Child Summary holding the sum and count for a single label set.
|
|
*/
|
|
class SummaryChild {
|
|
constructor(collector, key) {
|
|
this._sumValues = collector._sumValues;
|
|
this._countValues = collector._countValues;
|
|
this._key = key;
|
|
|
|
this._sumValues.set(key, 0);
|
|
this._countValues.set(key, 0);
|
|
}
|
|
|
|
/**
|
|
* Sum of all observations for label set
|
|
* @type {number}
|
|
*/
|
|
get sum() {
|
|
return this._sumValues.get(this._key);
|
|
}
|
|
|
|
/**
|
|
* Count of observations for label set
|
|
* @type {number}
|
|
*/
|
|
get count() {
|
|
return this._countValues.get(this._key);
|
|
}
|
|
|
|
/**
|
|
* Observe a given value and increment matching buckets for label set
|
|
*
|
|
* @param {number} value - number to count into summary.
|
|
*/
|
|
observe(value) {
|
|
this._sumValues.set(this._key, this._sumValues.get(this._key) + value);
|
|
this._countValues.set(this._key, this._countValues.get(this._key) + 1);
|
|
}
|
|
|
|
/**
|
|
* Start a timer for observing a duration for label set
|
|
*
|
|
* @returns {function()}
|
|
* function that when called will store the duration in seconds from
|
|
* when the timer was started into the metric.
|
|
*/
|
|
startTimer() {
|
|
let start = process.hrtime.bigint();
|
|
return () => {
|
|
let end = process.hrtime.bigint();
|
|
this.observe(Number(end - start) / 1e9);
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Summary metric
|
|
*
|
|
* Sample observations into a count and sum. This is useful when you have
|
|
* an operation that reports a metric that you want insight into the rate
|
|
* and average size of values for. A common case for this is request
|
|
* duration, for example:
|
|
*
|
|
* ```js
|
|
* const requestDuration = new Summary(
|
|
* "app_request_duration_seconds",
|
|
* "Time to process app requests",
|
|
* );
|
|
*
|
|
* async function handleRequest(...) {
|
|
* const observeDuration = requestDuration.startTimer();
|
|
* try {
|
|
* // Do async stuff.
|
|
* } finally {
|
|
* observeDuration();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that reporting quantiles is note supported. This makes the Summary
|
|
* less insightful than the Histogram while using less resources.
|
|
*
|
|
* The `.startTimer()` method provides a convenient interface for adding
|
|
* observed durations to the summary. Other types of values can be added to
|
|
* a summary with the `.observe()` method.
|
|
*
|
|
* Like with the Counter and Gauge the Summary collector also supports
|
|
* labels. When using labels the methods for observing values into the
|
|
* summary is no longer usable directly on the counter itself, instead a
|
|
* child summary with label values set has to be retrieved with the
|
|
* `.labels()` method. For example:
|
|
*
|
|
* ```js
|
|
* const requestDuration = new Summary(
|
|
* "app_request_duration_seconds",
|
|
* "Time to process app requests",
|
|
* { labels: ["endpoint"] }
|
|
* );
|
|
*
|
|
* async function handleRequest(endpoint, ...) {
|
|
* const observeDuration = requestDuration.labels(endpoint).startTimer();
|
|
* try {
|
|
* // Do async stuff.
|
|
* } finally {
|
|
* observeDuration();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* When using labels the summary is no longer initalized with a default
|
|
* value, and this may cause issues with aggregating and querying the
|
|
* summary in Prometheus. When dynamically setting labels it is recommended
|
|
* where possible to initialize all possible combinations of label values
|
|
* that will be used. This is done by calling `.labels()` for each
|
|
* combination, for example:
|
|
*
|
|
* ```js
|
|
* for (let endpoint of ["/status", "/api", ...) {
|
|
* requestDuration.labels(endpoint);
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that every combination of label values used creates two new time
|
|
* series that need to be stored and processed. You should carefully
|
|
* evaluate which labels you actually need as resource usage for a metric
|
|
* increases exponentially with the number of labels used.
|
|
*
|
|
* @static
|
|
*/
|
|
class Summary extends LabeledCollector {
|
|
/**
|
|
* Create optionally labeled summary
|
|
*
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object=} options - options for collector.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called when the metric is
|
|
* collected. The collector being collected is passed as the
|
|
* argument.
|
|
*/
|
|
constructor(name, help, options = {}) {
|
|
super("summary", name, help, options, SummaryChild);
|
|
|
|
this._sumValues = new Map();
|
|
this._countValues = new Map();
|
|
this._defaultChild = this.metric.labels.length ? null : this.labels({});
|
|
}
|
|
|
|
async* collect() {
|
|
if (this.callback) { await this.callback(this); }
|
|
yield {
|
|
metric: this.metric,
|
|
samples: new Map([
|
|
["_sum", this._sumValues],
|
|
["_count", this._countValues],
|
|
]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sum of all observations for label set
|
|
*
|
|
* Note: Only available if this is an unlabeled collector.
|
|
* @type {number}
|
|
*/
|
|
get sum() {
|
|
return this._defaultChild.sum;
|
|
}
|
|
|
|
/**
|
|
* Count of observations for label set
|
|
*
|
|
* Note: Only available if this is an unlabeled collector.
|
|
* @type {number}
|
|
*/
|
|
get count() {
|
|
return this._defaultChild.count;
|
|
}
|
|
|
|
/**
|
|
* Observe a given value and increment matching buckets
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - number to count into histogram buckets.
|
|
*/
|
|
observe(value) {
|
|
this._defaultChild.observe(value);
|
|
}
|
|
|
|
/**
|
|
* Start a timer for observing a duration
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @returns {function()}
|
|
* function that when called will store the duration in seconds from
|
|
* when the timer was started into the metric.
|
|
*/
|
|
startTimer() {
|
|
return this._defaultChild.startTimer();
|
|
}
|
|
|
|
remove(...labels) {
|
|
let key = super.remove(...labels);
|
|
this._sumValues.delete(key);
|
|
this._countValues.delete(key);
|
|
}
|
|
|
|
removeAll(labels) {
|
|
super.removeAll(labels);
|
|
removeMatchingLabels(this._sumValues, labels);
|
|
removeMatchingLabels(this._countValues, labels);
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this._sumValues = new Map();
|
|
this._countValues = new Map();
|
|
}
|
|
}
|
|
|
|
function formatValue(value) {
|
|
if (value === Infinity) {
|
|
return "+Inf";
|
|
}
|
|
if (value === -Infinity) {
|
|
return "-Inf";
|
|
}
|
|
return value.toString();
|
|
}
|
|
|
|
function formatBucketKey(bucket, key) {
|
|
return `${key === "" ? "" : `${key},`}le="${formatValue(bucket)}"`;
|
|
}
|
|
|
|
/**
|
|
* Child histogram holding the buckets for a single label set.
|
|
*/
|
|
class HistogramChild {
|
|
constructor(collector, key) {
|
|
this._bucketValues = collector._bucketValues;
|
|
this._sumValues = collector._sumValues;
|
|
this._countValues = collector._countValues;
|
|
this._key = key;
|
|
|
|
this._bucketKeys = new Map();
|
|
for (let bucket of collector._buckets) {
|
|
let bucketKey = formatBucketKey(bucket, this._key);
|
|
|
|
this._bucketKeys.set(bucket, bucketKey);
|
|
this._bucketValues.set(bucketKey, 0);
|
|
}
|
|
|
|
this._sumValues.set(key, 0);
|
|
this._countValues.set(key, 0);
|
|
}
|
|
|
|
/**
|
|
* Mapping of bucket upper bounds to count of observations for label set
|
|
* @type {Map<number, number>}
|
|
*/
|
|
get buckets() {
|
|
return new Map(
|
|
[...this._bucketKeys].map(
|
|
([bucket, key]) => [bucket, this._bucketValues.get(key)]
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sum of all observations for label set
|
|
* @type {number}
|
|
*/
|
|
get sum() {
|
|
return this._sumValues.get(this._key);
|
|
}
|
|
|
|
/**
|
|
* Count of observations for label set
|
|
* @type {number}
|
|
*/
|
|
get count() {
|
|
return this._countValues.get(this._key);
|
|
}
|
|
|
|
/**
|
|
* Observe a given value and increment matching buckets for label set
|
|
*
|
|
* @param {number} value - number to count into histogram buckets.
|
|
*/
|
|
observe(value) {
|
|
for (let [bound, key] of this._bucketKeys) {
|
|
if (value <= bound) {
|
|
this._bucketValues.set(key, this._bucketValues.get(key) + 1);
|
|
}
|
|
}
|
|
|
|
this._sumValues.set(this._key, this._sumValues.get(this._key) + value);
|
|
this._countValues.set(this._key, this._countValues.get(this._key) + 1);
|
|
}
|
|
|
|
/**
|
|
* Start a timer for observing a duration for label set
|
|
*
|
|
* @returns {function()}
|
|
* function that when called will store the duration in seconds from
|
|
* when the timer was started into the metric.
|
|
*/
|
|
startTimer() {
|
|
let start = process.hrtime.bigint();
|
|
return () => {
|
|
let end = process.hrtime.bigint();
|
|
this.observe(Number(end - start) / 1e9);
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Histogram metric
|
|
*
|
|
* Keeps track of observed values in a set of buckets in a similar vein to a
|
|
* histogram. This is useful when you have a frequent operation that
|
|
* reports a metric that you want insight into the distribution of values
|
|
* for. A common case for this is request duration, for example:
|
|
*
|
|
* ```js
|
|
* const requestDuration = new Histogram(
|
|
* "app_request_duration_seconds",
|
|
* "Time to process app requests",
|
|
* );
|
|
*
|
|
* async function handleRequest(...) {
|
|
* const observeDuration = requestDuration.startTimer();
|
|
* try {
|
|
* // Do async stuff.
|
|
* } finally {
|
|
* observeDuration();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* The default buckets used for the histogram is suitable for observing
|
|
* HTTP requests durations in seconds, and the `.startTimer()` method
|
|
* provides a convenient interface for adding observed durations to the
|
|
* histogram. Other types values can be added to a histogram with the
|
|
* `.observe()` method.
|
|
*
|
|
* Like with the Counter and Gauge the Histogram collector also supports
|
|
* labels. When using labels the methods for observing values into the
|
|
* histogram is no longer usable directly on the counter itself, instead a
|
|
* child histogram with label values set has to be retrieved with the
|
|
* `.labels()` method. For example:
|
|
*
|
|
* ```js
|
|
* const requestDuration = new Histogram(
|
|
* "app_request_duration_seconds",
|
|
* "Time to process app requests",
|
|
* { labels: ["endpoint"] }
|
|
* );
|
|
*
|
|
* async function handleRequest(endpoint, ...) {
|
|
* const observeDuration = requestDuration.labels(endpoint).startTimer();
|
|
* try {
|
|
* // Do async stuff.
|
|
* } finally {
|
|
* observeDuration();
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* When using labels the histogram is no longer initalized with a default value, and this may
|
|
* cause issues with aggregating and querying the histogram in
|
|
* Prometheus. When dynamically setting labels it is recommended where
|
|
* possible to initialize all possible combinations of label values that
|
|
* will be used. This is done by calling `.labels()` for each combination,
|
|
* for example:
|
|
*
|
|
* ```js
|
|
* for (let endpoint of ["/status", "/api", ...) {
|
|
* requestDuration.labels(endpoint);
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that every combination of label values used creates a new time
|
|
* series for each bucket in the histogram to be stored and processed.
|
|
* (e.g., 10 buckets with 10 possible label combinations will result in 100
|
|
* time series being made.) You should carefully evaluate which labels you
|
|
* actually need as resource usage for a metric increases exponentially with
|
|
* the number of labels used.
|
|
*
|
|
* @static
|
|
*/
|
|
class Histogram extends LabeledCollector {
|
|
/**
|
|
* Create optionally labeled histogram
|
|
*
|
|
* @param {string} name - Name of the metric.
|
|
* @param {string} help - Help text for metric.
|
|
* @param {Object=} options - options for collector.
|
|
* @param {Array<number>=} options.buckets -
|
|
* Buckets to use for the histogram. This is an array of inclusive
|
|
* upper bounds. Values observed will increment a bucket if it is
|
|
* less than or equal to the upper bound.
|
|
* @param {Array<string>=} options.labels -
|
|
* Labels for this metric, defaults to no labels.
|
|
* @param {boolean=} options.register -
|
|
* If true registers this collector with the default registry.
|
|
* Defaults to true.
|
|
* @param {function()=} options.callback -
|
|
* Possibly async function that is called when the metric is
|
|
* collected. The collector being collected is passed as the
|
|
* argument.
|
|
*/
|
|
constructor(name, help, options = {}) {
|
|
// These defaults are taken from the Python Prometheus client
|
|
let buckets = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, Infinity];
|
|
|
|
let parentOptions = { _reservedLabels: ["le"] };
|
|
for (let [key, value] of Object.entries(options)) {
|
|
if (key === "buckets") {
|
|
buckets = value;
|
|
} else {
|
|
parentOptions[key] = value;
|
|
}
|
|
}
|
|
|
|
super("histogram", name, help, parentOptions, HistogramChild);
|
|
|
|
if (buckets.slice(-1)[0] !== Infinity) {
|
|
buckets = [...buckets, Infinity];
|
|
}
|
|
|
|
this._buckets = buckets;
|
|
this._bucketValues = new Map();
|
|
this._sumValues = new Map();
|
|
this._countValues = new Map();
|
|
this._defaultChild = this.metric.labels.length ? null : this.labels({});
|
|
}
|
|
|
|
async* collect() {
|
|
if (this.callback) { await this.callback(this); }
|
|
yield {
|
|
metric: this.metric,
|
|
samples: new Map([
|
|
["_bucket", this._bucketValues],
|
|
["_sum", this._sumValues],
|
|
["_count", this._countValues],
|
|
]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mapping of bucket upper bounds to count of observations for label set
|
|
*
|
|
* Note: Only available if this is an unlabeled collector.
|
|
* @type {Map<number,number>}
|
|
*/
|
|
get buckets() {
|
|
return this._defaultChild.buckets;
|
|
}
|
|
|
|
/**
|
|
* Sum of all observations for label set
|
|
*
|
|
* Note: Only available if this is an unlabeled collector.
|
|
* @type {number}
|
|
*/
|
|
get sum() {
|
|
return this._defaultChild.sum;
|
|
}
|
|
|
|
/**
|
|
* Count of observations for label set
|
|
*
|
|
* Note: Only available if this is an unlabeled collector.
|
|
* @type {number}
|
|
*/
|
|
get count() {
|
|
return this._defaultChild.count;
|
|
}
|
|
|
|
/**
|
|
* Observe a given value and increment matching buckets
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @param {number} value - number to count into histogram buckets.
|
|
*/
|
|
observe(value) {
|
|
this._defaultChild.observe(value);
|
|
}
|
|
|
|
/**
|
|
* Start a timer for observing a duration
|
|
*
|
|
* Note: Only works if this is an unlabeled collector.
|
|
* @returns {function()}
|
|
* function that when called will store the duration in seconds from
|
|
* when the timer was started into the metric.
|
|
*/
|
|
startTimer() {
|
|
return this._defaultChild.startTimer();
|
|
}
|
|
|
|
remove(...labels) {
|
|
let key = super.remove(...labels);
|
|
for (let bucket of this._buckets) {
|
|
let bucketKey = formatBucketKey(bucket, key);
|
|
this._bucketValues.delete(bucketKey);
|
|
}
|
|
this._sumValues.delete(key);
|
|
this._countValues.delete(key);
|
|
}
|
|
|
|
removeAll(labels) {
|
|
super.removeAll(labels);
|
|
for (let bucket of this._buckets) {
|
|
removeMatchingLabels(
|
|
this._bucketValues, { ...labels, le: formatValue(bucket) }
|
|
);
|
|
}
|
|
removeMatchingLabels(this._sumValues, labels);
|
|
removeMatchingLabels(this._countValues, labels);
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this._bucketValues = new Map();
|
|
this._sumValues = new Map();
|
|
this._countValues = new Map();
|
|
}
|
|
|
|
/**
|
|
* Helper function for generating linear bucket series
|
|
*
|
|
* Creates an array of numbers suitable for using as buckets to a
|
|
* histogram. The array returned contains `count` numbers starting at
|
|
* `start` and with each subsequent number being equal to the previous
|
|
* one plus `width`.
|
|
*
|
|
* @param {number} start - Number to start buckets at.
|
|
* @param {number} width - Distance between ecah bucket.
|
|
* @param {number} count - Number of buckets.
|
|
* @returns {Array<number>} array of buckets.
|
|
*/
|
|
static linear(start, width, count) {
|
|
let buckets = [];
|
|
for (let i = 0; i < count; i++) {
|
|
buckets.push(start);
|
|
start += width;
|
|
}
|
|
return buckets;
|
|
}
|
|
|
|
/**
|
|
* Helper function for generating exponential bucket series
|
|
*
|
|
* Creates an array of numbers suitable for using as buckets to a
|
|
* histogram. The array returned contains `count` numbers starting at
|
|
* `start` and with each subsequent number being equal to the previous
|
|
* multiplied with `factor`.
|
|
*
|
|
* @param {number} start - Number to start buckets at.
|
|
* @param {number} factor - Ratio between each bucket.
|
|
* @param {number} count - Number of buckets.
|
|
* @returns {Array<number>} array of buckets.
|
|
*/
|
|
static exponential(start, factor, count) {
|
|
let buckets = [];
|
|
for (let i = 0; i < count; i++) {
|
|
buckets.push(start);
|
|
start *= factor;
|
|
}
|
|
return buckets;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collection of collectors
|
|
*
|
|
* Provides convienece methods for grouping collectors together and
|
|
* gathering the metrics from all of the contained collectors. By default
|
|
* collectors are registered and collected from the default registry so
|
|
* using this is usually not necessary.
|
|
*
|
|
* The basic way to use a custom registry is to create Collectors without
|
|
* registring them to the default registry and then adding them to your own
|
|
* registry, for example:
|
|
*
|
|
* ```js
|
|
* const myRegistry = new CollectorRegistry();
|
|
* const myCounter = new Counter( "a_counter", "A counter.", { register: false });
|
|
* myRegistry.register(myCounter);
|
|
*
|
|
* // In the /metrics HTTP request handler
|
|
* let text = await exposition(myRegistry.collect());
|
|
* ```
|
|
*
|
|
* The same collector can be registered to multiple registries. This may be
|
|
* used to implement responding with different sets of metrics depending on
|
|
* what is requested.
|
|
*
|
|
* @static
|
|
*/
|
|
class CollectorRegistry {
|
|
constructor() {
|
|
this.collectors = [];
|
|
}
|
|
|
|
/**
|
|
* Collect metrics from all registered collectors.
|
|
*
|
|
* @returns {*} Async iterator of {@link CollectorResult}.
|
|
*/
|
|
async* collect() {
|
|
for (let collector of this.collectors) {
|
|
for await (let result of collector.collect()) {
|
|
yield result;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add collector to the registry.
|
|
*
|
|
* @param {lib/prometheus.Collector} collector - Collector to add.
|
|
*/
|
|
register(collector) {
|
|
let index = this.collectors.lastIndexOf(collector);
|
|
if (index !== -1) {
|
|
throw new Error(
|
|
"Collector is already registered in this registry."
|
|
);
|
|
}
|
|
|
|
this.collectors.push(collector);
|
|
}
|
|
|
|
/**
|
|
* Remove collector from the registry.
|
|
*
|
|
* @param {lib/prometheus.Collector} collector - Collector to remove.
|
|
*/
|
|
unregister(collector) {
|
|
let index = this.collectors.lastIndexOf(collector);
|
|
if (index === -1) {
|
|
throw new Error(
|
|
"Collector is not registered in this registry."
|
|
);
|
|
}
|
|
|
|
this.collectors.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
defaultRegistry = new CollectorRegistry();
|
|
|
|
function escapeHelp(help) {
|
|
return help
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/\n/g, "\\n")
|
|
;
|
|
}
|
|
|
|
async function* expositionLines(resultsIterator) {
|
|
let first = true;
|
|
for await (let result of resultsIterator) {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
yield "\n";
|
|
}
|
|
|
|
yield `# HELP ${result.metric.name} ${escapeHelp(result.metric.help)}\n`;
|
|
yield `# TYPE ${result.metric.name} ${result.metric.type}\n`;
|
|
|
|
for (let [suffix, samples] of result.samples) {
|
|
for (let [key, value] of samples) {
|
|
if (key === "") {
|
|
yield `${result.metric.name}${suffix} ${formatValue(value)}\n`;
|
|
} else {
|
|
yield `${result.metric.name}${suffix}{${key}} ${formatValue(value)}\n`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialize into Prometheus text exposition format
|
|
*
|
|
* Asynchronously collects metrics and converts them into the text based
|
|
* exposition format for Prometheus. The collectors collected by default is
|
|
* taken from the default registry, but this can be overidden by passing a
|
|
* resultsIterator either from another registry or custom created.
|
|
*
|
|
* The resulting text from calling this method should be served to
|
|
* prometheus, typically by hosting an HTTP server in the app and responding
|
|
* to the /metrics endpoint. See example below for how this is done with
|
|
* express.js.
|
|
*
|
|
* @example
|
|
* const app = require("express")();
|
|
* async function getMetrics(req, res) {
|
|
* // By default exposition uses the default collector registry
|
|
* let text = await exposition();
|
|
* res.set("Content-Type", exposition.contentType);
|
|
* res.send(text);
|
|
* }
|
|
* app.get("/metrics", (req, res, next) => getMetrics(req, res).catch(next));
|
|
* app.listen(9100);
|
|
*
|
|
* @param {*} resultsIterator -
|
|
* Asynchronously itreable of {@link CollectorResult} results to create
|
|
* exposition for. Defaults to collecting results from {@link
|
|
* module:lib/prometheus.defaultRegistry}.
|
|
* @returns {string} Prometheus exposition.
|
|
*
|
|
* @property {string} exposition.contentType
|
|
* HTTP Content-Type for the exposition format that's implemented
|
|
*
|
|
* @static
|
|
*/
|
|
async function exposition(resultsIterator = defaultRegistry.collect()) {
|
|
let lines = "";
|
|
for await (let line of expositionLines(resultsIterator)) {
|
|
lines += line;
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
exposition.contentType = "text/plain; version=0.0.4";
|
|
|
|
/**
|
|
* Serialize CollectorResult into a plain object
|
|
*
|
|
* Converts a {@link module:lib/prometheus~CollectorResult} into a plain
|
|
* object form that can be stringified to JSON.
|
|
*
|
|
* @param {module:lib/prometheus~CollectorResult} result -
|
|
* Result to serialize into plain object form.
|
|
* @param {Object} options - Options for controlling the serialization.
|
|
* @param {string} options.addLabels -
|
|
* Additional labels to append to each value. This may be used if
|
|
* multiple sources are combined have the same metric and a qualifier is
|
|
* needed to make sure the label sets are unique.
|
|
* @param {string} options.metricName - Override metric name of the result.
|
|
* @param {string} options.metricHelp - Override metric help of the result.
|
|
* @returns {Object} plain object form of the result.
|
|
* @static
|
|
*/
|
|
function serializeResult(result, options = {}) {
|
|
let addLabels = null;
|
|
let metricName = result.metric.name;
|
|
let metricHelp = result.metric.help;
|
|
for (let [name, value] of Object.entries(options)) {
|
|
if (name === "addLabels") {
|
|
addLabels = value;
|
|
} else if (name === "metricName") {
|
|
metricName = value;
|
|
} else if (name === "metricHelp") {
|
|
metricHelp = value;
|
|
} else {
|
|
throw new Error(`Unrecognized option '${name}'`);
|
|
}
|
|
}
|
|
|
|
let samples;
|
|
if (addLabels === null) {
|
|
samples = result.samples;
|
|
addLabels = {};
|
|
|
|
} else {
|
|
samples = new Map();
|
|
let key = labelsToKey([addLabels], [...Object.keys(addLabels)]);
|
|
for (let [suffix, suffixSamples] of result.samples) {
|
|
let labeledSamples = new Map();
|
|
for (let [labels, value] of suffixSamples) {
|
|
if (labels === "") {
|
|
labeledSamples.set(key, value);
|
|
} else {
|
|
labeledSamples.set(`${labels},${key}`, value);
|
|
}
|
|
}
|
|
samples.set(suffix, labeledSamples);
|
|
}
|
|
}
|
|
|
|
return {
|
|
metric: {
|
|
type: result.metric.type,
|
|
name: metricName,
|
|
help: metricHelp,
|
|
labels: [...result.metric.labels, ...Object.keys(addLabels)],
|
|
},
|
|
samples: [...samples].map(([name, metricSamples]) => [name, [...metricSamples]]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deserialize CollectorResult from plain object
|
|
*
|
|
* Reverse counterpart to {@link module:lib/prometheus.serializeResult}.
|
|
*
|
|
* @param {Object} serializedResult - Previously serialized result object.
|
|
* @returns {module:lib/prometheus~CollectorResult} deserialized result.
|
|
* @static
|
|
*/
|
|
function deserializeResult(serializedResult) {
|
|
return {
|
|
metric: new Metric(
|
|
serializedResult.metric.type,
|
|
serializedResult.metric.name,
|
|
serializedResult.metric.help,
|
|
serializedResult.metric.labels,
|
|
),
|
|
samples: new Map(
|
|
serializedResult.samples.map(
|
|
([suffix, suffixSamples]) => [suffix, new Map(suffixSamples)]
|
|
)
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Default collectors provided by this library
|
|
* @type {Object<string, module:lib/prometheus.Collector>}
|
|
*/
|
|
let defaultCollectors = {};
|
|
defaultCollectors.processCpuSecondsTotal = new Gauge(
|
|
"process_cpu_seconds_total",
|
|
"Total user and system CPU time spent in seconds.",
|
|
{
|
|
callback: async function(collector) {
|
|
let usage = process.cpuUsage();
|
|
collector.set((usage.user + usage.system) / 1e6);
|
|
},
|
|
},
|
|
);
|
|
|
|
defaultCollectors.processResidentMemoryBytes = new Gauge(
|
|
"process_resident_memory_bytes",
|
|
"Resident memory size in bytes.",
|
|
{
|
|
callback: function(collector) {
|
|
collector.set(process.memoryUsage().rss);
|
|
},
|
|
},
|
|
);
|
|
|
|
defaultCollectors.processHeapBytes = new Gauge(
|
|
"process_heap_bytes",
|
|
"Process heap size in bytes.",
|
|
{
|
|
callback: function(collector) {
|
|
collector.set(process.memoryUsage().heapUsed);
|
|
},
|
|
},
|
|
);
|
|
|
|
defaultCollectors.processStartTimeSeconds = new Gauge(
|
|
"process_start_time_seconds",
|
|
"Start time of the process since unix epoch in seconds."
|
|
);
|
|
defaultCollectors.processStartTimeSeconds.setToCurrentTime();
|
|
|
|
|
|
module.exports = {
|
|
Counter,
|
|
Gauge,
|
|
Summary,
|
|
Histogram,
|
|
CollectorRegistry,
|
|
Collector,
|
|
LabeledCollector,
|
|
ValueCollector,
|
|
Metric,
|
|
|
|
exposition,
|
|
defaultRegistry,
|
|
defaultCollectors,
|
|
serializeResult,
|
|
deserializeResult,
|
|
};
|