explore.ts 14 KB

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