UNPKG

@metrics/guard

Version:

Module to guard against excessive metric permutation creation in a metric stream

166 lines (137 loc) 4.09 kB
"use strict"; /* eslint prefer-rest-params: "off" */ const stream = require("readable-stream"); const crypto = require("crypto"); const SEPARATOR = "~"; const push = Symbol("push"); const MetricsGuard = class MetricsGuard extends stream.Transform { constructor({ permutationThreshold = 1000, metricsThreshold = 60, enabled = true, id } = {}) { super( Object.assign( { objectMode: true, }, ...arguments, ), ); if (typeof enabled !== "boolean") throw new Error('Provided value to argument "enabled" must be a Boolean'); if (!Number.isFinite(permutationThreshold)) throw new Error('Provided value to argument "permutationThreshold" must be a Number'); if (!Number.isFinite(metricsThreshold)) throw new Error('Provided value to argument "metricsThreshold" must be a Number'); Object.defineProperty(this, "id", { value: id || crypto.randomBytes(3 * 4).toString("base64"), enumerable: true, }); Object.defineProperty(this, "metricsThreshold", { value: metricsThreshold, }); Object.defineProperty(this, "permutationWarnThreshold", { value: Math.floor((permutationThreshold / 100) * 80), }); Object.defineProperty(this, "permutationThreshold", { value: permutationThreshold, }); Object.defineProperty(this, "enabled", { value: enabled, }); Object.defineProperty(this, "registry", { value: new Map(), }); // Avoid hitting the max listeners limit when multiple // streams is piped into the same stream. this.on("pipe", () => { this.setMaxListeners(this.getMaxListeners() + 1); }); this.on("unpipe", () => { this.setMaxListeners(this.getMaxListeners() - 1); }); } get [Symbol.toStringTag]() { return "MetricsGuard"; } [push](metric) { const met = metric; met.source = this.id; if (this._readableState.flowing) { this.push(met); return; } this.emit("drop", met); } getMetrics() { return Array.from(this.registry.keys()); } getLabels(metric) { const labels = this.registry.get(metric); if (labels) { return Array.from(labels.keys()).map((label) => { const [name, value] = label.split(SEPARATOR); if (!name) { this.emit("warn", "metrics", `is null ${name} ${value}`); } return { name, value }; }); } return []; } reset() { this.registry.clear(); } _transform(metric, enc, next) { // guarding is disabeled, pass metrich through if (!this.enabled) { next(null, metric); return; } // number of metrics registered exceeds the threshold // emit warning if (this.registry.size >= this.metricsThreshold) { this.emit("warn", "metrics", this.registry.size); } let entry = this.registry.get(metric.name); if (entry) { // too many label permutations is registered on // the metric so drop the metric if (entry.size >= this.permutationThreshold) { this.emit("drop", metric); next(null); return; } // getting close to the threshold of too many label // permutations registered on the metric so warn if (entry.size >= this.permutationWarnThreshold) { this.emit("warn", "permutation", metric.name); } } else { // metric was not in registry, greate a new entry // in the registry entry = new Set(); this.registry.set(metric.name, entry); } let invalidLabels = []; // push all label permutations on the metric into the // Set for the metric in the registry. the size of // this Set is compared with the threshold metric.labels.forEach((label) => { if (label && (typeof label.name === "undefined" || label.name === null)) { invalidLabels.push(`${metric.name} label name="${label.name}", value=${label.value}`); } else { entry.add(`${label.name}${SEPARATOR}${label.value}`); } }); if (metric.source === this.id) { this.emit("drop", metric); next(null); return; } if (invalidLabels.length > 0) { // Omit a warning when having invalid label data this.emit("warn", "labels", invalidLabels.toString()); next(null); return; } next(null, metric); } }; module.exports = MetricsGuard;