completer.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import { PrometheusDatasource } from './datasource';
  2. import _ from 'lodash';
  3. import { TemplateSrv } from 'app/features/templating/template_srv';
  4. export interface CompleterPosition {
  5. row: number;
  6. column: number;
  7. }
  8. export interface CompleterToken {
  9. type: string;
  10. value: string;
  11. row: number;
  12. column: number;
  13. index: number;
  14. }
  15. export interface CompleterSession {
  16. getTokenAt: (row: number, column: number) => CompleterToken;
  17. getTokens: (row: number) => CompleterToken[];
  18. }
  19. export class PromCompleter {
  20. labelQueryCache: any;
  21. labelNameCache: any;
  22. labelValueCache: any;
  23. templateVariableCompletions: any;
  24. identifierRegexps = [/\[/, /[a-zA-Z0-9_:]/];
  25. constructor(private datasource: PrometheusDatasource, private templateSrv: TemplateSrv) {
  26. this.labelQueryCache = {};
  27. this.labelNameCache = {};
  28. this.labelValueCache = {};
  29. this.templateVariableCompletions = this.templateSrv.variables.map((variable: any) => {
  30. return {
  31. caption: '$' + variable.name,
  32. value: '$' + variable.name,
  33. meta: 'variable',
  34. score: Number.MAX_VALUE,
  35. };
  36. });
  37. }
  38. getCompletions(editor: any, session: CompleterSession, pos: CompleterPosition, prefix: string, callback: Function) {
  39. const wrappedCallback = (err: any, completions: any[]) => {
  40. completions = completions.concat(this.templateVariableCompletions);
  41. return callback(err, completions);
  42. };
  43. const token = session.getTokenAt(pos.row, pos.column);
  44. switch (token.type) {
  45. case 'entity.name.tag.label-matcher':
  46. this.getCompletionsForLabelMatcherName(session, pos).then(completions => {
  47. wrappedCallback(null, completions);
  48. });
  49. return;
  50. case 'string.quoted.label-matcher':
  51. this.getCompletionsForLabelMatcherValue(session, pos).then(completions => {
  52. wrappedCallback(null, completions);
  53. });
  54. return;
  55. case 'entity.name.tag.label-list-matcher':
  56. this.getCompletionsForBinaryOperator(session, pos).then(completions => {
  57. wrappedCallback(null, completions);
  58. });
  59. return;
  60. }
  61. if (token.type === 'paren.lparen' && token.value === '[') {
  62. const vectors = [];
  63. for (const unit of ['s', 'm', 'h']) {
  64. for (const value of [1, 5, 10, 30]) {
  65. vectors.push({
  66. caption: value + unit,
  67. value: '[' + value + unit,
  68. meta: 'range vector',
  69. });
  70. }
  71. }
  72. vectors.unshift({
  73. caption: '$__interval_ms',
  74. value: '[$__interval_ms',
  75. meta: 'range vector',
  76. });
  77. vectors.unshift({
  78. caption: '$__interval',
  79. value: '[$__interval',
  80. meta: 'range vector',
  81. });
  82. wrappedCallback(null, vectors);
  83. return;
  84. }
  85. const query = prefix;
  86. return this.datasource.performSuggestQuery(query, true).then((metricNames: string[]) => {
  87. wrappedCallback(
  88. null,
  89. metricNames.map(name => {
  90. let value = name;
  91. if (prefix === '(') {
  92. value = '(' + name;
  93. }
  94. return {
  95. caption: name,
  96. value: value,
  97. meta: 'metric',
  98. };
  99. })
  100. );
  101. });
  102. }
  103. getCompletionsForLabelMatcherName(session: CompleterSession, pos: CompleterPosition) {
  104. const metricName = this.findMetricName(session, pos.row, pos.column);
  105. if (!metricName) {
  106. return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name'));
  107. }
  108. if (this.labelNameCache[metricName]) {
  109. return Promise.resolve(this.labelNameCache[metricName]);
  110. }
  111. return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
  112. const labelNames = this.transformToCompletions(
  113. _.uniq(
  114. _.flatten(
  115. result.map((r: any) => {
  116. return Object.keys(r);
  117. })
  118. )
  119. ),
  120. 'label name'
  121. );
  122. this.labelNameCache[metricName] = labelNames;
  123. return Promise.resolve(labelNames);
  124. });
  125. }
  126. getCompletionsForLabelMatcherValue(session: CompleterSession, pos: CompleterPosition) {
  127. const metricName = this.findMetricName(session, pos.row, pos.column);
  128. if (!metricName) {
  129. return Promise.resolve([]);
  130. }
  131. const labelNameToken = this.findToken(
  132. session,
  133. pos.row,
  134. pos.column,
  135. 'entity.name.tag.label-matcher',
  136. null,
  137. 'paren.lparen.label-matcher'
  138. );
  139. if (!labelNameToken) {
  140. return Promise.resolve([]);
  141. }
  142. const labelName = labelNameToken.value;
  143. if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) {
  144. return Promise.resolve(this.labelValueCache[metricName][labelName]);
  145. }
  146. return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
  147. const labelValues = this.transformToCompletions(
  148. _.uniq(
  149. result.map((r: any) => {
  150. return r[labelName];
  151. })
  152. ),
  153. 'label value'
  154. );
  155. this.labelValueCache[metricName] = this.labelValueCache[metricName] || {};
  156. this.labelValueCache[metricName][labelName] = labelValues;
  157. return Promise.resolve(labelValues);
  158. });
  159. }
  160. getCompletionsForBinaryOperator(session: CompleterSession, pos: CompleterPosition) {
  161. const keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier');
  162. if (!keywordOperatorToken) {
  163. return Promise.resolve([]);
  164. }
  165. let rparenToken: CompleterToken, expr: string;
  166. switch (keywordOperatorToken.value) {
  167. case 'by':
  168. case 'without':
  169. rparenToken = this.findToken(
  170. session,
  171. keywordOperatorToken.row,
  172. keywordOperatorToken.column,
  173. 'paren.rparen',
  174. null,
  175. 'identifier'
  176. );
  177. if (!rparenToken) {
  178. return Promise.resolve([]);
  179. }
  180. expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
  181. if (expr === '') {
  182. return Promise.resolve([]);
  183. }
  184. return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
  185. const labelNames = this.transformToCompletions(
  186. _.uniq(
  187. _.flatten(
  188. result.map((r: any) => {
  189. return Object.keys(r);
  190. })
  191. )
  192. ),
  193. 'label name'
  194. );
  195. this.labelNameCache[expr] = labelNames;
  196. return labelNames;
  197. });
  198. case 'on':
  199. case 'ignoring':
  200. case 'group_left':
  201. case 'group_right':
  202. const binaryOperatorToken = this.findToken(
  203. session,
  204. keywordOperatorToken.row,
  205. keywordOperatorToken.column,
  206. 'keyword.operator.binary',
  207. null,
  208. 'identifier'
  209. );
  210. if (!binaryOperatorToken) {
  211. return Promise.resolve([]);
  212. }
  213. rparenToken = this.findToken(
  214. session,
  215. binaryOperatorToken.row,
  216. binaryOperatorToken.column,
  217. 'paren.rparen',
  218. null,
  219. 'identifier'
  220. );
  221. if (rparenToken) {
  222. expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
  223. if (expr === '') {
  224. return Promise.resolve([]);
  225. }
  226. return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
  227. const labelNames = this.transformToCompletions(
  228. _.uniq(
  229. _.flatten(
  230. result.map((r: any) => {
  231. return Object.keys(r);
  232. })
  233. )
  234. ),
  235. 'label name'
  236. );
  237. this.labelNameCache[expr] = labelNames;
  238. return labelNames;
  239. });
  240. } else {
  241. const metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column);
  242. return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
  243. const labelNames = this.transformToCompletions(
  244. _.uniq(
  245. _.flatten(
  246. result.map((r: any) => {
  247. return Object.keys(r);
  248. })
  249. )
  250. ),
  251. 'label name'
  252. );
  253. this.labelNameCache[metricName] = labelNames;
  254. return Promise.resolve(labelNames);
  255. });
  256. }
  257. }
  258. return Promise.resolve([]);
  259. }
  260. getLabelNameAndValueForExpression(expr: string, type: string): Promise<any> {
  261. if (this.labelQueryCache[expr]) {
  262. return Promise.resolve(this.labelQueryCache[expr]);
  263. }
  264. let query = expr;
  265. if (type === 'metricName') {
  266. let op = '=~';
  267. if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(expr)) {
  268. op = '=';
  269. }
  270. query = '{__name__' + op + '"' + expr + '"}';
  271. }
  272. const { start, end } = this.datasource.getTimeRange();
  273. const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end;
  274. return this.datasource.metadataRequest(url).then((response: any) => {
  275. this.labelQueryCache[expr] = response.data.data;
  276. return response.data.data;
  277. });
  278. }
  279. transformToCompletions(words: string[], meta: any) {
  280. return words.map(name => {
  281. return {
  282. caption: name,
  283. value: name,
  284. meta,
  285. score: Number.MAX_VALUE,
  286. };
  287. });
  288. }
  289. findMetricName(session: CompleterSession, row: number, column: number) {
  290. let metricName = '';
  291. let tokens: CompleterToken[];
  292. const nameLabelNameToken = this.findToken(
  293. session,
  294. row,
  295. column,
  296. 'entity.name.tag.label-matcher',
  297. '__name__',
  298. 'paren.lparen.label-matcher'
  299. );
  300. if (nameLabelNameToken) {
  301. tokens = session.getTokens(nameLabelNameToken.row);
  302. const nameLabelValueToken = tokens[nameLabelNameToken.index + 2];
  303. if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted.label-matcher') {
  304. metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation
  305. }
  306. } else {
  307. const metricNameToken = this.findToken(session, row, column, 'identifier', null, null);
  308. if (metricNameToken) {
  309. tokens = session.getTokens(metricNameToken.row);
  310. metricName = metricNameToken.value;
  311. }
  312. }
  313. return metricName;
  314. }
  315. findToken(session: CompleterSession, row: number, column: number, target: string, value: string, guard: string) {
  316. let tokens: CompleterToken[], idx: number;
  317. // find index and get column of previous token
  318. for (let r = row; r >= 0; r--) {
  319. let c: number;
  320. tokens = session.getTokens(r);
  321. if (r === row) {
  322. // current row
  323. c = 0;
  324. for (idx = 0; idx < tokens.length; idx++) {
  325. const nc = c + tokens[idx].value.length;
  326. if (nc >= column) {
  327. break;
  328. }
  329. c = nc;
  330. }
  331. } else {
  332. idx = tokens.length - 1;
  333. c =
  334. _.sum(
  335. tokens.map(t => {
  336. return t.value.length;
  337. })
  338. ) - tokens[tokens.length - 1].value.length;
  339. }
  340. for (; idx >= 0; idx--) {
  341. if (tokens[idx].type === guard) {
  342. return null;
  343. }
  344. if (tokens[idx].type === target && (!value || tokens[idx].value === value)) {
  345. tokens[idx].row = r;
  346. tokens[idx].column = c;
  347. tokens[idx].index = idx;
  348. return tokens[idx];
  349. }
  350. c -= tokens[idx].value.length;
  351. }
  352. }
  353. return null;
  354. }
  355. findExpressionMatchedParen(session: CompleterSession, row: number, column: number) {
  356. let tokens: CompleterToken[], idx: number;
  357. let deep = 1;
  358. let expression = ')';
  359. for (let r = row; r >= 0; r--) {
  360. tokens = session.getTokens(r);
  361. if (r === row) {
  362. // current row
  363. let c = 0;
  364. for (idx = 0; idx < tokens.length; idx++) {
  365. c += tokens[idx].value.length;
  366. if (c >= column) {
  367. break;
  368. }
  369. }
  370. } else {
  371. idx = tokens.length - 1;
  372. }
  373. for (; idx >= 0; idx--) {
  374. expression = tokens[idx].value + expression;
  375. if (tokens[idx].type === 'paren.rparen') {
  376. deep++;
  377. } else if (tokens[idx].type === 'paren.lparen') {
  378. deep--;
  379. if (deep === 0) {
  380. return expression;
  381. }
  382. }
  383. }
  384. }
  385. return expression;
  386. }
  387. }