datasource.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import _ from 'lodash';
  2. import * as dateMath from 'app/core/utils/datemath';
  3. import {
  4. getAnnotationsFromResult,
  5. getTableModelFromResult,
  6. getTimeSeriesFromResult,
  7. getValuesFromResult,
  8. parseResults,
  9. } from './response_parser';
  10. import expandMacros from './metric_find_query';
  11. function serializeParams(params) {
  12. if (!params) {
  13. return '';
  14. }
  15. return _.reduce(
  16. params,
  17. (memo, value, key) => {
  18. if (value === null || value === undefined) {
  19. return memo;
  20. }
  21. memo.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
  22. return memo;
  23. },
  24. []
  25. ).join('&');
  26. }
  27. const MAX_SERIES = 20;
  28. export default class InfluxDatasource {
  29. type: string;
  30. url: string;
  31. username: string;
  32. password: string;
  33. name: string;
  34. orgName: string;
  35. database: any;
  36. basicAuth: any;
  37. withCredentials: any;
  38. interval: any;
  39. supportAnnotations: boolean;
  40. supportMetrics: boolean;
  41. /** @ngInject */
  42. constructor(instanceSettings, private backendSrv, private templateSrv) {
  43. this.type = 'influxdb-ifql';
  44. this.url = instanceSettings.url.trim();
  45. this.username = instanceSettings.username;
  46. this.password = instanceSettings.password;
  47. this.name = instanceSettings.name;
  48. this.orgName = instanceSettings.orgName || 'defaultorgname';
  49. this.database = instanceSettings.database;
  50. this.basicAuth = instanceSettings.basicAuth;
  51. this.withCredentials = instanceSettings.withCredentials;
  52. this.interval = (instanceSettings.jsonData || {}).timeInterval;
  53. this.supportAnnotations = true;
  54. this.supportMetrics = true;
  55. }
  56. prepareQueryTarget(target, options) {
  57. // Replace grafana variables
  58. const timeFilter = this.getTimeFilter(options);
  59. options.scopedVars.range = { value: timeFilter };
  60. const interpolated = this.templateSrv.replace(target.query, options.scopedVars);
  61. return {
  62. ...target,
  63. query: interpolated,
  64. };
  65. }
  66. query(options) {
  67. const queryTargets = options.targets
  68. .filter(target => target.query)
  69. .map(target => this.prepareQueryTarget(target, options));
  70. if (queryTargets.length === 0) {
  71. return Promise.resolve({ data: [] });
  72. }
  73. const queries = queryTargets.map(target => {
  74. const { query, resultFormat } = target;
  75. if (resultFormat === 'table') {
  76. return this._seriesQuery(query, options)
  77. .then(response => parseResults(response.data))
  78. .then(results => results.map(getTableModelFromResult));
  79. } else {
  80. return this._seriesQuery(query, options)
  81. .then(response => parseResults(response.data))
  82. .then(results => results.map(getTimeSeriesFromResult));
  83. }
  84. });
  85. return Promise.all(queries).then((series: any) => {
  86. let seriesList = _.flattenDeep(series).slice(0, MAX_SERIES);
  87. return { data: seriesList };
  88. });
  89. }
  90. annotationQuery(options) {
  91. if (!options.annotation.query) {
  92. return Promise.reject({
  93. message: 'Query missing in annotation definition',
  94. });
  95. }
  96. const { query } = options.annotation;
  97. const queryOptions = {
  98. scopedVars: {},
  99. ...options,
  100. silent: true,
  101. };
  102. const target = this.prepareQueryTarget({ query }, queryOptions);
  103. return this._seriesQuery(target.query, queryOptions).then(response => {
  104. const results = parseResults(response.data);
  105. if (results.length === 0) {
  106. throw { message: 'No results in response from InfluxDB' };
  107. }
  108. const annotations = _.flatten(results.map(result => getAnnotationsFromResult(result, options.annotation)));
  109. return annotations;
  110. });
  111. }
  112. metricFindQuery(query: string, options?: any) {
  113. const interpreted = expandMacros(query);
  114. // Use normal querier in silent mode
  115. const queryOptions = {
  116. rangeRaw: { to: 'now', from: 'now - 1h' },
  117. scopedVars: {},
  118. ...options,
  119. silent: true,
  120. };
  121. const target = this.prepareQueryTarget({ query: interpreted }, queryOptions);
  122. return this._seriesQuery(target.query, queryOptions).then(response => {
  123. const results = parseResults(response.data);
  124. const values = _.uniq(_.flatten(results.map(getValuesFromResult)));
  125. return values
  126. .filter(value => value && value[0] !== '_') // Ignore internal fields
  127. .map(value => ({ text: value }));
  128. });
  129. }
  130. _seriesQuery(query: string, options?: any) {
  131. if (!query) {
  132. return Promise.resolve({ data: '' });
  133. }
  134. return this._influxRequest('POST', '/v1/query', { q: query }, options);
  135. }
  136. testDatasource() {
  137. const query = `from(db:"${this.database}") |> last()`;
  138. return this._influxRequest('POST', '/v1/query', { q: query })
  139. .then(res => {
  140. if (res && res.data && res.data.trim()) {
  141. return { status: 'success', message: 'Data source connected and database found.' };
  142. }
  143. return {
  144. status: 'error',
  145. message:
  146. 'Data source connected, but has no data. Verify the "Database" field and make sure the database has data.',
  147. };
  148. })
  149. .catch(err => {
  150. return { status: 'error', message: err.message };
  151. });
  152. }
  153. _influxRequest(method: string, url: string, data: any, options?: any) {
  154. let params: any = {
  155. orgName: this.orgName,
  156. };
  157. if (this.username) {
  158. params.u = this.username;
  159. params.p = this.password;
  160. }
  161. // data sent as GET param
  162. _.extend(params, data);
  163. data = null;
  164. let req: any = {
  165. method: method,
  166. url: this.url + url,
  167. params: params,
  168. data: data,
  169. precision: 'ms',
  170. inspect: { type: this.type },
  171. paramSerializer: serializeParams,
  172. };
  173. req.headers = req.headers || {};
  174. if (this.basicAuth || this.withCredentials) {
  175. req.withCredentials = true;
  176. }
  177. if (this.basicAuth) {
  178. req.headers.Authorization = this.basicAuth;
  179. }
  180. return this.backendSrv.datasourceRequest(req).then(
  181. result => {
  182. return result;
  183. },
  184. function(err) {
  185. if (err.status !== 0 || err.status >= 300) {
  186. if (err.data && err.data.error) {
  187. throw {
  188. message: 'InfluxDB Error: ' + err.data.error,
  189. data: err.data,
  190. config: err.config,
  191. };
  192. } else {
  193. throw {
  194. message: 'Network Error: ' + err.statusText + '(' + err.status + ')',
  195. data: err.data,
  196. config: err.config,
  197. };
  198. }
  199. }
  200. }
  201. );
  202. }
  203. getTimeFilter(options) {
  204. const from = this.getInfluxTime(options.rangeRaw.from, false);
  205. const to = this.getInfluxTime(options.rangeRaw.to, true);
  206. if (to === 'now') {
  207. return `start: ${from}`;
  208. }
  209. return `start: ${from}, stop: ${to}`;
  210. }
  211. getInfluxTime(date, roundUp) {
  212. if (_.isString(date)) {
  213. if (date === 'now') {
  214. return date;
  215. }
  216. const parts = /^now\s*-\s*(\d+)([d|h|m|s])$/.exec(date);
  217. if (parts) {
  218. const amount = parseInt(parts[1]);
  219. const unit = parts[2];
  220. return '-' + amount + unit;
  221. }
  222. date = dateMath.parse(date, roundUp);
  223. }
  224. return date.toISOString();
  225. }
  226. }