-
Notifications
You must be signed in to change notification settings - Fork 378
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: support aggregating metrics in clusters
- Loading branch information
Showing
16 changed files
with
603 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
'use strict'; | ||
|
||
const cluster = require('cluster'); | ||
const express = require('express'); | ||
const metricsServer = express(); | ||
const AggregatorRegistry = require('../').AggregatorRegistry; | ||
|
||
if (cluster.isMaster) { | ||
for (let i = 0; i < 4; i++) { | ||
cluster.fork(); | ||
} | ||
|
||
metricsServer.get('/cluster_metrics', (req, res) => { | ||
AggregatorRegistry.clusterMetrics((err, metrics) => { | ||
if (err) console.log(err); | ||
res.set('Content-Type', AggregatorRegistry.contentType); | ||
res.send(metrics); | ||
}); | ||
}); | ||
|
||
metricsServer.listen(3001); | ||
console.log( | ||
'Cluster metrics server listening to 3001, metrics exposed on /cluster_metrics' | ||
); | ||
} else { | ||
require('./server.js'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
'use strict'; | ||
|
||
/** | ||
* In cluster masters, extends Registry class with a `clusterMetrics` method, | ||
* which returns aggregated metrics for all workers. | ||
* | ||
* In cluster workers, listens for and responds to requests for metrics by the | ||
* cluster master. | ||
*/ | ||
|
||
const cluster = require('cluster'); | ||
const Registry = require('./registry'); | ||
const AGGREGATORS = require('./metricAggregators').AGGREGATORS; | ||
|
||
const GET_METRICS_REQ = 'prom-client:getMetricsReq'; | ||
const GET_METRICS_RES = 'prom-client:getMetricsRes'; | ||
|
||
// Default metrics that don't use summation: | ||
const DEFAULT_METRIC_AGGREGATORS = { | ||
nodejs_version_info: AGGREGATORS.FIRST, | ||
process_start_time_seconds: AGGREGATORS.OMIT, | ||
nodejs_eventloop_lag_seconds: AGGREGATORS.AVERAGE | ||
}; | ||
|
||
let requestCtr = 0; // Concurrency control | ||
const requests = new Map(); // Pending requests for workers' local metrics. | ||
|
||
class AggregatorRegistry extends Registry { | ||
/** | ||
* Gets aggregated metrics for all workers. | ||
* @param {Function} callback (err, metrics) => any | ||
* @return {undefined} undefined | ||
*/ | ||
clusterMetrics(callback) { | ||
const requestId = requestCtr++; | ||
|
||
const request = { | ||
responses: [], | ||
pending: Object.keys(cluster.workers).length, | ||
callback, | ||
errorTimeout: setTimeout(() => { | ||
request.failed = true; | ||
request.callback(new Error('Operation timed out.')); | ||
}, 5000), | ||
failed: false | ||
}; | ||
requests.set(requestId, request); | ||
|
||
const message = { | ||
type: GET_METRICS_REQ, | ||
requestId | ||
}; | ||
for (const id in cluster.workers) cluster.workers[id].send(message); | ||
} | ||
|
||
/** | ||
* Creates a new Registry instance from an array of metrics that were | ||
* created by `registry.getMetricsAsJSON()`. Metrics registered on the | ||
* globalRegistry are aggregated using their `aggregate` method if defined, | ||
* or by summation if not registered or missing an `aggregate` method. | ||
* @param {Array} metricsArr Array of metrics, each of which created by | ||
* `registry.getMetricsAsJSON()`. | ||
* @return {Registry} aggregated registry. | ||
*/ | ||
static aggregate(metricsArr) { | ||
const aggregatedRegistry = new Registry(); | ||
const metricsByName = new Map(); | ||
|
||
// Gather by name | ||
metricsArr.forEach(metrics => { | ||
metrics.forEach(metric => { | ||
if (metricsByName.has(metric.name)) { | ||
metricsByName.get(metric.name).push(metric); | ||
} else { | ||
metricsByName.set(metric.name, [metric]); | ||
} | ||
}); | ||
}); | ||
|
||
// Aggregate gathered metrics. Default to summation. | ||
metricsByName.forEach((metrics, metricName) => { | ||
let aggregatedMetric; | ||
const metric = Registry.globalRegistry.getSingleMetric(metricName); | ||
const defaultMetricAggregator = DEFAULT_METRIC_AGGREGATORS[metricName]; | ||
if (metric && metric.aggregate) { | ||
// Metric is in global registry and has aggregator fn | ||
aggregatedMetric = metric.aggregate(metrics); | ||
} else if (defaultMetricAggregator) { | ||
// Metric is not in global registry or has no aggregator fn, | ||
// but is a default metric | ||
aggregatedMetric = defaultMetricAggregator(metrics); | ||
} else { | ||
aggregatedMetric = AGGREGATORS.SUM(metrics); | ||
} | ||
if (aggregatedMetric) { | ||
// OMIT aggregator returns undefined | ||
const aggregatedMetricWrapper = Object.assign( | ||
{ | ||
get: () => aggregatedMetric | ||
}, | ||
aggregatedMetric | ||
); | ||
aggregatedRegistry.registerMetric(aggregatedMetricWrapper); | ||
} | ||
}); | ||
|
||
return aggregatedRegistry; | ||
} | ||
} | ||
|
||
module.exports = AggregatorRegistry; | ||
|
||
if (cluster.isMaster) { | ||
// Listen for worker responses to requests for local metrics | ||
cluster.on('message', (worker, message) => { | ||
if (arguments.length === 2) { | ||
// pre-Node.js v6.0 | ||
message = worker; | ||
worker = undefined; | ||
} | ||
|
||
if (message.type === GET_METRICS_RES) { | ||
const request = requests.get(message.requestId); | ||
request.responses.push(message.metrics); | ||
request.pending--; | ||
|
||
if (request.pending === 0) { | ||
// finalize | ||
requests.delete(message.requestId); | ||
clearTimeout(request.errorTimeout); | ||
|
||
if (request.failed) return; // Callback already run with Error. | ||
|
||
const registry = Registry.aggregate(request.responses); | ||
const promString = registry.metrics(); | ||
request.callback(null, promString); | ||
} | ||
} | ||
}); | ||
} else if (cluster.isWorker) { | ||
// Respond to master's requests for worker's local metrics. | ||
process.on('message', message => { | ||
if (message.type === GET_METRICS_REQ) { | ||
process.send({ | ||
type: GET_METRICS_RES, | ||
requestId: message.requestId, | ||
metrics: Registry.globalRegistry.getMetricsAsJSON() | ||
}); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.