explore.ts 13.9 KB
Newer Older
1
// Libraries
David Kaltschmidt committed
2
import _ from 'lodash';
3
import { Unsubscribable } from 'rxjs';
4
// Services & Utils
5
import {
6
  CoreApp,
7
  DataQuery,
8 9
  DataQueryError,
  DataQueryRequest,
10 11
  DataSourceApi,
  dateMath,
12
  DefaultTimeZone,
13
  HistoryItem,
14 15 16
  IntervalValues,
  LogRowModel,
  LogsDedupStrategy,
17
  LogsSortOrder,
18 19 20 21 22
  RawTimeRange,
  TimeFragment,
  TimeRange,
  TimeZone,
  toUtc,
23
  urlUtil,
24
  ExploreUrlState,
25
  rangeUtil,
26
} from '@grafana/data';
27
import store from 'app/core/store';
28
import { v4 as uuidv4 } from 'uuid';
Peter Holmberg committed
29
import { getNextRefIdChar } from './query';
30
// Types
31
import { RefreshPicker } from '@grafana/ui';
32
import { QueryOptions, QueryTransaction } from 'app/types/explore';
33
import { config } from '../config';
34
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
35
import { DataSourceSrv } from '@grafana/runtime';
36
import { PanelModel } from 'app/features/dashboard/state';
37 38

export const DEFAULT_RANGE = {
39
  from: 'now-1h',
40 41
  to: 'now',
};
42

Dominik Prokop committed
43 44 45 46
export const DEFAULT_UI_STATE = {
  showingTable: true,
  showingGraph: true,
  showingLogs: true,
47
  dedupStrategy: LogsDedupStrategy.none,
Dominik Prokop committed
48 49
};

50 51
const MAX_HISTORY_ITEMS = 100;

52
export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
53
export const lastUsedDatasourceKeyForOrgId = (orgId: number) => `${LAST_USED_DATASOURCE_KEY}.${orgId}`;
54

55 56 57 58 59 60 61 62
/**
 * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
 *
 * @param panelTargets The origin panel's query targets
 * @param panelDatasource The origin panel's datasource
 * @param datasourceSrv Datasource service to query other datasources in case the panel datasource is mixed
 * @param timeSrv Time service to get the current dashboard range from
 */
63 64 65 66 67 68 69
export interface GetExploreUrlArguments {
  panel: PanelModel;
  panelTargets: DataQuery[];
  panelDatasource: DataSourceApi;
  datasourceSrv: DataSourceSrv;
  timeSrv: TimeSrv;
}
70

Andrej Ocenas committed
71
export async function getExploreUrl(args: GetExploreUrlArguments): Promise<string | undefined> {
72
  const { panel, panelTargets, panelDatasource, datasourceSrv, timeSrv } = args;
73
  let exploreDatasource = panelDatasource;
74 75 76 77 78

  /** In Explore, we don't have legend formatter and we don't want to keep
   * legend formatting as we can't change it
   */
  let exploreTargets: DataQuery[] = panelTargets.map(t => _.omit(t, 'legendFormat'));
Andrej Ocenas committed
79
  let url: string | undefined;
80 81

  // Mixed datasources need to choose only one datasource
Andrej Ocenas committed
82
  if (panelDatasource.meta?.id === 'mixed' && exploreTargets) {
83
    // Find first explore datasource among targets
84
    for (const t of exploreTargets) {
Andrej Ocenas committed
85
      const datasource = await datasourceSrv.get(t.datasource || undefined);
86 87 88
      if (datasource) {
        exploreDatasource = datasource;
        exploreTargets = panelTargets.filter(t => t.datasource === datasource.name);
89 90 91 92 93
        break;
      }
    }
  }

94
  if (exploreDatasource) {
95
    const range = timeSrv.timeRangeForUrl();
96
    let state: Partial<ExploreUrlState> = { range };
97
    if (exploreDatasource.interpolateVariablesInQueries) {
98
      const scopedVars = panel.scopedVars || {};
99 100 101 102
      state = {
        ...state,
        datasource: exploreDatasource.name,
        context: 'explore',
103
        queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars),
104
      };
105 106 107
    } else {
      state = {
        ...state,
108
        datasource: exploreDatasource.name,
109
        context: 'explore',
110
        queries: exploreTargets.map(t => ({ ...t, datasource: exploreDatasource.name })),
111 112 113
      };
    }

114
    const exploreState = JSON.stringify({ ...state, originPanelId: panel.getSavedId() });
115
    url = urlUtil.renderUrl('/explore', { left: exploreState });
116
  }
