11'use strict' ;
2- const datadogApiClient = require ( '@datadog/datadog-api-client ' ) ;
3- const { AuthorizationError } = require ( './errors' ) ;
2+ const { fetch } = require ( 'cross-fetch ' ) ;
3+ const { AuthorizationError, DatadogMetricsError , HttpError } = require ( './errors' ) ;
44const { logDebug, logDeprecation } = require ( './logging' ) ;
55
66const RETRYABLE_ERROR_CODES = new Set ( [
@@ -27,39 +27,49 @@ class NullReporter {
2727
2828/**
2929 * @private
30- * A custom HTTP implementation for Datadog that retries failed requests.
31- * Datadog has retries built in, but they don't handle network errors (just
32- * HTTP errors), and we want to retry in both cases. This inherits from the
33- * built-in HTTP library since we want to use the same fetch implementation
34- * Datadog uses instead of adding another dependency.
30+ * Manages HTTP requests and associated retry/error handling logic.
3531 */
36- class RetryHttp extends datadogApiClient . client . IsomorphicFetchHttpLibrary {
37- constructor ( options = { } ) {
38- super ( options ) ;
39-
40- // HACK: ensure enableRetry is always `false` so the base class logic
41- // does not actually retry (since we manage retries here).
42- Object . defineProperty ( this , 'enableRetry' , {
43- get ( ) { return false ; } ,
44- set ( ) { } ,
45- } ) ;
32+ class HttpApi {
33+ constructor ( options ) {
34+ this . maxRetries = options . maxRetries ;
35+ this . backoffBase = options . backoffBase ;
36+ this . backoffMultiplier = 2 ;
4637 }
4738
48- async send ( request ) {
39+ async send ( url , options ) {
4940 let i = 0 ;
5041 while ( true ) { // eslint-disable-line no-constant-condition
51- let response , error ;
42+ let response , body , error ;
5243 try {
53- response = await super . send ( request ) ;
44+ logDebug ( `Sending HTTP request to "${ url } "` ) ;
45+ response = await fetch ( url , options ) ;
46+ body = await response . json ( ) ;
5447 } catch ( e ) {
5548 error = e ;
5649 }
5750
51+ const details = this . getLogDetails ( url , response , error ) ;
5852 if ( this . isRetryable ( response || error , i ) ) {
59- await sleep ( this . retryDelay ( response || error , i ) ) ;
53+ const delay = this . retryDelay ( response || error , i ) ;
54+ logDebug ( `HTTP request failed, retrying in ${ delay } ms. ${ details } ` ) ;
55+
56+ await sleep ( delay ) ;
6057 } else if ( response ) {
61- return response ;
58+ if ( response . status >= 400 ) {
59+ logDebug ( `HTTP request failed. ${ details } ` ) ;
60+
61+ let message = `Could not fetch ${ url } ` ;
62+ if ( body && body . errors ) {
63+ message += ` (${ body . errors . join ( ', ' ) } )` ;
64+ }
65+ throw new HttpError ( message , { response } ) ;
66+ }
67+
68+ logDebug ( `HTTP request succeeded. ${ details } ` ) ;
69+ return body ;
6270 } else {
71+ logDebug ( `HTTP request failed. ${ details } ` ) ;
72+
6373 throw error ;
6474 }
6575
@@ -75,8 +85,8 @@ class RetryHttp extends datadogApiClient.client.IsomorphicFetchHttpLibrary {
7585 isRetryable ( response , tryCount ) {
7686 return tryCount < this . maxRetries && (
7787 RETRYABLE_ERROR_CODES . has ( response . code )
78- || response . httpStatusCode === 429
79- || response . httpStatusCode >= 500
88+ || response . status === 429
89+ || response . status >= 500
8090 ) ;
8191 }
8292
@@ -87,16 +97,16 @@ class RetryHttp extends datadogApiClient.client.IsomorphicFetchHttpLibrary {
8797 * @returns {number }
8898 */
8999 retryDelay ( response , tryCount ) {
90- if ( response . httpStatusCode === 429 ) {
100+ if ( response . status === 429 ) {
91101 // Datadog's official client supports just the 'x-ratelimit-reset'
92102 // header, so we support that here in addition to the standardized
93103 // 'retry-after' heaer.
94104 // There is also an upcoming IETF standard for 'ratelimit', but it
95105 // has moved away from the syntax used in 'x-ratelimit-reset'. This
96106 // stuff might change in the future.
97107 // https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
98- const delayHeader = response . headers [ 'retry-after' ]
99- || response . headers [ 'x-ratelimit-reset' ] ;
108+ const delayHeader = response . headers . get ( 'retry-after' )
109+ || response . headers . get ( 'x-ratelimit-reset' ) ;
100110 const delayValue = parseInt ( delayHeader , 10 ) ;
101111 if ( ! isNaN ( delayValue ) && delayValue > 0 ) {
102112 return delayValue * 1000 ;
@@ -105,6 +115,17 @@ class RetryHttp extends datadogApiClient.client.IsomorphicFetchHttpLibrary {
105115
106116 return this . backoffMultiplier ** tryCount * this . backoffBase * 1000 ;
107117 }
118+
119+ /**
120+ * @private
121+ * @param {string } url
122+ * @param {Response? } response
123+ * @param {Error? } error
124+ */
125+ getLogDetails ( url , response , error ) {
126+ let result = response ? `HTTP status: ${ response . status } ` : `error: ${ error } ` ;
127+ return `URL: "${ url } ", ${ result } ` ;
128+ }
108129}
109130
110131/**
@@ -117,8 +138,8 @@ class RetryHttp extends datadogApiClient.client.IsomorphicFetchHttpLibrary {
117138 * wait this long multiplied by 2^(retry count).
118139 */
119140
120- /** @type {WeakMap<DatadogReporter, datadogApiClient.v1.MetricsApi > } */
121- const datadogClients = new WeakMap ( ) ;
141+ /** @type {WeakMap<DatadogReporter, string > } */
142+ const datadogApiKeys = new WeakMap ( ) ;
122143
123144/**
124145 * Create a reporter that sends metrics to Datadog's API.
@@ -142,43 +163,34 @@ class DatadogReporter {
142163 }
143164
144165 const apiKey = options . apiKey || process . env . DATADOG_API_KEY || process . env . DD_API_KEY ;
145- this . site = options . site
146- || process . env . DATADOG_SITE
147- || process . env . DD_SITE
148- || process . env . DATADOG_API_HOST ;
149166
150167 if ( ! apiKey ) {
151- throw new Error (
168+ throw new DatadogMetricsError (
152169 'Datadog API key not found. You must specify one via the ' +
153170 '`apiKey` configuration option or the DATADOG_API_KEY or ' +
154171 'DD_API_KEY environment variable.'
155172 ) ;
156173 }
157174
158- const configuration = datadogApiClient . client . createConfiguration ( {
159- authMethods : {
160- apiKeyAuth : apiKey ,
161- } ,
162- httpApi : new RetryHttp ( ) ,
175+ /** @private @type {HttpApi } */
176+ this . httpApi = new HttpApi ( {
163177 maxRetries : options . retries >= 0 ? options . retries : 2 ,
178+ retryBackoff : options . retryBackoff >= 0 ? options . retryBackoff : 1
164179 } ) ;
165180
166- // HACK: Specify backoff here rather than in configration options to
167- // support values less than 2 (mainly for faster tests).
168- const backoff = options . retryBackoff >= 0 ? options . retryBackoff : 1 ;
169- configuration . httpApi . backoffBase = backoff ;
170-
171- if ( this . site ) {
172- // Strip leading `app.` from the site in case someone copy/pasted the
173- // URL from their web browser. More details on correct configuration:
174- // https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site
175- this . site = this . site . replace ( / ^ a p p \. / i, '' ) ;
176- configuration . setServerVariables ( {
177- site : this . site
178- } ) ;
179- }
181+ /** @private @type {string } */
182+ this . site = options . site
183+ || process . env . DATADOG_SITE
184+ || process . env . DD_SITE
185+ || process . env . DATADOG_API_HOST
186+ || 'datadoghq.com' ;
180187
181- datadogClients . set ( this , new datadogApiClient . v1 . MetricsApi ( configuration ) ) ;
188+ // Strip leading `app.` from the site in case someone copy/pasted the
189+ // URL from their web browser. More details on correct configuration:
190+ // https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site
191+ this . site = this . site . replace ( / ^ a p p \. / i, '' ) ;
192+
193+ datadogApiKeys . set ( this , apiKey ) ;
182194 }
183195
184196 /**
@@ -201,25 +213,19 @@ class DatadogReporter {
201213 }
202214 }
203215
204- const metricsApi = datadogClients . get ( this ) ;
205-
206216 let submissions = [ ] ;
207217 if ( metrics . length ) {
208- submissions . push ( metricsApi . submitMetrics ( {
209- body : { series : metrics }
210- } ) ) ;
218+ submissions . push ( this . sendMetrics ( metrics ) ) ;
211219 }
212220 if ( distributions . length ) {
213- submissions . push ( metricsApi . submitDistributionPoints ( {
214- body : { series : distributions }
215- } ) ) ;
221+ submissions . push ( this . sendDistributions ( distributions ) ) ;
216222 }
217223
218224 try {
219225 await Promise . all ( submissions ) ;
220226 logDebug ( 'sent metrics successfully' ) ;
221227 } catch ( error ) {
222- if ( error . code === 403 ) {
228+ if ( error . status === 403 ) {
223229 throw new AuthorizationError (
224230 'Your Datadog API key is not authorized to send ' +
225231 'metrics. Check to make sure the DATADOG_API_KEY or ' +
@@ -235,6 +241,45 @@ class DatadogReporter {
235241 throw error ;
236242 }
237243 }
244+
245+ /**
246+ * Send an array of metrics to the Datadog API.
247+ * @private
248+ * @param {any[] } series
249+ * @returns {Promise }
250+ */
251+ sendMetrics ( series ) {
252+ return this . sendHttp ( '/v1/series' , { body : { series } } ) ;
253+ }
254+
255+ /**
256+ * Send an array of distributions to the Datadog API.
257+ * @private
258+ * @param {any[] } series
259+ * @returns {Promise }
260+ */
261+ sendDistributions ( series ) {
262+ return this . sendHttp ( '/v1/distribution_points' , { body : { series } } ) ;
263+ }
264+
265+ /**
266+ * @private
267+ * @param {string } path
268+ * @param {any } options
269+ * @returns {Promise }
270+ */
271+ async sendHttp ( path , options ) {
272+ const url = `https://api.${ this . site } /api${ path } ` ;
273+ const fetchOptions = {
274+ method : 'POST' ,
275+ headers : {
276+ 'DD-API-KEY' : datadogApiKeys . get ( this ) ,
277+ 'Content-Type' : 'application/json'
278+ } ,
279+ body : JSON . stringify ( options . body )
280+ } ;
281+ return await this . httpApi . send ( url , fetchOptions ) ;
282+ }
238283}
239284
240285/**
0 commit comments