explore.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. // Libraries
  2. import _ from 'lodash';
  3. // Services & Utils
  4. import * as dateMath from '@grafana/ui/src/utils/datemath';
  5. import { renderUrl } from 'app/core/utils/url';
  6. import kbn from 'app/core/utils/kbn';
  7. import store from 'app/core/store';
  8. import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
  9. import { getNextRefIdChar } from './query';
  10. // Types
  11. import {
  12. colors,
  13. TimeRange,
  14. RawTimeRange,
  15. TimeZone,
  16. IntervalValues,
  17. DataQuery,
  18. DataSourceApi,
  19. toSeriesData,
  20. guessFieldTypes,
  21. TimeFragment,
  22. DataQueryError,
  23. LogRowModel,
  24. LogsModel,
  25. LogsDedupStrategy,
  26. } from '@grafana/ui';
  27. import TimeSeries from 'app/core/time_series2';
  28. import {
  29. ExploreUrlState,
  30. HistoryItem,
  31. QueryTransaction,
  32. ResultType,
  33. QueryIntervals,
  34. QueryOptions,
  35. ResultGetter,
  36. } from 'app/types/explore';
  37. import { seriesDataToLogsModel } from 'app/core/logs_model';
  38. import { toUtc } from '@grafana/ui/src/utils/moment_wrapper';
  39. import { isLive } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
  40. export const DEFAULT_RANGE = {
  41. from: 'now-6h',
  42. to: 'now',
  43. };
  44. export const DEFAULT_UI_STATE = {
  45. showingTable: true,
  46. showingGraph: true,
  47. showingLogs: true,
  48. dedupStrategy: LogsDedupStrategy.none,
  49. };
  50. const MAX_HISTORY_ITEMS = 100;
  51. export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
  52. /**
  53. * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  54. *
  55. * @param panel Origin panel of the jump to Explore
  56. * @param panelTargets The origin panel's query targets
  57. * @param panelDatasource The origin panel's datasource
  58. * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
  59. * @param timeSrv Time service to get the current dashboard range from
  60. */
  61. export async function getExploreUrl(
  62. panel: any,
  63. panelTargets: any[],
  64. panelDatasource: any,
  65. datasourceSrv: any,
  66. timeSrv: any
  67. ) {
  68. let exploreDatasource = panelDatasource;
  69. let exploreTargets: DataQuery[] = panelTargets;
  70. let url: string;
  71. // Mixed datasources need to choose only one datasource
  72. if (panelDatasource.meta.id === 'mixed' && panelTargets) {
  73. // Find first explore datasource among targets
  74. let mixedExploreDatasource;
  75. for (const t of panel.targets) {
  76. const datasource = await datasourceSrv.get(t.datasource);
  77. if (datasource && datasource.meta.explore) {
  78. mixedExploreDatasource = datasource;
  79. break;
  80. }
  81. }
  82. // Add all its targets
  83. if (mixedExploreDatasource) {
  84. exploreDatasource = mixedExploreDatasource;
  85. exploreTargets = panelTargets.filter(t => t.datasource === mixedExploreDatasource.name);
  86. }
  87. }
  88. if (panelDatasource) {
  89. const range = timeSrv.timeRangeForUrl();
  90. let state: Partial<ExploreUrlState> = { range };
  91. if (exploreDatasource.getExploreState) {
  92. state = { ...state, ...exploreDatasource.getExploreState(exploreTargets) };
  93. } else {
  94. state = {
  95. ...state,
  96. datasource: panelDatasource.name,
  97. queries: exploreTargets.map(t => ({ ...t, datasource: panelDatasource.name })),
  98. };
  99. }
  100. const exploreState = JSON.stringify(state);
  101. url = renderUrl('/explore', { left: exploreState });
  102. }
  103. return url;
  104. }
  105. export function buildQueryTransaction(
  106. queries: DataQuery[],
  107. resultType: ResultType,
  108. queryOptions: QueryOptions,
  109. range: TimeRange,
  110. queryIntervals: QueryIntervals,
  111. scanning: boolean
  112. ): QueryTransaction {
  113. const { interval, intervalMs } = queryIntervals;
  114. const configuredQueries = queries.map(query => ({ ...query, ...queryOptions }));
  115. const key = queries.reduce((combinedKey, query) => {
  116. combinedKey += query.key;
  117. return combinedKey;
  118. }, '');
  119. // Clone range for query request
  120. // const queryRange: RawTimeRange = { ...range };
  121. // const { from, to, raw } = this.timeSrv.timeRange();
  122. // Most datasource is using `panelId + query.refId` for cancellation logic.
  123. // Using `format` here because it relates to the view panel that the request is for.
  124. // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
  125. // Therefore panel id has to be unique.
  126. const panelId = `${queryOptions.format}-${key}`;
  127. const options = {
  128. interval,
  129. intervalMs,
  130. panelId,
  131. targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
  132. range,
  133. rangeRaw: range.raw,
  134. scopedVars: {
  135. __interval: { text: interval, value: interval },
  136. __interval_ms: { text: intervalMs, value: intervalMs },
  137. },
  138. maxDataPoints: queryOptions.maxDataPoints,
  139. };
  140. return {
  141. queries,
  142. options,
  143. resultType,
  144. scanning,
  145. id: generateKey(), // reusing for unique ID
  146. done: false,
  147. latency: 0,
  148. };
  149. }
  150. export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
  151. const metricProperties = ['expr', 'target', 'datasource'];
  152. const isMetricSegment = (segment: { [key: string]: string }) =>
  153. metricProperties.some(prop => segment.hasOwnProperty(prop));
  154. const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui');
  155. enum ParseUrlStateIndex {
  156. RangeFrom = 0,
  157. RangeTo = 1,
  158. Datasource = 2,
  159. SegmentsStart = 3,
  160. }
  161. enum ParseUiStateIndex {
  162. Graph = 0,
  163. Logs = 1,
  164. Table = 2,
  165. Strategy = 3,
  166. }
  167. export const safeParseJson = (text: string) => {
  168. if (!text) {
  169. return;
  170. }
  171. try {
  172. return JSON.parse(decodeURI(text));
  173. } catch (error) {
  174. console.error(error);
  175. }
  176. };
  177. export const safeStringifyValue = (value: any, space?: number) => {
  178. if (!value) {
  179. return '';
  180. }
  181. try {
  182. return JSON.stringify(value, null, space);
  183. } catch (error) {
  184. console.error(error);
  185. }
  186. return '';
  187. };
  188. export function parseUrlState(initial: string | undefined): ExploreUrlState {
  189. const parsed = safeParseJson(initial);
  190. const errorResult = {
  191. datasource: null,
  192. queries: [],
  193. range: DEFAULT_RANGE,
  194. ui: DEFAULT_UI_STATE,
  195. };
  196. if (!parsed) {
  197. return errorResult;
  198. }
  199. if (!Array.isArray(parsed)) {
  200. return parsed;
  201. }
  202. if (parsed.length <= ParseUrlStateIndex.SegmentsStart) {
  203. console.error('Error parsing compact URL state for Explore.');
  204. return errorResult;
  205. }
  206. const range = {
  207. from: parsed[ParseUrlStateIndex.RangeFrom],
  208. to: parsed[ParseUrlStateIndex.RangeTo],
  209. };
  210. const datasource = parsed[ParseUrlStateIndex.Datasource];
  211. const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
  212. const queries = parsedSegments.filter(segment => isMetricSegment(segment));
  213. const uiState = parsedSegments.filter(segment => isUISegment(segment))[0];
  214. const ui = uiState
  215. ? {
  216. showingGraph: uiState.ui[ParseUiStateIndex.Graph],
  217. showingLogs: uiState.ui[ParseUiStateIndex.Logs],
  218. showingTable: uiState.ui[ParseUiStateIndex.Table],
  219. dedupStrategy: uiState.ui[ParseUiStateIndex.Strategy],
  220. }
  221. : DEFAULT_UI_STATE;
  222. return { datasource, queries, range, ui };
  223. }
  224. export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
  225. if (compact) {
  226. return JSON.stringify([
  227. urlState.range.from,
  228. urlState.range.to,
  229. urlState.datasource,
  230. ...urlState.queries,
  231. {
  232. ui: [
  233. !!urlState.ui.showingGraph,
  234. !!urlState.ui.showingLogs,
  235. !!urlState.ui.showingTable,
  236. urlState.ui.dedupStrategy,
  237. ],
  238. },
  239. ]);
  240. }
  241. return JSON.stringify(urlState);
  242. }
  243. export function generateKey(index = 0): string {
  244. return `Q-${Date.now()}-${Math.random()}-${index}`;
  245. }
  246. export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
  247. return { refId: getNextRefIdChar(queries), key: generateKey(index) };
  248. }
  249. export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => {
  250. const key = generateKey(index);
  251. const refId = target.refId || getNextRefIdChar(queries);
  252. return { ...target, refId, key };
  253. };
  254. /**
  255. * Ensure at least one target exists and that targets have the necessary keys
  256. */
  257. export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
  258. if (queries && typeof queries === 'object' && queries.length > 0) {
  259. const allQueries = [];
  260. for (let index = 0; index < queries.length; index++) {
  261. const query = queries[index];
  262. const key = generateKey(index);
  263. let refId = query.refId;
  264. if (!refId) {
  265. refId = getNextRefIdChar(allQueries);
  266. }
  267. allQueries.push({
  268. ...query,
  269. refId,
  270. key,
  271. });
  272. }
  273. return allQueries;
  274. }
  275. return [{ ...generateEmptyQuery(queries) }];
  276. }
  277. /**
  278. * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  279. */
  280. export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
  281. return (
  282. queries &&
  283. queries.some(
  284. query =>
  285. Object.keys(query)
  286. .map(k => query[k])
  287. .filter(v => v).length > 2
  288. )
  289. );
  290. }
  291. export function calculateResultsFromQueryTransactions(result: any, resultType: ResultType, graphInterval: number) {
  292. const flattenedResult: any[] = _.flatten(result);
  293. const graphResult = resultType === 'Graph' && result ? result : null;
  294. const tableResult =
  295. resultType === 'Table' && result
  296. ? mergeTablesIntoModel(
  297. new TableModel(),
  298. ...flattenedResult.filter((r: any) => r.columns && r.rows).map((r: any) => r as TableModel)
  299. )
  300. : mergeTablesIntoModel(new TableModel());
  301. const logsResult =
  302. resultType === 'Logs' && result
  303. ? seriesDataToLogsModel(flattenedResult.map(r => guessFieldTypes(toSeriesData(r))), graphInterval)
  304. : null;
  305. return {
  306. graphResult,
  307. tableResult,
  308. logsResult,
  309. };
  310. }
  311. export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues {
  312. if (!resolution) {
  313. return { interval: '1s', intervalMs: 1000 };
  314. }
  315. return kbn.calculateInterval(range, resolution, lowLimit);
  316. }
  317. export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => {
  318. // Prevent multiple Graph transactions to have the same colors
  319. let colorIndexOffset = 0;
  320. for (const other of allTransactions) {
  321. // Only need to consider transactions that came before the current one
  322. if (other === transaction) {
  323. break;
  324. }
  325. // Count timeseries of previous query results
  326. if (other.resultType === 'Graph' && other.done) {
  327. colorIndexOffset += other.result.length;
  328. }
  329. }
  330. return dataList.map((seriesData, index: number) => {
  331. const datapoints = seriesData.datapoints || [];
  332. const alias = seriesData.target;
  333. const colorIndex = (colorIndexOffset + index) % colors.length;
  334. const color = colors[colorIndex];
  335. const series = new TimeSeries({
  336. datapoints,
  337. alias,
  338. color,
  339. unit: seriesData.unit,
  340. });
  341. return series;
  342. });
  343. };
  344. /**
  345. * Update the query history. Side-effect: store history in local storage
  346. */
  347. export function updateHistory<T extends DataQuery = any>(
  348. history: Array<HistoryItem<T>>,
  349. datasourceId: string,
  350. queries: T[]
  351. ): Array<HistoryItem<T>> {
  352. const ts = Date.now();
  353. queries.forEach(query => {
  354. history = [{ query, ts }, ...history];
  355. });
  356. if (history.length > MAX_HISTORY_ITEMS) {
  357. history = history.slice(0, MAX_HISTORY_ITEMS);
  358. }
  359. // Combine all queries of a datasource type into one history
  360. const historyKey = `grafana.explore.history.${datasourceId}`;
  361. store.setObject(historyKey, history);
  362. return history;
  363. }
  364. export function clearHistory(datasourceId: string) {
  365. const historyKey = `grafana.explore.history.${datasourceId}`;
  366. store.delete(historyKey);
  367. }
  368. export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => {
  369. const queryKeys = queries.reduce((newQueryKeys, query, index) => {
  370. const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
  371. return newQueryKeys.concat(`${primaryKey}-${index}`);
  372. }, []);
  373. return queryKeys;
  374. };
  375. export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => {
  376. return {
  377. from: dateMath.parse(rawRange.from, false, timeZone.raw as any),
  378. to: dateMath.parse(rawRange.to, true, timeZone.raw as any),
  379. raw: rawRange,
  380. };
  381. };
  382. const parseRawTime = (value): TimeFragment => {
  383. if (value === null) {
  384. return null;
  385. }
  386. if (value.indexOf('now') !== -1) {
  387. return value;
  388. }
  389. if (value.length === 8) {
  390. return toUtc(value, 'YYYYMMDD');
  391. }
  392. if (value.length === 15) {
  393. return toUtc(value, 'YYYYMMDDTHHmmss');
  394. }
  395. // Backward compatibility
  396. if (value.length === 19) {
  397. return toUtc(value, 'YYYY-MM-DD HH:mm:ss');
  398. }
  399. if (!isNaN(value)) {
  400. const epoch = parseInt(value, 10);
  401. return toUtc(epoch);
  402. }
  403. return null;
  404. };
  405. export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => {
  406. const raw = {
  407. from: parseRawTime(range.from),
  408. to: parseRawTime(range.to),
  409. };
  410. return {
  411. from: dateMath.parse(raw.from, false, timeZone.raw as any),
  412. to: dateMath.parse(raw.to, true, timeZone.raw as any),
  413. raw,
  414. };
  415. };
  416. export const instanceOfDataQueryError = (value: any): value is DataQueryError => {
  417. return value.message !== undefined && value.status !== undefined && value.statusText !== undefined;
  418. };
  419. export const getValueWithRefId = (value: any): any | null => {
  420. if (!value) {
  421. return null;
  422. }
  423. if (typeof value !== 'object') {
  424. return null;
  425. }
  426. if (value.refId) {
  427. return value;
  428. }
  429. const keys = Object.keys(value);
  430. for (let index = 0; index < keys.length; index++) {
  431. const key = keys[index];
  432. const refId = getValueWithRefId(value[key]);
  433. if (refId) {
  434. return refId;
  435. }
  436. }
  437. return null;
  438. };
  439. export const getFirstQueryErrorWithoutRefId = (errors: DataQueryError[]) => {
  440. if (!errors) {
  441. return null;
  442. }
  443. return errors.filter(error => (error.refId ? false : true))[0];
  444. };
  445. export const getRefIds = (value: any): string[] => {
  446. if (!value) {
  447. return [];
  448. }
  449. if (typeof value !== 'object') {
  450. return [];
  451. }
  452. const keys = Object.keys(value);
  453. const refIds = [];
  454. for (let index = 0; index < keys.length; index++) {
  455. const key = keys[index];
  456. if (key === 'refId') {
  457. refIds.push(value[key]);
  458. continue;
  459. }
  460. refIds.push(getRefIds(value[key]));
  461. }
  462. return _.uniq(_.flatten(refIds));
  463. };
  464. const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
  465. if (a.timeEpochMs < b.timeEpochMs) {
  466. return -1;
  467. }
  468. if (a.timeEpochMs > b.timeEpochMs) {
  469. return 1;
  470. }
  471. return 0;
  472. };
  473. const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
  474. if (a.timeEpochMs > b.timeEpochMs) {
  475. return -1;
  476. }
  477. if (a.timeEpochMs < b.timeEpochMs) {
  478. return 1;
  479. }
  480. return 0;
  481. };
  482. export const sortLogsResult = (logsResult: LogsModel, refreshInterval: string) => {
  483. const rows = logsResult ? logsResult.rows : [];
  484. const live = isLive(refreshInterval);
  485. live ? rows.sort(sortInAscendingOrder) : rows.sort(sortInDescendingOrder);
  486. const result: LogsModel = logsResult ? { ...logsResult, rows } : { hasUniqueLabels: false, rows };
  487. return result;
  488. };