From e3274d676560bf7600991ce931d77119247ae578 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 25 Oct 2017 17:23:09 +0900 Subject: [PATCH] (prometheus) show label name in paren after by/without/on/ignoring/group_left/group_right --- .../datasource/prometheus/completer.ts | 338 +++++++++++++----- .../datasource/prometheus/mode-prometheus.js | 46 ++- .../prometheus/specs/completer_specs.ts | 87 ++++- 3 files changed, 360 insertions(+), 111 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 958131feac1..e9b17f14143 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -19,66 +19,22 @@ export class PromCompleter { getCompletions(editor, session, pos, prefix, callback) { let token = session.getTokenAt(pos.row, pos.column); - var metricName; switch (token.type) { - 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; - } - - 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); + case 'entity.name.tag.label-matcher': + this.getCompletionsForLabelMatcherName(session, pos).then(completions => { + callback(null, completions); }); - 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; - } - - 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; + case 'string.quoted.label-matcher': + this.getCompletionsForLabelMatcherValue(session, pos).then(completions => { + callback(null, completions); }); + return; + case 'entity.name.tag.label-list-matcher': + this.getCompletionsForBinaryOperator(session, pos).then(completions => { + callback(null, completions); + }); + return; } if (token.type === 'paren.lparen' && token.value === '[') { @@ -127,17 +83,186 @@ export class PromCompleter { }); } - getLabelNameAndValueForMetric(metricName) { - if (this.labelQueryCache[metricName]) { - return Promise.resolve(this.labelQueryCache[metricName]); + getCompletionsForLabelMatcherName(session, pos) { + let metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); } - var op = '=~'; - if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) { - op = '='; + + if (this.labelNameCache[metricName]) { + return Promise.resolve(this.labelNameCache[metricName]); } - 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 this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { + var labelNames = this.transformToCompletions( + _.uniq( + _.flatten( + result.map(r => { + return Object.keys(r.metric); + }) + ) + ), + 'label name' + ); + this.labelNameCache[metricName] = labelNames; + return Promise.resolve(labelNames); + }); + } + + getCompletionsForLabelMatcherValue(session, pos) { + let metricName = this.findMetricName(session, pos.row, pos.column); + if (!metricName) { + return Promise.resolve([]); + } + + var labelNameToken = this.findToken( + session, + pos.row, + pos.column, + 'entity.name.tag.label-matcher', + null, + 'paren.lparen.label-matcher' + ); + if (!labelNameToken) { + return Promise.resolve([]); + } + var labelName = labelNameToken.value; + + if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) { + return Promise.resolve(this.labelValueCache[metricName][labelName]); + } + + return this.getLabelNameAndValueForExpression(metricName, '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; + return Promise.resolve(labelValues); + }); + } + + getCompletionsForBinaryOperator(session, pos) { + let keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); + if (!keywordOperatorToken) { + return Promise.resolve([]); + } + let rparenToken, expr; + switch (keywordOperatorToken.value) { + case 'by': + case 'without': + rparenToken = this.findToken( + session, + keywordOperatorToken.row, + keywordOperatorToken.column, + 'paren.rparen', + null, + 'identifier' + ); + if (!rparenToken) { + return Promise.resolve([]); + } + expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column); + if (expr === '') { + return Promise.resolve([]); + } + return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => { + var labelNames = this.transformToCompletions( + _.uniq( + _.flatten( + result.map(r => { + return Object.keys(r.metric); + }) + ) + ), + 'label name' + ); + this.labelNameCache[expr] = labelNames; + return labelNames; + }); + case 'on': + case 'ignoring': + case 'group_left': + case 'group_right': + let binaryOperatorToken = this.findToken( + session, + keywordOperatorToken.row, + keywordOperatorToken.column, + 'keyword.operator.binary', + null, + 'identifier' + ); + if (!binaryOperatorToken) { + return Promise.resolve([]); + } + rparenToken = this.findToken( + session, + binaryOperatorToken.row, + binaryOperatorToken.column, + 'paren.rparen', + null, + 'identifier' + ); + if (rparenToken) { + expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column); + if (expr === '') { + return Promise.resolve([]); + } + return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => { + var labelNames = this.transformToCompletions( + _.uniq( + _.flatten( + result.map(r => { + return Object.keys(r.metric); + }) + ) + ), + 'label name' + ); + this.labelNameCache[expr] = labelNames; + return labelNames; + }); + } else { + let metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); + return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { + var labelNames = this.transformToCompletions( + _.uniq( + _.flatten( + result.map(r => { + return Object.keys(r.metric); + }) + ) + ), + 'label name' + ); + this.labelNameCache[metricName] = labelNames; + return Promise.resolve(labelNames); + }); + } + } + + return Promise.resolve([]); + } + + getLabelNameAndValueForExpression(expr, type) { + if (this.labelQueryCache[expr]) { + return Promise.resolve(this.labelQueryCache[expr]); + } + let query = expr; + if (type === 'metricName') { + let op = '=~'; + if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(expr)) { + op = '='; + } + query = '{__name__' + op + '"' + expr + '"}'; + } + return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => { + this.labelQueryCache[expr] = response.data.data.result; return response.data.data.result; }); } @@ -157,20 +282,25 @@ export class PromCompleter { var metricName = ''; var tokens; - var nameLabelNameToken = this.findToken(session, row, column, 'entity.name.tag', '__name__', 'paren.lparen'); + var nameLabelNameToken = this.findToken( + session, + row, + column, + 'entity.name.tag.label-matcher', + '__name__', + 'paren.lparen.label-matcher' + ); if (nameLabelNameToken) { tokens = session.getTokens(nameLabelNameToken.row); var nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted') { + if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted.label-matcher') { 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; - } + metricName = metricNameToken.value; } } @@ -179,11 +309,57 @@ export class PromCompleter { findToken(session, row, column, target, value, guard) { var tokens, idx; + // find index and get column of previous token for (var r = row; r >= 0; r--) { + let c; tokens = session.getTokens(r); if (r === row) { // current row - var c = 0; + c = 0; + for (idx = 0; idx < tokens.length; idx++) { + let nc = c + tokens[idx].value.length; + if (nc >= column) { + break; + } + c = nc; + } + } else { + idx = tokens.length - 1; + c = + _.sum( + tokens.map(t => { + return t.value.length; + }) + ) - tokens[tokens.length - 1].value.length; + } + + 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].column = c; + tokens[idx].index = idx; + return tokens[idx]; + } + c -= tokens[idx].value.length; + } + } + + return null; + } + + findExpressionMatchedParen(session, row, column) { + let tokens, idx; + let deep = 1; + let expression = ')'; + for (let r = row; r >= 0; r--) { + tokens = session.getTokens(r); + if (r === row) { + // current row + let c = 0; for (idx = 0; idx < tokens.length; idx++) { c += tokens[idx].value.length; if (c >= column) { @@ -195,18 +371,18 @@ export class PromCompleter { } 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]; + expression = tokens[idx].value + expression; + if (tokens[idx].type === 'paren.rparen') { + deep++; + } else if (tokens[idx].type === 'paren.lparen') { + deep--; + if (deep === 0) { + return expression; + } } } } - return null; + return expression; } } diff --git a/public/app/plugins/datasource/prometheus/mode-prometheus.js b/public/app/plugins/datasource/prometheus/mode-prometheus.js index 60f9a0501ac..f49c39c698c 100644 --- a/public/app/plugins/datasource/prometheus/mode-prometheus.js +++ b/public/app/plugins/datasource/prometheus/mode-prometheus.js @@ -8,7 +8,6 @@ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; var PrometheusHighlightRules = function() { var keywords = ( - "by|without|keep_common|offset|bool|and|or|unless|ignoring|on|group_left|group_right|" + "count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile" ); @@ -41,45 +40,66 @@ var PrometheusHighlightRules = function() { }, { token : "constant.language", // time regex : "\\d+[smhdwy]" + }, { + token : "keyword.operator.binary", + regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>|and|or|unless" + }, { + token : "keyword.other", + regex : "keep_common|offset|bool" + }, { + token : "keyword.control", + regex : "by|without|on|ignoring|group_left|group_right", + next : "start-label-list-matcher" }, { token : keywordMapper, regex : "[a-zA-Z_:][a-zA-Z0-9_:]*" - }, { - token : "keyword.operator", - regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>" }, { token : "paren.lparen", regex : "[[(]" }, { - token : "paren.lparen", + token : "paren.lparen.label-matcher", regex : "{", next : "start-label-matcher" }, { token : "paren.rparen", regex : "[\\])]" }, { - token : "paren.rparen", + token : "paren.rparen.label-matcher", regex : "}" }, { token : "text", regex : "\\s+" } ], "start-label-matcher" : [ { - token : "entity.name.tag", + token : "entity.name.tag.label-matcher", regex : '[a-zA-Z_][a-zA-Z0-9_]*' }, { - token : "keyword.operator", + token : "keyword.operator.label-matcher", regex : '=~|=|!~|!=' }, { - token : "string.quoted", + token : "string.quoted.label-matcher", regex : '"[^"]*"|\'[^\']*\'' }, { - token : "punctuation.operator", + token : "punctuation.operator.label-matcher", regex : "," }, { - token : "paren.rparen", + token : "paren.rparen.label-matcher", regex : "}", next : "start" + } ], + "start-label-list-matcher" : [ { + token : "paren.lparen.label-list-matcher", + regex : "[(]" + }, { + token : "entity.name.tag.label-list-matcher", + regex : '[a-zA-Z_][a-zA-Z0-9_]*' + }, { + token : "punctuation.operator.label-list-matcher", + regex : "," + }, { + token : "paren.rparen.label-list-matcher", + regex : "[)]", + next : "start" } ] }; @@ -395,7 +415,9 @@ var PrometheusCompletions = function() {}; (function() { this.getCompletions = function(state, session, pos, prefix, callback) { var token = session.getTokenAt(pos.row, pos.column); - if (token.type === 'entity.name.tag' || token.type === 'string.quoted') { + if (token.type === 'entity.name.tag.label-matcher' + || token.type === 'string.quoted.label-matcher' + || token.type === 'entity.name.tag.label-list-matcher') { 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 1c556a7101c..88cefcad1cf 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer_specs.ts @@ -61,16 +61,21 @@ describe('Prometheus editor completer', function() { it('Should return label name list', () => { const session = getSessionStub({ currentToken: { - type: 'entity.name.tag', + type: 'entity.name.tag.label-matcher', 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: '}' }, + { type: 'paren.lparen.label-matcher', value: '{' }, + { + type: 'entity.name.tag.label-matcher', + value: 'j', + index: 2, + start: 9, + }, + { type: 'paren.rparen.label-matcher', value: '}' }, ], line: 'node_cpu{j}', }); @@ -85,19 +90,24 @@ describe('Prometheus editor completer', function() { it('Should return label name list', () => { const session = getSessionStub({ currentToken: { - type: 'entity.name.tag', + type: 'entity.name.tag.label-matcher', 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: '}' }, + { type: 'paren.lparen.label-matcher', value: '{' }, + { type: 'entity.name.tag.label-matcher', value: '__name__' }, + { type: 'keyword.operator.label-matcher', value: '=~' }, + { type: 'string.quoted.label-matcher', value: '"node_cpu"' }, + { type: 'punctuation.operator.label-matcher', value: ',' }, + { + type: 'entity.name.tag.label-matcher', + value: 'j', + index: 5, + start: 22, + }, + { type: 'paren.rparen.label-matcher', value: '}' }, ], line: '{__name__=~"node_cpu",j}', }); @@ -112,18 +122,23 @@ describe('Prometheus editor completer', function() { it('Should return label value list', () => { const session = getSessionStub({ currentToken: { - type: 'string.quoted', + type: 'string.quoted.label-matcher', 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: '}' }, + { type: 'paren.lparen.label-matcher', value: '{' }, + { type: 'entity.name.tag.label-matcher', value: 'job' }, + { type: 'keyword.operator.label-matcher', value: '=' }, + { + type: 'string.quoted.label-matcher', + value: '"n"', + index: 4, + start: 13, + }, + { type: 'paren.rparen.label-matcher', value: '}' }, ], line: 'node_cpu{job="n"}', }); @@ -133,4 +148,40 @@ describe('Prometheus editor completer', function() { }); }); }); + + describe('When inside by', () => { + it('Should return label name list', () => { + const session = getSessionStub({ + currentToken: { + type: 'entity.name.tag.label-list-matcher', + value: 'm', + index: 9, + start: 22, + }, + tokens: [ + { type: 'paren.lparen', value: '(' }, + { type: 'keyword', value: 'count' }, + { type: 'paren.lparen', value: '(' }, + { type: 'identifier', value: 'node_cpu' }, + { type: 'paren.rparen', value: '))' }, + { type: 'text', value: ' ' }, + { type: 'keyword.control', value: 'by' }, + { type: 'text', value: ' ' }, + { type: 'paren.lparen.label-list-matcher', value: '(' }, + { + type: 'entity.name.tag.label-list-matcher', + value: 'm', + index: 9, + start: 22, + }, + { type: 'paren.rparen.label-list-matcher', value: ')' }, + ], + line: '(count(node_cpu)) by (m)', + }); + + return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'm', (s, res) => { + expect(res[0].meta).to.eql('label name'); + }); + }); + }); });