Переглянути джерело

Merge pull request #9208 from mtanda/prometheus_ace_complete_improve_label_name_complete

(prometheus) support label name/value completion
Carl Bergquist 8 роки тому
батько
коміт
8e4efeeece

+ 142 - 11
public/app/plugins/datasource/prometheus/completer.ts

@@ -1,29 +1,77 @@
 ///<reference path="../../../headers/common.d.ts" />
 
 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;
+  }
+
 }

+ 3 - 3
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, []);
     }
 

+ 92 - 5
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 sessionData = {
+    currentToken: {},
+    tokens: [],
+    line: ''
+  };
   let session = {
-    getTokenAt: sinon.stub().returns({}),
-    getLine:  sinon.stub().returns(""),
+    getTokenAt: sinon.stub().returns(sessionData.currentToken),
+    getTokens: sinon.stub().returns(sessionData.tokens),
+    getLine: sinon.stub().returns(sessionData.line),
   };
+  let editor = { session: session };
 
-  let datasourceStub = <PrometheusDatasource>{};
+  let datasourceStub = <PrometheusDatasource>{
+    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'});
+      });
+    });
+
+  });
+
 });