explore.ts 14 KB

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