explore.ts 14 KB

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