Skip to content

Commit

Permalink
metric: add noop metrics.
Browse files Browse the repository at this point in the history
Add a noop type of metric not registered by service-runner Metric interface.
This is used to delegate collection of default prometheus
metrics to prom-client.collectDefaultMetrics.
  • Loading branch information
gmodena authored and jdforrester committed May 4, 2024
1 parent 54bcb18 commit fd28088
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 18 deletions.
21 changes: 21 additions & 0 deletions lib/heapwatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ const cluster = require( 'cluster' );

class HeapWatch {
constructor( conf, logger, metrics ) {
if ( metrics !== null ) {
const prometheusOptions = metrics.options.find( ( option ) =>
option.type === 'prometheus' && option.collect_default === true
);
// When true, a noop dummy metric will be instantiated
// and passed down to a PrometheusClient instance.
// This metric won't be registered directly by service-runner,
// but will be used to trigger a call to prom-client.collectDefaultMetrics().
this.collect_default = !!prometheusOptions;
}
this.conf = conf = conf || {};
this.limit = ( conf.limitMB || 1500 ) * 1024 * 1024;
this.logger = logger;
Expand All @@ -15,6 +25,17 @@ class HeapWatch {

watch() {
const usage = process.memoryUsage();
if ( this.collect_default ) {
// collect some default metrics recommended by Prometheus itself.
// https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors
this.metrics.makeMetric( {
type: 'noop',
name: 'prometheus.default',
prometheus: {
collect_default: true
}
} );
}
this.metrics.makeMetric( {
type: 'Gauge',
name: 'heap.rss',
Expand Down
15 changes: 10 additions & 5 deletions lib/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,17 @@ class Metrics {
// }
// }
makeMetric( options ) {
let metric = this.cache.get( options.name );
if ( metric === undefined ) {
metric = new Metric( this.clients, this.logger, options );
this.cache.set( options.name, metric );
// noop is a kind of metric not registered by service-runner Metric interface.
// This is used to delegate collection of default prometheus
// metrics to prom-client.collectDefaultMetrics.
if ( options.collect_default === undefined || options.type !== 'noop' ) {
let metric = this.cache.get( options.name );
if ( metric === undefined ) {
metric = new Metric( this.clients, this.logger, options );
this.cache.set( options.name, metric );
}
return metric;
}
return metric;
}

close() {
Expand Down
1 change: 1 addition & 0 deletions lib/metrics/metric.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class Metric {
this.logger.log( 'error/metrics', `endTiming() unsupported for metric type ${ this.type }` );
}
}

}

module.exports = Metric;
30 changes: 18 additions & 12 deletions lib/metrics/prometheus.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ class PrometheusMetric {
constructor( options, client ) {
this.client = client;
this.options = cloneDeep( options );

// TODO: update eslint rules to allow optional chaining operator `?`
if ( this.options.prometheus && this.options.prometheus.collect_default === true ) {
this.client.collectDefaultMetrics();
}
if ( this.options.labels === undefined ) {
this.options.labels = { names: [] };
}
Expand All @@ -26,13 +21,24 @@ class PrometheusMetric {
this.options.labels.names = this.options.labels.names.map( this._normalize );

this.options.prometheus.name = this._normalize( this.options.prometheus.name );
this.metric = new this.client[ this.options.type ]( {
name: this.options.prometheus.name,
help: this.options.prometheus.help,
labelNames: this.options.labels.names,
buckets: this.options.prometheus.buckets,
percentiles: this.options.prometheus.percentiles
} );
if ( this.options.type !== 'noop' ) {
this.metric = new this.client[ this.options.type ]( {
name: this.options.prometheus.name,
help: this.options.prometheus.help,
labelNames: this.options.labels.names,
buckets: this.options.prometheus.buckets,
percentiles: this.options.prometheus.percentiles
} );
} else if ( this.options.prometheus && this.options.prometheus.collect_default === true ) {
// TODO: update eslint rules to allow optional chaining operator `?`
// A no op metric.
// Invoke collectDefaultMetrics() but don't register any new metric
// via the service-runner Metric interface.
// https://prometheus.io/docs/instrumenting/writing_clientlibs/#standard-and-runtime-collectors
// TODO: collectDefaultMetrics() does not support providing labels.
// Default labels should be set for the whole registry.
this.client.collectDefaultMetrics();
}
}

/**
Expand Down
22 changes: 22 additions & 0 deletions test/features/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ describe( 'service-runner tests', () => {
.finally( () => process.removeListener( 'warning', warningListener ) );
} );

it( 'Must produce prometheus default metrics when hit ', () => {
const server = new TestServer( `${__dirname}/../utils/simple_config_no_workers_collect_default.yaml` );

Check failure on line 107 in test/features/tests.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Expected space(s) after '${'

Check failure on line 107 in test/features/tests.js

View workflow job for this annotation

GitHub Actions / build (18.x)

Expected space(s) before '}'
const response = { status: null, body: null };
return server.start()
.then( () => {
preq.get( { uri: 'http://127.0.0.1:9000' } )
.then( ( res ) => {
response.status = res.status;
response.body = res.body;
} );
} )
.delay( 1000 )
.then( () => {
// nodejs_version_info is reported by calls to prom-client.collectDefaultMetrics()
assert.ok(
response.body.includes( 'nodejs_version_info ' ),
'Must collect default metrics prometheus output.'
);
} )
.finally( () => server.stop() );
} );

// preq prevents the AssertionErrors from surfacing and failing the test
// performing the test this way presents them correctly
it( 'Must increment hitcount metrics when hit, no workers', () => {
Expand Down
1 change: 0 additions & 1 deletion test/utils/simple_config_no_workers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ logging:
metrics:
- type: prometheus
port: 9000
collect_default: true
- type: statsd
host: localhost
port: 8125
Expand Down
15 changes: 15 additions & 0 deletions test/utils/simple_config_no_workers_collect_default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
num_workers: 0
logging:
level: fatal
metrics:
- type: prometheus
port: 9000
collect_default: true
- type: statsd
host: localhost
port: 8125
services:
- name: test
module: test/utils/simple_server.js
conf:
port: 12345

0 comments on commit fd28088

Please sign in to comment.