@talks.converse/js-monitoring
Version:
Express monitoring middleware with Datadog and Prometheus backends, plus conditional logging.
203 lines (182 loc) • 6.82 kB
JavaScript
const client = require('prom-client');
const helper = require('./helper');
const prometheusEndpointsInclude = process.env.PROMETHEUS_ENDPOINTS_INCLUDE;
const prometheusEndpointExclude = process.env.PROMETHEUS_ENDPOINTS_EXCLUDE;
// ---- Registry & default metrics ----
const register = new client.Registry();
const prefix = helper.normalizeName(process.env.APP_NAME) + '_';
// ---- Dynamic metric caches (safe, bounded tags only) ----
const counters = new Map();
const gauges = new Map();
const histograms = new Map();
client.collectDefaultMetrics({ prefix, register });
/**
* Main delegate to record metrics depending on environment variable.
* Records either a histogram or a combination of counter and gauge.
* @param {string} app_name - The application or metric base name.
* @param {number} latencyMs - Latency in milliseconds.
* @param {Array<string>|Object} tags - Tags or label object associated with the metric.
*/
const monitor = (app_name, latencyMs, tags) => {
if (process.env.MONITORING_METRIC_TYPE === 'histogram'){
const latencyName = asSecondsName(`${app_name}_metrics`);
recordHistogram(latencyName, latencyMs, tags);
} else {
const counterName = asCounterName(`${app_name}_count`);
const latencyName = asSecondsName(`${app_name}_latency`);
recordCounter(counterName, 1, tags);
recordGauge(latencyName, latencyMs / 1000, tags);
}
};
/**
* Registers an Express route to expose Prometheus metrics at the given path.
* @param {object} app - Express app instance.
* @param {string} [path='/metrics'] - Route path to serve metrics.
*/
const registerMetricsRoute = (app, path = '/metrics') => {
app.get(path, async (_req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (err) {
res.status(500).end(`# metrics error: ${String(err)}`);
}
});
};
/**
* No-op close function since Prometheus scrapes metrics; no connection to close.
*/
const closeConnection = () => {
// nothing to close; Prometheus scrapes us
};
// ---- Helpers: tags → tags, name normalization ----
/**
* Ensures a metric name follows Prometheus convention for counters ending with '_total'.
* @param {string} name - Metric name.
* @returns {string} Normalized counter metric name.
*/
const asCounterName = (name) =>
name.endsWith('_total') ? name : `${name}_total`;
/**
* Ensures a metric name follows Prometheus convention for histograms/gauges ending with '_seconds'.
* @param {string} name - Metric name.
* @returns {string} Normalized seconds metric name.
*/
const asSecondsName = (name) => {
return name.endsWith('_seconds') ? name : `${name}_seconds`;
};
/**
* Builds include and exclude regex configurations from environment variables
* for filtering Prometheus monitoring endpoints.
* @returns {object} Object with 'include' and 'exclude' regex lists.
*/
const buildPrometheusMonitoringEndpointsConfig = () => ({
include: helper.compileRegexList(prometheusEndpointsInclude),
exclude: helper.compileRegexList(prometheusEndpointExclude),
});
// ---- Metric recorders ----
/**
* Increments a counter metric by the given value with associated tags.
* @param {string} metric - Metric name.
* @param {number} value - Increment value (default 1).
* @param {Array|Object} tags - Tags or label object.
*/
const recordCounter = (metric, value = 1, tags = []) => {
const counter = getOrCreateCounter(metric, Object.keys(tags));
counter.inc(tags, Number(value) || 1);
};
/**
* Retrieves a cached counter metric or creates and caches a new one if not present.
* @param {string} name - Metric name.
* @param {Array<string>} labelNames - List of label names.
* @returns {client.Counter} Prometheus Counter metric instance.
*/
const getOrCreateCounter = (name, labelNames) => {
const key = `${name}|${labelNames.sort().join(',')}`;
if (!counters.has(key)) {
counters.set(
key,
new client.Counter({
name,
help: `Counter for ${name}`,
labelNames,
registers: [register],
})
);
}
return counters.get(key);
};
/**
* Sets a gauge metric to the given value with associated tags.
* @param {string} metric - Metric name.
* @param {number} value - Value to set.
* @param {Array|Object} tags - Tags or label object.
*/
const recordGauge = (metric, value, tags = []) => {
const gauge = getOrCreateGauge(metric, Object.keys(tags));
gauge.set(tags, Number(value) || 0);
};
/**
* Retrieves a cached gauge metric or creates and caches a new one if not present.
* @param {string} name - Metric name.
* @param {Array<string>} labelNames - List of label names.
* @returns {client.Gauge} Prometheus Gauge metric instance.
*/
const getOrCreateGauge = (name, labelNames) => {
const key = `${name}|${labelNames.sort().join(',')}`;
if (!gauges.has(key)) {
gauges.set(
key,
new client.Gauge({
name,
help: `Gauge for ${name}`,
labelNames,
registers: [register],
})
);
}
return gauges.get(key);
};
/**
* Records an observation value (in milliseconds) into a histogram metric (stored in seconds).
* @param {string} metric - Metric name.
* @param {number} value - Numeric value in milliseconds.
* @param {Array|Object} tags - Tags or label object.
*/
const recordHistogram = (metric, value, tags = []) => {
const hist = getOrCreateHistogram(metric, Object.keys(tags));
hist.observe(tags, Number(value));
};
/**
* Retrieves a cached histogram metric or creates and caches a new one if not present.
* @param {string} name - Metric name.
* @param {Array<string>} labelNames - List of label names.
* @returns {client.Histogram} Prometheus Histogram metric instance.
*/
const getOrCreateHistogram = (name, labelNames) => {
const key = `${name}|${labelNames.sort().join(',')}`;
if (!histograms.has(key)) {
histograms.set(
key,
new client.Histogram({
name,
help: `Histogram for ${name}`,
labelNames,
// default buckets; override by defining a dedicated metric above if needed
buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [register],
})
);
}
return histograms.get(key);
};
module.exports = {
monitor,
recordCounter,
recordGauge,
recordHistogram,
closeConnection,
registerMetricsRoute,
buildPrometheusMonitoringEndpointsConfig,
register, // exported in case you want to plug into other collectors
};