Skip to content

Commit 4552c76

Browse files
[Custom threshold] Reintroduce no data setting in the custom threshold rule (#188300)
Fixes #188229, related to #183921 Documentation request: elastic/observability-docs#4068 ## Summary **Note**: I've added an item to deprecate/remove one of the no-data settings in v9. Fixes not showing no data setting and set the related settings to false by default. Based on @maciejforcone's input, we can combine these 2 settings for simplicity, as one of them works at a time. I also changed the tooltip according to which setting is relevant: (we use one action group for both of them in connectors) |No data (without group)|Missing group (with group)| |---|---| |![image](https://github.com/user-attachments/assets/ecf45dd2-d2a7-46ce-abd0-e2a07426f28e)|![image](https://github.com/user-attachments/assets/8dedd0fe-bb4b-4e51-808f-f65f54ee73fd)| Here is how the setting is applied in API: https://github.com/user-attachments/assets/52c52724-6011-4f6d-8464-023cd9a9ea10
1 parent d5d3f42 commit 4552c76

File tree

2 files changed

+131
-22
lines changed

2 files changed

+131
-22
lines changed

Diff for: x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx

+67-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('Expression', () => {
4242

4343
async function setup(
4444
currentOptions?: CustomThresholdPrefillOptions,
45-
customRuleParams?: Record<string, unknown>
45+
customRuleParams?: Partial<RuleTypeParams & AlertParams>
4646
) {
4747
const ruleParams: RuleTypeParams & AlertParams = {
4848
criteria: [],
@@ -164,7 +164,8 @@ describe('Expression', () => {
164164
it('should prefill the rule using the context metadata', async () => {
165165
const index = 'changedMockedIndex';
166166
const currentOptions: CustomThresholdPrefillOptions = {
167-
alertOnGroupDisappear: false,
167+
alertOnGroupDisappear: true,
168+
alertOnNoData: true,
168169
groupBy: ['host.hostname'],
169170
searchConfiguration: {
170171
index,
@@ -191,7 +192,8 @@ describe('Expression', () => {
191192

192193
const { ruleParams } = await setup(currentOptions, { searchConfiguration: undefined });
193194

194-
expect(ruleParams.alertOnGroupDisappear).toEqual(false);
195+
expect(ruleParams.alertOnGroupDisappear).toEqual(true);
196+
expect(ruleParams.alertOnNoData).toEqual(true);
195197
expect(ruleParams.groupBy).toEqual(['host.hostname']);
196198
expect((ruleParams.searchConfiguration.query as Query).query).toBe('foo');
197199
expect(ruleParams.searchConfiguration.index).toBe(index);
@@ -211,6 +213,68 @@ describe('Expression', () => {
211213
]);
212214
});
213215

216+
it('should only set alertOnGroupDisappear to true if there is a group by field', async () => {
217+
const customRuleParams: Partial<RuleTypeParams & AlertParams> = {
218+
groupBy: ['host.hostname'],
219+
};
220+
221+
const { ruleParams, wrapper } = await setup({}, customRuleParams);
222+
223+
act(() => {
224+
wrapper
225+
.find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]')
226+
.at(1)
227+
.prop('onChange')?.({
228+
target: { checked: true },
229+
} as React.ChangeEvent<HTMLInputElement>);
230+
});
231+
232+
expect(ruleParams.alertOnGroupDisappear).toEqual(true);
233+
expect(ruleParams.alertOnNoData).toEqual(false);
234+
235+
// Uncheck
236+
act(() => {
237+
wrapper
238+
.find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]')
239+
.at(1)
240+
.prop('onChange')?.({
241+
target: { checked: false },
242+
} as React.ChangeEvent<HTMLInputElement>);
243+
});
244+
245+
expect(ruleParams.alertOnGroupDisappear).toEqual(false);
246+
expect(ruleParams.alertOnNoData).toEqual(false);
247+
});
248+
249+
it('should only set alertOnNoData to true if there is no group by', async () => {
250+
const { ruleParams, wrapper } = await setup();
251+
252+
act(() => {
253+
wrapper
254+
.find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]')
255+
.at(1)
256+
.prop('onChange')?.({
257+
target: { checked: true },
258+
} as React.ChangeEvent<HTMLInputElement>);
259+
});
260+
261+
expect(ruleParams.alertOnGroupDisappear).toEqual(false);
262+
expect(ruleParams.alertOnNoData).toEqual(true);
263+
264+
// Uncheck
265+
act(() => {
266+
wrapper
267+
.find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]')
268+
.at(1)
269+
.prop('onChange')?.({
270+
target: { checked: false },
271+
} as React.ChangeEvent<HTMLInputElement>);
272+
});
273+
274+
expect(ruleParams.alertOnGroupDisappear).toEqual(false);
275+
expect(ruleParams.alertOnNoData).toEqual(false);
276+
});
277+
214278
it('should show an error message when searchSource throws an error', async () => {
215279
const errorMessage = 'Error in searchSource create';
216280
const kibanaMock = kibanaStartMock.startContract();

Diff for: x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx

+64-19
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,22 @@ export default function Expressions(props: Props) {
7575
},
7676
} = useKibana().services;
7777

78+
const hasGroupBy = useMemo<boolean>(
79+
() => !!ruleParams.groupBy && ruleParams.groupBy.length > 0,
80+
[ruleParams.groupBy]
81+
);
82+
7883
const [timeSize, setTimeSize] = useState<number | undefined>(1);
7984
const [timeUnit, setTimeUnit] = useState<TimeUnitChar | undefined>('m');
8085
const [dataView, setDataView] = useState<DataView>();
8186
const [dataViewTimeFieldError, setDataViewTimeFieldError] = useState<string>();
8287
const [searchSource, setSearchSource] = useState<ISearchSource>();
8388
const [paramsError, setParamsError] = useState<Error>();
8489
const [paramsWarning, setParamsWarning] = useState<string>();
90+
const [isNoDataChecked, setIsNoDataChecked] = useState<boolean>(
91+
(hasGroupBy && !!ruleParams.alertOnGroupDisappear) ||
92+
(!hasGroupBy && !!ruleParams.alertOnNoData)
93+
);
8594
const derivedIndexPattern = useMemo<DataViewBase>(
8695
() => ({
8796
fields: dataView?.fields || [],
@@ -177,11 +186,15 @@ export default function Expressions(props: Props) {
177186
}
178187

179188
if (typeof ruleParams.alertOnNoData === 'undefined') {
180-
setRuleParams('alertOnNoData', true);
189+
preFillAlertOnNoData();
181190
}
182191
if (typeof ruleParams.alertOnGroupDisappear === 'undefined') {
183192
preFillAlertOnGroupDisappear();
184193
}
194+
setIsNoDataChecked(
195+
(hasGroupBy && !!ruleParams.alertOnGroupDisappear) ||
196+
(!hasGroupBy && !!ruleParams.alertOnNoData)
197+
);
185198
}, [metadata]); // eslint-disable-line react-hooks/exhaustive-deps
186199

187200
const onSelectDataView = useCallback(
@@ -250,9 +263,12 @@ export default function Expressions(props: Props) {
250263

251264
const onGroupByChange = useCallback(
252265
(group: string | null | string[]) => {
266+
const hasGroup = !!group && group.length > 0;
253267
setRuleParams('groupBy', group && group.length ? group : '');
268+
setRuleParams('alertOnGroupDisappear', hasGroup && isNoDataChecked);
269+
setRuleParams('alertOnNoData', !hasGroup && isNoDataChecked);
254270
},
255-
[setRuleParams]
271+
[setRuleParams, isNoDataChecked]
256272
);
257273

258274
const emptyError = useMemo(() => {
@@ -314,20 +330,24 @@ export default function Expressions(props: Props) {
314330
}
315331
}, [metadata, setRuleParams]);
316332

333+
const preFillAlertOnNoData = useCallback(() => {
334+
const md = metadata;
335+
if (md && typeof md.currentOptions?.alertOnNoData !== 'undefined') {
336+
setRuleParams('alertOnNoData', md.currentOptions.alertOnNoData);
337+
} else {
338+
setRuleParams('alertOnNoData', false);
339+
}
340+
}, [metadata, setRuleParams]);
341+
317342
const preFillAlertOnGroupDisappear = useCallback(() => {
318343
const md = metadata;
319344
if (md && typeof md.currentOptions?.alertOnGroupDisappear !== 'undefined') {
320345
setRuleParams('alertOnGroupDisappear', md.currentOptions.alertOnGroupDisappear);
321346
} else {
322-
setRuleParams('alertOnGroupDisappear', true);
347+
setRuleParams('alertOnGroupDisappear', false);
323348
}
324349
}, [metadata, setRuleParams]);
325350

326-
const hasGroupBy = useMemo(
327-
() => ruleParams.groupBy && ruleParams.groupBy.length > 0,
328-
[ruleParams.groupBy]
329-
);
330-
331351
if (paramsError) {
332352
return (
333353
<>
@@ -540,30 +560,55 @@ export default function Expressions(props: Props) {
540560
<EuiSpacer size="s" />
541561
<EuiCheckbox
542562
id="metrics-alert-group-disappear-toggle"
563+
data-test-subj="thresholdRuleAlertOnNoDataCheckbox"
543564
label={
544565
<>
545566
{i18n.translate(
546567
'xpack.observability.customThreshold.rule.alertFlyout.alertOnGroupDisappear',
547568
{
548-
defaultMessage: 'Alert me if a group stops reporting data',
569+
defaultMessage: "Alert me if there's no data",
549570
}
550571
)}{' '}
551572
<EuiIconTip
552573
type="questionInCircle"
553574
color="subdued"
554-
content={i18n.translate(
555-
'xpack.observability.customThreshold.rule.alertFlyout.groupDisappearHelpText',
556-
{
557-
defaultMessage:
558-
'Enable this to trigger the action if a previously detected group begins to report no results. This is not recommended for dynamically scaling infrastructures that may rapidly start and stop nodes automatically.',
559-
}
560-
)}
575+
content={
576+
hasGroupBy
577+
? i18n.translate(
578+
'xpack.observability.customThreshold.rule.alertFlyout.groupDisappearHelpText',
579+
{
580+
defaultMessage:
581+
'Enable this to trigger a no data alert if a previously detected group begins to report no results. This is not recommended for dynamically scaling infrastructures that may rapidly start and stop nodes automatically.',
582+
}
583+
)
584+
: i18n.translate(
585+
'xpack.observability.customThreshold.rule.alertFlyout.noDataHelpText',
586+
{
587+
defaultMessage:
588+
'Enable this to trigger a no data alert if the condition(s) do not report any data over the expected time period, or if the alert fails to query Elasticsearch',
589+
}
590+
)
591+
}
561592
/>
562593
</>
563594
}
564-
disabled={!hasGroupBy}
565-
checked={Boolean(hasGroupBy && ruleParams.alertOnGroupDisappear)}
566-
onChange={(e) => setRuleParams('alertOnGroupDisappear', e.target.checked)}
595+
checked={isNoDataChecked}
596+
onChange={(e) => {
597+
const checked = e.target.checked;
598+
setIsNoDataChecked(checked);
599+
if (!checked) {
600+
setRuleParams('alertOnGroupDisappear', false);
601+
setRuleParams('alertOnNoData', false);
602+
} else {
603+
if (hasGroupBy) {
604+
setRuleParams('alertOnGroupDisappear', true);
605+
setRuleParams('alertOnNoData', false);
606+
} else {
607+
setRuleParams('alertOnGroupDisappear', false);
608+
setRuleParams('alertOnNoData', true);
609+
}
610+
}
611+
}}
567612
/>
568613
<EuiSpacer size="m" />
569614
</>

0 commit comments

Comments
 (0)