Commit 477d5643 by kay delaney Committed by GitHub

Explore: Prometheus query errors now show (#17470)

* Explore: Prometheus query errors now show
Fixes: #17435

* Explore: Adds test to ensure prometheus query errors are reported

* Explore: removes implicit anys introduced previously
parent 12e417f2
// Libraries // Libraries
import _ from 'lodash'; import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import { from, Observable } from 'rxjs'; import { from, of, Observable } from 'rxjs';
import { single, map, filter } from 'rxjs/operators'; import { single, map, filter, catchError } from 'rxjs/operators';
// Services & Utils // Services & Utils
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
...@@ -25,6 +25,7 @@ import { ...@@ -25,6 +25,7 @@ import {
DataQueryError, DataQueryError,
DataStreamObserver, DataStreamObserver,
LoadingState, LoadingState,
DataStreamState,
DataQueryResponseData, DataQueryResponseData,
} from '@grafana/ui/src/types'; } from '@grafana/ui/src/types';
import { ExploreUrlState } from 'app/types/explore'; import { ExploreUrlState } from 'app/types/explore';
...@@ -199,31 +200,32 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -199,31 +200,32 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
single(), // unsubscribes automatically after first result single(), // unsubscribes automatically after first result
filter((response: any) => (response.cancelled ? false : true)), filter((response: any) => (response.cancelled ? false : true)),
map((response: any) => { map((response: any) => {
return this.processResult(response, query, target, queries.length); const delta = this.processResult(response, query, target, queries.length);
const state: DataStreamState = {
key: `prometheus-${target.refId}`,
state: query.instant ? LoadingState.Loading : LoadingState.Done,
request: options,
delta,
unsubscribe: () => undefined,
};
return state;
}),
catchError(err => {
const error = this.handleErrors(err, target);
const state: DataStreamState = {
key: `prometheus-${target.refId}`,
request: options,
state: LoadingState.Error,
error,
unsubscribe: () => undefined,
};
return of(state);
}) })
) )
.subscribe({ .subscribe({
next: series => { next: state => observer(state),
if (query.instant) {
observer({
key: `prometheus-${target.refId}`,
state: LoadingState.Loading,
request: options,
series: null,
delta: series,
unsubscribe: () => undefined,
});
} else {
observer({
key: `prometheus-${target.refId}`,
state: LoadingState.Done,
request: options,
series: null,
delta: series,
unsubscribe: () => undefined,
});
}
},
}); });
} }
}; };
...@@ -399,9 +401,13 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -399,9 +401,13 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
data['timeout'] = this.queryTimeout; data['timeout'] = this.queryTimeout;
} }
return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => {
this.handleErrors(err, query) if (err.cancelled) {
); return err;
}
throw this.handleErrors(err, query);
});
} }
performInstantQuery(query: PromQueryRequest, time: number) { performInstantQuery(query: PromQueryRequest, time: number) {
...@@ -415,16 +421,16 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -415,16 +421,16 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
data['timeout'] = this.queryTimeout; data['timeout'] = this.queryTimeout;
} }
return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => return this._request(url, data, { requestId: query.requestId, headers: query.headers }).catch((err: any) => {
this.handleErrors(err, query) if (err.cancelled) {
); return err;
}
throw this.handleErrors(err, query);
});
} }
handleErrors = (err: any, target: PromQuery) => { handleErrors = (err: any, target: PromQuery) => {
if (err.cancelled) {
return err;
}
const error: DataQueryError = { const error: DataQueryError = {
message: 'Unknown error during query transaction. Please check JS console logs.', message: 'Unknown error during query transaction. Please check JS console logs.',
refId: target.refId, refId: target.refId,
...@@ -445,7 +451,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions> ...@@ -445,7 +451,7 @@ export class PrometheusDatasource extends DataSourceApi<PromQuery, PromOptions>
error.status = err.status; error.status = err.status;
error.statusText = err.statusText; error.statusText = err.statusText;
throw error; return error;
}; };
performSuggestQuery(query: string, cache = false) { performSuggestQuery(query: string, cache = false) {
......
...@@ -425,49 +425,84 @@ const timeSrv = ({ ...@@ -425,49 +425,84 @@ const timeSrv = ({
describe('PrometheusDatasource', () => { describe('PrometheusDatasource', () => {
describe('When querying prometheus with one target using query editor target spec', () => { describe('When querying prometheus with one target using query editor target spec', () => {
let results: any; describe('and query syntax is valid', () => {
const query = { let results: any;
range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, const query = {
targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }], range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
interval: '60s', targets: [{ expr: 'test{job="testjob"}', format: 'time_series' }],
}; interval: '60s',
// Interval alignment with step };
const urlExpected = // Interval alignment with step
'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=180&step=60'; const urlExpected =
'proxied/api/v1/query_range?query=' + encodeURIComponent('test{job="testjob"}') + '&start=60&end=180&step=60';
beforeEach(async () => { beforeEach(async () => {
const response = { const response = {
data: {
status: 'success',
data: { data: {
resultType: 'matrix', status: 'success',
result: [ data: {
{ resultType: 'matrix',
metric: { __name__: 'test', job: 'testjob' }, result: [
values: [[60, '3846']], {
}, metric: { __name__: 'test', job: 'testjob' },
], values: [[60, '3846']],
},
],
},
}, },
};
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
await ctx.ds.query(query).then((data: any) => {
results = data;
});
});
it('should generate the correct query', () => {
const res = backendSrv.datasourceRequest.mock.calls[0][0];
expect(res.method).toBe('GET');
expect(res.url).toBe(urlExpected);
});
it('should return series list', async () => {
expect(results.data.length).toBe(1);
expect(results.data[0].target).toBe('test{job="testjob"}');
});
});
describe('and query syntax is invalid', () => {
let results: string;
const query = {
range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
targets: [{ expr: 'tes;;t{job="testjob"}', format: 'time_series' }],
interval: '60s',
};
const errMessage = 'parse error at char 25: could not parse remaining input';
const response = {
data: {
status: 'error',
errorType: 'bad_data',
error: errMessage,
}, },
}; };
backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
await ctx.ds.query(query).then((data: any) => { beforeEach(async () => {
results = data; backendSrv.datasourceRequest = jest.fn(() => Promise.reject(response));
ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv as any, timeSrv as any);
await ctx.ds.query(query).catch((e: any) => {
results = e.message;
});
}); });
});
it('should generate the correct query', () => { it('should generate an error', () => {
const res = backendSrv.datasourceRequest.mock.calls[0][0]; expect(results).toBe(`"${errMessage}"`);
expect(res.method).toBe('GET'); });
expect(res.url).toBe(urlExpected);
});
it('should return series list', async () => {
expect(results.data.length).toBe(1);
expect(results.data[0].target).toBe('test{job="testjob"}');
}); });
}); });
describe('When querying prometheus with one target which returns multiple series', () => { describe('When querying prometheus with one target which returns multiple series', () => {
let results: any; let results: any;
const start = 60; const start = 60;
...@@ -536,6 +571,7 @@ describe('PrometheusDatasource', () => { ...@@ -536,6 +571,7 @@ describe('PrometheusDatasource', () => {
expect(results.data[1].datapoints[3][0]).toBe(null); expect(results.data[1].datapoints[3][0]).toBe(null);
}); });
}); });
describe('When querying prometheus with one target and instant = true', () => { describe('When querying prometheus with one target and instant = true', () => {
let results: any; let results: any;
const urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123'; const urlExpected = 'proxied/api/v1/query?query=' + encodeURIComponent('test{job="testjob"}') + '&time=123';
...@@ -578,6 +614,7 @@ describe('PrometheusDatasource', () => { ...@@ -578,6 +614,7 @@ describe('PrometheusDatasource', () => {
expect(results.data[0].target).toBe('test{job="testjob"}'); expect(results.data[0].target).toBe('test{job="testjob"}');
}); });
}); });
describe('When performing annotationQuery', () => { describe('When performing annotationQuery', () => {
let results: any; let results: any;
...@@ -1227,6 +1264,7 @@ describe('PrometheusDatasource', () => { ...@@ -1227,6 +1264,7 @@ describe('PrometheusDatasource', () => {
}); });
}); });
}); });
describe('The __range, __range_s and __range_ms variables', () => { describe('The __range, __range_s and __range_ms variables', () => {
const response = { const response = {
status: 'success', status: 'success',
...@@ -1329,12 +1367,14 @@ describe('PrometheusDatasource for POST', () => { ...@@ -1329,12 +1367,14 @@ describe('PrometheusDatasource for POST', () => {
results = data; results = data;
}); });
}); });
it('should generate the correct query', () => { it('should generate the correct query', () => {
const res = backendSrv.datasourceRequest.mock.calls[0][0]; const res = backendSrv.datasourceRequest.mock.calls[0][0];
expect(res.method).toBe('POST'); expect(res.method).toBe('POST');
expect(res.url).toBe(urlExpected); expect(res.url).toBe(urlExpected);
expect(res.data).toEqual(dataExpected); expect(res.data).toEqual(dataExpected);
}); });
it('should return series list', () => { it('should return series list', () => {
expect(results.data.length).toBe(1); expect(results.data.length).toBe(1);
expect(results.data[0].target).toBe('test{job="testjob"}'); expect(results.data[0].target).toBe('test{job="testjob"}');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment