-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
📈 PaaS-friendly metrics #4874
Merged
Merged
📈 PaaS-friendly metrics #4874
Changes from 75 commits
Commits
Show all changes
86 commits
Select commit
Hold shift + click to select a range
77d04df
prom-client JSON to InfluxDB line protocol converter
platan 119d610
Converts a metric with separate names
platan f0a14c8
prom-client JSON to InfluxDB line protocol (version 2) converter
platan b0dc8a7
Server has instance id
platan f844e9f
Read the instance id from an environment variable
platan dcd34cc
More unit tests for instance-metadata
platan 2ef4820
Log instance id
platan 8a74971
Push influx metrics
platan 1fd5bc1
INSTANCE_ID with dyno metadata
platan 1029661
Prepare influx metrics in one place
platan a95079d
Influx metrics endpoint should return metrics
platan c89cfc1
More readable tests
platan 2ea4311
Env added to instance metadata
platan 9adf0cb
hostname as an instance label value
platan 2e7fa64
HEROKU_DYNO_ID as an instance id for heroku
platan c91c667
Instance env can be set by env variable
platan 2a4f0e0
HEROKU_APP_NAME as an instance env
platan fb2fb9b
Log instance metadata as a JSON
platan 44bf13c
Typo fix
platan d755b6f
Code refactoring in tests
platan 5680463
Merge remote-tracking branch 'badges/master' into paas-friendly-metrics
platan aaa1d1d
wait-for-expect dev dependency added
platan 1d811ea
Test for pushing metrics
platan dfe2721
Test for pushing metrics
platan 61f9e3c
Use basic authentication for pushing metrics
platan fafc622
intervalSeconds=2 for development env
platan c297eba
Using existing methods
platan b7749e0
TODOs removed
platan 1a9fc93
Schema for influx credentials
platan 6792b6a
Influx config removed from config files
platan 628b5b9
Require username and password when influx metrics are enabled
platan 6fe4956
Unused args removed
platan a59a94f
pushing component should log errors
platan 2c1b3b2
Speed up tests
platan d259568
should log error responses
platan 633362a
InstanceMetadata class replaces by simple object
platan ade7c2c
Influx metrics can be configuredd by env variables
platan 1282cf5
Use application label name instead of service
platan ee0082c
Unused code removed
platan 1bff31c
Integration test for prom-client and converter
platan d2eb269
metrics.influx.enabled configuration option added
platan a15a7e9
Improved influx configuration schema
platan 8c46bce
instanceMetadata validation
platan 31f5cc9
Merge remote-tracking branch 'badges/master' into paas-friendly-metrics
platan d091d3b
Typo fix
platan 96d4086
Default value for env
platan 68b6e72
metrics.infux.hostnameAsAInstanceId added
platan b2f9583
should add hostname as an instance label when hostnameAsAInstanceId i…
platan 1f55d31
Default values for influx configuration
platan a43d423
Merge remote-tracking branch 'badges/master' into paas-friendly-metrics
platan e2e6dfd
flatMap is not available in Node.js 9.4
platan 6d6b78e
Env vars removed from Procfile
platan 749d2e1
Merge remote-tracking branch 'badges/master' into paas-friendly-metrics
platan b63be09
Better instance metadata values in tests
platan 282ce88
Typo fix
platan 8a091c7
lodash.groupby added to prod dependencies
platan 0970871
Allow other keys in private config
platan 78bb612
Missing test - should allow other private keys when influx metrics ar…
platan 8542af3
Missing test - should require private metrics config when influx conf…
platan 95d42ad
log.error instead of console.log
platan acc35fe
metrics.influx.uri -> metrics.influx.url
platan 86beb55
Unused arguments removed
platan 8ff4743
async removed
platan 55af35b
Merge remote-tracking branch 'badges/master' into paas-friendly-metrics
platan e15ce4e
promisify sendMetrics
platan 0abf2ef
Allow to disable prometheus metrics
platan 5582cee
Create test server with custom config
platan 14a0f98
'metrics-influx' resource removed
platan fe5b4b9
'metrics-influx' resource removed
platan 5054400
Private config schema flattened out
platan 3418797
Merge branch 'master' into paas-friendly-metrics
platan 92bb991
Extra code removed in Prometheus tests
platan 0632a63
promisify moved outside of the class
platan 18939dc
Do not throw errors from got in a specific test
platan bc79028
hostnameAliases added
platan 9f7953e
instanceIdFrom added
platan 43af149
instanceIdEnvVarName added
platan aac697d
envLabel added to schema
platan 64915a0
instanceMetadata is not used by InfluxMetrics
platan 95c763a
Instance metadata removed
platan 53b29b4
hostnameAsAnInstanceId removed
platan d621ed8
A comment added
platan 3d8e385
waitForExpect removed
platan 32ec8cc
Merge branch 'master' into paas-friendly-metrics
platan 1e2999c
Unused code removed
platan 7f75fa0
Merge branch 'master' into paas-friendly-metrics
platan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
---|---|---|
|
@@ -2,6 +2,7 @@ public: | |
metrics: | ||
prometheus: | ||
enabled: true | ||
endpointEnabled: true | ||
|
||
ssl: | ||
isSecure: true | ||
|
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,72 @@ | ||
'use strict' | ||
|
||
const { promisify } = require('util') | ||
const { post } = require('request') | ||
const postAsync = promisify(post) | ||
const { promClientJsonToInfluxV2 } = require('./metrics/format-converters') | ||
const log = require('./log') | ||
|
||
module.exports = class InfluxMetrics { | ||
constructor(metricInstance, instanceMetadata, config) { | ||
this._metricInstance = metricInstance | ||
this._instanceMetadata = instanceMetadata | ||
this._config = config | ||
} | ||
|
||
async sendMetrics() { | ||
const auth = { | ||
user: this._config.username, | ||
pass: this._config.password, | ||
} | ||
const request = { | ||
uri: this._config.url, | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: this.metrics(), | ||
timeout: this._config.timeoutMillseconds, | ||
auth, | ||
} | ||
|
||
let response | ||
try { | ||
response = await postAsync(request) | ||
} catch (error) { | ||
log.error( | ||
new Error(`Cannot push metrics. Cause: ${error.name}: ${error.message}`) | ||
) | ||
} | ||
if (response && response.statusCode >= 300) { | ||
log.error( | ||
new Error( | ||
`Cannot push metrics. ${response.request.href} responded with status code ${response.statusCode}` | ||
) | ||
) | ||
} | ||
} | ||
|
||
startPushingMetrics() { | ||
this._intervalId = setInterval( | ||
() => this.sendMetrics(), | ||
this._config.intervalSeconds * 1000 | ||
) | ||
} | ||
|
||
metrics() { | ||
const { env, hostname, id } = this._instanceMetadata | ||
const { hostnameAliases = {}, hostnameAsAnInstanceId } = this._config | ||
const instance = hostnameAsAnInstanceId | ||
? hostnameAliases[hostname] || hostname | ||
: id | ||
return promClientJsonToInfluxV2(this._metricInstance.metrics(), { | ||
env, | ||
application: 'shields', | ||
instance, | ||
}) | ||
} | ||
|
||
stopPushingMetrics() { | ||
if (this._intervalId) { | ||
clearInterval(this._intervalId) | ||
this._intervalId = undefined | ||
} | ||
} | ||
} |
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,177 @@ | ||
'use strict' | ||
|
||
const nock = require('nock') | ||
const sinon = require('sinon') | ||
const waitForExpect = require('wait-for-expect') | ||
const { expect } = require('chai') | ||
const log = require('./log') | ||
const InfluxMetrics = require('./influx-metrics') | ||
require('../register-chai-plugins.spec') | ||
describe('Influx metrics', function() { | ||
const metricInstance = { | ||
metrics() { | ||
return [ | ||
{ | ||
help: 'counter 1 help', | ||
name: 'counter1', | ||
type: 'counter', | ||
values: [{ value: 11, labels: {} }], | ||
aggregator: 'sum', | ||
}, | ||
] | ||
}, | ||
} | ||
const instanceMetadata = { | ||
id: 'instance2', | ||
env: 'test-env', | ||
} | ||
const config = {} | ||
describe('"metrics" function', function() { | ||
it('should add instance id as an instance label', async function() { | ||
const influxMetrics = new InfluxMetrics( | ||
metricInstance, | ||
instanceMetadata, | ||
config | ||
) | ||
|
||
expect(influxMetrics.metrics()).to.contain('instance=instance2') | ||
}) | ||
|
||
it('should add hostname as an instance label when hostnameAsAnInstanceId is enabled', async function() { | ||
const instanceMetadata = { | ||
id: 'instance2', | ||
env: 'test-env', | ||
hostname: 'test-hostname', | ||
} | ||
const customConfig = { | ||
hostnameAsAnInstanceId: true, | ||
} | ||
const influxMetrics = new InfluxMetrics( | ||
metricInstance, | ||
instanceMetadata, | ||
customConfig | ||
) | ||
|
||
expect(influxMetrics.metrics()).to.be.contain('instance=test-hostname') | ||
}) | ||
|
||
it('should use a hostname alias as an instance label', async function() { | ||
const instanceMetadata = { | ||
id: 'instance2', | ||
env: 'test-env', | ||
hostname: 'test-hostname', | ||
} | ||
const customConfig = { | ||
hostnameAsAnInstanceId: true, | ||
hostnameAliases: { 'test-hostname': 'test-hostname-aliass' }, | ||
} | ||
const influxMetrics = new InfluxMetrics( | ||
metricInstance, | ||
instanceMetadata, | ||
customConfig | ||
) | ||
|
||
expect(influxMetrics.metrics()).to.be.contain( | ||
'instance=test-hostname-alias' | ||
) | ||
}) | ||
}) | ||
|
||
describe('startPushingMetrics', function() { | ||
let influxMetrics | ||
afterEach(function() { | ||
influxMetrics.stopPushingMetrics() | ||
nock.cleanAll() | ||
nock.enableNetConnect() | ||
}) | ||
it('should send metrics', async function() { | ||
const scope = nock('http://shields-metrics.io/', { | ||
reqheaders: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
}, | ||
}) | ||
.persist() | ||
.post( | ||
'/metrics', | ||
'prometheus,application=shields,env=test-env,instance=instance2 counter1=11' | ||
) | ||
.basicAuth({ user: 'metrics-username', pass: 'metrics-password' }) | ||
.reply(200) | ||
influxMetrics = new InfluxMetrics(metricInstance, instanceMetadata, { | ||
url: 'http://shields-metrics.io/metrics', | ||
timeoutMillseconds: 100, | ||
intervalSeconds: 0, | ||
username: 'metrics-username', | ||
password: 'metrics-password', | ||
}) | ||
|
||
influxMetrics.startPushingMetrics() | ||
|
||
await waitForExpect( | ||
() => { | ||
expect(scope.isDone()).to.be.equal( | ||
true, | ||
`pending mocks: ${scope.pendingMocks()}` | ||
) | ||
}, | ||
1000, | ||
1 | ||
) | ||
}) | ||
}) | ||
|
||
describe('sendMetrics', function() { | ||
beforeEach(function() { | ||
sinon.spy(log, 'error') | ||
}) | ||
afterEach(function() { | ||
log.error.restore() | ||
nock.cleanAll() | ||
nock.enableNetConnect() | ||
}) | ||
|
||
const influxMetrics = new InfluxMetrics(metricInstance, instanceMetadata, { | ||
url: 'http://shields-metrics.io/metrics', | ||
timeoutMillseconds: 50, | ||
intervalSeconds: 0, | ||
username: 'metrics-username', | ||
password: 'metrics-password', | ||
}) | ||
it('should log errors', async function() { | ||
nock.disableNetConnect() | ||
|
||
await influxMetrics.sendMetrics() | ||
|
||
expect(log.error).to.be.calledWith( | ||
sinon.match | ||
.instanceOf(Error) | ||
.and( | ||
sinon.match.has( | ||
'message', | ||
'Cannot push metrics. Cause: NetConnectNotAllowedError: Nock: Disallowed net connect for "shields-metrics.io:80/metrics"' | ||
) | ||
) | ||
) | ||
}) | ||
|
||
it('should log error responses', async function() { | ||
nock('http://shields-metrics.io/') | ||
.persist() | ||
.post('/metrics') | ||
.reply(400) | ||
|
||
await influxMetrics.sendMetrics() | ||
|
||
expect(log.error).to.be.calledWith( | ||
sinon.match | ||
.instanceOf(Error) | ||
.and( | ||
sinon.match.has( | ||
'message', | ||
'Cannot push metrics. http://shields-metrics.io/metrics responded with status code 400' | ||
) | ||
) | ||
) | ||
}) | ||
}) | ||
}) |
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,10 @@ | ||
'use strict' | ||
|
||
function generateInstanceId() { | ||
// from https://gist.github.com/gordonbrander/2230317 | ||
return Math.random() | ||
.toString(36) | ||
.substr(2, 9) | ||
} | ||
|
||
module.exports = generateInstanceId |
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 groupBy = require('lodash.groupby') | ||
|
||
function promClientJsonToInfluxV2(metrics, extraLabels = {}) { | ||
// TODO Replace with Array.prototype.flatMap() after migrating to Node.js >= 11 | ||
const flatMap = (f, arr) => arr.reduce((acc, x) => acc.concat(f(x)), []) | ||
return flatMap(metric => { | ||
const valuesByLabels = groupBy(metric.values, value => | ||
JSON.stringify(Object.entries(value.labels).sort()) | ||
) | ||
return Object.values(valuesByLabels).map(metricsWithSameLabel => { | ||
const labels = Object.entries(metricsWithSameLabel[0].labels) | ||
.concat(Object.entries(extraLabels)) | ||
.sort((a, b) => a[0].localeCompare(b[0])) | ||
.map(labelEntry => `${labelEntry[0]}=${labelEntry[1]}`) | ||
.join(',') | ||
const labelsFormatted = labels ? `,${labels}` : '' | ||
const values = metricsWithSameLabel | ||
.sort((a, b) => a.metricName.localeCompare(b.metricName)) | ||
.map(value => `${value.metricName || metric.name}=${value.value}`) | ||
.join(',') | ||
return `prometheus${labelsFormatted} ${values}` | ||
}) | ||
}, metrics).join('\n') | ||
} | ||
|
||
module.exports = { promClientJsonToInfluxV2 } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