UNPKG

hot-shots

Version:

Node.js client for StatsD, DogStatsD, and Telegraf

445 lines (405 loc) 17.3 kB
const helpers = require('./helpers'); const util = require('util'); const debug = util.debuglog('hot-shots'); /** * Separated out of statsd.js for clarity, these are the timing and other stats functions that are what are called the most * when using hot-shots */ function applyStatsFns (Client) { /** * Represents the timing stat * @param stat {String|Array} The stat(s) to send * @param time {Number|Date} The time in milliseconds to send or Date object of which the difference is calculated * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.timing = function (stat, time, sampleRate, tags, callback) { const t = time instanceof Date ? new Date() - time : time; debug('timing: stat=%s, time=%s, sampleRate=%s, tags=%j', stat, t, sampleRate, tags); this.sendAll(stat, t, 'ms', sampleRate, tags, callback); }; /** * Represents the timing stat by recording the duration a function takes to run (in milliseconds) * @param func {Function} The function to run. Receives a context object as the last argument * with an `addTags(tags)` method to add tags dynamically during execution. * @param stat {String|Array} The stat(s) to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.timer = function (func, stat, sampleRate, tags, callback) { const _this = this; return (...args) => { const ctx = createTimerContext(); const start = process.hrtime.bigint(); try { return func(...args, ctx); } finally { const duration = Number(process.hrtime.bigint() - start) / 1e6; const finalTags = mergeTags(tags, ctx.getTags(), _this.telegraf); _this.timing( stat, duration, sampleRate, finalTags, callback ); } }; }; /** * Decorates an async function with timing recording behaviour. * * This version of `timer` will record the time take for the asynchronous action returned by `func` * not just the execution time of `func` itself. * * @param func {<T,A>(...A):Promise<T>} The function to run. Receives a context object as the last * argument with an `addTags(tags)` method to add tags dynamically during execution. * @param stat {String|Array} The stat(s) to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.asyncTimer = function (func, stat, sampleRate, tags, callback) { const self = this; return (...args) => { const ctx = createTimerContext(); const end = hrtimer(); const p = func(...args, ctx); const recordStat = () => { const finalTags = mergeTags(tags, ctx.getTags(), self.telegraf); self.timing(stat, end(), sampleRate, finalTags, callback); }; p.then(recordStat, recordStat); return p; }; }; /** * Decorates an async function with timing recording behaviour, reported as a distribution. * * This version of `timer` will record the time take for the asynchronous action returned by `func` * not just the execution time of `func` itself. * * @param func {<T,A>(...A):Promise<T>} The function to run. Receives a context object as the last * argument with an `addTags(tags)` method to add tags dynamically during execution. * @param stat {String|Array} The stat(s) to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.asyncDistTimer = function (func, stat, sampleRate, tags, callback) { const self = this; return (...args) => { const ctx = createTimerContext(); const end = hrtimer(); const p = func(...args, ctx); const recordStat = () => { const finalTags = mergeTags(tags, ctx.getTags(), self.telegraf); self.distribution(stat, end(), sampleRate, finalTags, callback); }; p.then(recordStat, recordStat); return p; }; }; /** * High-resolution timer */ function hrtimer() { const start = process.hrtime.bigint(); return () => { return Number(process.hrtime.bigint() - start) / 1e6; }; } /** * Creates a timer context object that allows adding tags during function execution */ function createTimerContext() { const dynamicTags = []; return { /** * Add tags to be included when the metric is recorded * @param tags {Object|Array} Tags to add. Can be an object like {key: 'value'} or array like ['key:value'] */ addTags: function (tags) { // Guard against null/undefined to prevent crashes if (!tags) { return; } if (Array.isArray(tags)) { dynamicTags.push(...tags); } else if (typeof tags === 'object') { // Convert object tags to array format Object.keys(tags).forEach(key => { dynamicTags.push(`${key}:${tags[key]}`); }); } }, /** * Get all accumulated tags */ getTags: function () { return dynamicTags; } }; } /** * Merge original tags with dynamically added tags. * Uses helpers.formatTags to ensure consistent tag formatting. */ function mergeTags(originalTags, dynamicTags, telegraf) { if (!dynamicTags || dynamicTags.length === 0) { return originalTags; } if (!originalTags) { return dynamicTags; } // Use the canonical formatTags helper for consistent formatting const tagsArray = helpers.formatTags(originalTags, telegraf); return tagsArray.concat(dynamicTags); } /** * Increments a stat by a specified amount * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.increment = function (stat, value, sampleRate, tags, callback) { // allow use of tags without explicit value or sampleRate if (typeof value !== 'number') { // Shift parameters if: less than 3 args, OR sampleRate looks like a callback // and tags wasn't explicitly provided (issue #139) if (arguments.length < 3 || (typeof sampleRate === 'function' && tags === undefined)) { callback = sampleRate; tags = value; sampleRate = undefined; value = undefined; } } // we explicitly check for undefined and null (and don't do a "! value" check) // so that 0 values are allowed and sent through as-is if (value === undefined || value === null) { value = 1; } debug('hot-shots increment: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, value, 'c', sampleRate, tags, callback); }; /** * Decrements a stat by a specified amount * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.decrement = function (stat, value, sampleRate, tags, callback) { // allow use of tags without explicit value or sampleRate if (typeof value !== 'number') { // Shift parameters if: less than 3 args, OR sampleRate looks like a callback // and tags wasn't explicitly provided (issue #139) if (arguments.length < 3 || (typeof sampleRate === 'function' && tags === undefined)) { callback = sampleRate; tags = value; sampleRate = undefined; value = undefined; } } // we explicitly check for undefined and null (and don't do a "! value" check) // so that 0 values are allowed and sent through as-is if (value === undefined || value === null) { value = 1; } this.sendAll(stat, -value, 'c', sampleRate, tags, callback); }; /** * Represents the histogram stat * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.histogram = function (stat, value, sampleRate, tags, callback) { debug('hot-shots histogram: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, value, 'h', sampleRate, tags, callback); }; /** * Represents the distribution stat * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.distribution = function (stat, value, sampleRate, tags, callback) { debug('hot-shots distribution: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, value, 'd', sampleRate, tags, callback); }; /** * Gauges a stat by a specified amount * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.gauge = function (stat, value, sampleRate, tags, callback) { debug('hot-shots gauge: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, value, 'g', sampleRate, tags, callback); }; /** * Gauges a stat by a delta * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.gaugeDelta = function (stat, value, sampleRate, tags, callback) { const sign = value >= 0 ? '+' : '-'; debug('hot-shots gaugeDelta: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, `${sign}${Math.abs(value)}`, 'g', sampleRate, tags, callback); }; /** * Counts unique values by a specified amount * @param stat {String|Array} The stat(s) to send * @param value The value to send * @param sampleRate {Number=} The Number of times to sample (0 to 1). Optional. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.unique = Client.prototype.set = function (stat, value, sampleRate, tags, callback) { debug('hot-shots unique/set: stat=%s, value=%s, sampleRate=%s, tags=%j', stat, value, sampleRate, tags); this.sendAll(stat, value, 's', sampleRate, tags, callback); }; /** * Send a service check * @param name {String} The name of the service check * @param status {Number=} The status of the service check (0 to 3). * @param options * @option date_happened {Date} Assign a timestamp to the event. Default is now. * @option hostname {String} Assign a hostname to the check. * @option message {String} Assign a message to the check. * @param tags {Array=} The Array of tags to add to the check. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.check = function (name, status, options, tags, callback) { if (this.telegraf) { const err = new Error('Not supported by Telegraf / InfluxDB'); if (callback) { return callback(err); } else if (this.errorHandler) { return this.errorHandler(err); } throw err; } // Track service check in telemetry if (this.telemetry) { this.telemetry.recordServiceCheck(); } const check = ['_sc', this.prefix + name + this.suffix, status], metadata = options || {}; if (metadata.date_happened) { const timestamp = helpers.formatDate(metadata.date_happened); if (timestamp) { check.push(`d:${timestamp}`); } } if (metadata.hostname) { check.push(`h:${metadata.hostname}`); } let mergedTags = this.globalTags; if (tags && typeof(tags) === 'object') { mergedTags = helpers.overrideTags(mergedTags, tags, this.telegraf); } if (mergedTags.length > 0) { check.push(`#${mergedTags.join(',')}`); } // message has to be the last part of a service check if (metadata.message) { check.push(`m:${metadata.message}`); } // allow for tags to be omitted and callback to be used in its place if (typeof tags === 'function' && callback === undefined) { callback = tags; } const message = check.join('|'); // Service checks are unique in that message has to be the last element in // the stat if provided, so we can't append tags like other checks. This // directly calls the `_send` method to avoid appending tags, since we've // already added them. this._send(message, callback); }; /** * Send on an event * @param title {String} The title of the event * @param text {String} The description of the event. Optional- title is used if not given. * @param options * @option date_happened {Date} Assign a timestamp to the event. Default is now. * @option hostname {String} Assign a hostname to the event. * @option aggregation_key {String} Assign an aggregation key to the event, to group it with some others. * @option priority {String} Can be 'normal' or 'low'. Default is 'normal'. * @option source_type_name {String} Assign a source type to the event. * @option alert_type {String} Can be 'error', 'warning', 'info' or 'success'. Default is 'info'. * @param tags {Array=} The Array of tags to add to metrics. Optional. * @param callback {Function=} Callback when message is done being delivered. Optional. */ Client.prototype.event = function (title, text, options, tags, callback) { if (this.telegraf) { const err = new Error('Not supported by Telegraf / InfluxDB'); if (callback) { return callback(err); } else if (this.errorHandler) { return this.errorHandler(err); } throw err; } // Track event in telemetry if (this.telemetry) { this.telemetry.recordEvent(); } // Convert to strings let message; // Apply prefix and suffix to the title, similar to how check() does it const rawTitle = String(title ? title : ''); const msgTitle = this.prefix + rawTitle + this.suffix; let msgText = String(text ? text : rawTitle); // Escape new lines (unescaping is supported by DataDog) msgText = msgText.replace(/\n/g, '\\n'); // start out the message with the event-specific title and text info message = `_e{${msgTitle.length},${msgText.length}}:${msgTitle}|${msgText}`; // add in the event-specific options if (options) { if (options.date_happened) { const timestamp = helpers.formatDate(options.date_happened); if (timestamp) { message += `|d:${timestamp}`; } } if (options.hostname) { message += `|h:${options.hostname}`; } if (options.aggregation_key) { message += `|k:${options.aggregation_key}`; } if (options.priority) { message += `|p:${options.priority}`; } if (options.source_type_name) { message += `|s:${options.source_type_name}`; } if (options.alert_type) { message += `|t:${options.alert_type}`; } } // allow for tags to be omitted and callback to be used in its place if (typeof tags === 'function' && callback === undefined) { callback = tags; } this.send(message, tags, callback); }; } module.exports = applyStatsFns;