From 5e9ba1eebcac7150cba12802a3ff2b75b9c78325 Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Mon, 17 Jun 2019 18:24:00 +0300 Subject: [PATCH 1/6] feat: implement GC metrics collection without native(C++) modules --- CHANGELOG.md | 2 ++ example/server.js | 13 +++++++ lib/defaultMetrics.js | 4 ++- lib/metrics/gc.js | 78 ++++++++++++++++++++++++++++++++++++++++++ test/metrics/gcTest.js | 49 ++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 lib/metrics/gc.js create mode 100644 test/metrics/gcTest.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fc364750..d7abb2cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ project adheres to [Semantic Versioning](http://semver.org/). - chore: add test for `process_start_time_seconds` ### Added +- `nodejs_gc_runs` metric to the `collectDefaultMetrics()`. It counts number of GC runs with split by GC type. +- `nodejs_gc_duration_summary` metric to the `collectDefaultMetrics()`. It counts 0.5, 0.75, 0.9, 0.99 percentiles of GC duration (in seconds). ## [11.5.3] - 2019-06-27 diff --git a/example/server.js b/example/server.js index 890be2a1..28036672 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()); 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..b7cd71bd --- /dev/null +++ b/lib/metrics/gc.js @@ -0,0 +1,78 @@ +'use strict'; +const Counter = require('../counter'); +const Summary = require('../summary'); + +let perf_hooks; + +try { + // eslint-disable-next-line + perf_hooks = require('perf_hooks'); +} catch (e) { + // node version is too old +} + +const NODEJS_GC_RUNS = 'nodejs_gc_runs'; +const NODEJS_GC_DURATION_SUMMARY = 'nodejs_gc_duration_summary'; + +function gcKindToString(gcKind) { + let gcKindName = ''; + switch (gcKind) { + case perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR: + gcKindName = 'major'; + break; + case perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR: + gcKindName = 'minor'; + break; + case perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL: + gcKindName = 'incremental'; + break; + case perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB: + gcKindName = 'weakcb'; + break; + default: + gcKindName = 'unknown'; + break; + } + return gcKindName; +} + +module.exports = (registry, config = {}) => { + if (!perf_hooks) { + return () => {}; + } + + const namePrefix = config.prefix ? config.prefix : ''; + const gcCount = new Counter({ + name: namePrefix + NODEJS_GC_RUNS, + help: + 'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.', + labelNames: ['gc_type'], + registers: registry ? [registry] : undefined + }); + const gcSummary = new Summary({ + name: namePrefix + NODEJS_GC_DURATION_SUMMARY, + help: + 'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.', + labelNames: ['gc_type'], + maxAgeSeconds: 600, + ageBuckets: 5, + percentiles: [0.5, 0.75, 0.9, 0.99], + registers: registry ? [registry] : undefined + }); + + const obs = new perf_hooks.PerformanceObserver(list => { + const entry = list.getEntries()[0]; + const labels = { gc_type: gcKindToString(entry.kind) }; + + gcCount.inc(labels, 1); + // Convert duration from milliseconds to seconds + gcSummary.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_RUNS, NODEJS_GC_DURATION_SUMMARY]; diff --git a/test/metrics/gcTest.js b/test/metrics/gcTest.js new file mode 100644 index 00000000..d0bd4a77 --- /dev/null +++ b/test/metrics/gcTest.js @@ -0,0 +1,49 @@ +'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(2); + + expect(metrics[0].help).toEqual( + 'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.' + ); + expect(metrics[0].type).toEqual('counter'); + expect(metrics[0].name).toEqual('nodejs_gc_runs'); + + expect(metrics[1].help).toEqual( + 'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.' + ); + expect(metrics[1].type).toEqual('summary'); + expect(metrics[1].name).toEqual('nodejs_gc_duration_summary'); + } else { + expect(metrics).toHaveLength(0); + } + }); +}); From 57b69c1c40b40414b54dfb5d2bf9b23ab79ff51c Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Thu, 30 Jan 2020 14:36:49 +0300 Subject: [PATCH 2/6] fix: replace summury with histogram. Make metric names compatible with promtool check --- lib/metrics/gc.js | 72 ++++++++++++++++++++---------------------- test/metrics/gcTest.js | 10 +++--- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/lib/metrics/gc.js b/lib/metrics/gc.js index b7cd71bd..2bdbaeee 100644 --- a/lib/metrics/gc.js +++ b/lib/metrics/gc.js @@ -1,6 +1,6 @@ 'use strict'; const Counter = require('../counter'); -const Summary = require('../summary'); +const Histogram = require('../histogram'); let perf_hooks; @@ -11,30 +11,27 @@ try { // node version is too old } -const NODEJS_GC_RUNS = 'nodejs_gc_runs'; -const NODEJS_GC_DURATION_SUMMARY = 'nodejs_gc_duration_summary'; +const NODEJS_GC_RUNS_TOTAL = 'nodejs_gc_runs_total'; +const NODEJS_GC_DURATION = 'nodejs_gc_duration'; +const DEFAULT_GC_DURATION_BUCKETS = [ + 0.001, + 0.005, + 0.01, + 0.025, + 0.05, + 0.1, + 0.25, + 0.5, + 1, + 2.5, + 5 +]; -function gcKindToString(gcKind) { - let gcKindName = ''; - switch (gcKind) { - case perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR: - gcKindName = 'major'; - break; - case perf_hooks.constants.NODE_PERFORMANCE_GC_MINOR: - gcKindName = 'minor'; - break; - case perf_hooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL: - gcKindName = 'incremental'; - break; - case perf_hooks.constants.NODE_PERFORMANCE_GC_WEAKCB: - gcKindName = 'weakcb'; - break; - default: - gcKindName = 'unknown'; - break; - } - return gcKindName; -} +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) { @@ -42,31 +39,32 @@ module.exports = (registry, config = {}) => { } const namePrefix = config.prefix ? config.prefix : ''; + const buckets = config.gcDurationBuckets + ? config.gcDurationBuckets + : DEFAULT_GC_DURATION_BUCKETS; const gcCount = new Counter({ - name: namePrefix + NODEJS_GC_RUNS, + name: namePrefix + NODEJS_GC_RUNS_TOTAL, help: - 'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.', - labelNames: ['gc_type'], + 'Count of garbage collections. kind label is one of major, minor, incremental or weakcb.', + labelNames: ['kind'], registers: registry ? [registry] : undefined }); - const gcSummary = new Summary({ - name: namePrefix + NODEJS_GC_DURATION_SUMMARY, + const gcHistogram = new Histogram({ + name: namePrefix + NODEJS_GC_DURATION, help: - 'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.', - labelNames: ['gc_type'], - maxAgeSeconds: 600, - ageBuckets: 5, - percentiles: [0.5, 0.75, 0.9, 0.99], + 'Histogram of garbage collections. kind label is 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 = { gc_type: gcKindToString(entry.kind) }; + const labels = { kind: kinds[entry.kind] }; gcCount.inc(labels, 1); // Convert duration from milliseconds to seconds - gcSummary.observe(labels, entry.duration / 1000); + gcHistogram.observe(labels, entry.duration / 1000); }); // We do not expect too many gc events per second, so we do not use buffering @@ -75,4 +73,4 @@ module.exports = (registry, config = {}) => { return () => {}; }; -module.exports.metricNames = [NODEJS_GC_RUNS, NODEJS_GC_DURATION_SUMMARY]; +module.exports.metricNames = [NODEJS_GC_RUNS_TOTAL, NODEJS_GC_DURATION]; diff --git a/test/metrics/gcTest.js b/test/metrics/gcTest.js index d0bd4a77..ef65681d 100644 --- a/test/metrics/gcTest.js +++ b/test/metrics/gcTest.js @@ -32,16 +32,16 @@ describe('gc', () => { expect(metrics).toHaveLength(2); expect(metrics[0].help).toEqual( - 'Count of garbage collections. gc_type label is one of major, minor, incremental or weakcb.' + 'Count of garbage collections. kind label is one of major, minor, incremental or weakcb.' ); expect(metrics[0].type).toEqual('counter'); - expect(metrics[0].name).toEqual('nodejs_gc_runs'); + expect(metrics[0].name).toEqual('nodejs_gc_runs_total'); expect(metrics[1].help).toEqual( - 'Summary of garbage collections. gc_type label is one of major, minor, incremental or weakcb.' + 'Histogram of garbage collections. kind label is one of major, minor, incremental or weakcb.' ); - expect(metrics[1].type).toEqual('summary'); - expect(metrics[1].name).toEqual('nodejs_gc_duration_summary'); + expect(metrics[1].type).toEqual('histogram'); + expect(metrics[1].name).toEqual('nodejs_gc_duration'); } else { expect(metrics).toHaveLength(0); } From a54e5231d90c1271b09694debd1004c8211504c0 Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Fri, 31 Jan 2020 12:55:10 +0300 Subject: [PATCH 3/6] fix: remove duplicate metrics, update CHANGELOG.md --- CHANGELOG.md | 4 ++-- example/server.js | 5 ++++- index.d.ts | 1 + lib/metrics/gc.js | 32 +++++--------------------------- test/metrics/gcTest.js | 14 ++++---------- 5 files changed, 16 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7abb2cd..1c35156b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ project adheres to [Semantic Versioning](http://semver.org/). - chore: add test for `process_start_time_seconds` ### Added -- `nodejs_gc_runs` metric to the `collectDefaultMetrics()`. It counts number of GC runs with split by GC type. -- `nodejs_gc_duration_summary` metric to the `collectDefaultMetrics()`. It counts 0.5, 0.75, 0.9, 0.99 percentiles of GC duration (in seconds). + +- `nodejs_gc_duration_seconds` histogram metric to the `collectDefaultMetrics()`. Default buckets are [0.001, 0.01, 0.1, 1, 2, 5]. You can pass custom buckets inside configuration object: collectDefaultMetrics({timeout: 10000, gcDurationBuckets:[0.001, 0.005, 0.01,.....]}) ## [11.5.3] - 2019-06-27 diff --git a/example/server.js b/example/server.js index 28036672..1eeb9b3d 100644 --- a/example/server.js +++ b/example/server.js @@ -80,7 +80,10 @@ server.get('/metrics/counter', (req, res) => { }); //Enable collection of default metrics -require('../').collectDefaultMetrics(); +require('../').collectDefaultMetrics({ + timeout: 10000, + gcDurationBuckets: [0.001, 0.005, 0.01] +}); 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/metrics/gc.js b/lib/metrics/gc.js index 2bdbaeee..8e8051ca 100644 --- a/lib/metrics/gc.js +++ b/lib/metrics/gc.js @@ -1,5 +1,4 @@ 'use strict'; -const Counter = require('../counter'); const Histogram = require('../histogram'); let perf_hooks; @@ -11,21 +10,8 @@ try { // node version is too old } -const NODEJS_GC_RUNS_TOTAL = 'nodejs_gc_runs_total'; -const NODEJS_GC_DURATION = 'nodejs_gc_duration'; -const DEFAULT_GC_DURATION_BUCKETS = [ - 0.001, - 0.005, - 0.01, - 0.025, - 0.05, - 0.1, - 0.25, - 0.5, - 1, - 2.5, - 5 -]; +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'; @@ -42,17 +28,10 @@ module.exports = (registry, config = {}) => { const buckets = config.gcDurationBuckets ? config.gcDurationBuckets : DEFAULT_GC_DURATION_BUCKETS; - const gcCount = new Counter({ - name: namePrefix + NODEJS_GC_RUNS_TOTAL, - help: - 'Count of garbage collections. kind label is one of major, minor, incremental or weakcb.', - labelNames: ['kind'], - registers: registry ? [registry] : undefined - }); const gcHistogram = new Histogram({ - name: namePrefix + NODEJS_GC_DURATION, + name: namePrefix + NODEJS_GC_DURATION_SECONDS, help: - 'Histogram of garbage collections. kind label is one of major, minor, incremental or weakcb.', + 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.', labelNames: ['kind'], buckets, registers: registry ? [registry] : undefined @@ -62,7 +41,6 @@ module.exports = (registry, config = {}) => { const entry = list.getEntries()[0]; const labels = { kind: kinds[entry.kind] }; - gcCount.inc(labels, 1); // Convert duration from milliseconds to seconds gcHistogram.observe(labels, entry.duration / 1000); }); @@ -73,4 +51,4 @@ module.exports = (registry, config = {}) => { return () => {}; }; -module.exports.metricNames = [NODEJS_GC_RUNS_TOTAL, NODEJS_GC_DURATION]; +module.exports.metricNames = [NODEJS_GC_DURATION_SECONDS]; diff --git a/test/metrics/gcTest.js b/test/metrics/gcTest.js index ef65681d..0c20eee5 100644 --- a/test/metrics/gcTest.js +++ b/test/metrics/gcTest.js @@ -29,19 +29,13 @@ describe('gc', () => { } if (perf_hooks) { - expect(metrics).toHaveLength(2); + expect(metrics).toHaveLength(1); expect(metrics[0].help).toEqual( - 'Count of garbage collections. kind label is one of major, minor, incremental or weakcb.' + 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.' ); - expect(metrics[0].type).toEqual('counter'); - expect(metrics[0].name).toEqual('nodejs_gc_runs_total'); - - expect(metrics[1].help).toEqual( - 'Histogram of garbage collections. kind label is one of major, minor, incremental or weakcb.' - ); - expect(metrics[1].type).toEqual('histogram'); - expect(metrics[1].name).toEqual('nodejs_gc_duration'); + expect(metrics[0].type).toEqual('histogram'); + expect(metrics[0].name).toEqual('nodejs_gc_duration_seconds'); } else { expect(metrics).toHaveLength(0); } From be5b51f3ec8fa58bb4e3965975391677470018ef Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Sat, 1 Feb 2020 18:21:47 +0300 Subject: [PATCH 4/6] Update example/server.js Co-Authored-By: Sam Roberts --- example/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/server.js b/example/server.js index 1eeb9b3d..b6de8201 100644 --- a/example/server.js +++ b/example/server.js @@ -82,7 +82,7 @@ server.get('/metrics/counter', (req, res) => { //Enable collection of default metrics require('../').collectDefaultMetrics({ timeout: 10000, - gcDurationBuckets: [0.001, 0.005, 0.01] + 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'); From 3e904af77b284b24a69dfb4ebd684c3685a14cb0 Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Sat, 1 Feb 2020 18:22:02 +0300 Subject: [PATCH 5/6] Update CHANGELOG.md Co-Authored-By: Sam Roberts --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c35156b..5c65e463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added -- `nodejs_gc_duration_seconds` histogram metric to the `collectDefaultMetrics()`. Default buckets are [0.001, 0.01, 0.1, 1, 2, 5]. You can pass custom buckets inside configuration object: collectDefaultMetrics({timeout: 10000, gcDurationBuckets:[0.001, 0.005, 0.01,.....]}) +- `nodejs_gc_duration_seconds` histogram metric to the `collectDefaultMetrics()`. Default buckets are `[0.001, 0.01, 0.1, 1, 2, 5]`. You can pass custom buckets inside configuration object: `collectDefaultMetrics({gcDurationBuckets:[0.1, 0.2, 0.3]})` ## [11.5.3] - 2019-06-27 From 55fcbdc3ebe81b03400b9521be380eddddfbd16a Mon Sep 17 00:00:00 2001 From: Yuriy Vasiyarov Date: Sat, 1 Feb 2020 18:41:42 +0300 Subject: [PATCH 6/6] fix: update README --- CHANGELOG.md | 2 +- README.md | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c65e463..623c866c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Added -- `nodejs_gc_duration_seconds` histogram metric to the `collectDefaultMetrics()`. Default buckets are `[0.001, 0.01, 0.1, 1, 2, 5]`. You can pass custom buckets inside configuration object: `collectDefaultMetrics({gcDurationBuckets:[0.1, 0.2, 0.3]})` +- feat: implement GC metrics collection without native(C++) modules. ## [11.5.3] - 2019-06-27 diff --git a/README.md b/README.md index f6c16be7..094d54e7 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