Skip to content

Commit 31ababb

Browse files
authored
Auto-flush on process exit (#141)
This attaches to the process’s `beforeExit` event (supported in Node.js, Deno, and Bun) when auto-flushing is enabled in order to automatically flush when a program exits. Before, you needed to remember to call `metrics.flush()` manually at the end of your program, which is easy to forget (or not even know about in the first place!). It also adds a `stop()` method that disables auto-flushing and flushes any remaining buffered metrics. Fixes #136.
1 parent 06b0bb6 commit 31ababb

File tree

7 files changed

+259
-62
lines changed

7 files changed

+259
-62
lines changed

README.md

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ Where `options` is an object and can contain the following:
114114
* `prefix`: Sets a default prefix for all metrics. (optional)
115115
* Use this to namespace your metrics.
116116
* `flushIntervalSeconds`: How often to send metrics to Datadog. (optional)
117-
* This defaults to 15 seconds. Set it to 0 to disable auto-flushing which
118-
means you must call `flush()` manually.
117+
* This defaults to 15 seconds. Set it to `0` to disable auto-flushing (which
118+
means you must call `flush()` manually).
119119
* `site`: Sets the Datadog "site", or server where metrics are sent. (optional)
120120
* Defaults to `datadoghq.com`.
121121
* See more details on setting your site at:
@@ -197,12 +197,12 @@ metrics.init({
197197

198198
`metrics.gauge(key, value[, tags[, timestamp]])`
199199

200-
Record the current *value* of a metric. The most recent value in
201-
a given flush interval will be recorded. Optionally, specify a set of
202-
tags to associate with the metric. This should be used for sum values
203-
such as total hard disk space, process uptime, total number of active
204-
users, or number of rows in a database table. The optional timestamp
205-
is in milliseconds since 1 Jan 1970 00:00:00 UTC, e.g. from `Date.now()`.
200+
Record the current *value* of a metric. The most recent value since the last
201+
flush will be recorded. Optionally, specify a set of tags to associate with the
202+
metric. This should be used for sum values such as total hard disk space,
203+
process uptime, total number of active users, or number of rows in a database
204+
table. The optional timestamp is in milliseconds since 1 Jan 1970 00:00:00 UTC,
205+
e.g. from `Date.now()`.
206206

207207
Example:
208208

@@ -284,15 +284,34 @@ metrics.distribution('test.service_time', 0.248);
284284

285285
### Flushing
286286

287-
`metrics.flush()`
287+
By default, datadog-metrics will automatically flush, or send accumulated
288+
metrics to Datadog, at regular intervals, and, in environments that support it,
289+
before your program exits. (However, if you call `process.exit()` to cause a
290+
hard exit, datadog-metrics doesn’t get a chance to flush. In this case, you may
291+
want to call `await metrics.stop()` first.)
292+
293+
You can adjust the interval by using the `flushIntervalSeconds` option. Setting
294+
it to `0` will disable auto-flushing entirely:
295+
296+
```js
297+
// Set auto-flush interval to 10 seconds.
298+
metrics.init({ flushIntervalSeconds: 10 });
299+
```
300+
301+
You can also send accumulated metrics manually at any time by calling
302+
`metrics.flush()`.
303+
304+
Please note that, when calling the `BufferedMetricsLogger` constructor directly,
305+
`flushIntervalSeconds` defaults to `0` instead. When constructing your own
306+
logger this way, you must expicitly opt-in to auto-flushing by setting a
307+
positive value.
288308

289-
Calling `flush` sends any buffered metrics to Datadog and returns a promise.
290-
This function will be called automatically unless you set `flushIntervalSeconds`
291-
to `0`.
292309

293-
It can be useful to trigger a manual flush by calling if you want to
294-
make sure pending metrics have been sent before you quit the application
295-
process, for example.
310+
#### `metrics.flush()`
311+
312+
Sends any buffered metrics to Datadog and returns a promise. By default,
313+
`flush()` will be called for you automatically unless you set
314+
`flushIntervalSeconds` to `0` (see above for more details).
296315

297316
⚠️ This method used to take two callback arguments for handling successes and
298317
errors. That form is deprecated and will be removed in a future update:
@@ -318,6 +337,18 @@ metrics.flush()
318337
.catch((error) => console.log('Flush error:', error)) ;
319338
```
320339

340+
#### `metrics.stop(options)`
341+
342+
Stops auto-flushing (if enabled) and flushes any currently buffered metrics.
343+
This is mainly useful if you want to manually clean up and send remaining
344+
metrics before hard-quitting your program (usually by calling `process.exit()`).
345+
Returns a promise for the result of the flush.
346+
347+
Takes an optional object with properties:
348+
* `flush` (boolean) Whether to flush any remaining metrics after stopping.
349+
Defaults to `true`.
350+
351+
321352
## Logging
322353

323354
Datadog-metrics uses the [debug](https://github.com/visionmedia/debug)
@@ -344,7 +375,9 @@ TBD
344375

345376
**New Features:**
346377

347-
TBD
378+
* When auto-flushing is enabled, metrics are now also flushed before the process exits. In previous versions, you needed to do this manually by calling `metrics.flush()` at the every end of your program.
379+
380+
You will still need to flush manually if you set `flushIntervalSeconds` to `0` or you are quitting your program by calling `process.exit()` [(which interrupts a variety of operations)](https://nodejs.org/docs/latest/api/process.html#processexitcode).
348381

349382
**Deprecations:**
350383

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ function callOnSharedLogger(func) {
3535
// compiler that this satisfies the types. :(
3636
return (...args) => {
3737
if (sharedLogger === null) {
38+
// Special case: don't make a new logger just to stop it.
39+
// @ts-expect-error TypeScript compiler can't figure this one out.
40+
if (func === BufferedMetricsLogger.prototype.stop) {
41+
return Promise.resolve(undefined);
42+
}
43+
3844
init();
3945
}
4046
return func.apply(sharedLogger, args);
@@ -44,6 +50,7 @@ function callOnSharedLogger(func) {
4450
module.exports = {
4551
init,
4652
flush: callOnSharedLogger(BufferedMetricsLogger.prototype.flush),
53+
stop: callOnSharedLogger(BufferedMetricsLogger.prototype.stop),
4754
gauge: callOnSharedLogger(BufferedMetricsLogger.prototype.gauge),
4855
increment: callOnSharedLogger(BufferedMetricsLogger.prototype.increment),
4956
histogram: callOnSharedLogger(BufferedMetricsLogger.prototype.histogram),

lib/loggers.js

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ const Counter = require('./metrics').Counter;
77
const Histogram = require('./metrics').Histogram;
88
const Distribution = require('./metrics').Distribution;
99

10+
const supportsProcessExit = typeof process !== 'undefined'
11+
&& typeof process.once === 'function';
12+
1013
/**
1114
* @typedef {object} AggregatorType Buffers metrics to send.
1215
* @property {(
@@ -103,6 +106,9 @@ class BufferedMetricsLogger {
103106
opts.site = opts.site || opts.apiHost;
104107
}
105108

109+
this.performAutoFlush = this.performAutoFlush.bind(this);
110+
this.handleProcessExit = this.handleProcessExit.bind(this);
111+
106112
/** @private */
107113
this.aggregator = opts.aggregator || new Aggregator(opts.defaultTags);
108114
/** @private @type {ReporterType} */
@@ -117,34 +123,27 @@ class BufferedMetricsLogger {
117123
/** @private */
118124
this.prefix = opts.prefix || '';
119125
/** @private */
120-
this.flushIntervalSeconds = opts.flushIntervalSeconds;
121-
/** @private */
122126
this.histogramOptions = opts.histogram;
123127

128+
/** @private */
129+
this.onError = null;
124130
if (typeof opts.onError === 'function') {
125-
/** @private */
126131
this.onError = opts.onError;
127132
} else if (opts.onError != null) {
128133
throw new TypeError('The `onError` option must be a function');
129134
}
130135

131-
if (this.flushIntervalSeconds) {
132-
logDebug('Auto-flushing every %d seconds', this.flushIntervalSeconds);
136+
/** @private */
137+
this.flushTimer = null;
138+
/** @private */
139+
this.flushIntervalSeconds = 0;
140+
if (opts.flushIntervalSeconds < 0) {
141+
throw new TypeError(`flushIntervalSeconds must be >= 0 (got: ${opts.flushIntervalSeconds})`);
133142
} else {
134-
logDebug('Auto-flushing is disabled');
143+
this.flushIntervalSeconds = opts.flushIntervalSeconds;
135144
}
136145

137-
const autoFlushCallback = () => {
138-
this.flush();
139-
if (this.flushIntervalSeconds) {
140-
const interval = this.flushIntervalSeconds * 1000;
141-
const tid = setTimeout(autoFlushCallback, interval);
142-
// Let the event loop exit if this is the only active timer.
143-
if (tid.unref) tid.unref();
144-
}
145-
};
146-
147-
autoFlushCallback();
146+
this.start();
148147
}
149148

150149
/**
@@ -326,6 +325,60 @@ class BufferedMetricsLogger {
326325

327326
return result;
328327
}
328+
329+
/**
330+
* Start auto-flushing metrics.
331+
*/
332+
start() {
333+
if (this.flushTimer) {
334+
logDebug('Auto-flushing is already enabled');
335+
} else if (this.flushIntervalSeconds > 0) {
336+
logDebug('Auto-flushing every %d seconds', this.flushIntervalSeconds);
337+
if (supportsProcessExit) {
338+
process.once('beforeExit', this.handleProcessExit);
339+
}
340+
this.performAutoFlush();
341+
} else {
342+
logDebug('Auto-flushing is disabled');
343+
}
344+
}
345+
346+
/**
347+
* Stop auto-flushing metrics. By default, this will also flush any
348+
* currently buffered metrics. You can leave them in the buffer and not
349+
* flush by setting the `flush` option to `false`.
350+
* @param {Object} [options]
351+
* @param {boolean} [options.flush] Whether to flush before returning.
352+
* Defaults to true.
353+
* @returns {Promise}
354+
*/
355+
async stop(options) {
356+
clearTimeout(this.flushTimer);
357+
this.flushTimer = null;
358+
if (supportsProcessExit) {
359+
process.off('beforeExit', this.handleProcessExit);
360+
}
361+
if (!options || options.flush) {
362+
await this.flush();
363+
}
364+
}
365+
366+
/** @private */
367+
performAutoFlush() {
368+
this.flush();
369+
if (this.flushIntervalSeconds) {
370+
const interval = this.flushIntervalSeconds * 1000;
371+
this.flushTimer = setTimeout(this.performAutoFlush, interval);
372+
// Let the event loop exit if this is the only active timer.
373+
if (this.flushTimer.unref) this.flushTimer.unref();
374+
}
375+
}
376+
377+
/** @private */
378+
async handleProcessExit() {
379+
logDebug('Auto-flushing before process exits...');
380+
this.flush();
381+
}
329382
}
330383

331384
module.exports = {

test-other/types_check.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
reporters,
55
init,
66
flush,
7+
stop,
78
gauge,
89
increment,
910
histogram,
@@ -17,6 +18,8 @@ function useLogger(logger: BufferedMetricsLogger) {
1718
logger.histogram('histogram.key', 11);
1819
logger.distribution('distribution.key', 11);
1920
logger.flush();
21+
logger.stop();
22+
logger.stop({ flush: false });
2023
}
2124

2225
useLogger(new BufferedMetricsLogger());
@@ -51,3 +54,5 @@ increment('increment.key');
5154
histogram('histogram.key', 11);
5255
distribution('distribution.key', 11);
5356
flush();
57+
stop();
58+
stop({ flush: false });

0 commit comments

Comments
 (0)