117

118
  return url;
119
}
120

121
export function buildQueryTransaction(
122
  queries: DataQuery[],
123
  queryOptions: QueryOptions,
124
  range: TimeRange,
125 126
  scanning: boolean,
  timeZone?: TimeZone
127
): QueryTransaction {
128 129 130 131
  const key = queries.reduce((combinedKey, query) => {
    combinedKey += query.key;
    return combinedKey;
  }, '');
132

133 134
  const { interval, intervalMs } = getIntervals(range, queryOptions.minInterval, queryOptions.maxDataPoints);

135 136 137 138
  // Most datasource is using `panelId + query.refId` for cancellation logic.
  // Using `format` here because it relates to the view panel that the request is for.
  // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
  // Therefore panel id has to be unique.
139
  const panelId = `${key}`;
140

141
  const request: DataQueryRequest = {
142
    app: CoreApp.Explore,
143 144
    dashboardId: 0,
    // TODO probably should be taken from preferences but does not seem to be used anyway.
145
    timezone: timeZone || DefaultTimeZone,
146
    startTime: Date.now(),
147 148
    interval,
    intervalMs,
149 150 151
    // TODO: the query request expects number and we are using string here. Seems like it works so far but can create
    // issues down the road.
    panelId: panelId as any,
152
    targets: queries, // Datasources rely on DataQueries being passed under the targets key.
153
    range,
154
    requestId: 'explore',
155
    rangeRaw: range.raw,
156 157 158 159
    scopedVars: {
      __interval: { text: interval, value: interval },
      __interval_ms: { text: intervalMs, value: intervalMs },
    },
160
    maxDataPoints: queryOptions.maxDataPoints,
161
    exploreMode: queryOptions.mode,
162 163 164
    liveStreaming: queryOptions.liveStreaming,
    showingGraph: queryOptions.showingGraph,
    showingTable: queryOptions.showingTable,
165 166 167
  };

  return {
168
    queries,
169
    request,
170 171 172 173 174 175 176
    scanning,
    id: generateKey(), // reusing for unique ID
    done: false,
    latency: 0,
  };
}

177
export const clearQueryKeys: (query: DataQuery) => object = ({ key, refId, ...rest }) => rest;
178

179 180
const isSegment = (segment: { [key: string]: string }, ...props: string[]) =>
  props.some(prop => segment.hasOwnProperty(prop));
181

182 183 184 185 186 187 188 189 190 191 192 193 194 195
enum ParseUrlStateIndex {
  RangeFrom = 0,
  RangeTo = 1,
  Datasource = 2,
  SegmentsStart = 3,
}

enum ParseUiStateIndex {
  Graph = 0,
  Logs = 1,
  Table = 2,
  Strategy = 3,
}

Andrej Ocenas committed
196
export const safeParseJson = (text?: string): any | undefined => {
197 198 199 200 201 202 203 204 205 206 207
  if (!text) {
    return;
  }

  try {
    return JSON.parse(decodeURI(text));
  } catch (error) {
    console.error(error);
  }
};

208 209 210 211 212 213 214 215 216 217 218 219 220 221
export const safeStringifyValue = (value: any, space?: number) => {
  if (!value) {
    return '';
  }

  try {
    return JSON.stringify(value, null, space);
  } catch (error) {
    console.error(error);
  }

  return '';
};

