explore.ts 14 KB

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