[SLO] Add support for document count to custom metric indicator (#170913)

## 🍒 Summary

This PR fixes #170905 by adding the aggregation menu to the Custom
Metric indicator to allow the user to pick either `doc_count` or `sum`
for the aggregation.

<img width="1152" alt="image"
src="35aea8bd-d21c-4780-bad6-1efe5fc8902b">
This commit is contained in:
Chris Cowan 2023-11-09 08:26:25 -07:00 committed by GitHub
parent e0da2ae3da
commit b9c08bac92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 274 additions and 73 deletions

View file

@ -136,22 +136,29 @@ const timesliceMetricIndicatorSchema = t.type({
]),
});
const metricCustomValidAggregations = t.keyof({
sum: true,
});
const metricCustomDocCountMetric = t.intersection([
t.type({
name: t.string,
aggregation: t.literal('doc_count'),
}),
t.partial({
filter: t.string,
}),
]);
const metricCustomBasicMetric = t.intersection([
t.type({
name: t.string,
aggregation: t.literal('sum'),
field: t.string,
}),
t.partial({
filter: t.string,
}),
]);
const metricCustomMetricDef = t.type({
metrics: t.array(
t.intersection([
t.type({
name: t.string,
aggregation: metricCustomValidAggregations,
field: t.string,
}),
t.partial({
filter: t.string,
}),
])
),
metrics: t.array(t.union([metricCustomBasicMetric, metricCustomDocCountMetric])),
equation: t.string,
});
const metricCustomIndicatorTypeSchema = t.literal('sli.metric.custom');
@ -267,6 +274,8 @@ export {
kqlCustomIndicatorTypeSchema,
metricCustomIndicatorSchema,
metricCustomIndicatorTypeSchema,
metricCustomDocCountMetric,
metricCustomBasicMetric,
timesliceMetricComparatorMapping,
timesliceMetricIndicatorSchema,
timesliceMetricIndicatorTypeSchema,

View file

@ -22,6 +22,10 @@ import { first, range, xor } from 'lodash';
import React, { useEffect, useState } from 'react';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
import { Field } from '../../../../hooks/slo/use_fetch_index_pattern_fields';
import {
aggValueToLabel,
CUSTOM_METRIC_AGGREGATION_OPTIONS,
} from '../../helpers/aggregation_options';
import { createOptionsFromFields, Option } from '../../helpers/create_options';
import { CreateSLOForm } from '../../types';
import { QueryBuilder } from '../common/query_builder';
@ -62,7 +66,8 @@ const metricTooltip = (
content={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.totalMetric.tooltip',
{
defaultMessage: 'This data from this field will be aggregated with the "sum" aggregation.',
defaultMessage:
'This data from this field will be aggregated with the "sum" aggregation or document count.',
}
)}
position="top"
@ -89,6 +94,7 @@ const equationTooltip = (
export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIndicatorProps) {
const { control, watch, setValue, register, getFieldState } = useFormContext<CreateSLOForm>();
const [options, setOptions] = useState<Option[]>(createOptionsFromFields(metricFields));
const [aggregationOptions, setAggregationOptions] = useState(CUSTOM_METRIC_AGGREGATION_OPTIONS);
useEffect(() => {
setOptions(createOptionsFromFields(metricFields));
@ -131,20 +137,25 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
{fields?.map((metric, index) => (
<EuiFlexGroup alignItems="center" gutterSize="xs" key={metric.id}>
<input hidden {...register(`indicator.params.${type}.metrics.${index}.name`)} />
<input hidden {...register(`indicator.params.${type}.metrics.${index}.aggregation`)} />
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid}
isInvalid={
getFieldState(`indicator.params.${type}.metrics.${index}.aggregation`).invalid
}
label={
<span>
{metricLabel} {metric.name} {metricTooltip}
{i18n.translate(
'xpack.observability.slo.sloEdit.customMetric.aggregationLabel',
{ defaultMessage: 'Aggregation' }
)}{' '}
{metric.name}
</span>
}
>
<Controller
name={`indicator.params.${type}.metrics.${index}.field`}
defaultValue=""
name={`indicator.params.${type}.metrics.${index}.aggregation`}
defaultValue="sum"
rules={{ required: true }}
control={control}
render={({ field: { ref, ...field }, fieldState }) => (
@ -153,17 +164,13 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
async
fullWidth
singleSelection={{ asPlainText: true }}
prepend={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel',
{ defaultMessage: 'Sum of' }
)}
placeholder={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
'xpack.observability.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
aria-label={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
'xpack.observability.slo.sloEdit.sliType.customMetric.aggregation.placeholder',
{ defaultMessage: 'Select an aggregation' }
)}
isClearable
isInvalid={fieldState.invalid}
@ -178,40 +185,112 @@ export function MetricIndicator({ type, metricFields, isLoadingIndex }: MetricIn
selectedOptions={
!!indexPattern &&
!!field.value &&
metricFields.some((metricField) => metricField.name === field.value)
CUSTOM_METRIC_AGGREGATION_OPTIONS.some((agg) => agg.value === field.value)
? [
{
value: field.value,
label: field.value,
label: aggValueToLabel(field.value),
},
]
: []
}
onSearchChange={(searchValue: string) => {
setOptions(
createOptionsFromFields(metricFields, ({ value }) =>
setAggregationOptions(
CUSTOM_METRIC_AGGREGATION_OPTIONS.filter(({ value }) =>
value.includes(searchValue)
)
);
}}
options={options}
options={aggregationOptions}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
{watch(`indicator.params.${type}.metrics.${index}.aggregation`) !== 'doc_count' && (
<EuiFlexItem>
<EuiFormRow
fullWidth
isInvalid={
getFieldState(`indicator.params.${type}.metrics.${index}.field`).invalid
}
label={
<span>
{metricLabel} {metric.name} {metricTooltip}
</span>
}
>
<Controller
name={`indicator.params.${type}.metrics.${index}.field`}
defaultValue=""
rules={{ required: true }}
shouldUnregister
control={control}
render={({ field: { ref, ...field }, fieldState }) => (
<EuiComboBox
{...field}
async
fullWidth
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
)}
aria-label={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder',
{ defaultMessage: 'Select a metric field' }
)}
isClearable
isInvalid={fieldState.invalid}
isDisabled={isLoadingIndex || !indexPattern}
isLoading={!!indexPattern && isLoadingIndex}
onChange={(selected: EuiComboBoxOptionOption[]) => {
if (selected.length) {
return field.onChange(selected[0].value);
}
field.onChange('');
}}
selectedOptions={
!!indexPattern &&
!!field.value &&
metricFields.some((metricField) => metricField.name === field.value)
? [
{
value: field.value,
label: field.value,
},
]
: []
}
onSearchChange={(searchValue: string) => {
setOptions(
createOptionsFromFields(metricFields, ({ value }) =>
value.includes(searchValue)
)
);
}}
options={options}
/>
)}
/>
</EuiFormRow>
</EuiFlexItem>
)}
<EuiFlexItem>
<QueryBuilder
dataTestSubj="customKqlIndicatorFormGoodQueryInput"
indexPatternString={watch('indicator.params.index')}
label={`${filterLabel} ${metric.name}`}
name={`indicator.params.${type}.metrics.${index}.filter`}
placeholder=""
placeholder={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.placeholder',
{ defaultMessage: 'KQL filter' }
)}
required={false}
tooltip={
<EuiIconTip
content={i18n.translate(
'xpack.observability.slo.sloEdit.sliType.customMetric.goodQuery.tooltip',
'xpack.observability.slo.sloEdit.sliType.customMetric.tooltip',
{
defaultMessage: 'This KQL query should return a subset of events.',
}

View file

@ -132,7 +132,7 @@ export function MetricInput({
selectedOptions={
!!indexPattern &&
!!field.value &&
AGGREGATION_OPTIONS.some((agg) => agg.value === agg.value)
AGGREGATION_OPTIONS.some((agg) => agg.value === field.value)
? [
{
value: field.value,

View file

@ -75,6 +75,10 @@ export const AGGREGATION_OPTIONS = [
},
];
export const CUSTOM_METRIC_AGGREGATION_OPTIONS = AGGREGATION_OPTIONS.filter((option) =>
['doc_count', 'sum'].includes(option.value)
);
export function aggValueToLabel(value: string) {
const aggregation = AGGREGATION_OPTIONS.find((agg) => agg.value === value);
if (aggregation) {

View file

@ -6,6 +6,8 @@
*/
import {
metricCustomBasicMetric,
metricCustomDocCountMetric,
MetricCustomIndicator,
timesliceMetricBasicMetricWithField,
TimesliceMetricIndicator,
@ -31,7 +33,16 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
const data = getValues('indicator.params.good') as MetricCustomIndicator['params']['good'];
const isEquationValid = !getFieldState('indicator.params.good.equation').invalid;
const areMetricsValid =
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
isObject(data) &&
(data.metrics ?? []).every((metric) => {
if (metricCustomDocCountMetric.is(metric)) {
return true;
}
if (metricCustomBasicMetric.is(metric) && metric.field != null) {
return true;
}
return false;
});
return isEquationValid && areMetricsValid;
};
@ -41,7 +52,16 @@ export function useSectionFormValidation({ getFieldState, getValues, formState,
) as MetricCustomIndicator['params']['total'];
const isEquationValid = !getFieldState('indicator.params.total.equation').invalid;
const areMetricsValid =
isObject(data) && (data.metrics ?? []).every((metric) => Boolean(metric.field));
isObject(data) &&
(data.metrics ?? []).every((metric) => {
if (metricCustomDocCountMetric.is(metric)) {
return true;
}
if (metricCustomBasicMetric.is(metric) && metric.field != null) {
return true;
}
return false;
});
return isEquationValid && areMetricsValid;
};

View file

@ -4,7 +4,7 @@ exports[`GetHistogramIndicatorAggregation should generate a aggregation for good
Object {
"_good_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -16,7 +16,7 @@ Object {
},
"_good_B": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "processed",
},
@ -29,8 +29,8 @@ Object {
"goodEvents": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>sum",
"B": "_good_B>sum",
"A": "_good_A>metric",
"B": "_good_B>metric",
},
"script": Object {
"lang": "painless",
@ -45,7 +45,7 @@ exports[`GetHistogramIndicatorAggregation should generate a aggregation for tota
Object {
"_total_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -58,7 +58,7 @@ Object {
"totalEvents": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>sum",
"A": "_total_A>metric",
},
"script": Object {
"lang": "painless",

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { MetricCustomIndicator } from '@kbn/slo-schema';
import { metricCustomDocCountMetric, MetricCustomIndicator } from '@kbn/slo-schema';
import { getElastichsearchQueryOrThrow } from '../transform_generators';
type MetricCustomMetricDef =
@ -20,12 +20,22 @@ export class GetCustomMetricIndicatorAggregation {
const filter = metric.filter
? getElastichsearchQueryOrThrow(metric.filter)
: { match_all: {} };
if (metricCustomDocCountMetric.is(metric)) {
return {
...acc,
[`_${type}_${metric.name}`]: {
filter,
},
};
}
return {
...acc,
[`_${type}_${metric.name}`]: {
filter,
aggs: {
sum: {
metric: {
[metric.aggregation]: { field: metric.field },
},
},
@ -42,10 +52,11 @@ export class GetCustomMetricIndicatorAggregation {
}
private buildMetricEquation(type: 'good' | 'total', metricDef: MetricCustomMetricDef) {
const bucketsPath = metricDef.metrics.reduce(
(acc, metric) => ({ ...acc, [metric.name]: `_${type}_${metric.name}>sum` }),
{}
);
const bucketsPath = metricDef.metrics.reduce((acc, metric) => {
const path = metricCustomDocCountMetric.is(metric) ? '_count' : 'metric';
return { ...acc, [metric.name]: `_${type}_${metric.name}>${path}` };
}, {});
return {
bucket_script: {
buckets_path: bucketsPath,

View file

@ -1,10 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Metric Custom Transform Generator aggregates using doc_count for the denominator equation with filter 1`] = `
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>_count",
},
"script": Object {
"lang": "painless",
"source": "params.A / 100",
},
},
}
`;
exports[`Metric Custom Transform Generator aggregates using doc_count the numerator equation with filter 1`] = `
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>_count",
},
"script": Object {
"lang": "painless",
"source": "params.A * 100",
},
},
}
`;
exports[`Metric Custom Transform Generator aggregates using the denominator equation 1`] = `
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>sum",
"A": "_total_A>metric",
},
"script": Object {
"lang": "painless",
@ -18,7 +46,7 @@ exports[`Metric Custom Transform Generator aggregates using the denominator equa
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>sum",
"A": "_total_A>metric",
},
"script": Object {
"lang": "painless",
@ -32,7 +60,7 @@ exports[`Metric Custom Transform Generator aggregates using the numerator equati
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>sum",
"A": "_good_A>metric",
},
"script": Object {
"lang": "painless",
@ -46,7 +74,7 @@ exports[`Metric Custom Transform Generator aggregates using the numerator equati
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>sum",
"A": "_good_A>metric",
},
"script": Object {
"lang": "painless",
@ -101,7 +129,7 @@ Object {
"aggregations": Object {
"_good_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -113,7 +141,7 @@ Object {
},
"_good_B": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "processed",
},
@ -125,7 +153,7 @@ Object {
},
"_total_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -138,7 +166,7 @@ Object {
"slo.denominator": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>sum",
"A": "_total_A>metric",
},
"script": Object {
"lang": "painless",
@ -158,8 +186,8 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>sum",
"B": "_good_B>sum",
"A": "_good_A>metric",
"B": "_good_B>metric",
},
"script": Object {
"lang": "painless",
@ -384,7 +412,7 @@ Object {
"aggregations": Object {
"_good_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -396,7 +424,7 @@ Object {
},
"_good_B": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "processed",
},
@ -408,7 +436,7 @@ Object {
},
"_total_A": Object {
"aggs": Object {
"sum": Object {
"metric": Object {
"sum": Object {
"field": "total",
},
@ -421,7 +449,7 @@ Object {
"slo.denominator": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_total_A>sum",
"A": "_total_A>metric",
},
"script": Object {
"lang": "painless",
@ -432,8 +460,8 @@ Object {
"slo.numerator": Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>sum",
"B": "_good_B>sum",
"A": "_good_A>metric",
"B": "_good_B>metric",
},
"script": Object {
"lang": "painless",
@ -629,3 +657,17 @@ Object {
"transform_id": Any<String>,
}
`;
exports[`Metric Custom Transform Generator support the same field used twice in the equation 1`] = `
Object {
"bucket_script": Object {
"buckets_path": Object {
"A": "_good_A>metric",
},
"script": Object {
"lang": "painless",
"source": "params.A + params.A * 100",
},
},
}
`;

View file

@ -142,6 +142,20 @@ describe('Metric Custom Transform Generator', () => {
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
});
it('support the same field used twice in the equation', async () => {
const anSLO = createSLO({
indicator: createMetricCustomIndicator({
good: {
metrics: [{ name: 'A', aggregation: 'sum', field: 'good' }],
equation: 'A + A * 100',
},
}),
});
const transform = generator.getTransformParams(anSLO);
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
});
it('aggregates using the numerator equation with filter', async () => {
const anSLO = createSLO({
indicator: createMetricCustomIndicator({
@ -158,6 +172,20 @@ describe('Metric Custom Transform Generator', () => {
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
});
it('aggregates using doc_count the numerator equation with filter', async () => {
const anSLO = createSLO({
indicator: createMetricCustomIndicator({
good: {
metrics: [{ name: 'A', aggregation: 'doc_count', filter: 'outcome: "success" ' }],
equation: 'A * 100',
},
}),
});
const transform = generator.getTransformParams(anSLO);
expect(transform.pivot!.aggregations!['slo.numerator']).toMatchSnapshot();
});
it('aggregates using the denominator equation', async () => {
const anSLO = createSLO({
indicator: createMetricCustomIndicator({
@ -185,4 +213,18 @@ describe('Metric Custom Transform Generator', () => {
expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot();
});
it('aggregates using doc_count for the denominator equation with filter', async () => {
const anSLO = createSLO({
indicator: createMetricCustomIndicator({
total: {
metrics: [{ name: 'A', aggregation: 'doc_count', filter: 'outcome: *' }],
equation: 'A / 100',
},
}),
});
const transform = generator.getTransformParams(anSLO);
expect(transform.pivot!.aggregations!['slo.denominator']).toMatchSnapshot();
});
});

View file

@ -29682,12 +29682,10 @@
"xpack.observability.slo.sloEdit.sliType.customMetric.equationHelpText": "Accepte les équations mathématiques de base, les caractères valides sont : A-Z, +, -, /, *, (, ), ?, !, &, :, |, >, <, =",
"xpack.observability.slo.sloEdit.sliType.customMetric.equationLabel": "Équation",
"xpack.observability.slo.sloEdit.sliType.customMetric.filterLabel": "Filtre",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodQuery.tooltip": "Cette requête KQL doit renvoyer un sous-ensemble d'événements.",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodTitle": "Bons événements",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "Sélectionner un champ dindicateur",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricLabel": "Indicateur",
"xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "Filtre de requête",
"xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "Somme de",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalEquation.tooltip": "Ceci est compatible avec des calculs de base (A + B / C) et la logique booléenne (A < B ? A : B).",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalMetric.tooltip": "Les données de ce champ seront agrégées avec lagréation de \"somme\".",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalTitle": "Total des événements",

View file

@ -29681,12 +29681,10 @@
"xpack.observability.slo.sloEdit.sliType.customMetric.equationHelpText": "基本的な数式をサポートします。有効な文字A-Z、+、-、/、*、(、)、?、!、&、:、|、>、<、=",
"xpack.observability.slo.sloEdit.sliType.customMetric.equationLabel": "式",
"xpack.observability.slo.sloEdit.sliType.customMetric.filterLabel": "フィルター",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodQuery.tooltip": "このKQLクエリはイベントのサブセットを返します。",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodTitle": "良好なイベント数",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "メトリックフィールドを選択",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricLabel": "メトリック",
"xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "クエリのフィルター",
"xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "の合計",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalEquation.tooltip": "これは基本的な数学ロジックA + B / CとブールロジックA < B ?A :Bをサポートします。",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalMetric.tooltip": "このフィールドのデータは「sum」集計で集約されます。",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalTitle": "合計イベント数",

View file

@ -29679,12 +29679,10 @@
"xpack.observability.slo.sloEdit.sliType.customMetric.equationHelpText": "支持基本数学方程有效字符包括A-Z、+、-、/、*、(、)、?、!、&、:、|、>、<、=",
"xpack.observability.slo.sloEdit.sliType.customMetric.equationLabel": "方程",
"xpack.observability.slo.sloEdit.sliType.customMetric.filterLabel": "筛选",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodQuery.tooltip": "此 KQL 查询应返回一个事件子集。",
"xpack.observability.slo.sloEdit.sliType.customMetric.goodTitle": "良好事件",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricField.placeholder": "选择指标字段",
"xpack.observability.slo.sloEdit.sliType.customMetric.metricLabel": "指标",
"xpack.observability.slo.sloEdit.sliType.customMetric.queryFilter": "查询筛选",
"xpack.observability.slo.sloEdit.sliType.customMetric.sumLabel": "求和",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalEquation.tooltip": "这支持基本数学 (A + B / C) 和布尔逻辑 (A < B ?A :B)。",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalMetric.tooltip": "来自该字段的此类数据将使用“求和”聚合进行汇总。",
"xpack.observability.slo.sloEdit.sliType.customMetric.totalTitle": "事件合计",