222
export function parseUrlState(initial: string | undefined): ExploreUrlState {
223
  const parsed = safeParseJson(initial);
224
  const errorResult: any = {
225 226 227 228
    datasource: null,
    queries: [],
    range: DEFAULT_RANGE,
    ui: DEFAULT_UI_STATE,
229
    mode: null,
230
    originPanelId: null,
231
  };
232 233 234 235 236 237 238 239 240 241 242 243

  if (!parsed) {
    return errorResult;
  }

  if (!Array.isArray(parsed)) {
    return parsed;
  }

  if (parsed.length <= ParseUrlStateIndex.SegmentsStart) {
    console.error('Error parsing compact URL state for Explore.');
    return errorResult;
244
  }
245 246 247 248 249 250 251

  const range = {
    from: parsed[ParseUrlStateIndex.RangeFrom],
    to: parsed[ParseUrlStateIndex.RangeTo],
  };
  const datasource = parsed[ParseUrlStateIndex.Datasource];
  const parsedSegments = parsed.slice(ParseUrlStateIndex.SegmentsStart);
252
  const queries = parsedSegments.filter(segment => !isSegment(segment, 'ui', 'originPanelId'));
253

254
  const uiState = parsedSegments.filter(segment => isSegment(segment, 'ui'))[0];
255 256 257 258 259 260 261 262 263
  const ui = uiState
    ? {
        showingGraph: uiState.ui[ParseUiStateIndex.Graph],
        showingLogs: uiState.ui[ParseUiStateIndex.Logs],
        showingTable: uiState.ui[ParseUiStateIndex.Table],
        dedupStrategy: uiState.ui[ParseUiStateIndex.Strategy],
      }
    : DEFAULT_UI_STATE;

264
  const originPanelId = parsedSegments.filter(segment => isSegment(segment, 'originPanelId'))[0];
265
  return { datasource, queries, range, ui, originPanelId };
266 267
}

268
export function generateKey(index = 0): string {
269
  return `Q-${uuidv4()}-${index}`;
270 271
}

272
export function generateEmptyQuery(queries: DataQuery[], index = 0): DataQuery {
Peter Holmberg committed
273
  return { refId: getNextRefIdChar(queries), key: generateKey(index) };
274 275
}

276 277 278 279 280 281 282
export const generateNewKeyAndAddRefIdIfMissing = (target: DataQuery, queries: DataQuery[], index = 0): DataQuery => {
  const key = generateKey(index);
  const refId = target.refId || getNextRefIdChar(queries);

  return { ...target, refId, key };
};

283 284 285
/**
 * Ensure at least one target exists and that targets have the necessary keys
 */
286 287
export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
  if (queries && typeof queries === 'object' && queries.length > 0) {
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    const allQueries = [];
    for (let index = 0; index < queries.length; index++) {
      const query = queries[index];
      const key = generateKey(index);
      let refId = query.refId;
      if (!refId) {
        refId = getNextRefIdChar(allQueries);
      }

      allQueries.push({
        ...query,
        refId,
        key,
      });
    }
    return allQueries;
304
  }
305
  return [{ ...generateEmptyQuery(queries ?? []) }];
306 307 308
}

/**
309
 * A target is non-empty when it has keys (with non-empty values) other than refId, key and context.
310
 */
311
const validKeys = ['refId', 'key', 'context'];
312
export function hasNonEmptyQuery<TQuery extends DataQuery = any>(queries: TQuery[]): boolean {
313 314
  return (
    queries &&
315
    queries.some((query: any) => {
316 317 318 319 320 321
      const keys = Object.keys(query)
        .filter(key => validKeys.indexOf(key) === -1)
        .map(k => query[k])
        .filter(v => v);
      return keys.length > 0;
    })
322
  );
323 324 325 326 327
}

/**
 * Update the query history. Side-effect: store history in local storage
 */
328 329 330 331 332
export function updateHistory<T extends DataQuery = any>(
  history: Array<HistoryItem<T>>,
  datasourceId: string,
  queries: T[]
): Array<HistoryItem<T>> {
333
  const ts = Date.now();
334
  let updatedHistory = history;
335
  queries.forEach(query => {
336
    updatedHistory = [{ query, ts }, ...updatedHistory];
337 338
  });

339 340
  if (updatedHistory.length > MAX_HISTORY_ITEMS) {
    updatedHistory = updatedHistory.slice(0, MAX_HISTORY_ITEMS);
341 342 343 344
  }

  // Combine all queries of a datasource type into one history
  const historyKey = `grafana.explore.history.${datasourceId}`;
345 346 347 348 349 350 351
  try {
    store.setObject(historyKey, updatedHistory);
    return updatedHistory;
  } catch (error) {
    console.error(error);
    return history;
  }
352
}
353 354 355 356 357

export function clearHistory(datasourceId: string) {
  const historyKey = `grafana.explore.history.${datasourceId}`;
  store.delete(historyKey);
}
358

