CloudWatch: Add default log groups to config page (#49286)

Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>
This commit is contained in:
Isabella Siu 2022-07-07 13:03:02 -04:00 committed by GitHub
parent 3d68023606
commit 8dd8c50dc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 259 additions and 81 deletions

View File

@ -86,8 +86,8 @@ exports[`no enzyme tests`] = {
"public/app/features/folders/FolderSettingsPage.test.tsx:1109052730": [
[0, 19, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:227258837": [
[0, 19, 13, "RegExp match", "2409514259"]
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:4057721851": [
[1, 19, 13, "RegExp match", "2409514259"]
],
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [
[0, 26, 13, "RegExp match", "2409514259"]
@ -6659,8 +6659,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/datasource/cloudwatch/components/LogsQueryEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@ -6668,7 +6672,8 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -7568,10 +7573,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"]
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
],
"public/app/plugins/datasource/influxdb/specs/influx_query_model.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -52,6 +52,7 @@ export function setupMockedDataSource({
datasource.getNamespaces = jest.fn().mockResolvedValue([]);
datasource.getRegions = jest.fn().mockResolvedValue([]);
datasource.defaultLogGroups = [];
const fetchMock = jest.fn().mockReturnValue(of({ data }));
setBackendSrv({ fetch: fetchMock } as any);

View File

@ -1,71 +1,92 @@
import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import React from 'react';
import selectEvent from 'react-select-event';
import { AwsAuthType } from '@grafana/aws-sdk';
import { setupMockedDataSource } from '../__mocks__/CloudWatchDataSource';
import { ConfigEditor, Props } from './ConfigEditor';
const ds = setupMockedDataSource();
jest.mock('app/features/plugins/datasource_srv', () => ({
getDatasourceSrv: () => ({
loadDatasource: jest.fn().mockImplementation(() =>
Promise.resolve({
getRegions: jest.fn().mockReturnValue([
{
label: 'ap-east-1',
value: 'ap-east-1',
},
]),
})
),
loadDatasource: jest.fn().mockResolvedValue({
getRegions: jest.fn().mockResolvedValue([
{
label: 'ap-east-1',
value: 'ap-east-1',
},
]),
describeLogGroups: jest.fn().mockResolvedValue(['logGroup-foo', 'logGroup-bar']),
getActualRegion: jest.fn().mockReturnValue('ap-east-1'),
getVariables: jest.fn().mockReturnValue([]),
}),
}),
}));
const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 1,
uid: 'z',
orgId: 1,
typeLogoUrl: '',
name: 'CloudWatch',
access: 'proxy',
url: '',
database: '',
type: 'cloudwatch',
typeName: 'Cloudwatch',
user: '',
basicAuth: false,
basicAuthUser: '',
isDefault: true,
readOnly: false,
withCredentials: false,
secureJsonFields: {
accessKey: false,
secretKey: false,
},
jsonData: {
assumeRoleArn: '',
externalId: '',
database: '',
customMetricsNamespaces: '',
authType: AwsAuthType.Keys,
defaultRegion: 'us-east-2',
timeField: '@timestamp',
},
secureJsonData: {
secretKey: '',
accessKey: '',
},
jest.mock('./XrayLinkConfig', () => ({
XrayLinkConfig: () => <></>,
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
put: jest.fn().mockResolvedValue({ datasource: ds.datasource }),
}),
}));
const props: Props = {
options: {
id: 1,
uid: 'z',
orgId: 1,
typeLogoUrl: '',
name: 'CloudWatch',
access: 'proxy',
url: '',
database: '',
type: 'cloudwatch',
typeName: 'Cloudwatch',
user: '',
basicAuth: false,
basicAuthUser: '',
isDefault: true,
readOnly: false,
withCredentials: false,
secureJsonFields: {
accessKey: false,
secretKey: false,
},
onOptionsChange: jest.fn(),
};
jsonData: {
assumeRoleArn: '',
externalId: '',
database: '',
customMetricsNamespaces: '',
authType: AwsAuthType.Keys,
defaultRegion: 'us-east-2',
timeField: '@timestamp',
},
secureJsonData: {
secretKey: '',
accessKey: '',
},
},
onOptionsChange: jest.fn(),
};
Object.assign(props, propOverrides);
const setup = (propOverrides?: object) => {
const newProps = { ...props, ...propOverrides };
return shallow(<ConfigEditor {...props} />);
return shallow(<ConfigEditor {...newProps} />);
};
describe('Render', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should render component', () => {
const wrapper = setup();
@ -107,4 +128,15 @@ describe('Render', () => {
});
expect(wrapper).toMatchSnapshot();
});
it('should load log groups when multiselect is opened', async () => {
(window as any).grafanaBootData = {
settings: {},
};
render(<ConfigEditor {...props} />);
const multiselect = await screen.findByLabelText('Log Groups');
selectEvent.openMenu(multiselect);
expect(await screen.findByText('logGroup-foo')).toBeInTheDocument();
});
});

