Graphite: Support request cancellation properly (Uses new backendSrv.fetch Observable request API) (#31928)

* changed so we use fetch.

* moved tests to be at the same level as the datasource.

* fixing tests after migrating to fetch.

* removed unused dep.

* refactorings according to feedback.

* adding missing typing.
This commit is contained in:
Marcus Andersson
2021-03-16 18:21:38 +01:00
committed by GitHub
parent 610999cfa2
commit 93f17bd2b3
2 changed files with 170 additions and 149 deletions

View File

@ -1,10 +1,12 @@
import { GraphiteDatasource } from '../datasource';
import { GraphiteDatasource } from './datasource';
import _ from 'lodash';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { dateTime, getFrameDisplayName } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { DEFAULT_GRAPHITE_VERSION } from '../versions';
import { of } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { DEFAULT_GRAPHITE_VERSION } from './versions';
jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object),
@ -17,7 +19,7 @@ interface Context {
}
describe('graphiteDatasource', () => {
const datasourceRequestMock = jest.spyOn(backendSrv, 'datasourceRequest');
const fetchMock = jest.spyOn(backendSrv, 'fetch');
let ctx = {} as Context;
@ -114,14 +116,14 @@ describe('graphiteDatasource', () => {
maxDataPoints: 500,
};
let results: any;
let response: any;
let requestOptions: any;
beforeEach(async () => {
datasourceRequestMock.mockImplementation((options: any) => {
beforeEach(() => {
fetchMock.mockImplementation((options: any) => {
requestOptions = options;
return Promise.resolve({
data: [
return of(
createFetchResponse([
{
target: 'prod1.count',
datapoints: [
@ -129,13 +131,11 @@ describe('graphiteDatasource', () => {
[12, 1],
],
},
],
});
])
);
});
await ctx.ds.query(query as any).then((data: any) => {
results = data;
});
response = ctx.ds.query(query as any);
});
it('X-Dashboard and X-Panel headers to be set!', () => {
@ -164,13 +164,19 @@ describe('graphiteDatasource', () => {
expect(params).not.toContain('cacheTimeout=undefined');
});
it('should return series list', () => {
expect(results.data.length).toBe(1);
expect(results.data[0].name).toBe('prod1.count');
it('should return series list', async () => {
await expect(response).toEmitValuesWith((values: any) => {
const results = values[0];
expect(results.data.length).toBe(1);
expect(results.data[0].name).toBe('prod1.count');
});
});
it('should convert to millisecond resolution', () => {
expect(results.data[0].fields[1].values.get(0)).toBe(10);
it('should convert to millisecond resolution', async () => {
await expect(response).toEmitValuesWith((values: any) => {
const results = values[0];
expect(results.data[0].fields[1].values.get(0)).toBe(10);
});
});
});
@ -189,21 +195,19 @@ describe('graphiteDatasource', () => {
};
describe('and tags are returned as string', () => {
const response = {
data: [
{
when: 1507222850,
tags: 'tag1 tag2',
data: 'some text',
id: 2,
what: 'Event - deploy',
},
],
};
const response = [
{
when: 1507222850,
tags: 'tag1 tag2',
data: 'some text',
id: 2,
what: 'Event - deploy',
},
];
beforeEach(async () => {
datasourceRequestMock.mockImplementation((options: any) => {
return Promise.resolve(response);
fetchMock.mockImplementation((options: any) => {
return of(createFetchResponse(response));
});
await ctx.ds.annotationQuery(options).then((data: any) => {
results = data;
@ -219,20 +223,19 @@ describe('graphiteDatasource', () => {
});
describe('and tags are returned as an array', () => {
const response = {
data: [
{
when: 1507222850,
tags: ['tag1', 'tag2'],
data: 'some text',
id: 2,
what: 'Event - deploy',
},
],
};
const response = [
{
when: 1507222850,
tags: ['tag1', 'tag2'],
data: 'some text',
id: 2,
what: 'Event - deploy',
},
];
beforeEach(() => {
datasourceRequestMock.mockImplementation((options: any) => {
return Promise.resolve(response);
fetchMock.mockImplementation((options: any) => {
return of(createFetchResponse(response));
});
ctx.ds.annotationQuery(options).then((data: any) => {
@ -350,11 +353,9 @@ describe('graphiteDatasource', () => {
let requestOptions: any;
beforeEach(() => {
datasourceRequestMock.mockImplementation((options: any) => {
fetchMock.mockImplementation((options: any) => {
requestOptions = options;
return Promise.resolve({
data: ['backend_01', 'backend_02'],
});
return of(createFetchResponse(['backend_01', 'backend_02']));
});
});

View File

@ -9,6 +9,7 @@ import {
ScopedVars,
toDataFrame,
TimeRange,
MetricFindValue,
} from '@grafana/data';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
import gfunc from './gfunc';
@ -18,6 +19,8 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
import { GraphiteOptions, GraphiteQuery, GraphiteType, MetricTankRequestMeta } from './types';
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
import { getSearchFilterScopedVar } from '../../../features/variables/utils';
import { Observable, of, OperatorFunction, pipe } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { DEFAULT_GRAPHITE_VERSION } from './versions';
export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOptions> {
@ -65,7 +68,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
};
}
async query(options: DataQueryRequest<GraphiteQuery>): Promise<DataQueryResponse> {
query(options: DataQueryRequest<GraphiteQuery>): Observable<DataQueryResponse> {
const graphOptions = {
from: this.translateTime(options.range.raw.from, false, options.timezone),
until: this.translateTime(options.range.raw.to, true, options.timezone),
@ -77,7 +80,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
const params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return Promise.resolve({ data: [] });
return of({ data: [] });
}
if (this.isMetricTank) {
@ -99,7 +102,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.requestId = this.name + '.panelId.' + options.panelId;
}
return this.doGraphiteRequest(httpOptions).then(this.convertResponseToDataFrames);
return this.doGraphiteRequest(httpOptions).pipe(map(this.convertResponseToDataFrames));
}
addTracingHeaders(httpOptions: { headers: any }, options: { dashboardId?: number; panelId?: number }) {
@ -225,30 +228,34 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
maxDataPoints: 100,
} as unknown) as DataQueryRequest<GraphiteQuery>;
return this.query(graphiteQuery).then((result) => {
const list = [];
return this.query(graphiteQuery)
.pipe(
map((result: any) => {
const list = [];
for (let i = 0; i < result.data.length; i++) {
const target = result.data[i];
for (let i = 0; i < result.data.length; i++) {
const target = result.data[i];
for (let y = 0; y < target.length; y++) {
const time = target.fields[0].values.get(y);
const value = target.fields[1].values.get(y);
for (let y = 0; y < target.length; y++) {
const time = target.fields[0].values.get(y);
const value = target.fields[1].values.get(y);
if (!value) {
continue;
if (!value) {
continue;
}
list.push({
annotation: options.annotation,
time,
title: target.name,
});
}
}
list.push({
annotation: options.annotation,
time,
title: target.name,
});
}
}
return list;
});
return list;
})
)
.toPromise();
} else {
// Graphite event as annotation
const tags = this.templateSrv.replace(options.annotation.tags);
@ -290,7 +297,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
'&until=' +
this.translateTime(options.range.raw.to, true, options.timezone) +
tags,
});
}).toPromise();
} catch (err) {
return Promise.reject(err);
}
@ -330,7 +337,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
return date.unix();
}
metricFindQuery(query: string, optionalOptions?: any) {
metricFindQuery(query: string, optionalOptions?: any): Promise<MetricFindValue[]> {
const options: any = optionalOptions || {};
let interpolatedQuery = this.templateSrv.replace(
query,
@ -390,14 +397,18 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.params.until = this.translateTime(options.range.to, true, options.timezone);
}
return this.doGraphiteRequest(httpOptions).then((results: any) => {
return _.map(results.data, (metric) => {
return {
text: metric.text,
expandable: metric.expandable ? true : false,
};
});
});
return this.doGraphiteRequest(httpOptions)
.pipe(
map((results: any) => {
return _.map(results.data, (metric) => {
return {
text: metric.text,
expandable: metric.expandable ? true : false,
};
});
})
)
.toPromise();
}
getTags(optionalOptions: any) {
@ -415,14 +426,18 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.params.until = this.translateTime(options.range.to, true, options.timezone);
}
return this.doGraphiteRequest(httpOptions).then((results: any) => {
return _.map(results.data, (tag) => {
return {
text: tag.tag,
id: tag.id,
};
});
});
return this.doGraphiteRequest(httpOptions)
.pipe(
map((results: any) => {
return _.map(results.data, (tag) => {
return {
text: tag.tag,
id: tag.id,
};
});
})
)
.toPromise();
}
getTagValues(options: any = {}) {
@ -438,18 +453,22 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.params.until = this.translateTime(options.range.to, true, options.timezone);
}
return this.doGraphiteRequest(httpOptions).then((results: any) => {
if (results.data && results.data.values) {
return _.map(results.data.values, (value) => {
return {
text: value.value,
id: value.id,
};
});
} else {
return [];
}
});
return this.doGraphiteRequest(httpOptions)
.pipe(
map((results: any) => {
if (results.data && results.data.values) {
return _.map(results.data.values, (value) => {
return {
text: value.value,
id: value.id,
};
});
} else {
return [];
}
})
)
.toPromise();
}
getTagsAutoComplete(expressions: any[], tagPrefix: any, optionalOptions?: any) {
@ -475,16 +494,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.params.from = this.translateTime(options.range.from, false, options.timezone);
httpOptions.params.until = this.translateTime(options.range.to, true, options.timezone);
}
return this.doGraphiteRequest(httpOptions).then((results: any) => {
if (results.data) {
return _.map(results.data, (tag) => {
return { text: tag };
});
} else {
return [];
}
});
return this.doGraphiteRequest(httpOptions).pipe(mapToTags()).toPromise();
}
getTagValuesAutoComplete(expressions: any[], tag: any, valuePrefix: any, optionalOptions: any) {
@ -511,16 +521,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
httpOptions.params.from = this.translateTime(options.range.from, false, options.timezone);
httpOptions.params.until = this.translateTime(options.range.to, true, options.timezone);
}
return this.doGraphiteRequest(httpOptions).then((results: any) => {
if (results.data) {
return _.map(results.data, (value) => {
return { text: value };
});
} else {
return [];
}
});
return this.doGraphiteRequest(httpOptions).pipe(mapToTags()).toPromise();
}
getVersion(optionalOptions: any) {
@ -533,16 +534,19 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
};
return this.doGraphiteRequest(httpOptions)
.then((results: any) => {
if (results.data) {
const semver = new SemVersion(results.data);
return semver.isValid() ? results.data : '';
}
return '';
})
.catch(() => {
return '';
});
.pipe(
map((results: any) => {
if (results.data) {
const semver = new SemVersion(results.data);
return semver.isValid() ? results.data : '';
}
return '';
}),
catchError(() => {
return of('');
})
)
.toPromise();
}
createFuncInstance(funcDef: any, options?: any) {
@ -573,22 +577,23 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
url: '/functions',
};
this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
.then((results: any) => {
if (results.status !== 200 || typeof results.data !== 'object') {
return this.doGraphiteRequest(httpOptions)
.pipe(
map((results: any) => {
if (results.status !== 200 || typeof results.data !== 'object') {
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
} else {
this.funcDefs = gfunc.parseFuncDefs(results.data);
}
return this.funcDefs;
}),
catchError((error: any) => {
console.error('Fetching graphite functions error', error);
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
} else {
this.funcDefs = gfunc.parseFuncDefs(results.data);
}
return this.funcDefs;
})
.catch((err: any) => {
console.error('Fetching graphite functions error', err);
this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
return this.funcDefs;
});
return this.funcDefsPromise;
return of(this.funcDefs);
})
)
.toPromise();
}
testDatasource() {
@ -601,9 +606,10 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
targets: [{ target: 'constantLine(100)' }],
maxDataPoints: 300,
} as unknown) as DataQueryRequest<GraphiteQuery>;
return this.query(query).then(() => {
return { status: 'success', message: 'Data source is working' };
});
return this.query(query)
.toPromise()
.then(() => ({ status: 'success', message: 'Data source is working' }));
}
doGraphiteRequest(options: {
@ -625,7 +631,7 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return getBackendSrv().datasourceRequest(options);
return getBackendSrv().fetch(options);
}
buildGraphiteParams(options: any, scopedVars?: ScopedVars): string[] {
@ -702,3 +708,17 @@ function supportsTags(version: string): boolean {
function supportsFunctionIndex(version: string): boolean {
return isVersionGtOrEq(version, '1.1');
}
function mapToTags(): OperatorFunction<any, Array<{ text: string }>> {
return pipe(
map((results: any) => {
if (results.data) {
return _.map(results.data, (value) => {
return { text: value };
});
} else {
return [];
}
})
);
}