mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
Add Histogram collector to prometheus client
Move common functionality between labeled collectors out of ValueCollector and into a new LabeledCollector class, and use this as the basis for implementing a Histogram collector.
This commit is contained in:
1 parent
e82f849118
commit
e5e1851852
4 files changed
+568
-70
No files matched your search
+1
-1
@@ -214,7 +214,7 @@ module.exports = {
|
||||
"no-useless-rename": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-var": "warn",
|
||||
"no-void": "error",
|
||||
"no-void": ["error", { "allowAsStatement": true }],
|
||||
"no-warning-comments": "off",
|
||||
"no-whitespace-before-property": "error",
|
||||
"nonblock-statement-body-position": "error",
|
||||
|
||||
+242
-41
@@ -1,6 +1,6 @@
|
||||
"use strict";
|
||||
class Metric {
|
||||
constructor(type, name, help, labels = []) {
|
||||
constructor(type, name, help, labels = [], _reserved_labels = []) {
|
||||
if (typeof name !== "string") {
|
||||
throw new Error("Expected name to be a string");
|
||||
}
|
||||
@@ -16,6 +16,8 @@ class Metric {
|
||||
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}'`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +122,17 @@ function removeMatchingLabels(mapping, labels) {
|
||||
}
|
||||
}
|
||||
|
||||
class ValueCollector extends Collector {
|
||||
class LabeledCollector extends Collector {
|
||||
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") {
|
||||
@@ -138,32 +143,21 @@ class ValueCollector extends Collector {
|
||||
}
|
||||
|
||||
// Make sure we don't register to the default registry if metric throws.
|
||||
let metric = new Metric(type, name, help, labels);
|
||||
let metric = new Metric(type, name, help, labels, reservedLabels);
|
||||
|
||||
super(register);
|
||||
|
||||
this.callback = callback;
|
||||
this.metric = metric;
|
||||
this._values = new Map();
|
||||
this._children = new Map();
|
||||
this._childClass = childClass;
|
||||
this._defaultChild = labels.length ? null : this.labels({});
|
||||
}
|
||||
|
||||
async* collect() {
|
||||
if (this.callback) { await this.callback(this); }
|
||||
yield { metric: this.metric, samples: this._values };
|
||||
}
|
||||
|
||||
get() {
|
||||
return this._defaultChild.get();
|
||||
}
|
||||
|
||||
labels(...labels) {
|
||||
let key = labelsToKey(labels, this.metric.labels);
|
||||
let child = this._children.get(key);
|
||||
if (child === undefined) {
|
||||
child = new this._childClass(this._values, key);
|
||||
child = new this._childClass(this, key);
|
||||
this._children.set(key, child);
|
||||
}
|
||||
return child;
|
||||
@@ -175,7 +169,7 @@ class ValueCollector extends Collector {
|
||||
throw new Error("labels cannot be empty");
|
||||
}
|
||||
this._children.delete(key);
|
||||
this._values.delete(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
removeAll(labels) {
|
||||
@@ -183,7 +177,6 @@ class ValueCollector extends Collector {
|
||||
throw new Error("labels cannot be empty");
|
||||
}
|
||||
|
||||
removeMatchingLabels(this._values, labels);
|
||||
removeMatchingLabels(this._children, labels);
|
||||
}
|
||||
|
||||
@@ -192,13 +185,45 @@ class ValueCollector extends Collector {
|
||||
throw new Error("Cannot clear unlabeled metric");
|
||||
}
|
||||
this._children = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
class ValueCollector extends LabeledCollector {
|
||||
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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
class ValueCollectorChild {
|
||||
constructor(values, key) {
|
||||
this._values = values;
|
||||
constructor(collector, key) {
|
||||
this._values = collector._values;
|
||||
this._key = key;
|
||||
|
||||
this._values.set(key, 0);
|
||||
@@ -270,6 +295,181 @@ class Gauge extends ValueCollector {
|
||||
}
|
||||
}
|
||||
|
||||
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)}"`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
get buckets() {
|
||||
return new Map(
|
||||
[...this._bucketKeys].map(
|
||||
([bucket, key]) => [bucket, this._bucketValues.get(key)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get sum() {
|
||||
return this._sumValues.get(this._key);
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this._countValues.get(this._key);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
let start = process.hrtime.bigint();
|
||||
return () => {
|
||||
let end = process.hrtime.bigint();
|
||||
this.observe(Number(end - start) / 1e9);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Histogram extends LabeledCollector {
|
||||
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],
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
get buckets() {
|
||||
return this._defaultChild.buckets;
|
||||
}
|
||||
|
||||
get sum() {
|
||||
return this._defaultChild.sum;
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this._defaultChild.count;
|
||||
}
|
||||
|
||||
observe(value) {
|
||||
this._defaultChild.observe(value);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Histogram.linear = function linear(start, width, count) {
|
||||
let buckets = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
buckets.push(start);
|
||||
start += width;
|
||||
}
|
||||
return buckets;
|
||||
};
|
||||
|
||||
Histogram.exponential = function exponential(start, factor, count) {
|
||||
let buckets = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
buckets.push(start);
|
||||
start *= factor;
|
||||
}
|
||||
return buckets;
|
||||
};
|
||||
|
||||
class CollectorRegistry {
|
||||
constructor() {
|
||||
this.collectors = [];
|
||||
@@ -315,16 +515,6 @@ function escapeHelp(help) {
|
||||
;
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (value === Infinity) {
|
||||
return "+Inf";
|
||||
}
|
||||
if (value === -Infinity) {
|
||||
return "-Inf";
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
async function* expositionLines(resultsIterator) {
|
||||
let first = true;
|
||||
for await (let result of resultsIterator) {
|
||||
@@ -337,11 +527,13 @@ async function* expositionLines(resultsIterator) {
|
||||
yield `# HELP ${result.metric.name} ${escapeHelp(result.metric.help)}\n`;
|
||||
yield `# TYPE ${result.metric.name} ${result.metric.type}\n`;
|
||||
|
||||
for (let [key, value] of result.samples) {
|
||||
if (key === "") {
|
||||
yield `${result.metric.name} ${formatValue(value)}\n`;
|
||||
} else {
|
||||
yield `${result.metric.name}{${key}} ${formatValue(value)}\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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,12 +574,16 @@ function serializeResult(result, options = {}) {
|
||||
} else {
|
||||
samples = new Map();
|
||||
let key = labelsToKey([addLabels], [...Object.keys(addLabels)]);
|
||||
for (let [labels, value] of result.samples.entries()) {
|
||||
if (labels === "") {
|
||||
samples.set(key, value);
|
||||
} else {
|
||||
samples.set(`${labels},${key}`, value);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +594,7 @@ function serializeResult(result, options = {}) {
|
||||
help: metricHelp,
|
||||
labels: [...result.metric.labels, ...Object.keys(addLabels)],
|
||||
},
|
||||
samples: [...samples.entries()],
|
||||
samples: [...samples].map(([name, metricSamples]) => [name, [...metricSamples]]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -410,7 +606,11 @@ function deserializeResult(serializedResult) {
|
||||
serializedResult.metric.help,
|
||||
serializedResult.metric.labels,
|
||||
),
|
||||
samples: new Map(serializedResult.samples),
|
||||
samples: new Map(
|
||||
serializedResult.samples.map(
|
||||
([suffix, suffixSamples]) => [suffix, new Map(suffixSamples)]
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -456,6 +656,7 @@ defaultCollectors.processStartTimeSeconds.setToCurrentTime();
|
||||
module.exports = {
|
||||
Counter,
|
||||
Gauge,
|
||||
Histogram,
|
||||
CollectorRegistry,
|
||||
Collector,
|
||||
ValueCollector,
|
||||
|
||||
@@ -168,8 +168,17 @@ async function getMetrics(req, res, next) {
|
||||
|
||||
} else {
|
||||
// Merge metrics received by multiple slaves
|
||||
let stored = resultMap.get(result.metric.name);
|
||||
stored.samples.push(...result.samples);
|
||||
let receivedSamples = new Map(result.samples);
|
||||
for (let [suffix, suffixSamples] of resultMap.get(result.metric.name).samples) {
|
||||
if (receivedSamples.has(suffix)) {
|
||||
suffixSamples.push(...receivedSamples.get(suffix));
|
||||
receivedSamples.delete(suffix);
|
||||
}
|
||||
}
|
||||
|
||||
for (let entry of receivedSamples) {
|
||||
result.samples.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+314
-26
@@ -200,7 +200,194 @@ describe("lib/prometheus", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Histogram", function() {
|
||||
let histogram;
|
||||
let labeledHistogram;
|
||||
beforeEach(function() {
|
||||
histogram = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, buckets: [1, 2, 3, 4, Infinity] }
|
||||
);
|
||||
labeledHistogram = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, buckets: [1, Infinity], labels: ["a", "b"] }
|
||||
);
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should have buckets, sum and count initialized to 0", function() {
|
||||
assert.deepEqual(
|
||||
histogram.buckets,
|
||||
new Map([[1, 0], [2, 0], [3, 0], [4, 0], [Infinity, 0]])
|
||||
);
|
||||
assert.equal(histogram.sum, 0);
|
||||
assert.equal(histogram.count, 0);
|
||||
});
|
||||
it("should provide a default for buckets", function() {
|
||||
histogram = new prometheus.Histogram(
|
||||
"test", "help", { register: false }
|
||||
);
|
||||
assert(histogram._buckets.length, "no default buckets were made");
|
||||
});
|
||||
it("should add inifinity bucket if not present", function() {
|
||||
histogram = new prometheus.Histogram(
|
||||
"test", "help", { buckets: [1], register: false }
|
||||
);
|
||||
assert.deepEqual(histogram._buckets, [1, Infinity]);
|
||||
});
|
||||
it("should not mutate bucket option passed to it", function() {
|
||||
let buckets = [1];
|
||||
histogram = new prometheus.Histogram(
|
||||
"test", "help", { buckets, register: false }
|
||||
);
|
||||
assert.deepEqual(buckets, [1]);
|
||||
});
|
||||
it("should throw on unrecoginzed option", function() {
|
||||
assert.throws(
|
||||
() => new prometheus.Histogram("test", "help", { invalid: 1 }),
|
||||
new Error("Unrecognized option 'invalid'")
|
||||
);
|
||||
});
|
||||
it("should throw on using le as label", function() {
|
||||
assert.throws(() => {
|
||||
let test = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, labels: ["le", "test"] }
|
||||
);
|
||||
}, new Error("Reserved label 'le'"));
|
||||
});
|
||||
});
|
||||
|
||||
describe(".observe()", function() {
|
||||
it("should increment the appropriate bucket", function() {
|
||||
histogram.observe(2);
|
||||
assert.deepEqual(
|
||||
histogram.buckets,
|
||||
new Map([[1, 0], [2, 1], [3, 1], [4, 1], [Infinity, 1]])
|
||||
);
|
||||
assert.equal(histogram.sum, 2);
|
||||
assert.equal(histogram.count, 1);
|
||||
|
||||
histogram.observe(4);
|
||||
assert.deepEqual(
|
||||
histogram.buckets,
|
||||
new Map([[1, 0], [2, 1], [3, 1], [4, 2], [Infinity, 2]])
|
||||
);
|
||||
assert.equal(histogram.sum, 6);
|
||||
assert.equal(histogram.count, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".startTimer()", function() {
|
||||
it("should add observations when closure is called", function() {
|
||||
let observeDuration = histogram.startTimer();
|
||||
assert.equal(histogram.count, 0);
|
||||
observeDuration();
|
||||
assert.equal(histogram.count, 1);
|
||||
observeDuration();
|
||||
assert.equal(histogram.count, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static .linear()", function() {
|
||||
it("should give back the appropriate buckets", function() {
|
||||
let linear = prometheus.Histogram.linear;
|
||||
assert.deepEqual(linear(1, 2, 3), [1, 3, 5]);
|
||||
assert.deepEqual(linear(-1, 4, 3), [-1, 3, 7]);
|
||||
assert.deepEqual(linear(1, 1, 4), [1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("static .exponential()", function() {
|
||||
it("should give back the appropriate buckets", function() {
|
||||
let exponential = prometheus.Histogram.exponential;
|
||||
assert.deepEqual(exponential(1, 2, 3), [1, 2, 4]);
|
||||
assert.deepEqual(exponential(2, 10, 3), [2, 20, 200]);
|
||||
assert.deepEqual(exponential(1, 1.5, 4), [1, 1.5, 2.25, 3.375]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".labels()", function() {
|
||||
it("should initialize a value for the label set", function() {
|
||||
labeledHistogram.labels({ a: "1", b: "2" });
|
||||
assert(labeledHistogram._bucketValues.has('a="1",b="2",le="1"'), "bucket value not set");
|
||||
assert(labeledHistogram._bucketValues.has('a="1",b="2",le="+Inf"'), "bucket value not set");
|
||||
assert(labeledHistogram._sumValues.has('a="1",b="2"'), "sum value not set");
|
||||
assert(labeledHistogram._countValues.has('a="1",b="2"'), "count value not set");
|
||||
|
||||
labeledHistogram.labels("4", "5");
|
||||
assert(labeledHistogram._bucketValues.has('a="4",b="5",le="1"'), "bucket value not set");
|
||||
assert(labeledHistogram._bucketValues.has('a="4",b="5",le="+Inf"'), "bucket value not set");
|
||||
assert(labeledHistogram._sumValues.has('a="4",b="5"'), "sum value not set");
|
||||
assert(labeledHistogram._countValues.has('a="4",b="5"'), "count value not set");
|
||||
});
|
||||
it("should return a child supporting the histogram methods", function() {
|
||||
let child = labeledHistogram.labels({ a: "1", b: "2" });
|
||||
void child.buckets;
|
||||
void child.sum;
|
||||
void child.count;
|
||||
child.observe(2);
|
||||
let observeDuration = child.startTimer();
|
||||
observeDuration();
|
||||
child = labeledHistogram.labels("7", "8");
|
||||
void child.buckets;
|
||||
void child.sum;
|
||||
void child.count;
|
||||
child.observe(2);
|
||||
observeDuration = child.startTimer();
|
||||
observeDuration();
|
||||
});
|
||||
it("should return the same child for the same labels", function() {
|
||||
let child = labeledHistogram.labels("1", "2");
|
||||
assert.equal(labeledHistogram.labels("1", "2"), child);
|
||||
assert.equal(labeledHistogram.labels({ a: "1", b: "2" }), child);
|
||||
});
|
||||
});
|
||||
describe(".remove()", function() {
|
||||
it("should remove a label set created with .labels()", function() {
|
||||
labeledHistogram.labels("1", "2");
|
||||
labeledHistogram.remove("1", "2");
|
||||
assert(!labeledHistogram._bucketValues.has('a="1",b="2",le="1"'), "bucket value not removed");
|
||||
assert(!labeledHistogram._bucketValues.has('a="1",b="2",le="+Inf"'), "bucket value not removed");
|
||||
assert(!labeledHistogram._sumValues.has('a="1",b="2"'), "sum value not removed");
|
||||
assert(!labeledHistogram._countValues.has('a="1",b="2"'), "count value not removed");
|
||||
});
|
||||
});
|
||||
describe(".removeAll()", function() {
|
||||
it("should remove a label set created with .labels()", function() {
|
||||
labeledHistogram.labels("1", "2");
|
||||
labeledHistogram.labels("1", "3");
|
||||
labeledHistogram.labels("4", "6");
|
||||
labeledHistogram.removeAll({ a: "1" });
|
||||
assert.deepEqual(
|
||||
labeledHistogram._bucketValues,
|
||||
new Map([['a="4",b="6",le="1"', 0], ['a="4",b="6",le="+Inf"', 0]])
|
||||
);
|
||||
assert.deepEqual(labeledHistogram._sumValues, new Map([['a="4",b="6"', 0]]));
|
||||
assert.deepEqual(labeledHistogram._countValues, new Map([['a="4",b="6"', 0]]));
|
||||
});
|
||||
});
|
||||
describe(".clear()", function() {
|
||||
it("should remove all label sets created with .labels()", function() {
|
||||
labeledHistogram.labels("1", "2");
|
||||
labeledHistogram.labels("1", "3");
|
||||
labeledHistogram.labels("4", "6");
|
||||
labeledHistogram.clear();
|
||||
assert(labeledHistogram._bucketValues.size === 0, "buckets were not cleared");
|
||||
assert(labeledHistogram._sumValues.size === 0, "sums were not cleared");
|
||||
assert(labeledHistogram._countValues.size === 0, "counts were not cleared");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class ValueCollector", function() {
|
||||
describe(".get()", function() {
|
||||
it("should return the current value", function() {
|
||||
let gauge = new prometheus.Gauge("test", "Help", { register: false });
|
||||
gauge.set(11);
|
||||
assert.equal(gauge.get(), 11);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class LabeledCollector", function() {
|
||||
describe("constructor", function() {
|
||||
it("should throw on invalid metric name", function() {
|
||||
for (let name of ["", " ", "0a", "$a"]) {
|
||||
@@ -242,14 +429,6 @@ describe("lib/prometheus", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".get()", function() {
|
||||
it("should return the current value", function() {
|
||||
let gauge = new prometheus.Gauge("test", "Help", { register: false });
|
||||
gauge.set(11);
|
||||
assert.equal(gauge.get(), 11);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".labels()", function() {
|
||||
let gauge;
|
||||
beforeEach(function() {
|
||||
@@ -310,6 +489,7 @@ describe("lib/prometheus", function() {
|
||||
it("should return the same child for the same labels", function() {
|
||||
let child = gauge.labels("1", "2");
|
||||
assert.equal(gauge.labels("1", "2"), child);
|
||||
assert.equal(gauge.labels({ a: "1", b: "2" }), child);
|
||||
});
|
||||
});
|
||||
describe(".remove()", function() {
|
||||
@@ -393,8 +573,9 @@ describe("lib/prometheus", function() {
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
let value = results[0].samples.get("").get("");
|
||||
assert(
|
||||
Math.abs(results[0].samples.get("") - Date.now() / 1000) < 1,
|
||||
Math.abs(value - Date.now() / 1000) < 1,
|
||||
"value set is not close to the current unix time"
|
||||
);
|
||||
});
|
||||
@@ -413,7 +594,7 @@ describe("lib/prometheus", function() {
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
let value = results[0].samples.get("");
|
||||
let value = results[0].samples.get("").get("");
|
||||
assert(
|
||||
value < 5 && value > 0,
|
||||
`process time is not between 0 and 5 seconds (${value})`
|
||||
@@ -434,7 +615,7 @@ describe("lib/prometheus", function() {
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
let value = results[0].samples.get("");
|
||||
let value = results[0].samples.get("").get("");
|
||||
assert(
|
||||
value < 200e6 && value > 0,
|
||||
`resident memory is not between 0 and 200 MB (${value})`
|
||||
@@ -455,7 +636,7 @@ describe("lib/prometheus", function() {
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
let value = results[0].samples.get("");
|
||||
let value = results[0].samples.get("").get("");
|
||||
assert(
|
||||
value < 200e6 && value > 0,
|
||||
`heap is not between 0 and 200 MB (${value})`
|
||||
@@ -500,6 +681,59 @@ describe("lib/prometheus", function() {
|
||||
'test{a="x",b="y"} 0\n'
|
||||
);
|
||||
});
|
||||
it("should format histogram metrics", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let histogram = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, buckets: [1, 2, 3, 4] }
|
||||
);
|
||||
for (let value of [2, 2, 5, 2]) {
|
||||
histogram.observe(value);
|
||||
}
|
||||
registry.register(histogram);
|
||||
let exposition = await prometheus.exposition(registry.collect());
|
||||
assert.equal(
|
||||
exposition,
|
||||
"# HELP test Help\n" +
|
||||
"# TYPE test histogram\n" +
|
||||
'test_bucket{le="1"} 0\n' +
|
||||
'test_bucket{le="2"} 3\n' +
|
||||
'test_bucket{le="3"} 3\n' +
|
||||
'test_bucket{le="4"} 3\n' +
|
||||
'test_bucket{le="+Inf"} 4\n' +
|
||||
"test_sum 11\n" +
|
||||
"test_count 4\n"
|
||||
);
|
||||
});
|
||||
it("should format histogram metrics with labels", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let histogram = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, buckets: [1], labels: ["l"] }
|
||||
);
|
||||
for (let [value, label] of [[1, "a"], [2, "b"], [3, "b"]]) {
|
||||
histogram.labels(label).observe(value);
|
||||
}
|
||||
histogram.labels("c");
|
||||
|
||||
registry.register(histogram);
|
||||
let exposition = await prometheus.exposition(registry.collect());
|
||||
assert.equal(
|
||||
exposition,
|
||||
"# HELP test Help\n" +
|
||||
"# TYPE test histogram\n" +
|
||||
'test_bucket{l="a",le="1"} 1\n' +
|
||||
'test_bucket{l="a",le="+Inf"} 1\n' +
|
||||
'test_bucket{l="b",le="1"} 0\n' +
|
||||
'test_bucket{l="b",le="+Inf"} 2\n' +
|
||||
'test_bucket{l="c",le="1"} 0\n' +
|
||||
'test_bucket{l="c",le="+Inf"} 0\n' +
|
||||
'test_sum{l="a"} 1\n' +
|
||||
'test_sum{l="b"} 5\n' +
|
||||
'test_sum{l="c"} 0\n' +
|
||||
'test_count{l="a"} 1\n' +
|
||||
'test_count{l="b"} 2\n' +
|
||||
'test_count{l="c"} 0\n'
|
||||
);
|
||||
});
|
||||
it("should format Infinity and NaN", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let gauge = new prometheus.Gauge(
|
||||
@@ -523,38 +757,64 @@ describe("lib/prometheus", function() {
|
||||
|
||||
let serializedResult = {
|
||||
metric: { type: "type", name: "name", help: "Help", labels: ["a", "b"] },
|
||||
samples: [['a="1",b="2"', 1], ['a="3",b="4"', 5]],
|
||||
samples: [["", [['a="1",b="2"', 1], ['a="3",b="4"', 5]]]],
|
||||
};
|
||||
let deserializedResult = {
|
||||
metric: new prometheus.Metric("type", "name", "Help", ["a", "b"]),
|
||||
samples: new Map([['a="1",b="2"', 1], ['a="3",b="4"', 5]]),
|
||||
samples: new Map([["", new Map([['a="1",b="2"', 1], ['a="3",b="4"', 5]])]]),
|
||||
};
|
||||
describe("serializeResult()", function() {
|
||||
it("should convert a result into plain object", function() {
|
||||
it("should convert real results into the reference serialization", async function() {
|
||||
let counter = new prometheus.Counter("a_counter", "Counter", { register: false });
|
||||
let gauge = new prometheus.Gauge("a_gauge", "Gauge", { register: false, labels: ["a", "b"] });
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
registry.register(counter);
|
||||
registry.register(gauge);
|
||||
|
||||
counter.inc(3);
|
||||
gauge.labels("1", "2").set(1);
|
||||
gauge.labels("3", "4").set(5);
|
||||
|
||||
let results = [];
|
||||
for await (let result of registry.collect()) {
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
let referenceSerialization = [
|
||||
{
|
||||
metric: { type: "counter", name: "a_counter", help: "Counter", labels: [] },
|
||||
samples: [["", [["", 3]]]],
|
||||
},
|
||||
{
|
||||
metric: { type: "gauge", name: "a_gauge", help: "Gauge", labels: ["a", "b"] },
|
||||
samples: [["", [['a="1",b="2"', 1], ['a="3",b="4"', 5]]]],
|
||||
},
|
||||
];
|
||||
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult(deserializedResult),
|
||||
serializedResult
|
||||
results.map(result => prometheus.serializeResult(result)),
|
||||
referenceSerialization
|
||||
);
|
||||
});
|
||||
it("should support adding labels", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric("type", "name", "Help"),
|
||||
samples: new Map([["", 2]]),
|
||||
samples: new Map([["name", new Map([["", 2]])]]),
|
||||
}, { addLabels: { c: "8" }}),
|
||||
{
|
||||
metric: { type: "type", name: "name", help: "Help", labels: ["c"] },
|
||||
samples: [['c="8"', 2]],
|
||||
samples: [["name", [['c="8"', 2]]]],
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric("type", "name", "Help", ["a"]),
|
||||
samples: new Map([['a="4"', 3]]),
|
||||
samples: new Map([["", new Map([['a="4"', 3]])]]),
|
||||
}, { addLabels: { c: "8" }}),
|
||||
{
|
||||
metric: { type: "type", name: "name", help: "Help", labels: ["a", "c"] },
|
||||
samples: [['a="4",c="8"', 3]],
|
||||
samples: [["", [['a="4",c="8"', 3]]]],
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -562,21 +822,21 @@ describe("lib/prometheus", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric("type", "name", "Help"),
|
||||
samples: new Map([["", 2]]),
|
||||
samples: new Map([["", new Map([["", 2]])]]),
|
||||
}, { metricName: "new" }),
|
||||
{
|
||||
metric: { type: "type", name: "new", help: "Help", labels: [] },
|
||||
samples: [["", 2]],
|
||||
samples: [["", [["", 2]]]],
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric("type", "name", "Help"),
|
||||
samples: new Map([["", 2]]),
|
||||
samples: new Map([["", new Map([["", 2]])]]),
|
||||
}, { metricHelp: "old" }),
|
||||
{
|
||||
metric: { type: "type", name: "name", help: "old", labels: [] },
|
||||
samples: [["", 2]],
|
||||
samples: [["", [["", 2]]]],
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -584,7 +844,7 @@ describe("lib/prometheus", function() {
|
||||
assert.throws(
|
||||
() => prometheus.serializeResult({
|
||||
metric: new prometheus.Metric("type", "name", "Help"),
|
||||
samples: new Map([["", 3]]),
|
||||
samples: new Map([["", new Map([["", 3]])]]),
|
||||
}, { invalid: true }),
|
||||
new Error("Unrecognized option 'invalid'")
|
||||
);
|
||||
@@ -600,6 +860,34 @@ describe("lib/prometheus", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration", function() {
|
||||
it("should round trip serialize histogram results", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let histogram = new prometheus.Histogram(
|
||||
"test", "Help", { register: false, buckets: [1], labels: ["l"] }
|
||||
);
|
||||
for (let [value, label] of [[1, "a"], [2, "b"], [3, "b"]]) {
|
||||
histogram.labels(label).observe(value);
|
||||
}
|
||||
histogram.labels("c");
|
||||
|
||||
registry.register(histogram);
|
||||
let results = [];
|
||||
for await (let result of registry.collect()) {
|
||||
results.push(result);
|
||||
}
|
||||
let exposition = await prometheus.exposition(results);
|
||||
|
||||
let serialized = results.map(res => prometheus.serializeResult(res));
|
||||
let jsonRoundtrip = JSON.parse(JSON.stringify(serialized));
|
||||
let deserialized = jsonRoundtrip.map(res => prometheus.deserializeResult(res));
|
||||
assert.deepEqual(deserialized, results);
|
||||
let roundTripExposition = await prometheus.exposition(deserialized);
|
||||
|
||||
assert.equal(roundTripExposition, exposition);
|
||||
});
|
||||
});
|
||||
|
||||
describe("post test checks", function() {
|
||||
it("should not have any collectors in the default registry", function() {
|
||||
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
|
||||
|
||||
Reference in new issue
Block a user