datasource.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import angular, { IQService } from 'angular';
  2. import _ from 'lodash';
  3. import { DataSourceApi, DataSourceInstanceSettings, DataQueryRequest, DataQueryResponse } from '@grafana/ui';
  4. import { ElasticResponse } from './elastic_response';
  5. import { IndexPattern } from './index_pattern';
  6. import { ElasticQueryBuilder } from './query_builder';
  7. import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
  8. import * as queryDef from './query_def';
  9. import { BackendSrv } from 'app/core/services/backend_srv';
  10. import { TemplateSrv } from 'app/features/templating/template_srv';
  11. import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
  12. import { ElasticsearchOptions, ElasticsearchQuery } from './types';
  13. export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, ElasticsearchOptions> {
  14. basicAuth: string;
  15. withCredentials: boolean;
  16. url: string;
  17. name: string;
  18. index: string;
  19. timeField: string;
  20. esVersion: number;
  21. interval: string;
  22. maxConcurrentShardRequests: number;
  23. queryBuilder: ElasticQueryBuilder;
  24. indexPattern: IndexPattern;
  25. logMessageField?: string;
  26. logLevelField?: string;
  27. /** @ngInject */
  28. constructor(
  29. instanceSettings: DataSourceInstanceSettings<ElasticsearchOptions>,
  30. private $q: IQService,
  31. private backendSrv: BackendSrv,
  32. private templateSrv: TemplateSrv,
  33. private timeSrv: TimeSrv
  34. ) {
  35. super(instanceSettings);
  36. this.basicAuth = instanceSettings.basicAuth;
  37. this.withCredentials = instanceSettings.withCredentials;
  38. this.url = instanceSettings.url;
  39. this.name = instanceSettings.name;
  40. this.index = instanceSettings.database;
  41. const settingsData = instanceSettings.jsonData || ({} as ElasticsearchOptions);
  42. this.timeField = settingsData.timeField;
  43. this.esVersion = settingsData.esVersion;
  44. this.indexPattern = new IndexPattern(this.index, settingsData.interval);
  45. this.interval = settingsData.timeInterval;
  46. this.maxConcurrentShardRequests = settingsData.maxConcurrentShardRequests;
  47. this.queryBuilder = new ElasticQueryBuilder({
  48. timeField: this.timeField,
  49. esVersion: this.esVersion,
  50. });
  51. this.logMessageField = settingsData.logMessageField || '';
  52. this.logLevelField = settingsData.logLevelField || '';
  53. if (this.logMessageField === '') {
  54. this.logMessageField = null;
  55. }
  56. if (this.logLevelField === '') {
  57. this.logLevelField = null;
  58. }
  59. }
  60. private request(method, url, data?) {
  61. const options: any = {
  62. url: this.url + '/' + url,
  63. method: method,
  64. data: data,
  65. };
  66. if (this.basicAuth || this.withCredentials) {
  67. options.withCredentials = true;
  68. }
  69. if (this.basicAuth) {
  70. options.headers = {
  71. Authorization: this.basicAuth,
  72. };
  73. }
  74. return this.backendSrv.datasourceRequest(options);
  75. }
  76. private get(url) {
  77. const range = this.timeSrv.timeRange();
  78. const indexList = this.indexPattern.getIndexList(range.from.valueOf(), range.to.valueOf());
  79. if (_.isArray(indexList) && indexList.length) {
  80. return this.request('GET', indexList[0] + url).then(results => {
  81. results.data.$$config = results.config;
  82. return results.data;
  83. });
  84. } else {
  85. return this.request('GET', this.indexPattern.getIndexForToday() + url).then(results => {
  86. results.data.$$config = results.config;
  87. return results.data;
  88. });
  89. }
  90. }
  91. private post(url, data) {
  92. return this.request('POST', url, data)
  93. .then(results => {
  94. results.data.$$config = results.config;
  95. return results.data;
  96. })
  97. .catch(err => {
  98. if (err.data && err.data.error) {
  99. throw {
  100. message: 'Elasticsearch error: ' + err.data.error.reason,
  101. error: err.data.error,
  102. };
  103. }
  104. throw err;
  105. });
  106. }
  107. annotationQuery(options) {
  108. const annotation = options.annotation;
  109. const timeField = annotation.timeField || '@timestamp';
  110. const queryString = annotation.query || '*';
  111. const tagsField = annotation.tagsField || 'tags';
  112. const textField = annotation.textField || null;
  113. const range = {};
  114. range[timeField] = {
  115. from: options.range.from.valueOf(),
  116. to: options.range.to.valueOf(),
  117. format: 'epoch_millis',
  118. };
  119. const queryInterpolated = this.templateSrv.replace(queryString, {}, 'lucene');
  120. const query = {
  121. bool: {
  122. filter: [
  123. { range: range },
  124. {
  125. query_string: {
  126. query: queryInterpolated,
  127. },
  128. },
  129. ],
  130. },
  131. };
  132. const data = {
  133. query: query,
  134. size: 10000,
  135. };
  136. // fields field not supported on ES 5.x
  137. if (this.esVersion < 5) {
  138. data['fields'] = [timeField, '_source'];
  139. }
  140. const header: any = {
  141. search_type: 'query_then_fetch',
  142. ignore_unavailable: true,
  143. };
  144. // old elastic annotations had index specified on them
  145. if (annotation.index) {
  146. header.index = annotation.index;
  147. } else {
  148. header.index = this.indexPattern.getIndexList(options.range.from, options.range.to);
  149. }
  150. const payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n';
  151. return this.post('_msearch', payload).then(res => {
  152. const list = [];
  153. const hits = res.responses[0].hits.hits;
  154. const getFieldFromSource = (source, fieldName) => {
  155. if (!fieldName) {
  156. return;
  157. }
  158. const fieldNames = fieldName.split('.');
  159. let fieldValue = source;
  160. for (let i = 0; i < fieldNames.length; i++) {
  161. fieldValue = fieldValue[fieldNames[i]];
  162. if (!fieldValue) {
  163. console.log('could not find field in annotation: ', fieldName);
  164. return '';
  165. }
  166. }
  167. return fieldValue;
  168. };
  169. for (let i = 0; i < hits.length; i++) {
  170. const source = hits[i]._source;
  171. let time = getFieldFromSource(source, timeField);
  172. if (typeof hits[i].fields !== 'undefined') {
  173. const fields = hits[i].fields;
  174. if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) {
  175. time = fields[timeField];
  176. }
  177. }
  178. const event = {
  179. annotation: annotation,
  180. time: toUtc(time).valueOf(),
  181. text: getFieldFromSource(source, textField),
  182. tags: getFieldFromSource(source, tagsField),
  183. };
  184. // legacy support for title tield
  185. if (annotation.titleField) {
  186. const title = getFieldFromSource(source, annotation.titleField);
  187. if (title) {
  188. event.text = title + '\n' + event.text;
  189. }
  190. }
  191. if (typeof event.tags === 'string') {
  192. event.tags = event.tags.split(',');
  193. }
  194. list.push(event);
  195. }
  196. return list;
  197. });
  198. }
  199. testDatasource() {
  200. // validate that the index exist and has date field
  201. return this.getFields({ type: 'date' }).then(
  202. dateFields => {
  203. const timeField: any = _.find(dateFields, { text: this.timeField });
  204. if (!timeField) {
  205. return {
  206. status: 'error',
  207. message: 'No date field named ' + this.timeField + ' found',
  208. };
  209. }
  210. return { status: 'success', message: 'Index OK. Time field name OK.' };
  211. },
  212. err => {
  213. console.log(err);
  214. if (err.data && err.data.error) {
  215. let message = angular.toJson(err.data.error);
  216. if (err.data.error.reason) {
  217. message = err.data.error.reason;
  218. }
  219. return { status: 'error', message: message };
  220. } else {
  221. return { status: 'error', message: err.status };
  222. }
  223. }
  224. );
  225. }
  226. getQueryHeader(searchType, timeFrom, timeTo) {
  227. const queryHeader: any = {
  228. search_type: searchType,
  229. ignore_unavailable: true,
  230. index: this.indexPattern.getIndexList(timeFrom, timeTo),
  231. };
  232. if (this.esVersion >= 56 && this.esVersion < 70) {
  233. queryHeader['max_concurrent_shard_requests'] = this.maxConcurrentShardRequests;
  234. }
  235. return angular.toJson(queryHeader);
  236. }
  237. query(options: DataQueryRequest<ElasticsearchQuery>): Promise<DataQueryResponse> {
  238. let payload = '';
  239. const targets = _.cloneDeep(options.targets);
  240. const sentTargets: ElasticsearchQuery[] = [];
  241. // add global adhoc filters to timeFilter
  242. const adhocFilters = this.templateSrv.getAdhocFilters(this.name);
  243. for (const target of targets) {
  244. if (target.hide) {
  245. continue;
  246. }
  247. let queryString = this.templateSrv.replace(target.query, options.scopedVars, 'lucene');
  248. // Elasticsearch queryString should always be '*' if empty string
  249. if (!queryString || queryString === '') {
  250. queryString = '*';
  251. }
  252. let queryObj;
  253. if (target.isLogsQuery) {
  254. target.bucketAggs = [queryDef.defaultBucketAgg()];
  255. target.metrics = [queryDef.defaultMetricAgg()];
  256. queryObj = this.queryBuilder.getLogsQuery(target, queryString);
  257. } else {
  258. if (target.alias) {
  259. target.alias = this.templateSrv.replace(target.alias, options.scopedVars, 'lucene');
  260. }
  261. queryObj = this.queryBuilder.build(target, adhocFilters, queryString);
  262. }
  263. const esQuery = angular.toJson(queryObj);
  264. const searchType = queryObj.size === 0 && this.esVersion < 5 ? 'count' : 'query_then_fetch';
  265. const header = this.getQueryHeader(searchType, options.range.from, options.range.to);
  266. payload += header + '\n';
  267. payload += esQuery + '\n';
  268. sentTargets.push(target);
  269. }
  270. if (sentTargets.length === 0) {
  271. return Promise.resolve({ data: [] });
  272. }
  273. payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf().toString());
  274. payload = payload.replace(/\$timeTo/g, options.range.to.valueOf().toString());
  275. payload = this.templateSrv.replace(payload, options.scopedVars);
  276. const url = this.getMultiSearchUrl();
  277. return this.post(url, payload).then(res => {
  278. const er = new ElasticResponse(sentTargets, res);
  279. if (sentTargets.some(target => target.isLogsQuery)) {
  280. return er.getLogs(this.logMessageField, this.logLevelField);
  281. }
  282. return er.getTimeSeries();
  283. });
  284. }
  285. getFields(query) {
  286. const configuredEsVersion = this.esVersion;
  287. return this.get('/_mapping').then(result => {
  288. const typeMap = {
  289. float: 'number',
  290. double: 'number',
  291. integer: 'number',
  292. long: 'number',
  293. date: 'date',
  294. string: 'string',
  295. text: 'string',
  296. scaled_float: 'number',
  297. nested: 'nested',
  298. };
  299. function shouldAddField(obj, key, query) {
  300. if (key[0] === '_') {
  301. return false;
  302. }
  303. if (!query.type) {
  304. return true;
  305. }
  306. // equal query type filter, or via typemap translation
  307. return query.type === obj.type || query.type === typeMap[obj.type];
  308. }
  309. // Store subfield names: [system, process, cpu, total] -> system.process.cpu.total
  310. const fieldNameParts = [];
  311. const fields = {};
  312. function getFieldsRecursively(obj) {
  313. for (const key in obj) {
  314. const subObj = obj[key];
  315. // Check mapping field for nested fields
  316. if (_.isObject(subObj.properties)) {
  317. fieldNameParts.push(key);
  318. getFieldsRecursively(subObj.properties);
  319. }
  320. if (_.isObject(subObj.fields)) {
  321. fieldNameParts.push(key);
  322. getFieldsRecursively(subObj.fields);
  323. }
  324. if (_.isString(subObj.type)) {
  325. const fieldName = fieldNameParts.concat(key).join('.');
  326. // Hide meta-fields and check field type
  327. if (shouldAddField(subObj, key, query)) {
  328. fields[fieldName] = {
  329. text: fieldName,
  330. type: subObj.type,
  331. };
  332. }
  333. }
  334. }
  335. fieldNameParts.pop();
  336. }
  337. for (const indexName in result) {
  338. const index = result[indexName];
  339. if (index && index.mappings) {
  340. const mappings = index.mappings;
  341. if (configuredEsVersion < 70) {
  342. for (const typeName in mappings) {
  343. const properties = mappings[typeName].properties;
  344. getFieldsRecursively(properties);
  345. }
  346. } else {
  347. const properties = mappings.properties;
  348. getFieldsRecursively(properties);
  349. }
  350. }
  351. }
  352. // transform to array
  353. return _.map(fields, value => {
  354. return value;
  355. });
  356. });
  357. }
  358. getTerms(queryDef) {
  359. const range = this.timeSrv.timeRange();
  360. const searchType = this.esVersion >= 5 ? 'query_then_fetch' : 'count';
  361. const header = this.getQueryHeader(searchType, range.from, range.to);
  362. let esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
  363. esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf().toString());
  364. esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf().toString());
  365. esQuery = header + '\n' + esQuery + '\n';
  366. const url = this.getMultiSearchUrl();
  367. return this.post(url, esQuery).then(res => {
  368. if (!res.responses[0].aggregations) {
  369. return [];
  370. }
  371. const buckets = res.responses[0].aggregations['1'].buckets;
  372. return _.map(buckets, bucket => {
  373. return {
  374. text: bucket.key_as_string || bucket.key,
  375. value: bucket.key,
  376. };
  377. });
  378. });
  379. }
  380. getMultiSearchUrl() {
  381. if (this.esVersion >= 70 && this.maxConcurrentShardRequests) {
  382. return `_msearch?max_concurrent_shard_requests=${this.maxConcurrentShardRequests}`;
  383. }
  384. return '_msearch';
  385. }
  386. metricFindQuery(query) {
  387. query = angular.fromJson(query);
  388. if (!query) {
  389. return this.$q.when([]);
  390. }
  391. if (query.find === 'fields') {
  392. query.field = this.templateSrv.replace(query.field, {}, 'lucene');
  393. return this.getFields(query);
  394. }
  395. if (query.find === 'terms') {
  396. query.field = this.templateSrv.replace(query.field, {}, 'lucene');
  397. query.query = this.templateSrv.replace(query.query || '*', {}, 'lucene');
  398. return this.getTerms(query);
  399. }
  400. }
  401. getTagKeys() {
  402. return this.getFields({});
  403. }
  404. getTagValues(options) {
  405. return this.getTerms({ field: options.key, query: '*' });
  406. }
  407. targetContainsTemplate(target) {
  408. if (this.templateSrv.variableExists(target.query) || this.templateSrv.variableExists(target.alias)) {
  409. return true;
  410. }
  411. for (const bucketAgg of target.bucketAggs) {
  412. if (this.templateSrv.variableExists(bucketAgg.field) || this.objectContainsTemplate(bucketAgg.settings)) {
  413. return true;
  414. }
  415. }
  416. for (const metric of target.metrics) {
  417. if (
  418. this.templateSrv.variableExists(metric.field) ||
  419. this.objectContainsTemplate(metric.settings) ||
  420. this.objectContainsTemplate(metric.meta)
  421. ) {
  422. return true;
  423. }
  424. }
  425. return false;
  426. }
  427. private isPrimitive(obj) {
  428. if (obj === null || obj === undefined) {
  429. return true;
  430. }
  431. if (['string', 'number', 'boolean'].some(type => type === typeof true)) {
  432. return true;
  433. }
  434. return false;
  435. }
  436. private objectContainsTemplate(obj) {
  437. if (!obj) {
  438. return false;
  439. }
  440. for (const key of Object.keys(obj)) {
  441. if (this.isPrimitive(obj[key])) {
  442. if (this.templateSrv.variableExists(obj[key])) {
  443. return true;
  444. }
  445. } else if (Array.isArray(obj[key])) {
  446. for (const item of obj[key]) {
  447. if (this.objectContainsTemplate(item)) {
  448. return true;
  449. }
  450. }
  451. } else {
  452. if (this.objectContainsTemplate(obj[key])) {
  453. return true;
  454. }
  455. }
  456. }
  457. return false;
  458. }
  459. }
  460. export function getMaxConcurrenShardRequestOrDefault(options: ElasticsearchOptions): number {
  461. if (options.maxConcurrentShardRequests === 5 && options.esVersion < 70) {
  462. return 256;
  463. }
  464. if (options.maxConcurrentShardRequests === 256 && options.esVersion >= 70) {
  465. return 5;
  466. }
  467. const defaultMaxConcurrentShardRequests = options.esVersion >= 70 ? 5 : 256;
  468. return options.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests;
  469. }