UNPKG

@talks.converse/js-monitoring

Version:

Express monitoring middleware with Datadog and Prometheus backends, plus conditional logging.

122 lines (108 loc) 4.85 kB
// 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 };