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:
Hornwitser committed 2020-11-18 21:27:18 +01:00
1 parent e82f849118
commit e5e1851852
4 files changed
+568 -70

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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,
+11 -2
View File
@@ -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
View File
@@ -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