Files
grafana/public/app/features/dashboard/state/runRequest.ts
Torkel Ödegaard 140ecbcf79 QueryProcessing: Observable query interface and RxJS for query & stream processing (#18899)
* I needed to learn some rxjs and understand this more, so just playing around

* Updated

* Removed all the complete calls

* Refactoring

* StreamHandler -> observable start

* progress

* simple singal works

* Handle update time range

* added error handling

* wrap old function

* minor changes

* handle data format in the subscribe function

* Use replay subject to return last value to subscribers

* Set loading state after no response in 50ms

* added missing file

* updated comment

* Added cancelation of network requests

* runRequest: Added unit test scenario framework

* Progress on tests

* minor refactor of unit tests

* updated test

* removed some old code

* Shared queries work again, and also became so much simplier

* unified query and observe methods

* implict any fix

* Fixed closed subject issue

* removed comment

* Use last returned data for loading state

* WIP: Explore to runRequest makover step1

* Minor progress

* Minor progress on explore and runRequest

* minor progress

* Things are starting to work in explore

* Updated prometheus to use new observable query response, greatly simplified code

* Revert refId change

* Found better solution for key/refId/requestId problem

* use observable with loki

* tests compile

* fix loki query prep

* Explore: correct first response handling

* Refactorings

* Refactoring

* Explore: Fixes LoadingState and GraphResults between runs (#18986)

* Refactor: Adds state to DataQueryResponse

* Fix: Fixes so we do not empty results before new data arrives
Fixes: #17409

* Transformations work

* observable test data

* remove single() from loki promise

* Fixed comment

* Explore: Fixes failing Loki and Prometheus unit tests (#18995)

* Tests: Makes datasource tests work again

* Fix: Fixes loki datasource so highligthing works

* Chore: Runs Prettier

* Fixed query runner tests

* Delay loading state indication to 200ms

* Fixed test

* fixed unit tests

* Clear cached calcs

* Fixed bug getProcesedDataFrames

* Fix the correct test is a better idea

* Fix: Fixes so queries in Explore are only run if Graph/Table is shown (#19000)

* Fix: Fixes so queries in Explore are only run if Graph/Table is shown
Fixes: #18618

* Refactor: Removes unnecessary condition

* PanelData: provide legacy data only when needed  (#19018)

* no legacy

* invert logic... now compiles

* merge getQueryResponseData and getDataRaw

* update comment about query editor

* use single getData() function

* only send legacy when it is used in explore

* pre process rather than post process

* pre process rather than post process

* Minor refactoring

* Add missing tags to test datasource response

* MixedDatasource: Adds query observable pattern to MixedDatasource (#19037)

* start mixed datasource

* Refactor: Refactors into observable parttern

* Tests: Fixes tests

* Tests: Removes console.log

* Refactor: Adds unique requestId
2019-09-12 17:28:46 +02:00

212 lines
5.8 KiB
TypeScript

// Libraries
import { Observable, of, timer, merge, from } from 'rxjs';
import { flatten, map as lodashMap, isArray, isString } from 'lodash';
import { map, catchError, takeUntil, mapTo, share, finalize } from 'rxjs/operators';
// Utils & Services
import { getBackendSrv } from 'app/core/services/backend_srv';
// Types
import {
DataSourceApi,
DataQueryRequest,
PanelData,
DataQueryResponse,
DataQueryResponseData,
DataQueryError,
} from '@grafana/ui';
import { LoadingState, dateMath, toDataFrame, DataFrame, guessFieldTypes } from '@grafana/data';
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
interface RunningQueryState {
packets: { [key: string]: DataQueryResponse };
panelData: PanelData;
}
/*
* This function should handle composing a PanelData from multiple responses
*/
export function processResponsePacket(packet: DataQueryResponse, state: RunningQueryState): RunningQueryState {
const request = state.panelData.request;
const packets: MapOfResponsePackets = {
...state.packets,
};
packets[packet.key || 'A'] = packet;
// Update the time range
let timeRange = request.range;
if (isString(timeRange.raw.from)) {
timeRange = {
from: dateMath.parse(timeRange.raw.from, false),
to: dateMath.parse(timeRange.raw.to, true),
raw: timeRange.raw,
};
}
const combinedData = flatten(
lodashMap(packets, (packet: DataQueryResponse) => {
return packet.data;
})
);
const panelData = {
state: packet.state || LoadingState.Done,
series: combinedData,
request: {
...request,
range: timeRange,
},
};
return { packets, panelData };
}
/**
* This function handles the excecution of requests & and processes the single or multiple response packets into
* a combined PanelData response.
* It will
* * Merge multiple responses into a single DataFrame array based on the packet key
* * Will emit a loading state if no response after 50ms
* * Cancel any still runnning network requests on unsubscribe (using request.requestId)
*/
export function runRequest(datasource: DataSourceApi, request: DataQueryRequest): Observable<PanelData> {
let state: RunningQueryState = {
panelData: {
state: LoadingState.Loading,
series: [],
request: request,
},
packets: {},
};
// Return early if there are no queries to run
if (!request.targets.length) {
request.endTime = Date.now();
state.panelData.state = LoadingState.Done;
return of(state.panelData);
}
const dataObservable = callQueryMethod(datasource, request).pipe(
// Transform response packets into PanelData with merged results
map((packet: DataQueryResponse) => {
if (!isArray(packet.data)) {
throw new Error(`Expected response data to be array, got ${typeof packet.data}.`);
}
request.endTime = Date.now();
state = processResponsePacket(packet, state);
return state.panelData;
}),
// handle errors
catchError(err =>
of({
...state.panelData,
state: LoadingState.Error,
error: processQueryError(err),
})
),
// finalize is triggered when subscriber unsubscribes
// This makes sure any still running network requests are cancelled
finalize(cancelNetworkRequestsOnUnsubscribe(request)),
// this makes it possible to share this observable in takeUntil
share()
);
// If 50ms without a response emit a loading state
// mapTo will translate the timer event into state.panelData (which has state set to loading)
// takeUntil will cancel the timer emit when first response packet is received on the dataObservable
return merge(
timer(200).pipe(
mapTo(state.panelData),
takeUntil(dataObservable)
),
dataObservable
);
}
function cancelNetworkRequestsOnUnsubscribe(req: DataQueryRequest) {
return () => {
getBackendSrv().resolveCancelerIfExists(req.requestId);
};
}
export function callQueryMethod(datasource: DataSourceApi, request: DataQueryRequest) {
const returnVal = datasource.query(request);
return from(returnVal);
}
export function processQueryError(err: any): DataQueryError {
const error = (err || {}) as DataQueryError;
if (!error.message) {
if (typeof err === 'string' || err instanceof String) {
return { message: err } as DataQueryError;
}
let message = 'Query error';
if (error.message) {
message = error.message;
} else if (error.data && error.data.message) {
message = error.data.message;
} else if (error.data && error.data.error) {
message = error.data.error;
} else if (error.status) {
message = `Query error: ${error.status} ${error.statusText}`;
}
error.message = message;
}
return error;
}
/**
* All panels will be passed tables that have our best guess at colum type set
*
* This is also used by PanelChrome for snapshot support
*/
export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] {
if (!isArray(results)) {
return [];
}
const dataFrames: DataFrame[] = [];
for (const result of results) {
const dataFrame = guessFieldTypes(toDataFrame(result));
// clear out any cached calcs
for (const field of dataFrame.fields) {
field.calcs = null;
}
dataFrames.push(dataFrame);
}
return dataFrames;
}
export function preProcessPanelData() {
let lastResult: PanelData = null;
return function mapper(data: PanelData) {
let { series } = data;
// for loading states with no data, use last result
if (data.state === LoadingState.Loading && series.length === 0) {
if (!lastResult) {
lastResult = data;
}
return { ...lastResult, state: LoadingState.Loading };
}
// Makes sure the data is properly formatted
series = getProcessedDataFrames(series);
lastResult = { ...data, series };
return lastResult;
};
}