datadog-metrics
Version:
Buffered metrics reporting via the Datadog HTTP API
265 lines (235 loc) • 8.87 kB
JavaScript
const datadogApiClient = require('@datadog/datadog-api-client');
const { AuthorizationError } = require('./errors');
const { logDebug, logDeprecation } = require('./logging');
const RETRYABLE_ERROR_CODES = new Set([
'ECONNREFUSED',
'ECONNRESET',
'ENOTFOUND',
'EPIPE',
'ETIMEDOUT'
]);
async function sleep(milliseconds) {
await new Promise((r) => setTimeout(r, milliseconds));
}
/**
* A Reporter that throws away metrics instead of sending them to Datadog. This
* is useful for disabling metrics in your application and for tests.
*/
class NullReporter {
async report(_series) {
// Do nothing.
}
}
/**
* @private
* A custom HTTP implementation for Datadog that retries failed requests.
* Datadog has retries built in, but they don't handle network errors (just
* HTTP errors), and we want to retry in both cases. This inherits from the
* built-in HTTP library since we want to use the same fetch implementation
* Datadog uses instead of adding another dependency.
*/
class RetryHttp extends datadogApiClient.client.IsomorphicFetchHttpLibrary {
constructor(options = {}) {
super(options);
// HACK: ensure enableRetry is always `false` so the base class logic
// does not actually retry (since we manage retries here).
Object.defineProperty(this, 'enableRetry', {
get () { return false; },
set () {},
});
}
async send(request) {
let i = 0;
while (true) { // eslint-disable-line no-constant-condition
let response, error;
try {
response = await super.send(request);
} catch (e) {
error = e;
}
if (this.isRetryable(response || error, i)) {
await sleep(this.retryDelay(response || error, i));
} else if (response) {
return response;
} else {
throw error;
}
i++;
}
}
/**
* @private
* @param {any} response HTTP response or error object
* @returns {boolean}
*/
isRetryable(response, tryCount) {
return tryCount < this.maxRetries && (
RETRYABLE_ERROR_CODES.has(response.code)
|| response.httpStatusCode === 429
|| response.httpStatusCode >= 500
);
}
/**
* @private
* @param {any} response HTTP response or error object
* @param {number} tryCount
* @returns {number}
*/
retryDelay(response, tryCount) {
if (response.httpStatusCode === 429) {
// Datadog's official client supports just the 'x-ratelimit-reset'
// header, so we support that here in addition to the standardized
// 'retry-after' heaer.
// There is also an upcoming IETF standard for 'ratelimit', but it
// has moved away from the syntax used in 'x-ratelimit-reset'. This
// stuff might change in the future.
// https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
const delayHeader = response.headers['retry-after']
|| response.headers['x-ratelimit-reset'];
const delayValue = parseInt(delayHeader, 10);
if (!isNaN(delayValue) && delayValue > 0) {
return delayValue * 1000;
}
}
return this.backoffMultiplier ** tryCount * this.backoffBase * 1000;
}
}
/**
* @typedef {Object} DatadogReporterOptions
* @property {string} [apiKey] Datadog API key.
* @property {string} [appKey] DEPRECATED! This option does nothing.
* @property {string} [site] The Datadog "site" to send metrics to.
* @property {number} [retries] Retry failed requests up to this many times.
* @property {number} [retryBackoff] Delay before retries. Subsequent retries
* wait this long multiplied by 2^(retry count).
*/
/** @type {WeakMap<DatadogReporter, datadogApiClient.v1.MetricsApi>} */
const datadogClients = new WeakMap();
/**
* Create a reporter that sends metrics to Datadog's API.
*/
class DatadogReporter {
/**
* Create a reporter that sends metrics to Datadog's API.
* @param {DatadogReporterOptions} [options]
*/
constructor(options = {}) {
if (typeof options !== 'object') {
throw new TypeError('DatadogReporter takes an options object, not multiple string arguments.');
}
if (options.appKey) {
logDeprecation(
'The `appKey` option is no longer supported since it is ' +
'not used for submitting metrics, distributions, events, ' +
'or logs.'
);
}
const apiKey = options.apiKey || process.env.DATADOG_API_KEY || process.env.DD_API_KEY;
this.site = options.site
|| process.env.DATADOG_SITE
|| process.env.DD_SITE
|| process.env.DATADOG_API_HOST;
if (!apiKey) {
throw new Error(
'Datadog API key not found. You must specify one via the ' +
'`apiKey` configuration option or the DATADOG_API_KEY or ' +
'DD_API_KEY environment variable.'
);
}
const configuration = datadogApiClient.client.createConfiguration({
authMethods: {
apiKeyAuth: apiKey,
},
httpApi: new RetryHttp(),
maxRetries: options.retries >= 0 ? options.retries : 2,
});
// HACK: Specify backoff here rather than in configration options to
// support values less than 2 (mainly for faster tests).
const backoff = options.retryBackoff >= 0 ? options.retryBackoff : 1;
configuration.httpApi.backoffBase = backoff;
if (this.site) {
// Strip leading `app.` from the site in case someone copy/pasted the
// URL from their web browser. More details on correct configuration:
// https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site
this.site = this.site.replace(/^app\./i, '');
configuration.setServerVariables({
site: this.site
});
}
datadogClients.set(this, new datadogApiClient.v1.MetricsApi(configuration));
}
/**
* Send an array of serialized metrics to Datadog.
* @param {any[]} series
* @returns {Promise}
*/
async report(series) {
logDebug('Calling report with %j', series);
// Distributions must be submitted via a different method than other
// metrics, so split them up.
const metrics = [];
const distributions = [];
for (const metric of series) {
if (metric.type === 'distribution') {
distributions.push(metric);
} else {
metrics.push(metric);
}
}
const metricsApi = datadogClients.get(this);
let submissions = [];
if (metrics.length) {
submissions.push(metricsApi.submitMetrics({
body: { series: metrics }
}));
}
if (distributions.length) {
submissions.push(metricsApi.submitDistributionPoints({
body: { series: distributions }
}));
}
try {
await Promise.all(submissions);
logDebug('sent metrics successfully');
} catch (error) {
if (error.code === 403) {
throw new AuthorizationError(
'Your Datadog API key is not authorized to send ' +
'metrics. Check to make sure the DATADOG_API_KEY or ' +
'DD_API_KEY environment variable or the `apiKey` init ' +
'option is set to a valid API key for your Datadog ' +
'account, and that it is not an *application* key. ' +
'For more, see: ' +
'https://docs.datadoghq.com/account_management/api-app-keys/',
{ cause: error }
);
}
throw error;
}
}
}
/**
* @deprecated Please use `DatadogReporter` instead.
*/
class DataDogReporter extends DatadogReporter {
/**
* Create a reporter that sends metrics to Datadog's API.
* @deprecated
* @param {string} [apiKey]
* @param {string} [appKey]
* @param {string} [site]
*/
constructor(apiKey, appKey, site) {
logDeprecation(
'DataDogReporter has been renamed to DatadogReporter (lower-case ' +
'D in "dog"); the old name will be removed in a future release.'
);
super({ apiKey, appKey, site });
}
}
module.exports = {
NullReporter,
DatadogReporter,
DataDogReporter
};
;