UNPKG

hot-shots

Version:

Node.js client for StatsD, DogStatsD, and Telegraf

218 lines (195 loc) 6.44 kB
const fs = require('fs'); /** * Replace any characters that can't be sent on with an underscore. * Used for tag keys where colons are not allowed (colon separates key from value). */ function sanitizeTags(value, telegraf) { // Characters that break the protocol in tag keys: // : - separates tag key from value (not allowed in keys) // | - separates metric components // , - separates tags // @ - used for sample rate (StatsD only) // # - tag prefix character (DogStatsD only) // \n - breaks the line protocol const blocklist = telegraf ? /:|\||,|\n/g : /:|\||@|,|#|\n/g; // Replace reserved chars with underscores. let sanitized = String(value).replace(blocklist, '_'); // For telegraf, replace trailing backslashes as they break the line protocol // by escaping the delimiter that comes after the tag value if (telegraf && sanitized.endsWith('\\')) { sanitized = sanitized.slice(0, -1) + '_'; } return sanitized; } /** * Replace any characters that can't be sent on with an underscore. * Used for tag values where colons ARE allowed (e.g., URLs). */ function sanitizeTagValue(value, telegraf) { // Characters that break the protocol in tag values: // | - separates metric components // , - separates tags // @ - used for sample rate (StatsD only) // # - tag prefix character (DogStatsD only) // \n - breaks the line protocol // Note: colons ARE allowed in tag values const blocklist = telegraf ? /\||,|\n/g : /\||@|,|#|\n/g; // Replace reserved chars with underscores. let sanitized = String(value).replace(blocklist, '_'); // For telegraf, replace trailing backslashes as they break the line protocol // by escaping the delimiter that comes after the tag value if (telegraf && sanitized.endsWith('\\')) { sanitized = sanitized.slice(0, -1) + '_'; } return sanitized; } /** * Replace any characters in metric names that can't be sent on with an underscore */ function sanitizeMetricName(value) { // Characters that break the protocol in metric names: // : - separates metric name from value // | - separates metric components // \n - breaks the line protocol const blocklist = /:|\||\n/g; return String(value).replace(blocklist, '_'); } /** * Format tags properly before sending on */ function formatTags(tags, telegraf) { if (Array.isArray(tags)) { // Sanitize each tag in the array return tags.map(tag => { // If tag contains a colon (not at position 0), sanitize key and value separately const colonIndex = typeof tag === 'string' ? tag.indexOf(':') : -1; if (colonIndex > 0) { const key = tag.substring(0, colonIndex); const value = tag.substring(colonIndex + 1); return `${sanitizeTags(key, telegraf)}:${sanitizeTagValue(value, telegraf)}`; } // For tags without colons (or colon at start), sanitize as a key (most restrictive) return sanitizeTags(tag, telegraf); }); } else { return Object.keys(tags).map(key => { return `${sanitizeTags(key, telegraf)}:${sanitizeTagValue(tags[key], telegraf)}`; }); } } /** * Overrides tags in parent with tags from child with the same name (case sensitive) and return the result as new * array. parent and child are not mutated. */ function overrideTags (parent, child, telegraf) { if (! child) { return parent; } const formattedChild = formatTags(child, telegraf); const childCopy = new Map(); const toAppend = []; formattedChild.forEach(tag => { const idx = typeof tag === 'string' ? tag.indexOf(':') : -1; if (idx < 1) { toAppend.push(tag); } else { const key = tag.substring(0, idx); const value = tag.substring(idx + 1); if (!childCopy.has(key)) { childCopy.set(key, []); } childCopy.get(key).push(value); } }); const result = parent.filter(tag => { const idx = typeof tag === 'string' ? tag.indexOf(':') : -1; if (idx < 1) { return true; } const key = tag.substring(0, idx); return !childCopy.has(key); }); for (const [key, values] of childCopy) { for (const value of values) { result.push(`${key}:${value}`); } } result.push(...toAppend); return result; } /** * Formats a date for use with DataDog */ function formatDate(date) { let timestamp; if (date instanceof Date) { // Datadog expects seconds. timestamp = Math.round(date.getTime() / 1000); } else if (date instanceof Number || typeof date === 'number') { // Make sure it is an integer, not a float. timestamp = Math.round(date); } return timestamp; } /** * Converts int to a string IP */ function intToIP(int) { const part1 = int & 255; const part2 = ((int >> 8) & 255); const part3 = ((int >> 16) & 255); const part4 = ((int >> 24) & 255); return `${part4}.${part3}.${part2}.${part1}`; } /** * Returns the system default interface on Linux */ function getDefaultRoute() { try { const fileContents = fs.readFileSync('/proc/net/route', 'utf8'); // eslint-disable-line no-sync const routes = fileContents.split('\n'); for (const routeIdx in routes) { const fields = routes[routeIdx].trim().split('\t'); if (fields[1] === '00000000') { const address = fields[2]; // Convert to little endian by splitting every 2 digits and reversing that list const littleEndianAddress = address.match(/.{2}/g).reverse().join(''); return intToIP(parseInt(littleEndianAddress, 16)); } } } catch (e) { console.error('Could not get default route from /proc/net/route'); } return null; } /** * Normalize prefix to ensure it ends with a period separator if non-empty */ function normalizePrefix(prefix) { if (prefix && !prefix.endsWith('.')) { return prefix + '.'; } return prefix || ''; } /** * Normalize suffix to ensure it starts with a period separator if non-empty */ function normalizeSuffix(suffix) { if (suffix && !suffix.startsWith('.')) { return '.' + suffix; } return suffix || ''; } module.exports = { formatTags: formatTags, overrideTags: overrideTags, formatDate: formatDate, getDefaultRoute: getDefaultRoute, sanitizeTags: sanitizeTags, sanitizeTagValue: sanitizeTagValue, sanitizeMetricName: sanitizeMetricName, normalizePrefix: normalizePrefix, normalizeSuffix: normalizeSuffix, // Expose intToIP for testing purposes intToIP: intToIP };