21
21
* gauges: EventLoopGauges;
22
22
* }} MetricsPayload
23
23
*/
24
- const { setInterval } = require ( 'node:timers' )
25
- const { URL } = require ( 'node:url' ) ;
26
- const { debuglog } = require ( 'node:util' ) ;
27
- const { monitorEventLoopDelay, PerformanceObserver, constants, performance } = require ( 'node:perf_hooks' )
24
+ const { setInterval } = require ( 'timers' )
25
+ const { URL } = require ( 'url' ) ;
26
+ const { debuglog } = require ( 'util' ) ;
27
+ const { monitorEventLoopDelay, PerformanceObserver, constants, performance } = require ( 'perf_hooks' )
28
+ const { request : insecureRequest } = require ( 'http' ) ;
29
+ const { request : secureRequest } = require ( 'https' ) ;
28
30
29
31
try {
30
32
// failures from the instrumentation shouldn't mess with the application
31
33
registerInstrumentation ( )
32
34
} catch ( e ) {
33
- log ( `An unexpected error occurred: ${ e . message } ` )
35
+ log ( `An unexpected error occurred: ${ e . stack } ` )
34
36
}
35
37
36
38
/**
@@ -53,33 +55,33 @@ function registerInstrumentation() {
53
55
const gcObserver = new PerformanceObserver ( ( value ) => {
54
56
value . getEntries ( ) . forEach ( entry => updateMemoryCounters ( memoryCounters , entry ) )
55
57
} )
56
- gcObserver . observe ( { type : 'gc' } )
58
+ gcObserver . observe ( { entryTypes : [ 'gc' ] } )
57
59
58
60
const eventLoopHistogram = monitorEventLoopDelay ( )
59
61
eventLoopHistogram . enable ( )
60
62
61
63
let previousEventLoopUtilization = performance . eventLoopUtilization ( )
62
64
63
65
const timeout = setInterval ( ( ) => {
64
- const eventLoopUtilization = performance . eventLoopUtilization ( previousEventLoopUtilization )
65
- eventLoopHistogram . disable ( )
66
- gcObserver . disconnect ( )
67
-
68
- const payload = {
69
- counters : { ...memoryCounters } ,
70
- gauges : captureEventLoopGauges ( eventLoopUtilization , eventLoopHistogram )
71
- }
66
+ try {
67
+ const eventLoopUtilization = performance . eventLoopUtilization ( previousEventLoopUtilization )
68
+ eventLoopHistogram . disable ( )
69
+ gcObserver . disconnect ( )
72
70
73
- sendMetrics ( herokuMetricsUrl , payload ) . catch ( ( err ) => {
74
- log ( `An error occurred while sending metrics - ${ err } ` )
75
- } )
71
+ sendMetrics ( herokuMetricsUrl , {
72
+ counters : { ...memoryCounters } ,
73
+ gauges : captureEventLoopGauges ( eventLoopUtilization , eventLoopHistogram )
74
+ } )
76
75
77
- // reset memory and event loop measures
78
- previousEventLoopUtilization = eventLoopUtilization
79
- memoryCounters = initializeMemoryCounters ( )
80
- gcObserver . observe ( { type : 'gc' } )
81
- eventLoopHistogram . reset ( )
82
- eventLoopHistogram . enable ( )
76
+ // reset memory and event loop measures
77
+ previousEventLoopUtilization = eventLoopUtilization
78
+ memoryCounters = initializeMemoryCounters ( )
79
+ gcObserver . observe ( { entryTypes : [ 'gc' ] } )
80
+ eventLoopHistogram . reset ( )
81
+ eventLoopHistogram . enable ( )
82
+ } catch ( e ) {
83
+ log ( `An unexpected error occurred: ${ e . stack } ` )
84
+ }
83
85
} , herokuMetricsInterval )
84
86
85
87
// `setInterval` actually returns a Timeout object but this isn't recognized by the type-checker which
@@ -171,19 +173,31 @@ function initializeMemoryCounters(){
171
173
* @param {PerformanceEntry } performanceEntry
172
174
*/
173
175
function updateMemoryCounters ( memoryCounters , performanceEntry ) {
174
- // only record entries with that contain a 'detail' object with 'kind' property
175
- // since these are only available for NodeGCPerformanceDetail entries
176
- if ( 'detail' in performanceEntry && 'kind' in performanceEntry . detail ) {
177
- const nsDuration = millisecondsToNanoseconds ( performanceEntry . duration )
178
- memoryCounters [ 'node.gc.collections' ] += 1
179
- memoryCounters [ 'node.gc.pause.ns' ] += nsDuration
180
- if ( performanceEntry . detail . kind === constants . NODE_PERFORMANCE_GC_MINOR ) {
181
- memoryCounters [ 'node.gc.young.collections' ] += 1
182
- memoryCounters [ 'node.gc.young.pause.ns' ] += nsDuration
183
- } else {
184
- memoryCounters [ 'node.gc.old.collections' ] += 1
185
- memoryCounters [ 'node.gc.old.pause.ns' ] += nsDuration
186
- }
176
+ const nsDuration = millisecondsToNanoseconds ( performanceEntry . duration )
177
+ memoryCounters [ 'node.gc.collections' ] += 1
178
+ memoryCounters [ 'node.gc.pause.ns' ] += nsDuration
179
+ if ( getGcPerformanceEntryKind ( performanceEntry ) === constants . NODE_PERFORMANCE_GC_MINOR ) {
180
+ memoryCounters [ 'node.gc.young.collections' ] += 1
181
+ memoryCounters [ 'node.gc.young.pause.ns' ] += nsDuration
182
+ } else {
183
+ memoryCounters [ 'node.gc.old.collections' ] += 1
184
+ memoryCounters [ 'node.gc.old.pause.ns' ] += nsDuration
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Reads the `kind` field for the 'gc' performance entry in a backwards-compatible way
190
+ * @param {PerformanceEntry } performanceEntry
191
+ * @returns {number }
192
+ */
193
+ function getGcPerformanceEntryKind ( performanceEntry ) {
194
+ // using try/catch to avoid triggering deprecation warnings
195
+ try {
196
+ // for v16 and up
197
+ return performanceEntry . detail . kind
198
+ } catch ( e ) {
199
+ // fallback for v14 & v15
200
+ return performanceEntry . kind
187
201
}
188
202
}
189
203
@@ -209,7 +223,7 @@ function captureEventLoopGauges(eventLoopUtilization, eventLoopHistogram) {
209
223
* @return {number }
210
224
*/
211
225
function millisecondsToNanoseconds ( ms ) {
212
- return ms * 1_000_000
226
+ return ms * 1e6 // 1_000_000
213
227
}
214
228
215
229
/**
@@ -218,26 +232,47 @@ function millisecondsToNanoseconds(ms) {
218
232
* @return {number }
219
233
*/
220
234
function nanosecondsToMilliseconds ( ns ) {
221
- return ns / 1_000_000
235
+ return ns / 1e6 // 1_000_000
222
236
}
223
237
224
238
/**
225
239
* Sends the collected metrics to the given endpoint using a POST request.
226
240
* @param {URL } url
227
241
* @param {MetricsPayload } payload
228
- * @returns { Promise< void> }
242
+ * @returns void
229
243
*/
230
244
function sendMetrics ( url , payload ) {
245
+ const request = url . protocol === 'https:' ? secureRequest : insecureRequest
246
+ const payloadAsJson = JSON . stringify ( payload )
247
+
231
248
log ( `Sending metrics to ${ url . toString ( ) } ` )
232
- return fetch ( url , {
249
+ const clientRequest = request ( {
233
250
method : 'POST' ,
234
- headers : { 'Content-Type' : 'application/json' } ,
235
- body : JSON . stringify ( payload )
236
- } ) . then ( res => {
237
- if ( res . ok ) {
251
+ protocol : url . protocol ,
252
+ hostname : url . hostname ,
253
+ port : url . port ,
254
+ path : url . pathname ,
255
+ headers : {
256
+ 'Content-Type' : 'application/json' ,
257
+ 'Content-Length' : Buffer . byteLength ( payloadAsJson )
258
+ }
259
+ } )
260
+
261
+ clientRequest . on ( 'response' , ( res ) => {
262
+ if ( res . statusCode === 200 ) {
238
263
log ( 'Metrics sent successfully' )
239
264
} else {
240
- log ( `Tried to send metrics but response was: ${ res . status } - ${ res . statusText } ` )
265
+ log ( `Tried to send metrics but response was: ${ res . statusCode } - ${ res . statusMessage } ` )
241
266
}
267
+ // consume response data to free up memory
268
+ // see: https://nodejs.org/docs/latest/api/http.html#http_class_http_clientrequest
269
+ res . resume ( )
242
270
} )
271
+
272
+ clientRequest . on ( 'error' , ( err ) => {
273
+ log ( `An error occurred while sending metrics - ${ err } ` )
274
+ } )
275
+
276
+ clientRequest . write ( payloadAsJson )
277
+ clientRequest . end ( )
243
278
}
0 commit comments