From 40b74e666446dc93c222899c8d62c68787f7dda2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 6 Sep 2017 20:50:32 +0900 Subject: [PATCH 1/6] (prometheus) support label name completion --- .../datasource/prometheus/completer.ts | 105 ++++++++++++++++-- .../datasource/prometheus/datasource.ts | 5 + 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index e1b0984399d..eaf3f034d67 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -1,11 +1,15 @@ /// import {PrometheusDatasource} from "./datasource"; +import _ from 'lodash'; export class PromCompleter { + labelNameCache: any; + identifierRegexps = [/[\[\]a-zA-Z_0-9=]/]; constructor(private datasource: PrometheusDatasource) { + this.labelNameCache = {}; } getCompletions(editor, session, pos, prefix, callback) { @@ -13,14 +17,31 @@ export class PromCompleter { switch (token.type) { case 'label.name': - callback(null, ['instance', 'job'].map(function (key) { - return { - caption: key, - value: key, - meta: "label name", - score: Number.MAX_VALUE - }; - })); + var metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + callback(null, this.transformToCompletions(['__name__', 'instance', 'job'])); + return; + } + + if (this.labelNameCache[metricName]) { + callback(null, this.labelNameCache[metricName]); + return; + } + + var op = '=~'; + if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { + op = '='; + } + var expr = '{__name__' + op + '"' + metricName + '"}'; + this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + var labelNames = this.transformToCompletions( + _.uniq(_.flatten(response.data.data.result.map(r => { + return Object.keys(r.metric); + }))) + ); + this.labelNameCache[metricName] = labelNames; + callback(null, labelNames); + }); return; case 'label.value': callback(null, []); @@ -57,4 +78,72 @@ export class PromCompleter { }); } + transformToCompletions(words) { + return words.map(name => { + return { + caption: name, + value: name, + meta: "label name", + score: Number.MAX_VALUE + }; + }); + } + + findMetricName(session, row, column) { + var metricName = ''; + + var tokens; + var nameLabelNameToken = this.findToken(session, row, column, 'label.name', '__name__', 'paren.lparen'); + if (nameLabelNameToken) { + tokens = session.getTokens(nameLabelNameToken.row); + var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; + if (nameLabelValueToken && nameLabelValueToken.type === 'label.value') { + metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation + } + } else { + var metricNameToken = this.findToken(session, row, column, 'identifier', null, null); + if (metricNameToken) { + tokens = session.getTokens(metricNameToken.row); + if (tokens[metricNameToken.index + 1].type === 'paren.lparen') { + metricName = metricNameToken.value; + } + } + } + + return metricName; + } + + findToken(session, row, column, target, value, guard) { + var tokens, idx; + for (var r = row; r >= 0; r--) { + tokens = session.getTokens(r); + if (r === row) { // current row + var c = 0; + for (idx = 0; idx < tokens.length; idx++) { + c += tokens[idx].value.length; + if (c >= column) { + break; + } + } + } else { + idx = tokens.length - 1; + } + + for (; idx >= 0; idx--) { + if (tokens[idx].type === guard) { + return null; + } + + if (tokens[idx].type === target + && (!value || tokens[idx].value === value)) { + tokens[idx].row = r; + tokens[idx].index = idx; + return tokens[idx]; + } + } + } + + return null; + } + } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index dd071d09fe6..ad03607baf0 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -158,6 +158,11 @@ export class PrometheusDatasource { return this._request('GET', url, query.requestId); } + performInstantQuery(query, time) { + var url = '/api/v1/query?query=' + encodeURIComponent(query.expr) + '&time=' + time; + return this._request('GET', url, query.requestId); + } + performSuggestQuery(query, cache = false) { var url = '/api/v1/label/__name__/values'; From d530ccff1c2e40cc51f19d8734da0ec64e07ee6e Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 11 Sep 2017 22:09:05 +0900 Subject: [PATCH 2/6] (prometheus) support label value completion --- .../datasource/prometheus/completer.ts | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index eaf3f034d67..c6af64584d4 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -4,22 +4,27 @@ import {PrometheusDatasource} from "./datasource"; import _ from 'lodash'; export class PromCompleter { + labelQueryCache: any; labelNameCache: any; + labelValueCache: any; identifierRegexps = [/[\[\]a-zA-Z_0-9=]/]; constructor(private datasource: PrometheusDatasource) { + this.labelQueryCache = {}; this.labelNameCache = {}; + this.labelValueCache = {}; } getCompletions(editor, session, pos, prefix, callback) { let token = session.getTokenAt(pos.row, pos.column); + var metricName; switch (token.type) { case 'label.name': - var metricName = this.findMetricName(session, pos.row, pos.column); + metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { - callback(null, this.transformToCompletions(['__name__', 'instance', 'job'])); + callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); return; } @@ -28,23 +33,45 @@ export class PromCompleter { return; } - var op = '=~'; - if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { - op = '='; - } - var expr = '{__name__' + op + '"' + metricName + '"}'; - this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + this.getLabelNameAndValueForMetric(metricName).then(result => { var labelNames = this.transformToCompletions( - _.uniq(_.flatten(response.data.data.result.map(r => { + _.uniq(_.flatten(result.map(r => { return Object.keys(r.metric); }))) - ); + , 'label name'); this.labelNameCache[metricName] = labelNames; callback(null, labelNames); }); return; case 'label.value': - callback(null, []); + metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + callback(null, []); + return; + } + + var labelNameToken = this.findToken(session, pos.row, pos.column, 'label.name', null, 'paren.lparen'); + if (!labelNameToken) { + callback(null, []); + return; + } + var labelName = labelNameToken.value; + + if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) { + callback(null, this.labelValueCache[metricName][labelName]); + return; + } + + this.getLabelNameAndValueForMetric(metricName).then(result => { + var labelValues = this.transformToCompletions( + _.uniq(result.map(r => { + return r.metric[labelName]; + })) + , 'label value'); + this.labelValueCache[metricName] = this.labelValueCache[metricName] || {}; + this.labelValueCache[metricName][labelName] = labelValues; + callback(null, labelValues); + }); return; } @@ -78,12 +105,27 @@ export class PromCompleter { }); } - transformToCompletions(words) { + getLabelNameAndValueForMetric(metricName) { + if (this.labelQueryCache[metricName]) { + return Promise.resolve(this.labelQueryCache[metricName]); + } + var op = '=~'; + if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { + op = '='; + } + var expr = '{__name__' + op + '"' + metricName + '"}'; + return this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => { + this.labelQueryCache[metricName] = response.data.data.result; + return response.data.data.result; + }); + } + + transformToCompletions(words, meta) { return words.map(name => { return { caption: name, value: name, - meta: "label name", + meta: meta, score: Number.MAX_VALUE }; }); From 56c0d91ee5245f5b7690f2491aa7045a02dcc85d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Mon, 11 Sep 2017 23:49:56 +0900 Subject: [PATCH 3/6] follow token name change --- public/app/plugins/datasource/prometheus/completer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index c6af64584d4..577b872c21b 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -21,7 +21,7 @@ export class PromCompleter { var metricName; switch (token.type) { - case 'label.name': + case 'keyword': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); @@ -43,14 +43,14 @@ export class PromCompleter { callback(null, labelNames); }); return; - case 'label.value': + case 'string': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, []); return; } - var labelNameToken = this.findToken(session, pos.row, pos.column, 'label.name', null, 'paren.lparen'); + var labelNameToken = this.findToken(session, pos.row, pos.column, 'keyword', null, 'paren.lparen'); if (!labelNameToken) { callback(null, []); return; @@ -135,11 +135,11 @@ export class PromCompleter { var metricName = ''; var tokens; - var nameLabelNameToken = this.findToken(session, row, column, 'label.name', '__name__', 'paren.lparen'); + var nameLabelNameToken = this.findToken(session, row, column, 'keyword', '__name__', 'paren.lparen'); if (nameLabelNameToken) { tokens = session.getTokens(nameLabelNameToken.row); var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'label.value') { + if (nameLabelValueToken && nameLabelValueToken.type === 'string') { metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation } } else { From 1a5e786467afdce45bedbd46b846311889a2384d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 12 Sep 2017 09:31:05 +0900 Subject: [PATCH 4/6] fix --- public/app/plugins/datasource/prometheus/completer.ts | 10 +++++----- .../plugins/datasource/prometheus/mode-prometheus.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 577b872c21b..799822eb571 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -21,7 +21,7 @@ export class PromCompleter { var metricName; switch (token.type) { - case 'keyword': + case 'entity.name.tag': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); @@ -43,14 +43,14 @@ export class PromCompleter { callback(null, labelNames); }); return; - case 'string': + case 'string.quoted': metricName = this.findMetricName(session, pos.row, pos.column); if (!metricName) { callback(null, []); return; } - var labelNameToken = this.findToken(session, pos.row, pos.column, 'keyword', null, 'paren.lparen'); + var labelNameToken = this.findToken(session, pos.row, pos.column, 'entity.name.tag', null, 'paren.lparen'); if (!labelNameToken) { callback(null, []); return; @@ -135,11 +135,11 @@ export class PromCompleter { var metricName = ''; var tokens; - var nameLabelNameToken = this.findToken(session, row, column, 'keyword', '__name__', 'paren.lparen'); + var nameLabelNameToken = this.findToken(session, row, column, 'entity.name.tag', '__name__', 'paren.lparen'); if (nameLabelNameToken) { tokens = session.getTokens(nameLabelNameToken.row); var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'string') { + if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted') { metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation } } else { diff --git a/public/app/plugins/datasource/prometheus/mode-prometheus.js b/public/app/plugins/datasource/prometheus/mode-prometheus.js index 78edbbd30d1..165c1a364c4 100644 --- a/public/app/plugins/datasource/prometheus/mode-prometheus.js +++ b/public/app/plugins/datasource/prometheus/mode-prometheus.js @@ -65,13 +65,13 @@ var PrometheusHighlightRules = function() { regex : "\\s+" } ], "start-label-matcher" : [ { - token : "keyword", + token : "entity.name.tag", regex : '[a-zA-Z_][a-zA-Z0-9_]*' }, { token : "keyword.operator", regex : '=~|=|!~|!=' }, { - token : "string", + token : "string.quoted", regex : '"[^"]*"|\'[^\']*\'' }, { token : "punctuation.operator", @@ -401,7 +401,7 @@ var PrometheusCompletions = function() {}; (function() { this.getCompletions = function(state, session, pos, prefix, callback) { var token = session.getTokenAt(pos.row, pos.column); - if (token.type === 'label.name' || token.type === 'label.value') { + if (token.type === 'entity.name.tag' || token.type === 'string.quoted') { return callback(null, []); } From 6bf81447939246c81e9fa0ae6d485d7f871258a5 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 12 Sep 2017 20:05:07 +0900 Subject: [PATCH 5/6] add test for completer --- .../prometheus/specs/completer_specs.ts | 99 +++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 903ce0b5d3b..3d0371da7ed 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -5,23 +5,110 @@ import {PrometheusDatasource} from '../datasource'; describe('Prometheus editor completer', function() { - let editor = {}; - let session = { - getTokenAt: sinon.stub().returns({}), - getLine: sinon.stub().returns(""), + let sessionData = { + currentToken: {}, + tokens: [], + line: '' }; + let session = { + getTokenAt: sinon.stub().returns(sessionData.currentToken), + getTokens: sinon.stub().returns(sessionData.tokens), + getLine: sinon.stub().returns(sessionData.line), + }; + let editor = { session: session }; - let datasourceStub = {}; + let datasourceStub = { + performInstantQuery: sinon.stub().returns(Promise.resolve( + [ + { + metric: { + job: 'node', + instance: 'localhost:9100' + } + } + ] + )), + performSuggestQuery: sinon.stub().returns(Promise.resolve( + [ + 'node_cpu' + ] + )) + }; let completer = new PromCompleter(datasourceStub); describe("When inside brackets", () => { it("Should return range vectors", () => { - completer.getCompletions(editor, session, 10, "[", (s, res) => { + completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s, res) => { expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'}); }); }); }); + describe("When inside label matcher, and located at label name", () => { + sessionData = { + currentToken: { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, + tokens: [ + { type: 'identifier', value: 'node_cpu' }, + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: 'j', index: 2, start: 9 }, + { type: 'paren.rparen', value: '}' } + ], + line: 'node_cpu{j}' + }; + + it("Should return label name list", () => { + completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s, res) => { + expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + }); + }); + + }); + + describe("When inside label matcher, and located at label name with __name__ match", () => { + sessionData = { + currentToken: { type: 'entity.name.tag', value: 'j', index: 5, start: 22 }, + tokens: [ + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: '__name__' }, + { type: 'keyword.operator', value: '=~' }, + { type: 'string.quoted', value: '"node_cpu"' }, + { type: 'punctuation.operator', value: ',' }, + { type: 'entity.name.tag', value: 'j', 'index': 5, 'start': 22 }, + { type: 'paren.rparen', value: '}' } + ], + line: '{__name__=~"node_cpu",j}' + }; + + it("Should return label name list", () => { + completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s, res) => { + expect(res[0]).to.eql({caption: 'job', value: 'job', meta: 'label name'}); + }); + }); + + }); + + describe("When inside label matcher, and located at label value", () => { + sessionData = { + currentToken: { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, + tokens: [ + { type: 'identifier', value: 'node_cpu' }, + { type: 'paren.lparen', value: '{' }, + { type: 'entity.name.tag', value: 'job' }, + { type: 'keyword.operator', value: '=' }, + { type: 'string.quoted', value: '"n"', index: 4, start: 13 }, + { type: 'paren.rparen', value: '}' } + ], + line: 'node_cpu{job="n"}' + }; + + it("Should return label value list", () => { + completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s, res) => { + expect(res[0]).to.eql({caption: 'node', value: 'node', meta: 'label value'}); + }); + }); + + }); + }); From 5bdd554671fc5e0aaf0486405d7607886b4ae875 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Sep 2017 11:43:16 +0900 Subject: [PATCH 6/6] check args for query --- .../plugins/datasource/prometheus/specs/completer_specs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 3d0371da7ed..c9fe4fa61c1 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -18,7 +18,7 @@ describe('Prometheus editor completer', function() { let editor = { session: session }; let datasourceStub = { - performInstantQuery: sinon.stub().returns(Promise.resolve( + performInstantQuery: sinon.stub().withArgs({ expr: '{__name__="node_cpu"' }).returns(Promise.resolve( [ { metric: { @@ -28,7 +28,7 @@ describe('Prometheus editor completer', function() { } ] )), - performSuggestQuery: sinon.stub().returns(Promise.resolve( + performSuggestQuery: sinon.stub().withArgs('node', true).returns(Promise.resolve( [ 'node_cpu' ]