moleculer
Version:
Fast & powerful microservices framework for Node.JS
229 lines (197 loc) • 5.02 kB
JavaScript
/*
* moleculer
* Copyright (c) 2023 MoleculerJS (https://github.com/moleculerjs/moleculer)
* MIT Licensed
*/
;
const BaseReporter = require("./base");
const _ = require("lodash");
const dgram = require("dgram");
const METRIC = require("../constants");
/**
* Import types
*
* @typedef {import("../registry")} MetricRegistry
* @typedef {import("./statsd").StatsDReporterOptions} StatsDReporterOptions
* @typedef {import("./statsd")} StatsDReporterClass
* @typedef {import("../types/base").BaseMetricPOJO} BaseMetricPOJO
* @typedef {import("../types/base")} BaseMetric
*/
/**
* UDP (StatsD) reporter for Moleculer.
*
* @class DatadogReporter
* @extends {BaseReporter}
* @implements {StatsDReporterClass}
*/
class StatsDReporter extends BaseReporter {
/**
* Constructor of StatsDReporters
*
* @param {StatsDReporterOptions} opts
* @memberof StatsDReporter
*/
constructor(opts) {
super(opts);
/** @type {StatsDReporterOptions} */
this.opts = _.defaultsDeep(this.opts, {
host: "localhost",
port: 8125,
types: null,
maxPayloadSize: 1300
});
}
/**
* Initialize reporter.
*
* @param {MetricRegistry} registry
* @memberof StatsDReporter
*/
init(registry) {
super.init(registry);
this.flush();
}
/**
* Flush metric data
*
* @memberof StatsDReporter
*/
flush() {
const series = this.generateStatsDSeries();
if (series.length === 0) return;
this.sendChunks(series);
}
/**
* Create & send chunks.
*
* @param {Array<Object>} series
*/
sendChunks(series) {
let len = 0;
const chunks = [];
while (series.length > 0 && (!this.opts.maxPayloadSize || len < this.opts.maxPayloadSize)) {
const item = series.shift();
chunks.push(item);
len += item.length;
}
if (chunks.length > 0) {
this.send(Buffer.from(chunks.join("\n")));
}
if (series.length > 0) {
setTimeout(() => this.sendChunks(series), 100);
}
}
/**
* Send concatenated data to StatsD server via UDP
*
* @param {Buffer} buf
*/
send(buf) {
//this.logger.info("Buffer\n" + buf.toString());
const sock = dgram.createSocket("udp4");
sock.send(buf, 0, buf.length, this.opts.port, this.opts.host, (err, bytes) => {
if (err) {
this.logger.warn(
"Unable to send metrics to StatsD server. Error:" + err.message,
err
);
} else {
this.logger.debug("Metrics are uploaded to StatsD. Sent bytes:", bytes);
}
sock.close();
});
}
/**
* Generate metric data.
*
* @returns {Array<Object>}
* @memberof StatsDReporter
*/
generateStatsDSeries() {
const series = [];
const list = this.registry.list({
types: this.opts.types,
includes: this.opts.includes,
excludes: this.opts.excludes
});
list.forEach(metric => {
metric.values.forEach(item => {
const line = this.generateStatDLine(metric, item);
if (line) series.push(line);
});
});
return series;
}
generateStatDLine(metric, item, lastValue) {
const metricName = this.formatMetricName(metric.name);
switch (metric.type) {
case METRIC.TYPE_COUNTER: {
let line = `${metricName}:${item.value}|c`;
if (metric.labelNames.length > 0) line += "|#" + this.labelsToTags(item.labels);
return line;
}
case METRIC.TYPE_GAUGE: {
let line = `${metricName}:${item.value}|g`;
if (metric.labelNames.length > 0) line += "|#" + this.labelsToTags(item.labels);
return line;
}
case METRIC.TYPE_INFO: {
let line = `${metricName}:${
typeof item.value == "number" ? item.value : '"' + item.value + '"'
}|s`;
if (metric.labelNames.length > 0) line += "|#" + this.labelsToTags(item.labels);
return line;
}
case METRIC.TYPE_HISTOGRAM: {
if (lastValue != null) {
let line = `${metricName}:${lastValue}|ms`;
if (metric.labelNames.length > 0) line += "|#" + this.labelsToTags(item.labels);
return line;
}
}
}
}
/**
* Some metric has been changed.
*
* @param {BaseMetric} metric
* @param {any} value
* @param {Object} labels
*
* @memberof BaseReporter
*/
metricChanged(metric, value, labels) {
if (!this.matchMetricName(metric.name)) return;
const line = this.generateStatDLine(metric, metric.get(labels), value);
if (line) {
this.send(Buffer.from(line));
}
}
/**
* Escape label value characters.
* @param {String} str
* @returns {String}
* @memberof DatadogReporter
*/
escapeLabelValue(str) {
if (typeof str == "string") return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return str;
}
/**
* Convert labels to StatsD label string
*
* @param {Object} itemLabels
* @returns {String}
*
* @memberof StatsDReporter
*/
labelsToTags(itemLabels) {
const labels = Object.assign({}, this.defaultLabels || {}, itemLabels || {});
const keys = Object.keys(labels);
if (keys.length === 0) return "";
return keys
.map(key => `${this.formatLabelName(key)}:${this.escapeLabelValue(labels[key])}`)
.join(",");
}
}
module.exports = StatsDReporter;