datasource.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import _ from 'lodash';
  2. import * as dateMath from 'app/core/utils/datemath';
  3. import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
  4. import gfunc from './gfunc';
  5. /** @ngInject */
  6. export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
  7. this.basicAuth = instanceSettings.basicAuth;
  8. this.url = instanceSettings.url;
  9. this.name = instanceSettings.name;
  10. this.graphiteVersion = instanceSettings.jsonData.graphiteVersion || '0.9';
  11. this.supportsTags = supportsTags(this.graphiteVersion);
  12. this.cacheTimeout = instanceSettings.cacheTimeout;
  13. this.withCredentials = instanceSettings.withCredentials;
  14. this.render_method = instanceSettings.render_method || 'POST';
  15. this.funcDefs = null;
  16. this.funcDefsPromise = null;
  17. this.getQueryOptionsInfo = function() {
  18. return {
  19. maxDataPoints: true,
  20. cacheTimeout: true,
  21. links: [
  22. {
  23. text: 'Help',
  24. url: 'http://docs.grafana.org/features/datasources/graphite/#using-graphite-in-grafana',
  25. },
  26. ],
  27. };
  28. };
  29. this.query = function(options) {
  30. var graphOptions = {
  31. from: this.translateTime(options.rangeRaw.from, false),
  32. until: this.translateTime(options.rangeRaw.to, true),
  33. targets: options.targets,
  34. format: options.format,
  35. cacheTimeout: options.cacheTimeout || this.cacheTimeout,
  36. maxDataPoints: options.maxDataPoints,
  37. };
  38. var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
  39. if (params.length === 0) {
  40. return $q.when({ data: [] });
  41. }
  42. var httpOptions: any = {
  43. method: 'POST',
  44. url: '/render',
  45. data: params.join('&'),
  46. headers: {
  47. 'Content-Type': 'application/x-www-form-urlencoded',
  48. },
  49. };
  50. if (options.panelId) {
  51. httpOptions.requestId = this.name + '.panelId.' + options.panelId;
  52. }
  53. return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
  54. };
  55. this.convertDataPointsToMs = function(result) {
  56. if (!result || !result.data) {
  57. return [];
  58. }
  59. for (var i = 0; i < result.data.length; i++) {
  60. var series = result.data[i];
  61. for (var y = 0; y < series.datapoints.length; y++) {
  62. series.datapoints[y][1] *= 1000;
  63. }
  64. }
  65. return result;
  66. };
  67. this.parseTags = function(tagString) {
  68. let tags = [];
  69. tags = tagString.split(',');
  70. if (tags.length === 1) {
  71. tags = tagString.split(' ');
  72. if (tags[0] === '') {
  73. tags = [];
  74. }
  75. }
  76. return tags;
  77. };
  78. this.annotationQuery = function(options) {
  79. // Graphite metric as annotation
  80. if (options.annotation.target) {
  81. var target = templateSrv.replace(options.annotation.target, {}, 'glob');
  82. var graphiteQuery = {
  83. rangeRaw: options.rangeRaw,
  84. targets: [{ target: target }],
  85. format: 'json',
  86. maxDataPoints: 100,
  87. };
  88. return this.query(graphiteQuery).then(function(result) {
  89. var list = [];
  90. for (var i = 0; i < result.data.length; i++) {
  91. var target = result.data[i];
  92. for (var y = 0; y < target.datapoints.length; y++) {
  93. var datapoint = target.datapoints[y];
  94. if (!datapoint[0]) {
  95. continue;
  96. }
  97. list.push({
  98. annotation: options.annotation,
  99. time: datapoint[1],
  100. title: target.target,
  101. });
  102. }
  103. }
  104. return list;
  105. });
  106. } else {
  107. // Graphite event as annotation
  108. var tags = templateSrv.replace(options.annotation.tags);
  109. return this.events({ range: options.rangeRaw, tags: tags }).then(results => {
  110. var list = [];
  111. for (var i = 0; i < results.data.length; i++) {
  112. var e = results.data[i];
  113. var tags = e.tags;
  114. if (_.isString(e.tags)) {
  115. tags = this.parseTags(e.tags);
  116. }
  117. list.push({
  118. annotation: options.annotation,
  119. time: e.when * 1000,
  120. title: e.what,
  121. tags: tags,
  122. text: e.data,
  123. });
  124. }
  125. return list;
  126. });
  127. }
  128. };
  129. this.events = function(options) {
  130. try {
  131. var tags = '';
  132. if (options.tags) {
  133. tags = '&tags=' + options.tags;
  134. }
  135. return this.doGraphiteRequest({
  136. method: 'GET',
  137. url:
  138. '/events/get_data?from=' +
  139. this.translateTime(options.range.from, false) +
  140. '&until=' +
  141. this.translateTime(options.range.to, true) +
  142. tags,
  143. });
  144. } catch (err) {
  145. return $q.reject(err);
  146. }
  147. };
  148. this.targetContainsTemplate = function(target) {
  149. return templateSrv.variableExists(target.target);
  150. };
  151. this.translateTime = function(date, roundUp) {
  152. if (_.isString(date)) {
  153. if (date === 'now') {
  154. return 'now';
  155. } else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
  156. date = date.substring(3);
  157. date = date.replace('m', 'min');
  158. date = date.replace('M', 'mon');
  159. return date;
  160. }
  161. date = dateMath.parse(date, roundUp);
  162. }
  163. // graphite' s from filter is exclusive
  164. // here we step back one minute in order
  165. // to guarantee that we get all the data that
  166. // exists for the specified range
  167. if (roundUp) {
  168. if (date.get('s')) {
  169. date.add(1, 'm');
  170. }
  171. } else if (roundUp === false) {
  172. if (date.get('s')) {
  173. date.subtract(1, 'm');
  174. }
  175. }
  176. return date.unix();
  177. };
  178. this.metricFindQuery = function(query, optionalOptions) {
  179. let options = optionalOptions || {};
  180. let interpolatedQuery = templateSrv.replace(query);
  181. // special handling for tag_values(<tag>[,<expression>]*), this is used for template variables
  182. let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/);
  183. if (matches) {
  184. const expressions = [];
  185. const exprRegex = /, *([^,]+)/g;
  186. let match;
  187. while ((match = exprRegex.exec(matches[2])) !== null) {
  188. expressions.push(match[1]);
  189. }
  190. options.limit = 10000;
  191. return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options);
  192. }
  193. // special handling for tags(<expression>[,<expression>]*), this is used for template variables
  194. matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/);
  195. if (matches) {
  196. const expressions = [];
  197. if (matches[1]) {
  198. expressions.push(matches[1]);
  199. const exprRegex = /, *([^,]+)/g;
  200. let match;
  201. while ((match = exprRegex.exec(matches[2])) !== null) {
  202. expressions.push(match[1]);
  203. }
  204. }
  205. options.limit = 10000;
  206. return this.getTagsAutoComplete(expressions, undefined, options);
  207. }
  208. let httpOptions: any = {
  209. method: 'GET',
  210. url: '/metrics/find',
  211. params: {
  212. query: interpolatedQuery,
  213. },
  214. // for cancellations
  215. requestId: options.requestId,
  216. };
  217. if (options.range) {
  218. httpOptions.params.from = this.translateTime(options.range.from, false);
  219. httpOptions.params.until = this.translateTime(options.range.to, true);
  220. }
  221. return this.doGraphiteRequest(httpOptions).then(results => {
  222. return _.map(results.data, metric => {
  223. return {
  224. text: metric.text,
  225. expandable: metric.expandable ? true : false,
  226. };
  227. });
  228. });
  229. };
  230. this.getTags = function(optionalOptions) {
  231. let options = optionalOptions || {};
  232. let httpOptions: any = {
  233. method: 'GET',
  234. url: '/tags',
  235. // for cancellations
  236. requestId: options.requestId,
  237. };
  238. if (options.range) {
  239. httpOptions.params.from = this.translateTime(options.range.from, false);
  240. httpOptions.params.until = this.translateTime(options.range.to, true);
  241. }
  242. return this.doGraphiteRequest(httpOptions).then(results => {
  243. return _.map(results.data, tag => {
  244. return {
  245. text: tag.tag,
  246. id: tag.id,
  247. };
  248. });
  249. });
  250. };
  251. this.getTagValues = function(tag, optionalOptions) {
  252. let options = optionalOptions || {};
  253. let httpOptions: any = {
  254. method: 'GET',
  255. url: '/tags/' + templateSrv.replace(tag),
  256. // for cancellations
  257. requestId: options.requestId,
  258. };
  259. if (options.range) {
  260. httpOptions.params.from = this.translateTime(options.range.from, false);
  261. httpOptions.params.until = this.translateTime(options.range.to, true);
  262. }
  263. return this.doGraphiteRequest(httpOptions).then(results => {
  264. if (results.data && results.data.values) {
  265. return _.map(results.data.values, value => {
  266. return {
  267. text: value.value,
  268. id: value.id,
  269. };
  270. });
  271. } else {
  272. return [];
  273. }
  274. });
  275. };
  276. this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => {
  277. let options = optionalOptions || {};
  278. let httpOptions: any = {
  279. method: 'GET',
  280. url: '/tags/autoComplete/tags',
  281. params: {
  282. expr: _.map(expressions, expression => templateSrv.replace(expression)),
  283. },
  284. // for cancellations
  285. requestId: options.requestId,
  286. };
  287. if (tagPrefix) {
  288. httpOptions.params.tagPrefix = tagPrefix;
  289. }
  290. if (options.limit) {
  291. httpOptions.params.limit = options.limit;
  292. }
  293. if (options.range) {
  294. httpOptions.params.from = this.translateTime(options.range.from, false);
  295. httpOptions.params.until = this.translateTime(options.range.to, true);
  296. }
  297. return this.doGraphiteRequest(httpOptions).then(results => {
  298. if (results.data) {
  299. return _.map(results.data, tag => {
  300. return { text: tag };
  301. });
  302. } else {
  303. return [];
  304. }
  305. });
  306. };
  307. this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => {
  308. let options = optionalOptions || {};
  309. let httpOptions: any = {
  310. method: 'GET',
  311. url: '/tags/autoComplete/values',
  312. params: {
  313. expr: _.map(expressions, expression => templateSrv.replace(expression)),
  314. tag: templateSrv.replace(tag),
  315. },
  316. // for cancellations
  317. requestId: options.requestId,
  318. };
  319. if (valuePrefix) {
  320. httpOptions.params.valuePrefix = valuePrefix;
  321. }
  322. if (options.limit) {
  323. httpOptions.params.limit = options.limit;
  324. }
  325. if (options.range) {
  326. httpOptions.params.from = this.translateTime(options.range.from, false);
  327. httpOptions.params.until = this.translateTime(options.range.to, true);
  328. }
  329. return this.doGraphiteRequest(httpOptions).then(results => {
  330. if (results.data) {
  331. return _.map(results.data, value => {
  332. return { text: value };
  333. });
  334. } else {
  335. return [];
  336. }
  337. });
  338. };
  339. this.getVersion = function(optionalOptions) {
  340. let options = optionalOptions || {};
  341. let httpOptions = {
  342. method: 'GET',
  343. url: '/version',
  344. requestId: options.requestId,
  345. };
  346. return this.doGraphiteRequest(httpOptions)
  347. .then(results => {
  348. if (results.data) {
  349. let semver = new SemVersion(results.data);
  350. return semver.isValid() ? results.data : '';
  351. }
  352. return '';
  353. })
  354. .catch(() => {
  355. return '';
  356. });
  357. };
  358. this.createFuncInstance = function(funcDef, options?) {
  359. return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
  360. };
  361. this.getFuncDef = function(name) {
  362. return gfunc.getFuncDef(name, this.funcDefs);
  363. };
  364. this.waitForFuncDefsLoaded = function() {
  365. return this.getFuncDefs();
  366. };
  367. this.getFuncDefs = function() {
  368. if (this.funcDefsPromise !== null) {
  369. return this.funcDefsPromise;
  370. }
  371. if (!supportsFunctionIndex(this.graphiteVersion)) {
  372. this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
  373. this.funcDefsPromise = Promise.resolve(this.funcDefs);
  374. return this.funcDefsPromise;
  375. }
  376. let httpOptions = {
  377. method: 'GET',
  378. url: '/functions',
  379. };
  380. this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
  381. .then(results => {
  382. if (results.status !== 200 || typeof results.data !== 'object') {
  383. this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
  384. return Promise.resolve(this.funcDefs);
  385. }
  386. this.funcDefs = {};
  387. _.forEach(results.data || {}, (funcDef, funcName) => {
  388. // skip graphite graph functions
  389. if (funcDef.group === 'Graph') {
  390. return;
  391. }
  392. var description = funcDef.description;
  393. if (description) {
  394. // tidy up some pydoc syntax that rst2html can't handle
  395. description = description
  396. .replace(/:py:func:`(.+)( <[^>]*>)?`/g, '``$1``')
  397. .replace(/.. seealso:: /g, 'See also: ')
  398. .replace(/.. code-block *:: *none/g, '.. code-block::');
  399. }
  400. var func = {
  401. name: funcDef.name,
  402. description: description,
  403. category: funcDef.group,
  404. params: [],
  405. defaultParams: [],
  406. fake: false,
  407. };
  408. // get rid of the first "seriesList" param
  409. if (/^seriesLists?$/.test(_.get(funcDef, 'params[0].type', ''))) {
  410. // handle functions that accept multiple seriesLists
  411. // we leave the param in place but mark it optional, so users can add more series if they wish
  412. if (funcDef.params[0].multiple) {
  413. funcDef.params[0].required = false;
  414. // otherwise chop off the first param, it'll be handled separately
  415. } else {
  416. funcDef.params.shift();
  417. }
  418. // tag function as fake
  419. } else {
  420. func.fake = true;
  421. }
  422. _.forEach(funcDef.params, rawParam => {
  423. var param = {
  424. name: rawParam.name,
  425. type: 'string',
  426. optional: !rawParam.required,
  427. multiple: !!rawParam.multiple,
  428. options: undefined,
  429. };
  430. if (rawParam.default !== undefined) {
  431. func.defaultParams.push(_.toString(rawParam.default));
  432. } else if (rawParam.suggestions) {
  433. func.defaultParams.push(_.toString(rawParam.suggestions[0]));
  434. } else {
  435. func.defaultParams.push('');
  436. }
  437. if (rawParam.type === 'boolean') {
  438. param.type = 'boolean';
  439. param.options = ['true', 'false'];
  440. } else if (rawParam.type === 'integer') {
  441. param.type = 'int';
  442. } else if (rawParam.type === 'float') {
  443. param.type = 'float';
  444. } else if (rawParam.type === 'node') {
  445. param.type = 'node';
  446. param.options = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
  447. } else if (rawParam.type === 'nodeOrTag') {
  448. param.type = 'node_or_tag';
  449. param.options = ['name', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
  450. } else if (rawParam.type === 'intOrInterval') {
  451. param.type = 'int_or_interval';
  452. } else if (rawParam.type === 'seriesList') {
  453. param.type = 'value_or_series';
  454. }
  455. if (rawParam.options) {
  456. param.options = _.map(rawParam.options, _.toString);
  457. } else if (rawParam.suggestions) {
  458. param.options = _.map(rawParam.suggestions, _.toString);
  459. }
  460. func.params.push(param);
  461. });
  462. this.funcDefs[funcName] = func;
  463. });
  464. return this.funcDefs;
  465. })
  466. .catch(err => {
  467. console.log('Fetching graphite functions error', err);
  468. this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
  469. return this.funcDefs;
  470. });
  471. return this.funcDefsPromise;
  472. };
  473. this.testDatasource = function() {
  474. return this.metricFindQuery('*').then(function() {
  475. return { status: 'success', message: 'Data source is working' };
  476. });
  477. };
  478. this.doGraphiteRequest = function(options) {
  479. if (this.basicAuth || this.withCredentials) {
  480. options.withCredentials = true;
  481. }
  482. if (this.basicAuth) {
  483. options.headers = options.headers || {};
  484. options.headers.Authorization = this.basicAuth;
  485. }
  486. options.url = this.url + options.url;
  487. options.inspect = { type: 'graphite' };
  488. return backendSrv.datasourceRequest(options);
  489. };
  490. this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  491. this.buildGraphiteParams = function(options, scopedVars) {
  492. var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
  493. var clean_options = [],
  494. targets = {};
  495. var target, targetValue, i;
  496. var regex = /\#([A-Z])/g;
  497. var intervalFormatFixRegex = /'(\d+)m'/gi;
  498. var hasTargets = false;
  499. options['format'] = 'json';
  500. function fixIntervalFormat(match) {
  501. return match.replace('m', 'min').replace('M', 'mon');
  502. }
  503. for (i = 0; i < options.targets.length; i++) {
  504. target = options.targets[i];
  505. if (!target.target) {
  506. continue;
  507. }
  508. if (!target.refId) {
  509. target.refId = this._seriesRefLetters[i];
  510. }
  511. targetValue = templateSrv.replace(target.target, scopedVars);
  512. targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
  513. targets[target.refId] = targetValue;
  514. }
  515. function nestedSeriesRegexReplacer(match, g1) {
  516. return targets[g1] || match;
  517. }
  518. for (i = 0; i < options.targets.length; i++) {
  519. target = options.targets[i];
  520. if (!target.target) {
  521. continue;
  522. }
  523. targetValue = targets[target.refId];
  524. targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
  525. targets[target.refId] = targetValue;
  526. if (!target.hide) {
  527. hasTargets = true;
  528. clean_options.push('target=' + encodeURIComponent(targetValue));
  529. }
  530. }
  531. _.each(options, function(value, key) {
  532. if (_.indexOf(graphite_options, key) === -1) {
  533. return;
  534. }
  535. if (value) {
  536. clean_options.push(key + '=' + encodeURIComponent(value));
  537. }
  538. });
  539. if (!hasTargets) {
  540. return [];
  541. }
  542. return clean_options;
  543. };
  544. }
  545. function supportsTags(version: string): boolean {
  546. return isVersionGtOrEq(version, '1.1');
  547. }
  548. function supportsFunctionIndex(version: string): boolean {
  549. return isVersionGtOrEq(version, '1.1');
  550. }