mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 15:24:52 +09:00
Add homebrew Prometheus client
Add an implementation of a Prometheus client. This is because the prom-client implementation seems to have missunderstood how Prometheus is supposed to work and doesn't allow updating collectors via asynchronous callback.
This commit is contained in:
1 parent
1f6574a89c
commit
b4c7824101
2 files changed
+997
No files matched your search
@@ -0,0 +1,432 @@
|
||||
class Metric {
|
||||
constructor(type, name, help, 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 name of labels) {
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
||||
throw new Error(`Invalid label '${name}'`);
|
||||
}
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.help = help;
|
||||
this.type = type;
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
|
||||
let defaultRegistry;
|
||||
|
||||
class Collector {
|
||||
constructor(register = true) {
|
||||
if (register) {
|
||||
defaultRegistry.register(this);
|
||||
}
|
||||
}
|
||||
|
||||
async* collect() { }
|
||||
}
|
||||
|
||||
function escapeLabelValue(value) {
|
||||
return value
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\n/g, "\\n")
|
||||
.replace(/"/g, "\\\"")
|
||||
;
|
||||
}
|
||||
|
||||
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])}"`);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class ValueCollector extends Collector {
|
||||
constructor(type, name, help, options, childClass) {
|
||||
let labels = [];
|
||||
let register = true;
|
||||
let callback = null;
|
||||
for (let [name, value] of Object.entries(options)) {
|
||||
if (name === 'labels') { labels = value; }
|
||||
else if (name === 'register') { register = value; }
|
||||
else if (name === 'callback') { callback = value; }
|
||||
else { throw new Error(`Unrecognized option '${name}'`); }
|
||||
}
|
||||
|
||||
// Make sure we don't register to the default registry if metric throws.
|
||||
let metric = new Metric(type, name, help, labels);
|
||||
|
||||
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);
|
||||
this._children.set(key, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
remove(...labels) {
|
||||
let key = labelsToKey(labels, this.metric.labels);
|
||||
if (key === '') {
|
||||
throw new Error("labels cannot be empty");
|
||||
}
|
||||
this._children.delete(key);
|
||||
this._values.delete(key);
|
||||
}
|
||||
|
||||
removeAll(labels) {
|
||||
if (!Object.keys(labels).length) {
|
||||
throw new Error("labels cannot be empty");
|
||||
}
|
||||
|
||||
function removeMatchingLabels(mapping, labels) {
|
||||
for (let key of mapping.keys()) {
|
||||
let candidate = keyToLabels(key);
|
||||
let hasLabels = Object.entries(labels).every(([name, value]) => {
|
||||
return candidate.get(name) === value;
|
||||
});
|
||||
if (hasLabels) {
|
||||
mapping.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeMatchingLabels(this._values, labels);
|
||||
removeMatchingLabels(this._children, labels);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (!this.metric.labels.length) {
|
||||
throw new Error("Cannot clear unlabeled metric");
|
||||
}
|
||||
this._children = new Map();
|
||||
this._values = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
class ValueCollectorChild {
|
||||
constructor(values, key) {
|
||||
this._values = values;
|
||||
this._key = key;
|
||||
|
||||
this._values.set(key, 0);
|
||||
}
|
||||
|
||||
get() {
|
||||
return this._values.get(this._key);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterChild extends ValueCollectorChild {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
class Counter extends ValueCollector {
|
||||
constructor(name, help, options = {}) {
|
||||
super("counter", name, help, options, CounterChild);
|
||||
}
|
||||
|
||||
inc(value = 1) {
|
||||
this._defaultChild.inc(value);
|
||||
}
|
||||
}
|
||||
|
||||
class GaugeChild extends ValueCollectorChild {
|
||||
inc(value = 1) {
|
||||
this._values.set(this._key, this._values.get(this._key) + value)
|
||||
}
|
||||
|
||||
dec(value = 1) {
|
||||
this._values.set(this._key, this._values.get(this._key) - value)
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this._values.set(this._key, value)
|
||||
}
|
||||
|
||||
setToCurrentTime() {
|
||||
this._values.set(this._key, Date.now() / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
class Gauge extends ValueCollector {
|
||||
constructor(name, help, options = {}) {
|
||||
super("gauge", name, help, options, GaugeChild);
|
||||
}
|
||||
|
||||
inc(value = 1) {
|
||||
this._defaultChild.inc(value);
|
||||
}
|
||||
|
||||
dec(value = 1) {
|
||||
this._defaultChild.dec(value);
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this._defaultChild.set(value);
|
||||
}
|
||||
|
||||
setToCurrentTime() {
|
||||
this._defaultChild.setToCurrentTime();
|
||||
}
|
||||
}
|
||||
|
||||
class CollectorRegistry {
|
||||
constructor() {
|
||||
this.collectors = [];
|
||||
}
|
||||
|
||||
async* collect() {
|
||||
for (let collector of this.collectors) {
|
||||
for await (let result of collector.collect()) {
|
||||
yield result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
register(metric) {
|
||||
let index = this.collectors.lastIndexOf(metric);
|
||||
if (index !== -1) {
|
||||
throw new Error(
|
||||
"Collector is already registered in this registry."
|
||||
);
|
||||
}
|
||||
|
||||
this.collectors.push(metric);
|
||||
}
|
||||
|
||||
unregister(metric) {
|
||||
let index = this.collectors.lastIndexOf(metric);
|
||||
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")
|
||||
;
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (value === Infinity) {
|
||||
return '+Inf';
|
||||
} else if (value === -Infinity) {
|
||||
return '-Inf';
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
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 [key, value] of result.samples) {
|
||||
if (key === '') {
|
||||
yield `${result.metric.name} ${formatValue(value)}\n`;
|
||||
} else {
|
||||
yield `${result.metric.name}{${key}} ${formatValue(value)}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function exposition(resultsIterator = defaultRegistry.collect()) {
|
||||
let exposition = '';
|
||||
for await (let line of expositionLines(resultsIterator)) {
|
||||
exposition += line;
|
||||
}
|
||||
return exposition;
|
||||
}
|
||||
|
||||
// HTTP Content-Type for the exposition format that's implemented
|
||||
exposition.contentType = 'text/plain; version=0.0.4';
|
||||
|
||||
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 [labels, value] of result.samples.entries()) {
|
||||
if (labels === '') { samples.set(key, value); }
|
||||
else { samples.set(`${labels},${key}`, value); }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metric: {
|
||||
type: result.metric.type,
|
||||
name: metricName,
|
||||
help: metricHelp,
|
||||
labels: [...result.metric.labels, ...Object.keys(addLabels)],
|
||||
},
|
||||
samples: [...samples.entries()],
|
||||
};
|
||||
}
|
||||
|
||||
function deserializeResult(serializedResult) {
|
||||
return {
|
||||
metric: new Metric(
|
||||
serializedResult.metric.type,
|
||||
serializedResult.metric.name,
|
||||
serializedResult.metric.help,
|
||||
serializedResult.metric.labels,
|
||||
),
|
||||
samples: new Map(serializedResult.samples),
|
||||
}
|
||||
}
|
||||
|
||||
let defaultCollectors = {};
|
||||
defaultCollectors.processCpuSecondsTotal = new Gauge(
|
||||
'process_cpu_seconds', "Total user and system CPU time spent in seconds.",
|
||||
{
|
||||
callback: async function() {
|
||||
let usage = process.cpuUsage();
|
||||
this.set((usage.user + usage.system) / 1e6);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
CollectorRegistry,
|
||||
Collector,
|
||||
ValueCollector,
|
||||
Metric,
|
||||
|
||||
exposition,
|
||||
defaultRegistry,
|
||||
defaultCollectors,
|
||||
serializeResult,
|
||||
deserializeResult,
|
||||
};
|
||||
@@ -0,0 +1,565 @@
|
||||
const assert = require('assert').strict;
|
||||
|
||||
const prometheus = require('lib/prometheus');
|
||||
|
||||
|
||||
describe("lib/prometheus", function() {
|
||||
describe("class CollectorRegistry", function() {
|
||||
let registry;
|
||||
beforeEach(function() {
|
||||
registry = new prometheus.CollectorRegistry();
|
||||
})
|
||||
|
||||
describe(".register()", function() {
|
||||
it("should register a collector", function() {
|
||||
let collector = new prometheus.Collector(false);
|
||||
registry.register(collector);
|
||||
assert(
|
||||
registry.collectors.includes(collector),
|
||||
"Collector was not registered"
|
||||
);
|
||||
});
|
||||
it("should throw when already registered", function() {
|
||||
let collector = new prometheus.Collector(false);
|
||||
registry.register(collector);
|
||||
assert.throws(
|
||||
() => registry.register(collector),
|
||||
new Error("Collector is already registered in this registry.")
|
||||
);
|
||||
});
|
||||
it("should allow being regitstered in another register", function() {
|
||||
let otherCollectorRegistry = new prometheus.CollectorRegistry();
|
||||
let collector = new prometheus.Collector(false);
|
||||
registry.register(collector);
|
||||
otherCollectorRegistry.register(collector);
|
||||
assert(
|
||||
registry.collectors.includes(collector),
|
||||
"Collector was not registered in first register"
|
||||
);
|
||||
assert(
|
||||
otherCollectorRegistry.collectors.includes(collector),
|
||||
"Collector was not registered in second register"
|
||||
);
|
||||
});
|
||||
});
|
||||
describe(".unregister()", function() {
|
||||
it("should removed a registered collector", function() {
|
||||
let collector = new prometheus.Collector(false);
|
||||
registry.register(collector);
|
||||
registry.unregister(collector);
|
||||
assert(
|
||||
!registry.collectors.includes(collector),
|
||||
"Collector was not removed"
|
||||
);
|
||||
});
|
||||
it("should throw when not registered", function() {
|
||||
let collector = new prometheus.Collector(false);
|
||||
assert.throws(
|
||||
() => registry.unregister(collector),
|
||||
new Error("Collector is not registered in this registry.")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultRegistry", function() {
|
||||
it("should have collectors registered to it by default", function() {
|
||||
let collector = new prometheus.Collector();
|
||||
assert(
|
||||
prometheus.defaultRegistry.collectors.includes(collector),
|
||||
"collector was not registered to defaultRegistry"
|
||||
);
|
||||
prometheus.defaultRegistry.unregister(collector);
|
||||
});
|
||||
it("should have the default collectors registered to it", function() {
|
||||
let collectors = Object.entries(prometheus.defaultCollectors);
|
||||
for (let [name, collector] of collectors) {
|
||||
assert(
|
||||
prometheus.defaultRegistry.collectors.includes(collector),
|
||||
`${name} is not registered`
|
||||
);
|
||||
}
|
||||
});
|
||||
it("should allow default collectors to be unregistered", function() {
|
||||
let collectors = Object.values(prometheus.defaultCollectors);
|
||||
for (let collector of collectors) {
|
||||
prometheus.defaultRegistry.unregister(collector);
|
||||
}
|
||||
assert(
|
||||
prometheus.defaultRegistry.collectors.length == 0,
|
||||
"Collectors left over after removing the default ones"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Collector", function() {
|
||||
it("should implement .collect()", async function() {
|
||||
let collector = new prometheus.Collector(false);
|
||||
for await (let result of collector.collect()) {
|
||||
assert.fail("Should not return any results");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Counter", function() {
|
||||
let counter;
|
||||
beforeEach(function() {
|
||||
counter = new prometheus.Counter('test', "Help", { register: false });
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should be initialized to 0", function() {
|
||||
assert.equal(counter._values.get(''), 0);
|
||||
});
|
||||
});
|
||||
describe(".inc()", function() {
|
||||
it("should increment the counter by 1 by default", function() {
|
||||
counter.inc();
|
||||
assert.equal(counter._values.get(''), 1);
|
||||
});
|
||||
it("should increment the counter by specified value", function() {
|
||||
counter.inc(3.2);
|
||||
assert.equal(counter._values.get(''), 3.2);
|
||||
});
|
||||
it("should throw if value is negative", function() {
|
||||
assert.throws(
|
||||
() => counter.inc(-1),
|
||||
new Error("Expected value to be a positive number")
|
||||
);
|
||||
});
|
||||
it("should throw if value is negative infinity", function() {
|
||||
assert.throws(
|
||||
() => counter.inc(-Infinity),
|
||||
new Error("Expected value to be a positive number")
|
||||
);
|
||||
});
|
||||
it("should throw if value is NaN", function() {
|
||||
assert.throws(
|
||||
() => counter.inc(NaN),
|
||||
new Error("Expected value to be a positive number")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class Gauge", function() {
|
||||
let gauge;
|
||||
beforeEach(function() {
|
||||
gauge = new prometheus.Gauge('test', "Help", { register: false });
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should be initialized to 0", function() {
|
||||
assert.equal(gauge._values.get(''), 0);
|
||||
});
|
||||
});
|
||||
describe(".inc()", function() {
|
||||
it("should increment the gauge by 1 by default", function() {
|
||||
gauge.inc();
|
||||
assert.equal(gauge._values.get(''), 1);
|
||||
});
|
||||
it("should increment the gauge by specified value", function() {
|
||||
gauge.inc(3.2);
|
||||
assert.equal(gauge._values.get(''), 3.2);
|
||||
});
|
||||
it("should decrement on negative value", function() {
|
||||
gauge.inc(-2);
|
||||
assert.equal(gauge._values.get(''), -2);
|
||||
});
|
||||
});
|
||||
describe(".dec()", function() {
|
||||
it("should decrement the gauge by 1 by default", function() {
|
||||
gauge.dec();
|
||||
assert.equal(gauge._values.get(''), -1);
|
||||
});
|
||||
it("should decrement the gauge by specified value", function() {
|
||||
gauge.dec(3.2);
|
||||
assert.equal(gauge._values.get(''), -3.2);
|
||||
});
|
||||
it("should increment on negative value", function() {
|
||||
gauge.dec(-2);
|
||||
assert.equal(gauge._values.get(''), 2);
|
||||
});
|
||||
});
|
||||
describe(".set()", function() {
|
||||
it("should set the gauge to the specified value", function() {
|
||||
gauge.set(7);
|
||||
assert.equal(gauge._values.get(''), 7);
|
||||
});
|
||||
});
|
||||
describe(".setToCurrentTime()", function() {
|
||||
it("should set the gauge to current unix time in seconds", function() {
|
||||
gauge.setToCurrentTime();
|
||||
assert(
|
||||
Math.abs(gauge._values.get('') - Date.now() / 1000) < 1,
|
||||
"value set is not close to the current unix time"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("class ValueCollector", function() {
|
||||
describe("constructor", function() {
|
||||
it("should throw on invalid metric name", function() {
|
||||
for (let name of ["", " ", "0a", "$a"]) {
|
||||
assert.throws(
|
||||
() => new prometheus.Gauge(
|
||||
name, "Help", { register: false }
|
||||
),
|
||||
new Error(`Invalid name '${name}'`)
|
||||
)
|
||||
}
|
||||
});
|
||||
it("should throw on invalid label names", function() {
|
||||
for (let name of ["", " ", "0a", "$a", "a:"]) {
|
||||
assert.throws(
|
||||
() => new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: [name] }
|
||||
),
|
||||
new Error(`Invalid label '${name}'`)
|
||||
)
|
||||
}
|
||||
});
|
||||
it("should throw on missing name", function() {
|
||||
assert.throws(
|
||||
() => new prometheus.Gauge(),
|
||||
new Error("Expected name to be a string")
|
||||
);
|
||||
});
|
||||
it("should throw on missing help", function() {
|
||||
assert.throws(
|
||||
() => new prometheus.Gauge("test"),
|
||||
new Error("Expected help to be a string")
|
||||
);
|
||||
});
|
||||
it("should throw on unrecoginzed option", function() {
|
||||
assert.throws(
|
||||
() => new prometheus.Gauge("test", "help", { invalid: 1 }),
|
||||
new Error("Unrecognized option 'invalid'")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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() {
|
||||
gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b'] }
|
||||
);
|
||||
});
|
||||
it("should require all labels set at creation", function() {
|
||||
assert.throws(
|
||||
() => gauge.labels({a: "1"}),
|
||||
new Error("Missing label 'b'")
|
||||
);
|
||||
assert.throws(
|
||||
() => gauge.labels({b: "1"}),
|
||||
new Error("Missing label 'a'")
|
||||
);
|
||||
assert.throws(
|
||||
() => gauge.labels({a: '1', b: '2', c: '3'}),
|
||||
new Error("Extra label 'c'")
|
||||
);
|
||||
assert.throws(
|
||||
() => gauge.labels('1'),
|
||||
new Error("Missing label 'b'")
|
||||
);
|
||||
assert.throws(
|
||||
() => gauge.labels('1', '2', '3'),
|
||||
new Error("Extra positional label")
|
||||
);
|
||||
});
|
||||
it("should throw if label value is not a string", function() {
|
||||
assert.throws(
|
||||
() => gauge.labels({a: "1", b: 2}),
|
||||
new Error("Expected value for label 'b' to be a string")
|
||||
);
|
||||
assert.throws(
|
||||
() => gauge.labels("1", 2),
|
||||
new Error("Expected value for label 'b' to be a string")
|
||||
);
|
||||
});
|
||||
it("should initialize a value for the label set", function() {
|
||||
gauge.labels({ a: '1', b: '2'});
|
||||
assert(gauge._values.has('a="1",b="2"'), "value was not set");
|
||||
gauge.labels('4', '5');
|
||||
assert(gauge._values.has('a="4",b="5"'), "value was not set");
|
||||
});
|
||||
it("should return a child supporting the usual methods", function() {
|
||||
let child = gauge.labels({ a: '1', b: '2'});
|
||||
child.get();
|
||||
child.inc();
|
||||
child.dec();
|
||||
child.setToCurrentTime();
|
||||
child = gauge.labels('7', '8');
|
||||
child.get();
|
||||
child.inc();
|
||||
child.dec();
|
||||
child.setToCurrentTime();
|
||||
});
|
||||
it("should return the same child for the same labels", function() {
|
||||
let child = gauge.labels('1', '2');
|
||||
assert.equal(gauge.labels('1', '2'), child);
|
||||
});
|
||||
});
|
||||
describe(".remove()", function() {
|
||||
it("should remove a label set created with .label()", function() {
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b'] }
|
||||
);
|
||||
gauge.labels({ a: '1', b: '2'});
|
||||
gauge.remove({ a: '1', b: '2'});
|
||||
assert(!gauge._values.has('a="1",b="2"'), "value was not removed");
|
||||
});
|
||||
it("should throw when removing the empty label set", function() {
|
||||
let gauge = new prometheus.Gauge('test', "Help", { register: false });
|
||||
assert.throws(
|
||||
() => gauge.remove({}), new Error("labels cannot be empty")
|
||||
);
|
||||
});
|
||||
});
|
||||
describe(".removeAll()", function() {
|
||||
it("should remove a label set created with .label()", function() {
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b'] }
|
||||
);
|
||||
gauge.labels('1', '2');
|
||||
gauge.labels('1', '3');
|
||||
gauge.labels('4', '6');
|
||||
gauge.removeAll({a: '1'});
|
||||
assert(gauge._values.size <= 1, "Matching labels remained")
|
||||
assert(
|
||||
gauge._values.has('a="4",b="6"'),
|
||||
"Non-matching label was removed"
|
||||
);
|
||||
});
|
||||
it("should throw when removing the empty label set", function() {
|
||||
let gauge = new prometheus.Gauge('test', "Help", { register: false });
|
||||
assert.throws(
|
||||
() => gauge.removeAll({}), new Error("labels cannot be empty")
|
||||
);
|
||||
});
|
||||
it("should handle special characters", function() {
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b'] }
|
||||
)
|
||||
gauge.labels('x\\\\\n\"\"\n\\', '2');
|
||||
gauge.removeAll({a: 'x\\\\\n\"\"\n\\'});
|
||||
assert(gauge._values.size === 0, "Matching labels remained")
|
||||
});
|
||||
});
|
||||
describe(".clear()", function() {
|
||||
it("should remove all label sets created with .label()", function() {
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b'] }
|
||||
);
|
||||
gauge.labels({ a: '1', b: '2'});
|
||||
gauge.labels({ a: '3', b: '4'});
|
||||
gauge.labels({ a: '5', b: '6'});
|
||||
gauge.clear();
|
||||
assert(gauge._values.size == 0, "values was not cleared");
|
||||
});
|
||||
it("should throw when used on an unlabled metric", function() {
|
||||
let gauge = new prometheus.Gauge('test', "Help", { register: false });
|
||||
assert.throws(
|
||||
() => gauge.clear(), new Error("Cannot clear unlabeled metric")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultCollectors", function() {
|
||||
describe("processStartTimeSeconds", function() {
|
||||
it("should give a close point in time", async function() {
|
||||
let results = []
|
||||
let collector = prometheus.defaultCollectors.processStartTimeSeconds;
|
||||
for await (let result of collector.collect()) {
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
assert(
|
||||
results.length == 1,
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
assert(
|
||||
Math.abs(results[0].samples.get('') - Date.now() / 1000) < 1,
|
||||
"value set is not close to the current unix time"
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("processCpuSecondsTotal", function() {
|
||||
it("should give a low value", async function() {
|
||||
let results = []
|
||||
let collector = prometheus.defaultCollectors.processCpuSecondsTotal;
|
||||
for await (let result of collector.collect()) {
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
assert(
|
||||
results.length == 1,
|
||||
"collector did not give exactly one result"
|
||||
);
|
||||
|
||||
let value = results[0].samples.get('')
|
||||
assert(
|
||||
value < 5 && value > 0,
|
||||
`process time is not between 0 and 5 seconds (${value})`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("exposition()", function() {
|
||||
it("should format simple metrics", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let counter = new prometheus.Counter('a', "Help", { register: false });
|
||||
registry.register(counter);
|
||||
let gauge = new prometheus.Gauge('b', "Help", { register: false });
|
||||
registry.register(gauge);
|
||||
let exposition = await prometheus.exposition(registry.collect());
|
||||
assert.equal(
|
||||
exposition,
|
||||
'# HELP a Help\n' +
|
||||
'# TYPE a counter\n' +
|
||||
'a 0\n' +
|
||||
'\n' +
|
||||
'# HELP b Help\n' +
|
||||
'# TYPE b gauge\n' +
|
||||
'b 0\n'
|
||||
);
|
||||
});
|
||||
it("should format labeled metrics", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['a', 'b']}
|
||||
);
|
||||
gauge.labels({a: '1', b: '2'});
|
||||
gauge.labels({b: 'y', a: 'x'});
|
||||
registry.register(gauge);
|
||||
let exposition = await prometheus.exposition(registry.collect());
|
||||
assert.equal(
|
||||
exposition,
|
||||
'# HELP test Help\n' +
|
||||
'# TYPE test gauge\n' +
|
||||
'test{a="1",b="2"} 0\n' +
|
||||
'test{a="x",b="y"} 0\n'
|
||||
);
|
||||
});
|
||||
it("should format Infinity and NaN", async function() {
|
||||
let registry = new prometheus.CollectorRegistry();
|
||||
let gauge = new prometheus.Gauge(
|
||||
'test', "Help", { register: false, labels: ['n']}
|
||||
);
|
||||
gauge.labels({n: '1'}).set(Infinity);
|
||||
gauge.labels({n: '2'}).set(-Infinity);
|
||||
gauge.labels({n: '3'}).set(NaN);
|
||||
registry.register(gauge);
|
||||
let exposition = await prometheus.exposition(registry.collect());
|
||||
assert.equal(
|
||||
exposition,
|
||||
'# HELP test Help\n' +
|
||||
'# TYPE test gauge\n' +
|
||||
'test{n="1"} +Inf\n' +
|
||||
'test{n="2"} -Inf\n' +
|
||||
'test{n="3"} NaN\n'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
let serializedResult = {
|
||||
metric: { type: 'type', name: 'name', help: "Help", labels: ['a', 'b'] },
|
||||
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]]),
|
||||
};
|
||||
describe("serializeResult()", function() {
|
||||
it("should convert a result into plain object", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult(deserializedResult),
|
||||
serializedResult
|
||||
)
|
||||
});
|
||||
it("should support adding labels", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric('type', 'name', "Help"),
|
||||
samples: new Map([['', 2]]),
|
||||
}, { addLabels: { c: '8' }}),
|
||||
{
|
||||
metric: {type: 'type', name: 'name', help: "Help", labels: ['c']},
|
||||
samples: [['c="8"', 2]],
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric('type', 'name', "Help", ['a']),
|
||||
samples: new Map([['a="4"', 3]]),
|
||||
}, { addLabels: { c: '8' }}),
|
||||
{
|
||||
metric: {type: 'type', name: 'name', help: "Help", labels: ['a', 'c']},
|
||||
samples: [['a="4",c="8"', 3]],
|
||||
}
|
||||
);
|
||||
});
|
||||
it("should support redefining name and help", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric('type', 'name', "Help"),
|
||||
samples: new Map([['', 2]]),
|
||||
}, { metricName: 'new', }),
|
||||
{
|
||||
metric: {type: 'type', name: 'new', help: "Help", labels: []},
|
||||
samples: [['', 2]],
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
prometheus.serializeResult({
|
||||
metric: new prometheus.Metric('type', 'name', "Help"),
|
||||
samples: new Map([['', 2]]),
|
||||
}, { metricHelp: "old", }),
|
||||
{
|
||||
metric: {type: 'type', name: 'name', help: "old", labels: []},
|
||||
samples: [['', 2]],
|
||||
}
|
||||
);
|
||||
});
|
||||
it("should throw on unrecoginzed option", function() {
|
||||
assert.throws(
|
||||
() => new prometheus.serializeResult({
|
||||
metric: new prometheus.Metric('type', 'name', "Help"),
|
||||
samples: new Map([['', 3]]),
|
||||
}, { invalid: true }),
|
||||
new Error("Unrecognized option 'invalid'")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserializeResult()", function() {
|
||||
it("should convert a plain object into a result", function() {
|
||||
assert.deepEqual(
|
||||
prometheus.deserializeResult(serializedResult),
|
||||
deserializedResult
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
describe("post test checks", function() {
|
||||
it("should not have any collectors in the default registry", function() {
|
||||
assert(
|
||||
prometheus.defaultRegistry.collectors.length == 0,
|
||||
"Collectors left over by test code"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user