@talks.converse/js-monitoring
Version:
Express monitoring middleware with Datadog and Prometheus backends, plus conditional logging.
122 lines (108 loc) • 4.85 kB
JavaScript
// Shared helpers for monitoring backends (Datadog & Prometheus)
/**
* Default endpoint regex patterns to exclude from monitoring/logging.
* Typically used to exclude health check and metrics endpoints.
* @type {RegExp[]}
*/
const DEFAULT_EXCLUDES = [/^\/healthz$/, /^\/metrics$/];
/**
* Sanitizes a metric name into a Prometheus-safe format.
* Replaces dots and non-alphanumeric characters with underscores,
* collapses multiple underscores, and trims leading/trailing underscores.
* @param {string} name - The original metric name.
* @returns {string} The normalized metric name.
*/
const normalizeName = (name) =>
String(name)
.replace(/\./g, '_')
.replace(/[^a-zA-Z0-9_]/g, '_')
.replace(/__+/g, '_')
.replace(/^_+|_+$/g, '');
/**
* Parses a comma-separated string into an array of RegExp objects.
* Supports inline flags if the token is enclosed with slashes (e.g. /pattern/flags).
* If no flags are specified, the regex is compiled without flags.
* Invalid regex tokens are skipped with optional debug warnings.
* @param {string} raw - Comma-separated regex patterns as a string.
* @returns {RegExp[]} Array of compiled RegExp objects.
*/
const compileRegexList = (raw) => {
const debug = process.env.MONITORING_DEBUG === 'true';
return (raw || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.map((token) => {
try {
if (token.startsWith('/') && token.lastIndexOf('/') > 0) {
const last = token.lastIndexOf('/');
const pattern = token.slice(1, last);
const flags = token.slice(last + 1);
return new RegExp(pattern, flags);
}
return new RegExp(token);
} catch (e) {
if (debug) console.warn('[monitoring] Invalid regex token skipped:', token, e.message);
return null;
}
})
.filter(Boolean);
};
/**
* Determines whether an endpoint should be tagged with its real path based on include/exclude filters.
* If both include and exclude lists are empty, all endpoints are included.
* If only include is specified, only endpoints matching include patterns are processed.
* If only exclude is specified, all except excluded endpoints are processed.
* If both are specified, endpoints included and not excluded are processed.
* @param {string} endpoint - The endpoint path to check.
* @param {Object} [cfg={}] - Configuration object with optional include and exclude arrays of RegExp.
* @param {RegExp[]} [cfg.include] - Regex patterns to include.
* @param {RegExp[]} [cfg.exclude] - Regex patterns to exclude.
* @returns {boolean} True if the endpoint should be processed/tagged; otherwise false.
*/
const isEndpointProcessed = (endpoint, cfg = {}) => {
const inc = Array.isArray(cfg.include) ? cfg.include : [];
const exc = Array.isArray(cfg.exclude) ? cfg.exclude : [];
const inInclude = inc.length > 0 && matchesAnyRegex(endpoint, inc);
const inExclude = exc.length > 0 && matchesAnyRegex(endpoint, exc);
// Include | Exclude | Result
// not provided | not provided -> include all
if (inc.length === 0 && exc.length === 0) return true;
// provided | not provided -> only included
if (inc.length > 0 && exc.length === 0) return inInclude;
// not provided | provided -> all except excluded
if (inc.length === 0 && exc.length > 0) return !inExclude;
// provided | provided -> (include - exclude)
return inInclude && !inExclude;
};
/**
* Checks whether a given text matches any regex pattern in the provided list.
* @param {string} text - The text to test.
* @param {RegExp[]} list - Array of regex patterns.
* @returns {boolean} True if any regex matches the text; otherwise false.
*/
const matchesAnyRegex = (text, list) => list.some((re) => re.test(text));
/**
* Resolves the endpoint path from an Express request object.
* Prefers the route template path if available; otherwise falls back to the raw request path.
* @param {Object} req - Express request object.
* @param {Object} [req.route] - Express route object, if available.
* @param {string} req.path - Raw request path.
* @returns {string} The resolved endpoint path.
*/
const getEndpointPath = (req) => (req.route ? req.route.path : req.path);
/**
* Converts an HTTP status code to its group string (e.g., 200 -> "2xx").
* @param {number|string} statusCode - The HTTP status code.
* @returns {string} The status group string.
*/
const getStatusGroup = (statusCode) => `${Math.floor(Number(statusCode) / 100)}xx`;
module.exports = {
normalizeName,
compileRegexList,
isEndpointProcessed,
matchesAnyRegex,
getEndpointPath,
getStatusGroup,
DEFAULT_EXCLUDES
};