datasource.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. import _ from "lodash";
  2. import kbn from "app/core/utils/kbn";
  3. import * as dateMath from "app/core/utils/datemath";
  4. import PrometheusMetricFindQuery from "./metric_find_query";
  5. import TableModel from "app/core/table_model";
  6. function prometheusSpecialRegexEscape(value) {
  7. return value.replace(/[\\^$*+?.()|[\]{}]/g, "\\\\$&");
  8. }
  9. export class PrometheusDatasource {
  10. type: string;
  11. editorSrc: string;
  12. name: string;
  13. supportMetrics: boolean;
  14. url: string;
  15. directUrl: string;
  16. basicAuth: any;
  17. withCredentials: any;
  18. metricsNameCache: any;
  19. interval: string;
  20. /** @ngInject */
  21. constructor(
  22. instanceSettings,
  23. private $q,
  24. private backendSrv,
  25. private templateSrv,
  26. private timeSrv
  27. ) {
  28. this.type = "prometheus";
  29. this.editorSrc = "app/features/prometheus/partials/query.editor.html";
  30. this.name = instanceSettings.name;
  31. this.supportMetrics = true;
  32. this.url = instanceSettings.url;
  33. this.directUrl = instanceSettings.directUrl;
  34. this.basicAuth = instanceSettings.basicAuth;
  35. this.withCredentials = instanceSettings.withCredentials;
  36. this.interval = instanceSettings.jsonData.timeInterval || "15s";
  37. }
  38. _request(method, url, requestId?) {
  39. var options: any = {
  40. url: this.url + url,
  41. method: method,
  42. requestId: requestId
  43. };
  44. if (this.basicAuth || this.withCredentials) {
  45. options.withCredentials = true;
  46. }
  47. if (this.basicAuth) {
  48. options.headers = {
  49. Authorization: this.basicAuth
  50. };
  51. }
  52. return this.backendSrv.datasourceRequest(options);
  53. }
  54. interpolateQueryExpr(value, variable, defaultFormatFn) {
  55. // if no multi or include all do not regexEscape
  56. if (!variable.multi && !variable.includeAll) {
  57. return value;
  58. }
  59. if (typeof value === "string") {
  60. return prometheusSpecialRegexEscape(value);
  61. }
  62. var escapedValues = _.map(value, prometheusSpecialRegexEscape);
  63. return escapedValues.join("|");
  64. }
  65. targetContainsTemplate(target) {
  66. return this.templateSrv.variableExists(target.expr);
  67. }
  68. query(options) {
  69. var self = this;
  70. var start = this.getPrometheusTime(options.range.from, false);
  71. var end = this.getPrometheusTime(options.range.to, true);
  72. var range = Math.ceil(end - start);
  73. var queries = [];
  74. var activeTargets = [];
  75. options = _.clone(options);
  76. for (let target of options.targets) {
  77. if (!target.expr || target.hide) {
  78. continue;
  79. }
  80. activeTargets.push(target);
  81. queries.push(this.createQuery(target, options, range));
  82. }
  83. // No valid targets, return the empty result to save a round trip.
  84. if (_.isEmpty(queries)) {
  85. return this.$q.when({ data: [] });
  86. }
  87. var allQueryPromise = _.map(queries, query => {
  88. if (!query.instant) {
  89. return this.performTimeSeriesQuery(query, start, end);
  90. } else {
  91. return this.performInstantQuery(query, end);
  92. }
  93. });
  94. return this.$q.all(allQueryPromise).then(responseList => {
  95. var result = [];
  96. _.each(responseList, (response, index) => {
  97. if (response.status === "error") {
  98. throw response.error;
  99. }
  100. if (activeTargets[index].format === "table") {
  101. result.push(
  102. self.transformMetricDataToTable(
  103. response.data.data.result,
  104. responseList.length,
  105. index
  106. )
  107. );
  108. } else {
  109. for (let metricData of response.data.data.result) {
  110. if (response.data.data.resultType === "matrix") {
  111. result.push(
  112. self.transformMetricData(
  113. metricData,
  114. activeTargets[index],
  115. start,
  116. end,
  117. queries[index].step
  118. )
  119. );
  120. } else if (response.data.data.resultType === "vector") {
  121. result.push(
  122. self.transformInstantMetricData(
  123. metricData,
  124. activeTargets[index]
  125. )
  126. );
  127. }
  128. }
  129. }
  130. });
  131. return { data: result };
  132. });
  133. }
  134. createQuery(target, options, range) {
  135. var query: any = {};
  136. query.instant = target.instant;
  137. var interval = kbn.interval_to_seconds(options.interval);
  138. // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise
  139. var minInterval = kbn.interval_to_seconds(
  140. this.templateSrv.replace(target.interval, options.scopedVars) ||
  141. options.interval
  142. );
  143. var intervalFactor = target.intervalFactor || 1;
  144. // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
  145. var adjustedInterval = this.adjustInterval(
  146. interval,
  147. minInterval,
  148. range,
  149. intervalFactor
  150. );
  151. var scopedVars = options.scopedVars;
  152. // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
  153. if (interval !== adjustedInterval) {
  154. interval = adjustedInterval;
  155. scopedVars = Object.assign({}, options.scopedVars, {
  156. __interval: { text: interval + "s", value: interval + "s" },
  157. __interval_ms: { text: interval * 1000, value: interval * 1000 }
  158. });
  159. }
  160. query.step = interval;
  161. // Only replace vars in expression after having (possibly) updated interval vars
  162. query.expr = this.templateSrv.replace(
  163. target.expr,
  164. scopedVars,
  165. this.interpolateQueryExpr
  166. );
  167. query.requestId = options.panelId + target.refId;
  168. return query;
  169. }
  170. adjustInterval(interval, minInterval, range, intervalFactor) {
  171. // Prometheus will drop queries that might return more than 11000 data points.
  172. // Calibrate interval if it is too small.
  173. if (interval !== 0 && range / intervalFactor / interval > 11000) {
  174. interval = Math.ceil(range / intervalFactor / 11000);
  175. }
  176. return Math.max(interval * intervalFactor, minInterval, 1);
  177. }
  178. performTimeSeriesQuery(query, start, end) {
  179. if (start > end) {
  180. throw { message: "Invalid time range" };
  181. }
  182. var url =
  183. "/api/v1/query_range?query=" +
  184. encodeURIComponent(query.expr) +
  185. "&start=" +
  186. start +
  187. "&end=" +
  188. end +
  189. "&step=" +
  190. query.step;
  191. return this._request("GET", url, query.requestId);
  192. }
  193. performInstantQuery(query, time) {
  194. var url =
  195. "/api/v1/query?query=" + encodeURIComponent(query.expr) + "&time=" + time;
  196. return this._request("GET", url, query.requestId);
  197. }
  198. performSuggestQuery(query, cache = false) {
  199. var url = "/api/v1/label/__name__/values";
  200. if (
  201. cache &&
  202. this.metricsNameCache &&
  203. this.metricsNameCache.expire > Date.now()
  204. ) {
  205. return this.$q.when(
  206. _.filter(this.metricsNameCache.data, metricName => {
  207. return metricName.indexOf(query) !== 1;
  208. })
  209. );
  210. }
  211. return this._request("GET", url).then(result => {
  212. this.metricsNameCache = {
  213. data: result.data.data,
  214. expire: Date.now() + 60 * 1000
  215. };
  216. return _.filter(result.data.data, metricName => {
  217. return metricName.indexOf(query) !== 1;
  218. });
  219. });
  220. }
  221. metricFindQuery(query) {
  222. if (!query) {
  223. return this.$q.when([]);
  224. }
  225. let interpolated = this.templateSrv.replace(
  226. query,
  227. {},
  228. this.interpolateQueryExpr
  229. );
  230. var metricFindQuery = new PrometheusMetricFindQuery(
  231. this,
  232. interpolated,
  233. this.timeSrv
  234. );
  235. return metricFindQuery.process();
  236. }
  237. annotationQuery(options) {
  238. var annotation = options.annotation;
  239. var expr = annotation.expr || "";
  240. var tagKeys = annotation.tagKeys || "";
  241. var titleFormat = annotation.titleFormat || "";
  242. var textFormat = annotation.textFormat || "";
  243. if (!expr) {
  244. return this.$q.when([]);
  245. }
  246. var interpolated = this.templateSrv.replace(
  247. expr,
  248. {},
  249. this.interpolateQueryExpr
  250. );
  251. var step = "60s";
  252. if (annotation.step) {
  253. step = this.templateSrv.replace(annotation.step);
  254. }
  255. var start = this.getPrometheusTime(options.range.from, false);
  256. var end = this.getPrometheusTime(options.range.to, true);
  257. var query = {
  258. expr: interpolated,
  259. step:
  260. this.adjustInterval(
  261. kbn.interval_to_seconds(step),
  262. 0,
  263. Math.ceil(end - start),
  264. 1
  265. ) + "s"
  266. };
  267. var self = this;
  268. return this.performTimeSeriesQuery(query, start, end).then(function(
  269. results
  270. ) {
  271. var eventList = [];
  272. tagKeys = tagKeys.split(",");
  273. _.each(results.data.data.result, function(series) {
  274. var tags = _.chain(series.metric)
  275. .filter(function(v, k) {
  276. return _.includes(tagKeys, k);
  277. })
  278. .value();
  279. for (let value of series.values) {
  280. if (value[1] === "1") {
  281. var event = {
  282. annotation: annotation,
  283. time: Math.floor(parseFloat(value[0])) * 1000,
  284. title: self.renderTemplate(titleFormat, series.metric),
  285. tags: tags,
  286. text: self.renderTemplate(textFormat, series.metric)
  287. };
  288. eventList.push(event);
  289. }
  290. }
  291. });
  292. return eventList;
  293. });
  294. }
  295. testDatasource() {
  296. return this.metricFindQuery("metrics(.*)").then(function() {
  297. return { status: "success", message: "Data source is working" };
  298. });
  299. }
  300. transformMetricData(md, options, start, end, step) {
  301. var dps = [],
  302. metricLabel = null;
  303. metricLabel = this.createMetricLabel(md.metric, options);
  304. var stepMs = step * 1000;
  305. var baseTimestamp = start * 1000;
  306. for (let value of md.values) {
  307. var dp_value = parseFloat(value[1]);
  308. if (_.isNaN(dp_value)) {
  309. dp_value = null;
  310. }
  311. var timestamp = parseFloat(value[0]) * 1000;
  312. for (let t = baseTimestamp; t < timestamp; t += stepMs) {
  313. dps.push([null, t]);
  314. }
  315. baseTimestamp = timestamp + stepMs;
  316. dps.push([dp_value, timestamp]);
  317. }
  318. var endTimestamp = end * 1000;
  319. for (let t = baseTimestamp; t <= endTimestamp; t += stepMs) {
  320. dps.push([null, t]);
  321. }
  322. return { target: metricLabel, datapoints: dps };
  323. }
  324. transformMetricDataToTable(md, resultCount: number, resultIndex: number) {
  325. var table = new TableModel();
  326. var i, j;
  327. var metricLabels = {};
  328. if (md.length === 0) {
  329. return table;
  330. }
  331. // Collect all labels across all metrics
  332. _.each(md, function(series) {
  333. for (var label in series.metric) {
  334. if (!metricLabels.hasOwnProperty(label)) {
  335. metricLabels[label] = 1;
  336. }
  337. }
  338. });
  339. // Sort metric labels, create columns for them and record their index
  340. var sortedLabels = _.keys(metricLabels).sort();
  341. table.columns.push({ text: "Time", type: "time" });
  342. _.each(sortedLabels, function(label, labelIndex) {
  343. metricLabels[label] = labelIndex + 1;
  344. table.columns.push({ text: label });
  345. });
  346. let valueText =
  347. resultCount > 1
  348. ? `Value #${String.fromCharCode(65 + resultIndex)}`
  349. : "Value";
  350. table.columns.push({ text: valueText });
  351. // Populate rows, set value to empty string when label not present.
  352. _.each(md, function(series) {
  353. if (series.value) {
  354. series.values = [series.value];
  355. }
  356. if (series.values) {
  357. for (i = 0; i < series.values.length; i++) {
  358. var values = series.values[i];
  359. var reordered: any = [values[0] * 1000];
  360. if (series.metric) {
  361. for (j = 0; j < sortedLabels.length; j++) {
  362. var label = sortedLabels[j];
  363. if (series.metric.hasOwnProperty(label)) {
  364. reordered.push(series.metric[label]);
  365. } else {
  366. reordered.push("");
  367. }
  368. }
  369. }
  370. reordered.push(parseFloat(values[1]));
  371. table.rows.push(reordered);
  372. }
  373. }
  374. });
  375. return table;
  376. }
  377. transformInstantMetricData(md, options) {
  378. var dps = [],
  379. metricLabel = null;
  380. metricLabel = this.createMetricLabel(md.metric, options);
  381. dps.push([parseFloat(md.value[1]), md.value[0] * 1000]);
  382. return { target: metricLabel, datapoints: dps };
  383. }
  384. createMetricLabel(labelData, options) {
  385. if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
  386. return this.getOriginalMetricName(labelData);
  387. }
  388. return (
  389. this.renderTemplate(
  390. this.templateSrv.replace(options.legendFormat),
  391. labelData
  392. ) || "{}"
  393. );
  394. }
  395. renderTemplate(aliasPattern, aliasData) {
  396. var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
  397. return aliasPattern.replace(aliasRegex, function(match, g1) {
  398. if (aliasData[g1]) {
  399. return aliasData[g1];
  400. }
  401. return g1;
  402. });
  403. }
  404. getOriginalMetricName(labelData) {
  405. var metricName = labelData.__name__ || "";
  406. delete labelData.__name__;
  407. var labelPart = _.map(_.toPairs(labelData), function(label) {
  408. return label[0] + '="' + label[1] + '"';
  409. }).join(",");
  410. return metricName + "{" + labelPart + "}";
  411. }
  412. getPrometheusTime(date, roundUp) {
  413. if (_.isString(date)) {
  414. date = dateMath.parse(date, roundUp);
  415. }
  416. return Math.ceil(date.valueOf() / 1000);
  417. }
  418. }