View File

@ -7,7 +7,9 @@ import {
DataSourcePluginOptionsEditorProps,
onUpdateDatasourceJsonDataOption,
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginOption,
} from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Input, InlineField } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { createWarningNotification } from 'app/core/copy/appNotification';
@ -17,16 +19,43 @@ import { store } from 'app/store/store';
import { CloudWatchDatasource } from '../datasource';
import { CloudWatchJsonData, CloudWatchSecureJsonData } from '../types';
import { LogGroupSelector } from './LogGroupSelector';
import { XrayLinkConfig } from './XrayLinkConfig';
export type Props = DataSourcePluginOptionsEditorProps<CloudWatchJsonData, CloudWatchSecureJsonData>;
export const ConfigEditor: FC<Props> = (props: Props) => {
const { options } = props;
const { defaultLogGroups, logsTimeout, defaultRegion } = options.jsonData;
const [saved, setSaved] = useState(!!options.version && options.version > 1);
const datasource = useDatasource(options.name);
const datasource = useDatasource(options.name, saved);
useAuthenticationWarning(options.jsonData);
const logsTimeoutError = useTimoutValidation(props.options.jsonData.logsTimeout);
const logsTimeoutError = useTimoutValidation(logsTimeout);
useEffect(() => {
setSaved(false);
}, [
props.options.jsonData.assumeRoleArn,
props.options.jsonData.authType,
props.options.jsonData.defaultRegion,
props.options.jsonData.endpoint,
props.options.jsonData.externalId,
props.options.jsonData.profile,
props.options.secureJsonData?.accessKey,
props.options.secureJsonData?.secretKey,
]);
const saveOptions = async (): Promise<void> => {
if (saved) {
return;
}
await getBackendSrv()
.put(`/api/datasources/${options.id}`, options)
.then((result: { datasource: any }) => {
updateDatasourcePluginOption(props, 'version', result.datasource.version);
});
setSaved(true);
};
return (
<>
@ -52,7 +81,7 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
<InlineField
label="Timeout"
labelWidth={28}
tooltip='Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as "15m" "30s" "2000ms" etc.'
tooltip='Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as "15m" "30s" "2000ms" etc.'
invalid={Boolean(logsTimeoutError)}
>
<Input
@ -63,6 +92,23 @@ export const ConfigEditor: FC<Props> = (props: Props) => {
title={'The timeout must be a valid duration string, such as "15m" "30s" "2000ms" etc.'}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
region={defaultRegion ?? ''}
selectedLogGroups={defaultLogGroups ?? []}
datasource={datasource}
onChange={(logGroups) => {
updateDatasourcePluginJsonDataOption(props, 'defaultLogGroups', logGroups);
}}
onOpenMenu={saveOptions}
width={60}
saved={saved}
/>
</InlineField>
</div>
<XrayLinkConfig
@ -91,10 +137,14 @@ function useAuthenticationWarning(jsonData: CloudWatchJsonData) {
}, [jsonData.authType, jsonData.database, jsonData.profile]);
}
function useDatasource(datasourceName: string) {
function useDatasource(datasourceName: string, saved: boolean) {
const [datasource, setDatasource] = useState<CloudWatchDatasource>();
useEffect(() => {
// reload the datasource when it's saved
if (!saved) {
return;
}
getDatasourceSrv()
.loadDatasource(datasourceName)
.then((datasource) => {
@ -102,7 +152,7 @@ function useDatasource(datasourceName: string) {
// So a "as" type assertion here is a necessary evil.
setDatasource(datasource as CloudWatchDatasource);
});
}, [datasourceName]);
}, [datasourceName, saved]);
return datasource;
}

View File

@ -85,7 +85,6 @@ describe('LogGroupSelector', () => {
'DeliciousGroup',
'DeliciousGroup2',
'DeliciousGroup3',
'VelvetGroup',
'VelvetGroup2',
'VelvetGroup3',

View File

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope
import React from 'react';
import { act } from 'react-dom/test-utils';
@ -34,4 +34,25 @@ describe('CloudWatchLogsQueryField', () => {
});
expect(onRunQuery).toHaveBeenCalled();
});
it('loads defaultLogGroups', async () => {
const onRunQuery = jest.fn();
const ds = setupMockedDataSource();
ds.datasource.defaultLogGroups = ['foo'];
render(
<CloudWatchLogsQueryField
absoluteRange={{ from: 1, to: 10 }}
exploreId={ExploreId.left}
datasource={ds.datasource}
query={{} as any}
onRunQuery={onRunQuery}
onChange={() => {}}
/>
);
await waitFor(() => {
expect(screen.getByText('foo')).toBeInTheDocument();
});
});
});