359
export const getQueryKeys = (queries: DataQuery[], datasourceInstance?: DataSourceApi | null): string[] => {
Andrej Ocenas committed
360
  const queryKeys = queries.reduce<string[]>((newQueryKeys, query, index) => {
361 362 363 364 365 366
    const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key;
    return newQueryKeys.concat(`${primaryKey}-${index}`);
  }, []);

  return queryKeys;
};
367 368 369

export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => {
  return {
370 371
    from: dateMath.parse(rawRange.from, false, timeZone as any)!,
    to: dateMath.parse(rawRange.to, true, timeZone as any)!,
372 373 374 375
    raw: rawRange,
  };
};

Andrej Ocenas committed
376
const parseRawTime = (value: any): TimeFragment | null => {
377 378 379 380 381 382 383 384
  if (value === null) {
    return null;
  }

  if (value.indexOf('now') !== -1) {
    return value;
  }
  if (value.length === 8) {
385
    return toUtc(value, 'YYYYMMDD');
386 387
  }
  if (value.length === 15) {
388
    return toUtc(value, 'YYYYMMDDTHHmmss');
389 390 391
  }
  // Backward compatibility
  if (value.length === 19) {
392
    return toUtc(value, 'YYYY-MM-DD HH:mm:ss');
393 394 395 396
  }

  if (!isNaN(value)) {
    const epoch = parseInt(value, 10);
397
    return toUtc(epoch);
398 399 400 401 402 403 404
  }

  return null;
};

export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => {
  const raw = {
405 406
    from: parseRawTime(range.from)!,
    to: parseRawTime(range.to)!,
407 408 409
  };

  return {
410 411
    from: dateMath.parse(raw.from, false, timeZone as any)!,
    to: dateMath.parse(raw.to, true, timeZone as any)!,
412 413 414
    raw,
  };
};
415

416 417 418
export const getValueWithRefId = (value?: any): any => {
  if (!value || typeof value !== 'object') {
    return undefined;
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
  }

  if (value.refId) {
    return value;
  }

  const keys = Object.keys(value);
  for (let index = 0; index < keys.length; index++) {
    const key = keys[index];
    const refId = getValueWithRefId(value[key]);
    if (refId) {
      return refId;
    }
  }

434
  return undefined;
435 436
};

Andrej Ocenas committed
437
export const getFirstQueryErrorWithoutRefId = (errors?: DataQueryError[]): DataQueryError | undefined => {
438
  if (!errors) {
439
    return undefined;
440 441
  }

442
  return errors.filter(error => (error && error.refId ? false : true))[0];
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
};

export const getRefIds = (value: any): string[] => {
  if (!value) {
    return [];
  }

  if (typeof value !== 'object') {
    return [];
  }

  const keys = Object.keys(value);
  const refIds = [];
  for (let index = 0; index < keys.length; index++) {
    const key = keys[index];
    if (key === 'refId') {
      refIds.push(value[key]);
      continue;
    }
    refIds.push(getRefIds(value[key]));
  }

  return _.uniq(_.flatten(refIds));
};
467

468
export const refreshIntervalToSortOrder = (refreshInterval?: string) =>
469
  RefreshPicker.isLive(refreshInterval) ? LogsSortOrder.Ascending : LogsSortOrder.Descending;
470 471 472 473 474

export const convertToWebSocketUrl = (url: string) => {
  const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
  let backend = `${protocol}${window.location.host}${config.appSubUrl}`;
  if (backend.endsWith('/')) {
475
    backend = backend.slice(0, -1);
476 477 478 479
  }
  return `${backend}${url}`;
};

480
export const stopQueryState = (querySubscription: Unsubscribable | undefined) => {
481 482
  if (querySubscription) {
    querySubscription.unsubscribe();
483 484
  }
};
485

486
export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues {
487 488 489 490
  if (!resolution) {
    return { interval: '1s', intervalMs: 1000 };
  }

491
  return rangeUtil.calculateInterval(range, resolution, lowLimit);
492
}
493 494 495 496

export function deduplicateLogRowsById(rows: LogRowModel[]) {
  return _.uniqBy(rows, 'uid');
}
497

Andrej Ocenas committed
498
export const getFirstNonQueryRowSpecificError = (queryErrors?: DataQueryError[]): DataQueryError | undefined => {
499
  const refId = getValueWithRefId(queryErrors);
Andrej Ocenas committed
500
  return refId ? undefined : getFirstQueryErrorWithoutRefId(queryErrors);
501
};