mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
Split up codebase into the packages @clsterio/lib, @clusterio/master, @clusterio/slave, and @clusterio/ctl using lerna to manage them. This will allow deployment to use only the parts needed reducing install size, and better separate the code into shared library, master, slave and control.
455 lines
9.8 KiB
JavaScript
455 lines
9.8 KiB
JavaScript
"use strict";
|
|
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]) => (
|
|
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",
|
|
"Total user and system CPU time spent in seconds.",
|
|
{
|
|
callback: async function() {
|
|
let usage = process.cpuUsage();
|
|
this.set((usage.user + usage.system) / 1e6);
|
|
},
|
|
},
|
|
);
|
|
|
|
defaultCollectors.processResidentMemoryBytes = new Gauge(
|
|
"process_resident_memory_bytes",
|
|
"Resident memory size in bytes.",
|
|
{
|
|
callback: function() {
|
|
this.set(process.memoryUsage().rss);
|
|
},
|
|
},
|
|
);
|
|
|
|
defaultCollectors.processHeapBytes = new Gauge(
|
|
"process_heap_bytes",
|
|
"Process heap size in bytes.",
|
|
{
|
|
callback: function() {
|
|
this.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,
|
|
CollectorRegistry,
|
|
Collector,
|
|
ValueCollector,
|
|
Metric,
|
|
|
|
exposition,
|
|
defaultRegistry,
|
|
defaultCollectors,
|
|
serializeResult,
|
|
deserializeResult,
|
|
};
|