View File

@ -66,10 +66,10 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
}
componentDidMount = () => {
const { query, onChange } = this.props;
const { query, datasource, onChange } = this.props;
if (onChange) {
onChange({ ...query, logGroupNames: query.logGroupNames ?? [] });
onChange({ ...query, logGroupNames: query.logGroupNames ?? datasource.defaultLogGroups });
}
};
@ -135,7 +135,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
inputEl={
<LogGroupSelector
region={region}
selectedLogGroups={logGroupNames ?? []}
selectedLogGroups={logGroupNames ?? datasource.defaultLogGroups}
datasource={datasource}
onChange={function (logGroups: string[]): void {
onChange({ ...query, logGroupNames: logGroups });

View File

@ -72,7 +72,7 @@ exports[`Render should disable access key id field 1`] = `
invalid={false}
label="Timeout"
labelWidth={28}
tooltip="Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
tooltip="Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
>
<Input
onChange={[Function]}
@ -82,6 +82,20 @@ exports[`Render should disable access key id field 1`] = `
width={60}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
onChange={[Function]}
onOpenMenu={[Function]}
region="us-east-2"
saved={false}
selectedLogGroups={Array []}
width={60}
/>
</InlineField>
</div>
<XrayLinkConfig
onChange={[Function]}
@ -156,7 +170,7 @@ exports[`Render should render component 1`] = `
invalid={false}
label="Timeout"
labelWidth={28}
tooltip="Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
tooltip="Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
>
<Input
onChange={[Function]}
@ -166,6 +180,20 @@ exports[`Render should render component 1`] = `
width={60}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
onChange={[Function]}
onOpenMenu={[Function]}
region="us-east-2"
saved={false}
selectedLogGroups={Array []}
width={60}
/>
</InlineField>
</div>
<XrayLinkConfig
onChange={[Function]}
@ -245,7 +273,7 @@ exports[`Render should show access key and secret access key fields 1`] = `
invalid={false}
label="Timeout"
labelWidth={28}
tooltip="Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
tooltip="Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
>
<Input
onChange={[Function]}
@ -255,6 +283,20 @@ exports[`Render should show access key and secret access key fields 1`] = `
width={60}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
onChange={[Function]}
onOpenMenu={[Function]}
region="us-east-2"
saved={false}
selectedLogGroups={Array []}
width={60}
/>
</InlineField>
</div>
<XrayLinkConfig
onChange={[Function]}
@ -334,7 +376,7 @@ exports[`Render should show arn role field 1`] = `
invalid={false}
label="Timeout"
labelWidth={28}
tooltip="Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
tooltip="Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
>
<Input
onChange={[Function]}
@ -344,6 +386,20 @@ exports[`Render should show arn role field 1`] = `
width={60}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
onChange={[Function]}
onOpenMenu={[Function]}
region="us-east-2"
saved={false}
selectedLogGroups={Array []}
width={60}
/>
</InlineField>
</div>
<XrayLinkConfig
onChange={[Function]}
@ -423,7 +479,7 @@ exports[`Render should show credentials profile name field 1`] = `
invalid={false}
label="Timeout"
labelWidth={28}
tooltip="Custom timout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
tooltip="Custom timeout for CloudWatch Logs insights queries which have max concurrency limits. Default is 15 minutes. Must be a valid duration string, such as \\"15m\\" \\"30s\\" \\"2000ms\\" etc."
>
<Input
onChange={[Function]}
@ -433,6 +489,20 @@ exports[`Render should show credentials profile name field 1`] = `
width={60}
/>
</InlineField>
<InlineField
label="Default Log Groups"
labelWidth={28}
tooltip="Optionally, specify default log groups for CloudWatch Logs queries."
>
<LogGroupSelector
onChange={[Function]}
onOpenMenu={[Function]}
region="us-east-2"
saved={false}
selectedLogGroups={Array []}
width={60}
/>
</InlineField>
</div>
<XrayLinkConfig
onChange={[Function]}

View File

@ -101,6 +101,7 @@ export class CloudWatchDatasource
tracingDataSourceUid?: string;
logsTimeout: string;
defaultLogGroups: string[];
type = 'cloudwatch';
standardStatistics = ['Average', 'Maximum', 'Minimum', 'Sum', 'SampleCount'];
@ -127,6 +128,7 @@ export class CloudWatchDatasource
this.languageProvider = new CloudWatchLanguageProvider(this);
this.tracingDataSourceUid = instanceSettings.jsonData.tracingDatasourceUid;
this.logsTimeout = instanceSettings.jsonData.logsTimeout || '15m';
this.defaultLogGroups = instanceSettings.jsonData.defaultLogGroups || [];
this.sqlCompletionItemProvider = new SQLCompletionItemProvider(this, this.templateSrv);
this.metricMathCompletionItemProvider = new MetricMathCompletionItemProvider(this, this.templateSrv);
this.variables = new CloudWatchVariableSupport(this);
@ -176,7 +178,14 @@ export class CloudWatchDatasource
logQueries: CloudWatchLogsQuery[],
options: DataQueryRequest<CloudWatchQuery>
): Observable<DataQueryResponse> => {
const validLogQueries = logQueries.filter((item) => item.logGroupNames?.length);
const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({
queryString: target.expression || '',
refId: target.refId,
logGroupNames: target.logGroupNames || this.defaultLogGroups,
region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'),
}));
const validLogQueries = queryParams.filter((item) => item.logGroupNames?.length);
if (logQueries.length > validLogQueries.length) {
return of({ data: [], error: { message: 'Log group is required' } });
}
@ -186,13 +195,6 @@ export class CloudWatchDatasource
return of({ data: [], state: LoadingState.Done });
}
const queryParams = logQueries.map((target: CloudWatchLogsQuery) => ({
queryString: target.expression || '',
refId: target.refId,
logGroupNames: target.logGroupNames,
region: this.replace(this.getActualRegion(target.region), options.scopedVars, true, 'region'),
}));
const startTime = new Date();
const timeoutFunc = () => {
return Date.now() >= startTime.valueOf() + rangeUtil.intervalToMs(this.logsTimeout);

View File

@ -121,6 +121,7 @@ export interface CloudWatchJsonData extends AwsAuthDataSourceJsonData {
logsTimeout?: string;
// Used to create links if logs contain traceId.
tracingDatasourceUid?: string;
defaultLogGroups?: string[];
}
export interface CloudWatchSecureJsonData extends AwsAuthDataSourceSecureJsonData {