Skip to content

Commit 5a208c6

Browse files
TheSpacyCatBenediktSeidl
authored andcommitted
list missing fields of query
The error message for an incomplete query was unspecific up until now. It now includes a list of all missing fields.
1 parent 4cfc9dc commit 5a208c6

File tree

4 files changed

+173
-4
lines changed

4 files changed

+173
-4
lines changed

src/backend/rest.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as process from 'process';
1818
import { CmkQuery } from '../types';
1919
import { createCmkContext, replaceVariables, toLiveStatusQuery, updateQuery } from '../utils';
2020
import { Backend, DatasourceOptions } from './types';
21+
import { validateRequestSpec } from './validate';
2122

2223
type RestApiError = {
2324
detail: string;
@@ -147,13 +148,16 @@ export default class RestApiBackend implements Backend {
147148
}
148149

149150
async query(request: DataQueryRequest<CmkQuery>): Promise<DataQueryResponse> {
151+
request.targets.forEach((query) => {
152+
validateRequestSpec(query.requestSpec, this.datasource.getEdition());
153+
});
154+
150155
const promises = request.targets
151156
.filter((target) => !target.hide)
152157
.map((target) => {
153158
return this.getSingleGraph(request.range, target);
154159
});
155-
const result = await Promise.all(promises).then((data) => ({ data }));
156-
return result;
160+
return await Promise.all(promises).then((data) => ({ data }));
157161
}
158162

159163
async testDatasource(): Promise<unknown> {

src/backend/validate.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { RequestSpec } from '../RequestSpec';
2+
import { Edition } from '../types';
3+
import { labelForRequestSpecKey } from '../ui/utils';
4+
5+
// 'graph_type' and 'aggregation' should always have a default value
6+
type PotentiallyRequiredKeys = 'site' | 'service' | 'host_name' | 'graph';
7+
8+
const missingRequiredFields = (rq: Partial<RequestSpec>, edition: Edition): string[] => {
9+
const result: PotentiallyRequiredKeys[] = [];
10+
if (rq.graph === undefined || rq.graph === '') {
11+
result.push('graph');
12+
}
13+
14+
if (edition === 'RAW') {
15+
if (rq.site === undefined) {
16+
result.push('site');
17+
}
18+
if (rq.service === undefined || rq.service === '') {
19+
result.push('service');
20+
}
21+
if (rq.host_name === undefined || rq.service === '') {
22+
result.push('host_name');
23+
}
24+
}
25+
return result.sort().map((value) => labelForRequestSpecKey(value, rq));
26+
};
27+
28+
export const validateRequestSpec = (rq: Partial<RequestSpec> | undefined, edition: Edition): void => {
29+
if (rq === undefined) {
30+
rq = {};
31+
}
32+
33+
const missingFields = missingRequiredFields(rq, edition);
34+
if (missingFields.length === 0) {
35+
return;
36+
}
37+
throw new Error(`Please specify a value for the following fields: ${missingFields.join(', ')}`);
38+
};

src/backend/web.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
createWebApiRequestSpecification,
1515
} from './../webapi';
1616
import { Backend, DatasourceOptions } from './types';
17+
import { validateRequestSpec } from './validate';
1718

1819
export default class WebApiBackend implements Backend {
1920
datasource: DatasourceOptions;
@@ -75,6 +76,10 @@ export default class WebApiBackend implements Backend {
7576
}
7677

7778
async query(options: DataQueryRequest<CmkQuery>): Promise<DataQueryResponse> {
79+
options.targets.forEach((query) => {
80+
validateRequestSpec(query.requestSpec, this.datasource.getEdition());
81+
});
82+
7883
const { range } = options;
7984
const from = range.from.unix();
8085
const to = range.to.unix();
@@ -140,7 +145,7 @@ export default class WebApiBackend implements Backend {
140145
}
141146
}
142147

143-
getGraphQuery = async (range: number[], query: CmkQuery): Promise<MutableDataFrame<unknown>> => {
148+
async getGraphQuery(range: number[], query: CmkQuery): Promise<MutableDataFrame<unknown>> {
144149
updateQuery(query);
145150
const graph = get(query, 'requestSpec.graph');
146151
if (isUndefined(graph) || graph === '') {
@@ -183,5 +188,5 @@ export default class WebApiBackend implements Backend {
183188
);
184189

185190
return frame;
186-
};
191+
}
187192
}

tests/unit/DataSource.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1+
import { MutableDataFrame } from '@grafana/data';
2+
import { cloneDeep } from 'lodash';
3+
4+
import { DataSource } from '../../src/DataSource';
5+
import { RequestSpec } from '../../src/RequestSpec';
6+
import RestApiBackend from '../../src/backend/rest';
7+
import WebApiBackend from '../../src/backend/web';
8+
import { Edition } from '../../src/types';
9+
import { labelForRequestSpecKey } from '../../src/ui/utils';
10+
import * as utils from '../../src/utils';
111
import { buildRequestBody, buildUrlWithParams } from '../../src/webapi';
212

13+
jest.mock('../../src/utils');
14+
315
describe('URL conversions', () => {
416
it('Params', () => {
517
expect(buildUrlWithParams('hi', { A: '5', TE: 'TTI' })).toBe('hi?A=5&TE=TTI');
@@ -8,3 +20,113 @@ describe('URL conversions', () => {
820
expect(buildRequestBody({ spec: ['comb', { site: 'heute' }] })).toBe('request={"spec":["comb",{"site":"heute"}]}');
921
});
1022
});
23+
24+
// from https://stackoverflow.com/questions/42773836/how-to-find-all-subsets-of-a-set-in-javascript-powerset-of-array
25+
const allSubsets = (values: string[]): string[][] =>
26+
values.reduce((subsets, value) => subsets.concat(subsets.map((set) => [value, ...set])), [[]]);
27+
28+
const fullExampleRequestSpec: RequestSpec = {
29+
graph_type: 'predefined_graph',
30+
aggregation: 'off',
31+
site: 'monitoring',
32+
host_name: 'example.com',
33+
host_name_regex: {
34+
value: '*.org',
35+
negated: false,
36+
},
37+
host_in_group: {
38+
value: 'printers',
39+
negated: true,
40+
},
41+
host_labels: ['os:linux'],
42+
host_tags: [{}, {}, {}],
43+
service: 'CPU load',
44+
service_regex: {
45+
value: 'CPU*',
46+
negated: true,
47+
},
48+
service_in_group: {
49+
value: 'web_servers',
50+
negated: false,
51+
},
52+
graph: 'Load Average',
53+
};
54+
55+
const getRequiredFields = (edition: Edition): Array<keyof RequestSpec> => {
56+
const result: Array<keyof RequestSpec> = ['graph'];
57+
if (edition === 'RAW') {
58+
result.push('site', 'host_name', 'service');
59+
}
60+
return result.sort();
61+
};
62+
63+
describe.each([{ edition: 'RAW' }, { edition: 'CEE' }])(
64+
'Query Validation in Edition $edition',
65+
(editionConfig: { edition: Edition }) => {
66+
describe.each([{ backend: 'rest' }, { backend: 'web' }])(
67+
'with backend $backend',
68+
(backendConfig: { backend: string }) => {
69+
let subject: DataSource;
70+
const requiredFields = getRequiredFields(editionConfig.edition);
71+
const cases = allSubsets(requiredFields)
72+
.filter((arr) => arr.length > 0)
73+
.map((arr) => arr.sort())
74+
.map((value) => ({ fields: value }));
75+
76+
jest
77+
.spyOn(RestApiBackend.prototype, 'getSingleGraph')
78+
.mockImplementation(() => Promise.resolve('It did succeed, sadly.'));
79+
80+
jest
81+
.spyOn(WebApiBackend.prototype, 'getGraphQuery')
82+
.mockImplementation(() => Promise.resolve(new MutableDataFrame()));
83+
84+
utils.replaceVariables.mockImplementation((rq) => rq);
85+
86+
beforeEach(() => {
87+
subject = new DataSource({ jsonData: { backend: backendConfig, edition: editionConfig.edition } } as any);
88+
});
89+
90+
it.each(cases)(
91+
'throws an informative error if the required fields $fields are missing in a query',
92+
async ({ fields }) => {
93+
const requestSpec = cloneDeep(fullExampleRequestSpec);
94+
for (const key of fields) {
95+
delete requestSpec[key];
96+
}
97+
const dataQueryRequest = {
98+
targets: [
99+
{
100+
requestSpec,
101+
},
102+
],
103+
range: [1, 2],
104+
} as any;
105+
106+
const errorMessageRegex = fields
107+
.map((value) => labelForRequestSpecKey(value as keyof RequestSpec, requestSpec))
108+
.join(', ');
109+
110+
// make sure this test doesn't fail pass on a resolved promise
111+
expect.assertions(1);
112+
await expect(subject.query(dataQueryRequest)).rejects.toThrow(errorMessageRegex);
113+
}
114+
);
115+
116+
it('validates even if site is an empty string', async () => {
117+
const requestSpec = cloneDeep(fullExampleRequestSpec);
118+
requestSpec['site'] = '';
119+
const dataQueryRequest = {
120+
targets: [
121+
{
122+
requestSpec,
123+
},
124+
],
125+
range: [1, 2],
126+
} as any;
127+
await expect(subject.query(dataQueryRequest)).resolves.not.toThrow();
128+
});
129+
}
130+
);
131+
}
132+
);

0 commit comments

Comments
 (0)