mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudWatch Logs: Adds template variable support to log groups (#25604)
* CloudWatch Logs: Adds template variable support to log groups Closes #25099
This commit is contained in:
parent
47a2ee5b24
commit
3b383149db
@ -12,7 +12,7 @@ import { CloudWatchLanguageProvider } from '../language_provider';
|
||||
import CloudWatchLink from './CloudWatchLink';
|
||||
import { css } from 'emotion';
|
||||
|
||||
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery>;
|
||||
type Props = QueryEditorProps<CloudWatchDatasource, CloudWatchQuery> & { allowCustomValue?: boolean };
|
||||
|
||||
const labelClass = css`
|
||||
margin-left: 3px;
|
||||
@ -20,7 +20,7 @@ const labelClass = css`
|
||||
`;
|
||||
|
||||
export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor(props: Props) {
|
||||
const { query, data, datasource, onRunQuery, onChange, exploreId, exploreMode } = props;
|
||||
const { query, data, datasource, onRunQuery, onChange, exploreId, exploreMode, allowCustomValue = false } = props;
|
||||
|
||||
let absolute: AbsoluteTimeRange;
|
||||
if (data?.request?.range?.from) {
|
||||
@ -55,6 +55,7 @@ export const CloudWatchLogsQueryEditor = memo(function CloudWatchLogsQueryEditor
|
||||
absoluteRange={absolute}
|
||||
syntaxLoaded={isSyntaxReady}
|
||||
syntax={syntax}
|
||||
allowCustomValue={allowCustomValue}
|
||||
ExtraFieldElement={
|
||||
<FormLabel className={`gf-form-label--btn ${labelClass}`} width="auto" tooltip="Link to Graph in AWS">
|
||||
<CloudWatchLink query={query as CloudWatchLogsQuery} panelData={data} datasource={datasource} />
|
||||
|
@ -42,6 +42,7 @@ export interface CloudWatchLogsQueryFieldProps extends ExploreQueryFieldProps<Cl
|
||||
syntaxLoaded: boolean;
|
||||
syntax: Grammar;
|
||||
exploreId: ExploreId;
|
||||
allowCustomValue?: boolean;
|
||||
}
|
||||
|
||||
const containerClass = css`
|
||||
@ -130,6 +131,14 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// No need to fetch matching log groups if the search term isn't valid
|
||||
// This is also useful for preventing searches when a user is typing out a log group with template vars
|
||||
// See https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_LogGroup.html for the source of the pattern below
|
||||
const logGroupNamePattern = /^[\.\-_/#A-Za-z0-9]+$/;
|
||||
if (!logGroupNamePattern.test(searchTerm)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingLogGroups: true,
|
||||
});
|
||||
@ -158,7 +167,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
|
||||
this.fetchLogGroupOptions(query.region).then(logGroups => {
|
||||
this.setState(state => {
|
||||
const selectedLogGroups = intersectionBy(state.selectedLogGroups, logGroups, 'value');
|
||||
const selectedLogGroups = state.selectedLogGroups;
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
@ -200,21 +209,22 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
}
|
||||
};
|
||||
|
||||
setSelectedLogGroups = (v: Array<SelectableValue<string>>) => {
|
||||
setSelectedLogGroups = (selectedLogGroups: Array<SelectableValue<string>>) => {
|
||||
this.setState({
|
||||
selectedLogGroups: v,
|
||||
selectedLogGroups,
|
||||
});
|
||||
|
||||
const { onChange, query } = this.props;
|
||||
onChange?.({
|
||||
...(query as CloudWatchLogsQuery),
|
||||
logGroupNames: selectedLogGroups.map(logGroupName => logGroupName.value!) ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
if (onChange) {
|
||||
const nextQuery = {
|
||||
...query,
|
||||
logGroupNames: v.map(logGroupName => logGroupName.value!) ?? [],
|
||||
};
|
||||
|
||||
onChange(nextQuery);
|
||||
}
|
||||
setCustomLogGroups = (v: string) => {
|
||||
const customLogGroup: SelectableValue<string> = { value: v, label: v };
|
||||
const selectedLogGroups = [...this.state.selectedLogGroups, customLogGroup];
|
||||
this.setSelectedLogGroups(selectedLogGroups);
|
||||
};
|
||||
|
||||
setSelectedRegion = async (v: SelectableValue<string>) => {
|
||||
@ -327,7 +337,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
}, 250);
|
||||
|
||||
render() {
|
||||
const { ExtraFieldElement, data, query, syntaxLoaded, datasource } = this.props;
|
||||
const { ExtraFieldElement, data, query, syntaxLoaded, datasource, allowCustomValue } = this.props;
|
||||
const {
|
||||
selectedLogGroups,
|
||||
availableLogGroups,
|
||||
@ -368,11 +378,15 @@ export class CloudWatchLogsQueryField extends React.PureComponent<CloudWatchLogs
|
||||
className="flex-grow-1"
|
||||
inputEl={
|
||||
<MultiSelect
|
||||
options={availableLogGroups}
|
||||
allowCustomValue={allowCustomValue}
|
||||
options={unionBy(availableLogGroups, selectedLogGroups, 'value')}
|
||||
value={selectedLogGroups}
|
||||
onChange={v => {
|
||||
this.setSelectedLogGroups(v);
|
||||
}}
|
||||
onCreateOption={v => {
|
||||
this.setCustomLogGroups(v);
|
||||
}}
|
||||
className={containerClass}
|
||||
closeMenuOnSelect={false}
|
||||
isClearable={true}
|
||||
|
@ -30,7 +30,11 @@ export class PanelQueryEditor extends PureComponent<Props> {
|
||||
}
|
||||
/>
|
||||
</QueryInlineField>
|
||||
{apiMode === ExploreMode.Logs ? <LogsQueryEditor {...this.props} /> : <MetricsQueryEditor {...this.props} />}
|
||||
{apiMode === ExploreMode.Logs ? (
|
||||
<LogsQueryEditor {...this.props} allowCustomValue />
|
||||
) : (
|
||||
<MetricsQueryEditor {...this.props} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -580,6 +580,13 @@ export class CloudWatchDatasource extends DataSourceApi<CloudWatchQuery, CloudWa
|
||||
}
|
||||
query.region = this.replace(query.region, scopedVars, true, 'region');
|
||||
query.region = this.getActualRegion(query.region);
|
||||
|
||||
// interpolate log groups
|
||||
if (query.logGroupNames) {
|
||||
query.logGroupNames = query.logGroupNames.map((logGroup: string) =>
|
||||
this.replace(logGroup, scopedVars, true, 'log groups')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -163,6 +163,7 @@ describe('CloudWatchDatasource', () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should stop querying when no more data retrieved past max attempts', async () => {
|
||||
const fakeFrames = genMockFrames(10);
|
||||
for (let i = 7; i < fakeFrames.length; i++) {
|
||||
@ -243,6 +244,21 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
expect(i).toBe(3);
|
||||
});
|
||||
|
||||
it('should call the replace method on provided log groups', () => {
|
||||
const replaceSpy = jest.spyOn(ctx.ds, 'replace').mockImplementation((target: string) => target);
|
||||
ctx.ds.makeLogActionRequest('StartQuery', [
|
||||
{
|
||||
queryString: 'test query string',
|
||||
region: 'default',
|
||||
logGroupNames: ['log-group', '${my_var}Variable', 'Cool${other_var}'],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(replaceSpy).toBeCalledWith('log-group', undefined, true, 'log groups');
|
||||
expect(replaceSpy).toBeCalledWith('${my_var}Variable', undefined, true, 'log groups');
|
||||
expect(replaceSpy).toBeCalledWith('Cool${other_var}', undefined, true, 'log groups');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing CloudWatch metrics query', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user