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
This commit is contained in:
Torkel Ödegaard
2019-09-12 17:28:46 +02:00
committed by GitHub
parent 3742db720f
commit 140ecbcf79
49 changed files with 1620 additions and 1525 deletions
+162
View File
@@ -0,0 +1,162 @@
import { LogLevel } from '@grafana/data';
let index = 0;
export function getRandomLogLevel(): LogLevel {
const v = Math.random();
if (v > 0.9) {
return LogLevel.critical;
}
if (v > 0.8) {
return LogLevel.error;
}
if (v > 0.7) {
return LogLevel.warning;
}
if (v > 0.4) {
return LogLevel.info;
}
if (v > 0.3) {
return LogLevel.debug;
}
if (v > 0.1) {
return LogLevel.trace;
}
return LogLevel.unknown;
}
export function getNextWord() {
index = (index + Math.floor(Math.random() * 5)) % words.length;
return words[index];
}
export function getRandomLine(length = 60) {
let line = getNextWord();
while (line.length < length) {
line += ' ' + getNextWord();
}
return line;
}
const words = [
'At',
'vero',
'eos',
'et',
'accusamus',
'et',
'iusto',
'odio',
'dignissimos',
'ducimus',
'qui',
'blanditiis',
'praesentium',
'voluptatum',
'deleniti',
'atque',
'corrupti',
'quos',
'dolores',
'et',
'quas',
'molestias',
'excepturi',
'sint',
'occaecati',
'cupiditate',
'non',
'provident',
'similique',
'sunt',
'in',
'culpa',
'qui',
'officia',
'deserunt',
'mollitia',
'animi',
'id',
'est',
'laborum',
'et',
'dolorum',
'fuga',
'Et',
'harum',
'quidem',
'rerum',
'facilis',
'est',
'et',
'expedita',
'distinctio',
'Nam',
'libero',
'tempore',
'cum',
'soluta',
'nobis',
'est',
'eligendi',
'optio',
'cumque',
'nihil',
'impedit',
'quo',
'minus',
'id',
'quod',
'maxime',
'placeat',
'facere',
'possimus',
'omnis',
'voluptas',
'assumenda',
'est',
'omnis',
'dolor',
'repellendus',
'Temporibus',
'autem',
'quibusdam',
'et',
'aut',
'officiis',
'debitis',
'aut',
'rerum',
'necessitatibus',
'saepe',
'eveniet',
'ut',
'et',
'voluptates',
'repudiandae',
'sint',
'et',
'molestiae',
'non',
'recusandae',
'Itaque',
'earum',
'rerum',
'hic',
'tenetur',
'a',
'sapiente',
'delectus',
'ut',
'aut',
'reiciendis',
'voluptatibus',
'maiores',
'alias',
'consequatur',
'aut',
'perferendis',
'doloribus',
'asperiores',
'repellat',
];
+52 -64
View File
@@ -3,95 +3,83 @@ import {
DataSourceApi,
DataQueryRequest,
DataSourceInstanceSettings,
DataStreamObserver,
DataQueryResponse,
MetricFindValue,
} from '@grafana/ui';
import { TableData, TimeSeries } from '@grafana/data';
import { TestDataQuery, Scenario } from './types';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { StreamHandler } from './StreamHandler';
import { queryMetricTree } from './metricTree';
import { Observable, from, merge } from 'rxjs';
import { runStream } from './runStreams';
import templateSrv from 'app/features/templating/template_srv';
type TestData = TimeSeries | TableData;
export interface TestDataRegistry {
[key: string]: TestData[];
}
export class TestDataDataSource extends DataSourceApi<TestDataQuery> {
streams = new StreamHandler();
/** @ngInject */
constructor(instanceSettings: DataSourceInstanceSettings) {
super(instanceSettings);
}
query(options: DataQueryRequest<TestDataQuery>, observer: DataStreamObserver) {
const queries = options.targets.map(item => {
return {
...item,
intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints,
datasourceId: this.id,
alias: templateSrv.replace(item.alias || ''),
};
});
query(options: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
const queries: any[] = [];
const streams: Array<Observable<DataQueryResponse>> = [];
if (queries.length === 0) {
return Promise.resolve({ data: [] });
// Start streams and prepare queries
for (const target of options.targets) {
if (target.scenarioId === 'streaming_client') {
streams.push(runStream(target, options));
} else {
queries.push({
...target,
intervalMs: options.intervalMs,
maxDataPoints: options.maxDataPoints,
datasourceId: this.id,
alias: templateSrv.replace(target.alias || ''),
});
}
}
// Currently we do not support mixed with client only streaming
const resp = this.streams.process(options, observer);
if (resp) {
return Promise.resolve(resp);
if (queries.length) {
const req: Promise<DataQueryResponse> = getBackendSrv()
.datasourceRequest({
method: 'POST',
url: '/api/tsdb/query',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: queries,
},
// This sets up a cancel token
requestId: options.requestId,
})
.then((res: any) => this.processQueryResult(queries, res));
streams.push(from(req));
}
return getBackendSrv()
.datasourceRequest({
method: 'POST',
url: '/api/tsdb/query',
data: {
from: options.range.from.valueOf().toString(),
to: options.range.to.valueOf().toString(),
queries: queries,
},
return merge(...streams);
}
// This sets up a cancel token
requestId: options.requestId,
})
.then((res: any) => {
const data: TestData[] = [];
processQueryResult(queries: any, res: any): DataQueryResponse {
const data: TestData[] = [];
// Returns data in the order it was asked for.
// if the response has data with different refId, it is ignored
for (const query of queries) {
const results = res.data.results[query.refId];
if (!results) {
console.warn('No Results for:', query);
continue;
}
for (const query of queries) {
const results = res.data.results[query.refId];
for (const t of results.tables || []) {
const table = t as TableData;
table.refId = query.refId;
table.name = query.alias;
data.push(table);
}
for (const t of results.tables || []) {
const table = t as TableData;
table.refId = query.refId;
table.name = query.alias;
data.push(table);
}
for (const series of results.series || []) {
data.push({
target: series.name,
datapoints: series.points,
refId: query.refId,
tags: series.tags,
});
}
}
for (const series of results.series || []) {
data.push({ target: series.name, datapoints: series.points, refId: query.refId, tags: series.tags });
}
}
return { data: data };
});
return { data };
}
annotationQuery(options: any) {
+1 -1
View File
@@ -1,7 +1,7 @@
import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
import { defaultQuery } from './StreamHandler';
import { defaultQuery } from './runStreams';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { dateTime } from '@grafana/data';
+224
View File
@@ -0,0 +1,224 @@
import { defaults } from 'lodash';
import { Observable } from 'rxjs';
import { DataQueryRequest, DataQueryResponse } from '@grafana/ui';
import { FieldType, CircularDataFrame, CSVReader, Field, LoadingState } from '@grafana/data';
import { TestDataQuery, StreamingQuery } from './types';
import { getRandomLine } from './LogIpsum';
export const defaultQuery: StreamingQuery = {
type: 'signal',
speed: 250, // ms
spread: 3.5,
noise: 2.2,
bands: 1,
};
export function runStream(target: TestDataQuery, req: DataQueryRequest<TestDataQuery>): Observable<DataQueryResponse> {
const query = defaults(target.stream, defaultQuery);
if ('signal' === query.type) {
return runSignalStream(target, query, req);
}
if ('logs' === query.type) {
return runLogsStream(target, query, req);
}
if ('fetch' === query.type) {
return runFetchStream(target, query, req);
}
throw new Error(`Unknown Stream Type: ${query.type}`);
}
export function runSignalStream(
target: TestDataQuery,
query: StreamingQuery,
req: DataQueryRequest<TestDataQuery>
): Observable<DataQueryResponse> {
return new Observable<DataQueryResponse>(subscriber => {
const streamId = `signal-${req.panelId}-${target.refId}`;
const maxDataPoints = req.maxDataPoints || 1000;
const data = new CircularDataFrame({
append: 'tail',
capacity: maxDataPoints,
});
data.refId = target.refId;
data.name = target.alias || 'Signal ' + target.refId;
data.addField({ name: 'time', type: FieldType.time });
data.addField({ name: 'value', type: FieldType.number });
const { spread, speed, bands, noise } = query;
for (let i = 0; i < bands; i++) {
const suffix = bands > 1 ? ` ${i + 1}` : '';
data.addField({ name: 'Min' + suffix, type: FieldType.number });
data.addField({ name: 'Max' + suffix, type: FieldType.number });
}
let value = Math.random() * 100;
let timeoutId: any = null;
const addNextRow = (time: number) => {
value += (Math.random() - 0.5) * spread;
let idx = 0;
data.fields[idx++].values.add(time);
data.fields[idx++].values.add(value);
let min = value;
let max = value;
for (let i = 0; i < bands; i++) {
min = min - Math.random() * noise;
max = max + Math.random() * noise;
data.fields[idx++].values.add(min);
data.fields[idx++].values.add(max);
}
};
// Fill the buffer on init
if (true) {
let time = Date.now() - maxDataPoints * speed;
for (let i = 0; i < maxDataPoints; i++) {
addNextRow(time);
time += speed;
}
}
const pushNextEvent = () => {
addNextRow(Date.now());
subscriber.next({
data: [data],
key: streamId,
});
timeoutId = setTimeout(pushNextEvent, speed);
};
// Send first event in 5ms
setTimeout(pushNextEvent, 5);
return () => {
console.log('unsubscribing to stream ' + streamId);
clearTimeout(timeoutId);
};
});
}
export function runLogsStream(
target: TestDataQuery,
query: StreamingQuery,
req: DataQueryRequest<TestDataQuery>
): Observable<DataQueryResponse> {
return new Observable<DataQueryResponse>(subscriber => {
const streamId = `logs-${req.panelId}-${target.refId}`;
const maxDataPoints = req.maxDataPoints || 1000;
const data = new CircularDataFrame({
append: 'tail',
capacity: maxDataPoints,
});
data.refId = target.refId;
data.name = target.alias || 'Logs ' + target.refId;
data.addField({ name: 'time', type: FieldType.time });
data.addField({ name: 'line', type: FieldType.string });
const { speed } = query;
let timeoutId: any = null;
const pushNextEvent = () => {
data.values.time.add(Date.now());
data.values.line.add(getRandomLine());
subscriber.next({
data: [data],
key: streamId,
});
timeoutId = setTimeout(pushNextEvent, speed);
};
// Send first event in 5ms
setTimeout(pushNextEvent, 5);
return () => {
console.log('unsubscribing to stream ' + streamId);
clearTimeout(timeoutId);
};
});
}
export function runFetchStream(
target: TestDataQuery,
query: StreamingQuery,
req: DataQueryRequest<TestDataQuery>
): Observable<DataQueryResponse> {
return new Observable<DataQueryResponse>(subscriber => {
const streamId = `fetch-${req.panelId}-${target.refId}`;
const maxDataPoints = req.maxDataPoints || 1000;
let data = new CircularDataFrame({
append: 'tail',
capacity: maxDataPoints,
});
data.refId = target.refId;
data.name = target.alias || 'Fetch ' + target.refId;
let reader: ReadableStreamReader<Uint8Array>;
const csv = new CSVReader({
callback: {
onHeader: (fields: Field[]) => {
// Clear any existing fields
if (data.fields.length) {
data = new CircularDataFrame({
append: 'tail',
capacity: maxDataPoints,
});
data.refId = target.refId;
data.name = 'Fetch ' + target.refId;
}
for (const field of fields) {
data.addField(field);
}
},
onRow: (row: any[]) => {
data.add(row);
},
},
});
const processChunk = (value: ReadableStreamReadResult<Uint8Array>): any => {
if (value.value) {
const text = new TextDecoder().decode(value.value);
csv.readCSV(text);
}
subscriber.next({
data: [data],
key: streamId,
state: value.done ? LoadingState.Done : LoadingState.Streaming,
});
if (value.done) {
console.log('Finished stream');
subscriber.complete(); // necessary?
return;
}
return reader.read().then(processChunk);
};
fetch(new Request(query.url)).then(response => {
reader = response.body.getReader();
reader.read().then(processChunk);
});
return () => {
// Cancel fetch?
console.log('unsubscribing to stream ' + streamId);
};
});
}