diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 2651f75df98..4c4c86cb3e3 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -1,29 +1,77 @@ /// 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': - callback(null, ['instance', 'job'].map(function (key) { - return { - caption: key, - value: key, - meta: "label name", - score: Number.MAX_VALUE - }; - })); + case 'entity.name.tag': + metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); + return; + } + + if (this.labelNameCache[metricName]) { + callback(null, this.labelNameCache[metricName]); + return; + } + + this.getLabelNameAndValueForMetric(metricName).then(result => { + var labelNames = this.transformToCompletions( + _.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, []); + 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, 'entity.name.tag', 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; } @@ -56,4 +104,87 @@ export class PromCompleter { }); } + 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: meta, + score: Number.MAX_VALUE + }; + }); + } + + findMetricName(session, row, column) { + var metricName = ''; + + var tokens; + 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.quoted') { + 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/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, []); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts index 37d428859ae..5d160453e13 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().withArgs({ expr: '{__name__="node_cpu"' }).returns(Promise.resolve( + [ + { + metric: { + job: 'node', + instance: 'localhost:9100' + } + } + ] + )), + performSuggestQuery: sinon.stub().withArgs('node', true).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'}); + }); + }); + + }); + });