diff --git a/lib/prometheus.js b/lib/prometheus.js index 3c7ccf9..b4fc724 100644 --- a/lib/prometheus.js +++ b/lib/prometheus.js @@ -1,7 +1,103 @@ +module.exports = { + sanitize, + normalize +}; + function sanitize(name) { return name.replace(/[^a-zA-Z0-9:_]/g, '_'); } -module.exports = { - sanitize -}; +function normalize(name) { + let normalizedName = name; + const labels = {}; + + /** + * handling the following metrics patterns: + * consul.dns.ptr_query. + * consul.dns.domain_query. + * consul.raft.replication.appendEntries.logs. + * consul.raft.replication.appendEntries.rpc. + * consul.raft.replication.heartbeat. + * @type {RegExp} + */ + const $1 = new RegExp('(consul.(?:dns.(?:ptr_query|domain_query)|raft.replication.(?:appendEntries.(?:logs|rpc)|heartbeat)))[.](.*)'); + if ($1.test(normalizedName)) { + const match = $1.exec(normalizedName); + normalizedName = match[1]; + labels.node = sanitize(match[2]); + } + + /** + * handling the following metrics patterns: + * consul.http.. + * @type {RegExp} + */ + const $2 = new RegExp('(consul.http)[.]([^.]*)[.](.*)'); + if ($2.test(normalizedName)) { + const match = $2.exec(normalizedName); + normalizedName = match[1]; + labels.verb = sanitize(match[2]); + labels.path = sanitize(match[3]); + } + + /** + * handling the following metrics patterns: + * consul.fsm.acl. + * consul.fsm.session. + * consul.fsm.kvs. + * consul.fsm.tombstone. + * consul.fsm.prepared-query. + * @type {RegExp} + */ + const $3 = new RegExp('(consul.fsm.(?:acl|session|kvs|tombstone|prepared-query))[.](.*)'); + if ($3.test(normalizedName)) { + const match = $3.exec(normalizedName); + normalizedName = match[1]; + labels.op = sanitize(match[2]); + } + + /** + * handling the following metrics patterns: + * consul.catalog.service.query. + * consul.catalog.service.not-found. + * consul.health.service.query. + * consul.health.service.not-found. + * @type {RegExp} + */ + const $4 = new RegExp('(consul.(?:catalog|health).service.(?:query|not-found))[.](.*)'); + if ($4.test(normalizedName)) { + const match = $4.exec(normalizedName); + normalizedName = match[1]; + labels.service = sanitize(match[2]); + } + + /** + * handling the following metrics patterns: + * consul.catalog.service.query-tag.. + * consul.health.service.query-tag.. + * @type {RegExp} + */ + const $5 = new RegExp('(consul.(?:catalog|health).service.query-tag)[.]([^.]*)[.](.*)'); + if ($5.test(normalizedName)) { + const match = $5.exec(normalizedName); + normalizedName = match[1]; + labels.service = sanitize(match[2]); + labels.tag = sanitize(match[3]); + } + + /** + * handling the following metrics patterns: + * consul.consul.* - in order to remove the duplication + * @type {RegExp} + */ + const $6 = new RegExp('consul[.](consul.*)'); + if ($6.test(normalizedName)) { + const match = $6.exec(normalizedName); + normalizedName = match[1]; + } + + return { + name: normalizedName, + labels: labels + }; +} diff --git a/routes/metrics.js b/routes/metrics.js index f046970..f3ec7cd 100644 --- a/routes/metrics.js +++ b/routes/metrics.js @@ -46,12 +46,12 @@ function createPrometheusMetrics(result) { return res; function _handleCounter(counter) { - _setGauge(counter.Name + '_count', _.extend({'statistic': 'count'}, counter.Labels), counter.Count); - _setGauge(counter.Name + '_sum', _.extend({'statistic': 'sum'}, counter.Labels), counter.Sum); - _setGauge(counter.Name + '_min', _.extend({'statistic': 'min'}, counter.Labels), counter.Min); - _setGauge(counter.Name + '_max', _.extend({'statistic': 'max'}, counter.Labels), counter.Max); - _setGauge(counter.Name + '_mean', _.extend({'statistic': 'mean'}, counter.Labels), counter.Mean); - _setGauge(counter.Name + '_stddev', _.extend({'statistic': 'stddev'}, counter.Labels), counter.Stddev); + _setGauge(counter.Name, _.extend({'statistic': 'count'}, counter.Labels), counter.Count); + _setGauge(counter.Name, _.extend({'statistic': 'sum'}, counter.Labels), counter.Sum); + _setGauge(counter.Name, _.extend({'statistic': 'min'}, counter.Labels), counter.Min); + _setGauge(counter.Name, _.extend({'statistic': 'max'}, counter.Labels), counter.Max); + _setGauge(counter.Name, _.extend({'statistic': 'mean'}, counter.Labels), counter.Mean); + _setGauge(counter.Name, _.extend({'statistic': 'stddev'}, counter.Labels), counter.Stddev); } function _handleGauge(gauge) { @@ -59,14 +59,16 @@ function createPrometheusMetrics(result) { } function _setGauge(name, labels, value) { - let metricName = prometheus.sanitize(name); + const metric = prometheus.normalize(name); + const normalizedLabels = _.extend({}, metric.labels, labels); + const metricName = prometheus.sanitize(metric.name); const gaugeMetric = metrics[metricName] || new client.Gauge({ name: metricName.toLowerCase(), help: metricName.toLowerCase() + '_help', - labelNames: Object.keys(labels), + labelNames: Object.keys(normalizedLabels), registers: [registry] }); - gaugeMetric.set(labels, value); + gaugeMetric.set(normalizedLabels, value); metrics[metricName] = gaugeMetric; } } diff --git a/test/lib/prometheus.spec.js b/test/lib/prometheus.spec.js index 8632a5a..23e043d 100644 --- a/test/lib/prometheus.spec.js +++ b/test/lib/prometheus.spec.js @@ -12,4 +12,167 @@ describe('prometheus test', function () { expect(prometheus.sanitize('.')).to.be('_'); }); + it('should return consul.dns.ptr_query with node tag', function () { + expect(prometheus.normalize('consul.dns.ptr_query.a')).to.be.eql({ + name: 'consul.dns.ptr_query', + labels: { + node: 'a' + } + }); + }); + + it('should return consul.raft.replication.appendEntries.rpc with node tag', function () { + expect(prometheus.normalize('consul.raft.replication.appendEntries.rpc.58deeea6-0cc7-fc76-720f-b187be80f900')).to.be.eql({ + name: 'consul.raft.replication.appendEntries.rpc', + labels: { + node: '58deeea6_0cc7_fc76_720f_b187be80f900' + } + }); + }); + + it('should return consul.raft.replication.appendEntries.logs with node tag', function () { + expect(prometheus.normalize('consul.raft.replication.appendEntries.logs.58deeea6-0cc7-fc76-720f-b187be80f900')).to.be.eql({ + name: 'consul.raft.replication.appendEntries.logs', + labels: { + node: '58deeea6_0cc7_fc76_720f_b187be80f900' + } + }); + }); + + it('should return consul.raft.replication.heartbeat with node tag', function () { + expect(prometheus.normalize('consul.raft.replication.heartbeat.58deeea6-0cc7-fc76-720f-b187be80f900')).to.be.eql({ + name: 'consul.raft.replication.heartbeat', + labels: { + node: '58deeea6_0cc7_fc76_720f_b187be80f900' + } + }); + }); + + it('should return consul.dns.domain_query with node tag', function () { + expect(prometheus.normalize('consul.dns.domain_query.a')).to.be.eql({ + name: 'consul.dns.domain_query', + labels: { + node: 'a' + } + }); + }); + + it('should return consul.http with verb tag and path tag', function () { + expect(prometheus.normalize('consul.http.GET.v1.agent.metrics')).to.be.eql({ + name: 'consul.http', + labels: { + verb: 'GET', + path: 'v1_agent_metrics', + } + }); + }); + + it('should return consul.fsm.acl with op tag', function () { + expect(prometheus.normalize('consul.fsm.acl.a')).to.be.eql({ + name: 'consul.fsm.acl', + labels: { + op: 'a' + } + }); + }); + + it('should return consul.fsm.session with op tag', function () { + expect(prometheus.normalize('consul.fsm.session.a')).to.be.eql({ + name: 'consul.fsm.session', + labels: { + op: 'a' + } + }); + }); + + it('should return consul.fsm.kvs with op tag', function () { + expect(prometheus.normalize('consul.fsm.kvs.a')).to.be.eql({ + name: 'consul.fsm.kvs', + labels: { + op: 'a' + } + }); + }); + + it('should return consul.fsm.tombstone with op tag', function () { + expect(prometheus.normalize('consul.fsm.tombstone.a')).to.be.eql({ + name: 'consul.fsm.tombstone', + labels: { + op: 'a' + } + }); + }); + + it('should return consul.fsm.prepared-query with op tag', function () { + expect(prometheus.normalize('consul.fsm.prepared-query.a')).to.be.eql({ + name: 'consul.fsm.prepared-query', + labels: { + op: 'a' + } + }); + }); + + it('should return consul.catalog.service.query with service tag', function () { + expect(prometheus.normalize('consul.catalog.service.query.a')).to.be.eql({ + name: 'consul.catalog.service.query', + labels: { + service: 'a' + } + }); + }); + + it('should return consul.catalog.service.query-tag with service tag and tag tag', function () { + expect(prometheus.normalize('consul.catalog.service.query-tag.a.b')).to.be.eql({ + name: 'consul.catalog.service.query-tag', + labels: { + service: 'a', + tag: 'b' + } + }); + }); + + it('should return consul.catalog.service.not-found with service tag', function () { + expect(prometheus.normalize('consul.catalog.service.not-found.a')).to.be.eql({ + name: 'consul.catalog.service.not-found', + labels: { + service: 'a' + } + }); + }); + + it('should return consul.health.service.query with service tag', function () { + expect(prometheus.normalize('consul.health.service.query.a')).to.be.eql({ + name: 'consul.health.service.query', + labels: { + service: 'a' + } + }); + }); + + it('should return consul.health.service.query-tag with service tag and tag tag', function () { + expect(prometheus.normalize('consul.health.service.query-tag.a.b')).to.be.eql({ + name: 'consul.health.service.query-tag', + labels: { + service: 'a', + tag: 'b' + } + }); + }); + + it('should return consul.health.service.not-found with service tag', function () { + expect(prometheus.normalize('consul.health.service.not-found.a')).to.be.eql({ + name: 'consul.health.service.not-found', + labels: { + service: 'a' + } + }); + }); + + it('should return single consul prefix', function () { + expect(prometheus.normalize('consul.consul.health.service')).to.be.eql({ + name: 'consul.health.service', + labels: {} + }); + }); + });