hot-shots
Version:
Node.js client for StatsD, DogStatsD, and Telegraf
218 lines (195 loc) • 6.44 kB
JavaScript
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
};