diff --git a/CHANGELOG.md b/CHANGELOG.md index dbedb999..c0037886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added +- feat: implement GC metrics collection without native(C++) modules. + ## [11.5.3] - 2019-06-27 ### Changed diff --git a/README.md b/README.md index eaf8319c..eeba8f87 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,15 @@ NOTE: Some of the metrics, concerning File Descriptors and Memory, are only available on Linux. In addition, some Node-specific metrics are included, such as event loop lag, -active handles and Node.js version. See what metrics there are in +active handles, GC and Node.js version. See what metrics there are in [lib/metrics](lib/metrics). -`collectDefaultMetrics` takes 1 options object with 3 entries, a timeout for how -often the probe should be fired, an optional prefix for metric names -and a registry to which metrics should be registered. By default probes are -launched every 10 seconds, but this can be modified like this: +`collectDefaultMetrics` takes 1 options object with up to 4 entries, a timeout for how +often the probe should be fired, an optional prefix for metric names, +a registry to which metrics should be registered and +`gcDurationBuckets` with custom buckets for GC duration histogram. +Default buckets of GC duration histogram are `[0.001, 0.01, 0.1, 1, 2, 5]` (in seconds). +By default probes are launched every 10 seconds, but this can be modified like this: ```js const client = require('prom-client'); @@ -77,6 +79,16 @@ const register = new Registry(); collectDefaultMetrics({ register }); ``` +To use custom buckets for GC duration histogram, pass it in as `gcDurationBuckets`: + +```js +const client = require('prom-client'); + +const collectDefaultMetrics = client.collectDefaultMetrics; + +collectDefaultMetrics({ gcDurationBuckets: [0.1, 0.2, 0.3] }); +``` + To prefix metric names with your own arbitrary string, pass in a `prefix`: ```js diff --git a/example/server.js b/example/server.js index 890be2a1..b6de8201 100644 --- a/example/server.js +++ b/example/server.js @@ -56,6 +56,19 @@ if (cluster.isWorker) { }, 2000); } +// Generate some garbage +const t = []; +setInterval(() => { + for (let i = 0; i < 100; i++) { + t.push(new Date()); + } +}, 10); +setInterval(() => { + while (t.length > 0) { + t.pop(); + } +}); + server.get('/metrics', (req, res) => { res.set('Content-Type', register.contentType); res.end(register.metrics()); @@ -67,7 +80,10 @@ server.get('/metrics/counter', (req, res) => { }); //Enable collection of default metrics -require('../').collectDefaultMetrics(); +require('../').collectDefaultMetrics({ + timeout: 10000, + gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5] // These are the default buckets. +}); console.log('Server listening to 3000, metrics exposed on /metrics endpoint'); server.listen(3000); diff --git a/index.d.ts b/index.d.ts index 4cc5859d..e66b1d11 100644 --- a/index.d.ts +++ b/index.d.ts @@ -650,6 +650,7 @@ export interface DefaultMetricsCollectorConfiguration { timestamps?: boolean; register?: Registry; prefix?: string; + gcDurationBuckets?: number[]; } /** diff --git a/lib/defaultMetrics.js b/lib/defaultMetrics.js index 1972b3c8..553ed24c 100644 --- a/lib/defaultMetrics.js +++ b/lib/defaultMetrics.js @@ -11,6 +11,7 @@ const processRequests = require('./metrics/processRequests'); const heapSizeAndUsed = require('./metrics/heapSizeAndUsed'); const heapSpacesSizeAndUsed = require('./metrics/heapSpacesSizeAndUsed'); const version = require('./metrics/version'); +const gc = require('./metrics/gc'); const { globalRegistry } = require('./registry'); const { printDeprecationCollectDefaultMetricsNumber } = require('./util'); @@ -25,7 +26,8 @@ const metrics = { processRequests, heapSizeAndUsed, heapSpacesSizeAndUsed, - version + version, + gc }; const metricsList = Object.keys(metrics); diff --git a/lib/metrics/gc.js b/lib/metrics/gc.js new file mode 100644 index 00000000..8e8051ca --- /dev/null +++ b/lib/metrics/gc.js @@ -0,0 +1,54 @@ +'use strict'; +const Histogram = require('../histogram'); + +let perf_hooks; + +try { + // eslint-disable-next-line + perf_hooks = require('perf_hooks'); +} catch (e) { + // node version is too old +} + +const NODEJS_GC_DURATION_SECONDS = 'nodejs_gc_duration_seconds'; +const DEFAULT_GC_DURATION_BUCKETS = [0.001, 0.01, 0.1, 1, 2, 5]; + +const kinds = []; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR] = 'major'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR] = 'minor'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL] = 'incremental'; +kinds[perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB] = 'weakcb'; + +module.exports = (registry, config = {}) => { + if (!perf_hooks) { + return () => {}; + } + + const namePrefix = config.prefix ? config.prefix : ''; + const buckets = config.gcDurationBuckets + ? config.gcDurationBuckets + : DEFAULT_GC_DURATION_BUCKETS; + const gcHistogram = new Histogram({ + name: namePrefix + NODEJS_GC_DURATION_SECONDS, + help: + 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.', + labelNames: ['kind'], + buckets, + registers: registry ? [registry] : undefined + }); + + const obs = new perf_hooks.PerformanceObserver(list => { + const entry = list.getEntries()[0]; + const labels = { kind: kinds[entry.kind] }; + + // Convert duration from milliseconds to seconds + gcHistogram.observe(labels, entry.duration / 1000); + }); + + // We do not expect too many gc events per second, so we do not use buffering + obs.observe({ entryTypes: ['gc'], buffered: false }); + + return () => {}; +}; + +module.exports.metricNames = [NODEJS_GC_DURATION_SECONDS]; diff --git a/test/metrics/gcTest.js b/test/metrics/gcTest.js new file mode 100644 index 00000000..0c20eee5 --- /dev/null +++ b/test/metrics/gcTest.js @@ -0,0 +1,43 @@ +'use strict'; + +describe('gc', () => { + const register = require('../../index').register; + const processHandles = require('../../lib/metrics/gc'); + + beforeAll(() => { + register.clear(); + }); + + afterEach(() => { + register.clear(); + }); + + it('should add metric to the registry', () => { + expect(register.getMetricsAsJSON()).toHaveLength(0); + + processHandles()(); + + const metrics = register.getMetricsAsJSON(); + + // Check if perf_hooks module is available + let perf_hooks; + try { + // eslint-disable-next-line + perf_hooks = require('perf_hooks'); + } catch (e) { + // node version is too old + } + + if (perf_hooks) { + expect(metrics).toHaveLength(1); + + expect(metrics[0].help).toEqual( + 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.' + ); + expect(metrics[0].type).toEqual('histogram'); + expect(metrics[0].name).toEqual('nodejs_gc_duration_seconds'); + } else { + expect(metrics).toHaveLength(0); + } + }); +});