feat: [UI] clickhouse queries in alert builder (#1706)

* feat: added ui changes to support clickhouse queries in alert builder

* chore: minor fix to alert rules ui

* feat: alert form changes: ch query support, alert type selection

* chore: resolved review comments

* chore: added list for alert type selection instead

* chore: removed hard coded color and added antd/colors

* fix: resolved some issues found during testing alerts

* fix: moved alert defaults and added default queries for logs and traces

* feat: added default queries for logs and traces to reflect ts vars

* chore: fixed px to rem

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Pranay Prateek <pranay@signoz.io>
This commit is contained in:
Amol Umbark 2022-11-24 13:21:46 +05:30 committed by GitHub
parent 4727dbc9f0
commit 220f848b04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 727 additions and 100 deletions

View File

@ -28,6 +28,7 @@
"condition_required": "at least one metric condition is required", "condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required", "alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL", "promql_required": "promql expression is required when query format is set to PromQL",
"chquery_required": "query is required when query format is set to ClickHouse",
"button_savechanges": "Save Rule", "button_savechanges": "Save Rule",
"button_createrule": "Create Rule", "button_createrule": "Create Rule",
"button_returntorules": "Return to rules", "button_returntorules": "Return to rules",
@ -55,6 +56,7 @@
"button_formula": "Formula", "button_formula": "Formula",
"tab_qb": "Query Builder", "tab_qb": "Query Builder",
"tab_promql": "PromQL", "tab_promql": "PromQL",
"tab_chquery": "ClickHouse Query",
"title_confirm": "Confirm", "title_confirm": "Confirm",
"button_ok": "Yes", "button_ok": "Yes",
"button_cancel": "No", "button_cancel": "No",
@ -88,5 +90,21 @@
"user_guide_pql_step3": "Step 3 -Alert Configuration", "user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions", "user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts" "user_guide_ch_step1": "Step 1 - Define the metric",
"user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial</0> to learn about query format and supported vars.",
"user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart",
"user_guide_ch_step2": "Step 2 - Define Alert Conditions",
"user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_ch_step2b": "Enter the Alert threshold",
"user_guide_ch_step3": "Step 3 -Alert Configuration",
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert:",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in metric data",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in logs data.",
"traces_based_alert": "Trace-based Alert",
"traces_based_alert_desc": "Send a notification when a condition occurs in traces data."
} }

View File

@ -28,6 +28,7 @@
"condition_required": "at least one metric condition is required", "condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required", "alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL", "promql_required": "promql expression is required when query format is set to PromQL",
"chquery_required": "query is required when query format is set to ClickHouse",
"button_savechanges": "Save Rule", "button_savechanges": "Save Rule",
"button_createrule": "Create Rule", "button_createrule": "Create Rule",
"button_returntorules": "Return to rules", "button_returntorules": "Return to rules",
@ -55,6 +56,7 @@
"button_formula": "Formula", "button_formula": "Formula",
"tab_qb": "Query Builder", "tab_qb": "Query Builder",
"tab_promql": "PromQL", "tab_promql": "PromQL",
"tab_chquery": "ClickHouse Query",
"title_confirm": "Confirm", "title_confirm": "Confirm",
"button_ok": "Yes", "button_ok": "Yes",
"button_cancel": "No", "button_cancel": "No",
@ -88,5 +90,21 @@
"user_guide_pql_step3": "Step 3 -Alert Configuration", "user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions", "user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed", "user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts" "user_guide_ch_step1": "Step 1 - Define the metric",
"user_guide_ch_step1a": "Write a Clickhouse query for alert evaluation. Follow <0>this tutorial</0> to learn about query format and supported vars.",
"user_guide_ch_step1b": "Format the legends based on labels you want to highlight in the preview chart",
"user_guide_ch_step2": "Step 2 - Define Alert Conditions",
"user_guide_ch_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_ch_step2b": "Enter the Alert threshold",
"user_guide_ch_step3": "Step 3 -Alert Configuration",
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts",
"choose_alert_type": "Choose a type for the alert:",
"metric_based_alert": "Metric based Alert",
"metric_based_alert_desc": "Send a notification when a condition occurs in metric data",
"log_based_alert": "Log-based Alert",
"log_based_alert_desc": "Send a notification when a condition occurs in logs data.",
"traces_based_alert": "Trace-based Alert",
"traces_based_alert_desc": "Send a notification when a condition occurs in traces data."
} }

View File

@ -0,0 +1,62 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertTypeCard, AlertTypeCards, SelectTypeContainer } from './styles';
interface OptionType {
title: string;
selection: AlertTypes;
description: string;
}
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']);
const renderOptions = (): JSX.Element => {
const optionList: OptionType[] = [
{
title: t('metric_based_alert'),
selection: AlertTypes.METRICS_BASED_ALERT,
description: t('metric_based_alert_desc'),
},
{
title: t('log_based_alert'),
selection: AlertTypes.LOGS_BASED_ALERT,
description: t('log_based_alert_desc'),
},
{
title: t('traces_based_alert'),
selection: AlertTypes.TRACES_BASED_ALERT,
description: t('traces_based_alert_desc'),
},
];
return (
<>
{optionList.map((o: OptionType) => (
<AlertTypeCard
key={o.selection}
title={o.title}
onClick={(): void => {
onSelect(o.selection);
}}
>
{o.description}
</AlertTypeCard>
))}
</>
);
};
return (
<SelectTypeContainer>
<h3> {t('choose_alert_type')} </h3>
<AlertTypeCards>{renderOptions()}</AlertTypeCards>
</SelectTypeContainer>
);
}
interface SelectAlertTypeProps {
onSelect: (typ: AlertTypes) => void;
}
export default SelectAlertType;

View File

@ -0,0 +1,22 @@
import { Card, Row } from 'antd';
import styled from 'styled-components';
export const SelectTypeContainer = styled.div`
&&& {
padding: 1rem;
}
`;
export const AlertTypeCards = styled(Row)`
&&& {
flex-wrap: nowrap;
}
`;
export const AlertTypeCard = styled(Card)`
&&& {
margin: 5px;
width: 21rem;
cursor: pointer;
}
`;

View File

@ -0,0 +1,139 @@
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
defaultCompareOp,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
export const alertDefaults: AlertDef = {
alertType: AlertTypes.METRICS_BASED_ALERT,
condition: {
compositeMetricQuery: {
builderQueries: {
A: {
queryName: 'A',
name: 'A',
formulaOnly: false,
metricName: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
aggregateOperator: 1,
expression: 'A',
disabled: false,
toggleDisable: false,
toggleDelete: false,
},
},
promQueries: {},
chQueries: {},
queryType: 1,
},
op: defaultCompareOp,
matchType: defaultMatchType,
},
labels: {
severity: 'warning',
},
annotations: {
description: 'A new alert',
},
evalWindow: defaultEvalWindow,
};
export const logAlertDefaults: AlertDef = {
alertType: AlertTypes.LOGS_BASED_ALERT,
condition: {
compositeMetricQuery: {
builderQueries: {
A: {
queryName: 'A',
name: 'A',
formulaOnly: false,
metricName: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
aggregateOperator: 1,
expression: 'A',
disabled: false,
toggleDisable: false,
toggleDelete: false,
},
},
promQueries: {},
chQueries: {
A: {
name: 'A',
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.logs \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
rawQuery: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.logs \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
legend: '',
disabled: false,
},
},
queryType: 2,
},
op: defaultCompareOp,
matchType: '4',
},
labels: {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/logs`,
},
annotations: {
description: 'A new log-based alert',
},
evalWindow: defaultEvalWindow,
};
export const traceAlertDefaults: AlertDef = {
alertType: AlertTypes.TRACES_BASED_ALERT,
condition: {
compositeMetricQuery: {
builderQueries: {
A: {
queryName: 'A',
name: 'A',
formulaOnly: false,
metricName: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
aggregateOperator: 1,
expression: 'A',
disabled: false,
toggleDisable: false,
toggleDelete: false,
},
},
promQueries: {},
chQueries: {
A: {
name: 'A',
rawQuery: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
query: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
legend: '',
disabled: false,
},
},
queryType: 2,
},
op: defaultCompareOp,
matchType: '4',
},
labels: {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/traces`,
},
annotations: {
description: 'A new trace-based alert',
},
evalWindow: defaultEvalWindow,
};

View File

@ -1,22 +1,53 @@
import { Form } from 'antd'; import { Form, Row } from 'antd';
import FormAlertRules from 'container/FormAlertRules'; import FormAlertRules from 'container/FormAlertRules';
import React from 'react'; import React, { useState } from 'react';
import { AlertDef } from 'types/api/alerts/def'; import { AlertTypes } from 'types/api/alerts/alertTypes';
function CreateRules({ initialValue }: CreateRulesProps): JSX.Element { import {
alertDefaults,
logAlertDefaults,
traceAlertDefaults,
} from './defaults';
import SelectAlertType from './SelectAlertType';
function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState(alertDefaults);
const [step, setStep] = useState(0);
const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT,
);
const [formInstance] = Form.useForm(); const [formInstance] = Form.useForm();
const onSelectType = (typ: AlertTypes): void => {
setAlertType(typ);
switch (typ) {
case AlertTypes.LOGS_BASED_ALERT:
setInitValues(logAlertDefaults);
break;
case AlertTypes.TRACES_BASED_ALERT:
setInitValues(traceAlertDefaults);
break;
default:
setInitValues(alertDefaults);
}
setStep(1);
};
if (step === 0) {
return (
<Row wrap={false}>
<SelectAlertType onSelect={onSelectType} />
</Row>
);
}
return ( return (
<FormAlertRules <FormAlertRules
alertType={alertType}
formInstance={formInstance} formInstance={formInstance}
initialValue={initialValue} initialValue={initValues}
ruleId={0} ruleId={0}
/> />
); );
} }
interface CreateRulesProps {
initialValue: AlertDef;
}
export default CreateRules; export default CreateRules;

View File

@ -1,6 +1,7 @@
import { Form } from 'antd'; import { Form } from 'antd';
import FormAlertRules from 'container/FormAlertRules'; import FormAlertRules from 'container/FormAlertRules';
import React from 'react'; import React from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def'; import { AlertDef } from 'types/api/alerts/def';
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element { function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
@ -8,6 +9,11 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
return ( return (
<FormAlertRules <FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance} formInstance={formInstance}
initialValue={initialValue} initialValue={initialValue}
ruleId={ruleId} ruleId={ruleId}

View File

@ -0,0 +1,53 @@
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query';
import { IClickHouseQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/types';
import React from 'react';
import { IChQueries } from 'types/api/alerts/compositeQuery';
function ChQuerySection({
chQueries,
setChQueries,
}: ChQuerySectionProps): JSX.Element {
const handleChQueryChange = ({
rawQuery,
legend,
toggleDelete,
}: IClickHouseQueryHandleChange): void => {
let chQuery = chQueries.A;
if (rawQuery) {
chQuery.rawQuery = rawQuery;
chQuery.query = rawQuery;
}
if (legend) chQuery.legend = legend;
if (toggleDelete) {
chQuery = {
rawQuery: '',
legend: '',
name: 'A',
disabled: false,
query: '',
};
}
setChQueries({
A: {
...chQuery,
},
});
};
return (
<ClickHouseQueryBuilder
key="A"
queryIndex="A"
queryData={{ ...chQueries?.A, name: 'A', rawQuery: chQueries?.A.query }}
handleQueryChange={handleChQueryChange}
/>
);
}
interface ChQuerySectionProps {
chQueries: IChQueries;
setChQueries: (q: IChQueries) => void;
}
export default ChQuerySection;

View File

@ -1,11 +1,12 @@
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph'; import { StaticLineProps } from 'components/Graph';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent'; import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import getChartData from 'lib/getChartData'; import getChartData from 'lib/getChartData';
import React from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
@ -22,6 +23,10 @@ export interface ChartPreviewProps {
selectedInterval?: Time; selectedInterval?: Time;
headline?: JSX.Element; headline?: JSX.Element;
threshold?: number | undefined; threshold?: number | undefined;
userQueryKey?: string;
}
interface QueryResponseError {
message?: string;
} }
function ChartPreview({ function ChartPreview({
@ -32,6 +37,7 @@ function ChartPreview({
selectedInterval = '5min', selectedInterval = '5min',
headline, headline,
threshold, threshold,
userQueryKey,
}: ChartPreviewProps): JSX.Element | null { }: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const staticLine: StaticLineProps | undefined = const staticLine: StaticLineProps | undefined =
@ -46,9 +52,34 @@ function ChartPreview({
} }
: undefined; : undefined;
const queryKey = JSON.stringify(query); const canQuery = useMemo((): boolean => {
if (!query || query == null) {
return false;
}
switch (query?.queryType) {
case EQueryType.PROM:
return query.promQL?.length > 0 && query.promQL[0].query !== '';
case EQueryType.CLICKHOUSE:
return (
query.clickHouse?.length > 0 && query.clickHouse[0].rawQuery?.length > 0
);
case EQueryType.QUERY_BUILDER:
return (
query.metricsBuilder?.queryBuilder?.length > 0 &&
query.metricsBuilder?.queryBuilder[0].metricName !== ''
);
default:
return false;
}
}, [query]);
const queryResponse = useQuery({ const queryResponse = useQuery({
queryKey: ['chartPreview', queryKey, selectedInterval], queryKey: [
'chartPreview',
userQueryKey || JSON.stringify(query),
selectedInterval,
],
queryFn: () => queryFn: () =>
GetMetricQueryRange({ GetMetricQueryRange({
query: query || { query: query || {
@ -64,14 +95,8 @@ function ChartPreview({
graphType, graphType,
selectedTime, selectedTime,
}), }),
enabled: retry: false,
query != null && enabled: canQuery,
((query.queryType === EQueryType.PROM &&
query.promQL?.length > 0 &&
query.promQL[0].query !== '') ||
(query.queryType === EQueryType.QUERY_BUILDER &&
query.metricsBuilder?.queryBuilder?.length > 0 &&
query.metricsBuilder?.queryBuilder[0].metricName !== '')),
}); });
const chartDataSet = queryResponse.isError const chartDataSet = queryResponse.isError
@ -89,15 +114,14 @@ function ChartPreview({
return ( return (
<ChartContainer> <ChartContainer>
{headline} {headline}
{(queryResponse?.data?.error || queryResponse?.isError) && ( {(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart"> <FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '} <InfoCircleOutlined />{' '}
{queryResponse?.data?.error || {(queryResponse?.error as QueryResponseError).message ||
queryResponse?.error ||
t('preview_chart_unexpected_error')} t('preview_chart_unexpected_error')}
</FailedMessageContainer> </FailedMessageContainer>
)} )}
{queryResponse.isLoading && <Spinner size="large" tip="Loading..." />}
{chartDataSet && !queryResponse.isError && ( {chartDataSet && !queryResponse.isError && (
<GridGraphComponent <GridGraphComponent
title={name} title={name}
@ -118,6 +142,7 @@ ChartPreview.defaultProps = {
selectedInterval: '5min', selectedInterval: '5min',
headline: undefined, headline: undefined,
threshold: undefined, threshold: undefined,
userQueryKey: '',
}; };
export default ChartPreview; export default ChartPreview;

View File

@ -1,3 +1,4 @@
import { red } from '@ant-design/colors';
import { Card, Tooltip } from 'antd'; import { Card, Tooltip } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
@ -10,7 +11,8 @@ export const NotFoundContainer = styled.div`
export const FailedMessageContainer = styled(Tooltip)` export const FailedMessageContainer = styled(Tooltip)`
position: absolute; position: absolute;
top: 10px; color: ${red};
top: 4rem;
left: 10px; left: 10px;
`; `;

View File

@ -1,5 +1,5 @@
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { notification, Tabs } from 'antd'; import { Button, notification, Tabs } from 'antd';
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula'; import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query'; import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
import { import {
@ -8,13 +8,16 @@ import {
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types'; } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { import {
IChQueries,
IFormulaQueries, IFormulaQueries,
IMetricQueries, IMetricQueries,
IPromQueries, IPromQueries,
} from 'types/api/alerts/compositeQuery'; } from 'types/api/alerts/compositeQuery';
import { EAggregateOperator, EQueryType } from 'types/common/dashboard'; import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection'; import PromqlSection from './PromqlSection';
import { FormContainer, QueryButton, StepHeading } from './styles'; import { FormContainer, QueryButton, StepHeading } from './styles';
import { toIMetricsBuilderQuery } from './utils'; import { toIMetricsBuilderQuery } from './utils';
@ -29,6 +32,10 @@ function QuerySection({
setFormulaQueries, setFormulaQueries,
promQueries, promQueries,
setPromQueries, setPromQueries,
chQueries,
setChQueries,
alertType,
runQuery,
}: QuerySectionProps): JSX.Element { }: QuerySectionProps): JSX.Element {
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
@ -49,6 +56,20 @@ function QuerySection({
}); });
} }
if (
parseInt(s, 10) === EQueryType.CLICKHOUSE &&
(!chQueries || Object.keys(chQueries).length === 0)
) {
setChQueries({
A: {
rawQuery: '',
name: 'A',
query: '',
legend: '',
disabled: false,
},
});
}
setQueryCategory(parseInt(s, 10)); setQueryCategory(parseInt(s, 10));
}; };
@ -196,6 +217,10 @@ function QuerySection({
); );
}; };
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderFormulaButton = (): JSX.Element => { const renderFormulaButton = (): JSX.Element => {
return ( return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}> <QueryButton onClick={addFormula} icon={<PlusOutlined />}>
@ -258,23 +283,84 @@ function QuerySection({
</div> </div>
); );
}; };
return (
<> const handleRunQuery = (): void => {
<StepHeading> {t('alert_form_step1')}</StepHeading> runQuery();
<FormContainer> };
<div style={{ display: 'flex' }}>
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
switch (typ) {
case AlertTypes.TRACES_BASED_ALERT:
case AlertTypes.LOGS_BASED_ALERT:
return (
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.CLICKHOUSE.toString()}
activeKey={queryCategory.toString()}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
{queryCategory === EQueryType.CLICKHOUSE && (
<Button type="primary" onClick={handleRunQuery}>
Run Query
</Button>
)}
</span>
}
>
<TabPane
tab={t('tab_qb')}
key={EQueryType.QUERY_BUILDER.toString()}
disabled
/>
<TabPane tab={t('tab_chquery')} key={EQueryType.CLICKHOUSE.toString()} />
</Tabs>
);
case AlertTypes.METRICS_BASED_ALERT:
default:
return (
<Tabs <Tabs
type="card" type="card"
style={{ width: '100%' }} style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()} defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
activeKey={queryCategory.toString()} activeKey={queryCategory.toString()}
onChange={handleQueryCategoryChange} onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
{queryCategory === EQueryType.CLICKHOUSE && (
<Button type="primary" onClick={handleRunQuery}>
Run Query
</Button>
)}
</span>
}
> >
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} /> <TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
<TabPane tab={t('tab_chquery')} key={EQueryType.CLICKHOUSE.toString()} />
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} /> <TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
</Tabs> </Tabs>
</div> );
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()} }
};
const renderQuerySection = (c: EQueryType): JSX.Element | null => {
switch (c) {
case EQueryType.PROM:
return renderPromqlUI();
case EQueryType.CLICKHOUSE:
return renderChQueryUI();
case EQueryType.QUERY_BUILDER:
return renderMetricUI();
default:
return null;
}
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
{renderQuerySection(queryCategory)}
</FormContainer> </FormContainer>
</> </>
); );
@ -289,6 +375,10 @@ interface QuerySectionProps {
setFormulaQueries: (b: IFormulaQueries) => void; setFormulaQueries: (b: IFormulaQueries) => void;
promQueries: IPromQueries; promQueries: IPromQueries;
setPromQueries: (p: IPromQueries) => void; setPromQueries: (p: IPromQueries) => void;
chQueries: IChQueries;
setChQueries: (q: IChQueries) => void;
alertType: AlertTypes;
runQuery: () => void;
} }
export default QuerySection; export default QuerySection;

View File

@ -1,7 +1,7 @@
import { Col, Row, Typography } from 'antd'; import { Col, Row, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { import {
@ -106,6 +106,63 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
); );
}; };
const renderStep1CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
<StyledList>
<StyledListItem>
<Trans
i18nKey="user_guide_ch_step1a"
t={t}
components={[
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
<a
key={1}
target="_blank"
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
/>,
]}
/>
</StyledListItem>
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3CH = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForCH = (): JSX.Element => {
return (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
};
return ( return (
<StyledMainContainer> <StyledMainContainer>
<Row> <Row>
@ -121,6 +178,7 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
</Row> </Row>
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()} {queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
{queryType === EQueryType.PROM && renderGuideForPQL()} {queryType === EQueryType.PROM && renderGuideForPQL()}
{queryType === EQueryType.CLICKHOUSE && renderGuideForCH()}
</StyledMainContainer> </StyledMainContainer>
); );
} }

View File

@ -9,7 +9,9 @@ import history from 'lib/history';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { import {
IChQueries,
IFormulaQueries, IFormulaQueries,
IMetricQueries, IMetricQueries,
IPromQueries, IPromQueries,
@ -45,6 +47,7 @@ import {
} from './utils'; } from './utils';
function FormAlertRules({ function FormAlertRules({
alertType,
formInstance, formInstance,
initialValue, initialValue,
ruleId, ruleId,
@ -57,6 +60,10 @@ function FormAlertRules({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// queryRunId helps to override of query caching for clickhouse query
// tab. A random string will be assigned for each execution
const [runQueryId, setRunQueryId] = useState<string>();
// alertDef holds the form values to be posted // alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue); const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
@ -82,9 +89,31 @@ function FormAlertRules({
...initQuery?.promQueries, ...initQuery?.promQueries,
}); });
// staged query is used to display chart preview // local state to handle promql queries
const [chQueries, setChQueries] = useState<IChQueries>({
...initQuery?.chQueries,
});
// staged query is used to display chart preview. the query gets
// auto refreshed when any of the params in query section change.
// though this is the source of chart data, the final query used
// by chart will be either debouncedStagedQuery or manualStagedQuery
// depending on the run option (auto-run or use of run query button)
const [stagedQuery, setStagedQuery] = useState<StagedQuery>(); const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
const debouncedStagedQuery = useDebounce(stagedQuery, 1000);
// manualStagedQuery requires manual staging of query
// when user clicks run query button. Useful for clickhouse tab where
// run query button is provided.
const [manualStagedQuery, setManualStagedQuery] = useState<StagedQuery>();
// delay to reduce load on backend api with auto-run query. only for clickhouse
// queries we have manual run, hence both debounce and debounceStagedQuery are not required
const debounceDelay = queryCategory !== EQueryType.CLICKHOUSE ? 1000 : 0;
// debounce query to delay backend api call and chart update.
// used in query builder and promql tabs to enable auto-refresh
// of chart on user edit
const debouncedStagedQuery = useDebounce(stagedQuery, debounceDelay);
// this use effect initiates staged query and // this use effect initiates staged query and
// other queries based on server data. // other queries based on server data.
@ -101,14 +130,26 @@ function FormAlertRules({
const fq = toFormulaQueries(initQuery?.builderQueries); const fq = toFormulaQueries(initQuery?.builderQueries);
// prepare staged query // prepare staged query
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries); const sq = prepareStagedQuery(
typ,
mq,
fq,
initQuery?.promQueries,
initQuery?.chQueries,
);
const pq = initQuery?.promQueries; const pq = initQuery?.promQueries;
const chq = initQuery?.chQueries;
setQueryCategory(typ); setQueryCategory(typ);
setMetricQueries(mq); setMetricQueries(mq);
setFormulaQueries(fq); setFormulaQueries(fq);
setPromQueries(pq); setPromQueries(pq);
setStagedQuery(sq); setStagedQuery(sq);
// also set manually staged query
setManualStagedQuery(sq);
setChQueries(chq);
setAlertDef(initialValue); setAlertDef(initialValue);
}, [initialValue]); }, [initialValue]);
@ -121,9 +162,15 @@ function FormAlertRules({
metricQueries, metricQueries,
formulaQueries, formulaQueries,
promQueries, promQueries,
chQueries,
); );
setStagedQuery(sq); setStagedQuery(sq);
}, [queryCategory, metricQueries, formulaQueries, promQueries]); }, [queryCategory, chQueries, metricQueries, formulaQueries, promQueries]);
const onRunQuery = (): void => {
setRunQueryId(Math.random().toString(36).substring(2, 15));
setManualStagedQuery(stagedQuery);
};
const onCancelHandler = useCallback(() => { const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT); history.replace(ROUTES.LIST_ALL_ALERT);
@ -169,6 +216,31 @@ function FormAlertRules({
return retval; return retval;
}, [t, promQueries, queryCategory]); }, [t, promQueries, queryCategory]);
const validateChQueryParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.CLICKHOUSE) return retval;
if (!chQueries || Object.keys(chQueries).length === 0) {
notification.error({
message: 'Error',
description: t('chquery_required'),
});
return false;
}
Object.keys(chQueries).forEach((key) => {
if (chQueries[key].rawQuery === '') {
notification.error({
message: 'Error',
description: t('chquery_required'),
});
retval = false;
}
});
return retval;
}, [t, chQueries, queryCategory]);
const validateQBParams = useCallback((): boolean => { const validateQBParams = useCallback((): boolean => {
let retval = true; let retval = true;
if (queryCategory !== EQueryType.QUERY_BUILDER) return true; if (queryCategory !== EQueryType.QUERY_BUILDER) return true;
@ -224,12 +296,17 @@ function FormAlertRules({
return false; return false;
} }
if (!validateChQueryParams()) {
return false;
}
return validateQBParams(); return validateQBParams();
}, [t, validateQBParams, alertDef, validatePromParams]); }, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
const preparePostData = (): AlertDef => { const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = { const postableAlert: AlertDef = {
...alertDef, ...alertDef,
alertType,
source: window?.location.toString(), source: window?.location.toString(),
ruleType: ruleType:
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule', queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
@ -238,6 +315,7 @@ function FormAlertRules({
compositeMetricQuery: { compositeMetricQuery: {
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries), builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
promQueries, promQueries,
chQueries,
queryType: queryCategory, queryType: queryCategory,
}, },
}, },
@ -251,6 +329,8 @@ function FormAlertRules({
metricQueries, metricQueries,
formulaQueries, formulaQueries,
promQueries, promQueries,
chQueries,
alertType,
]); ]);
const saveRule = useCallback(async () => { const saveRule = useCallback(async () => {
@ -380,6 +460,17 @@ function FormAlertRules({
); );
}; };
const renderChQueryChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
/>
);
};
return ( return (
<> <>
{Element} {Element}
@ -392,6 +483,7 @@ function FormAlertRules({
> >
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()} {queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
{queryCategory === EQueryType.PROM && renderPromChartPreview()} {queryCategory === EQueryType.PROM && renderPromChartPreview()}
{queryCategory === EQueryType.CLICKHOUSE && renderChQueryChartPreview()}
<QuerySection <QuerySection
queryCategory={queryCategory} queryCategory={queryCategory}
setQueryCategory={onQueryCategoryChange} setQueryCategory={onQueryCategoryChange}
@ -401,6 +493,10 @@ function FormAlertRules({
setFormulaQueries={setFormulaQueries} setFormulaQueries={setFormulaQueries}
promQueries={promQueries} promQueries={promQueries}
setPromQueries={setPromQueries} setPromQueries={setPromQueries}
chQueries={chQueries}
setChQueries={setChQueries}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={onRunQuery}
/> />
<RuleOptions <RuleOptions
@ -446,7 +542,12 @@ function FormAlertRules({
); );
} }
FormAlertRules.defaultProps = {
alertType: AlertTypes.METRICS_BASED_ALERT,
};
interface FormAlertRuleProps { interface FormAlertRuleProps {
alertType?: AlertTypes;
formInstance: FormInstance; formInstance: FormInstance;
initialValue: AlertDef; initialValue: AlertDef;
ruleId: number; ruleId: number;

View File

@ -1,6 +1,8 @@
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import { import {
IBuilderQueries, IBuilderQueries,
IChQueries,
IChQuery,
IFormulaQueries, IFormulaQueries,
IFormulaQuery, IFormulaQuery,
IMetricQueries, IMetricQueries,
@ -76,10 +78,12 @@ export const prepareStagedQuery = (
m: IMetricQueries, m: IMetricQueries,
f: IFormulaQueries, f: IFormulaQueries,
p: IPromQueries, p: IPromQueries,
c: IChQueries,
): IStagedQuery => { ): IStagedQuery => {
const qbList: IMetricQuery[] = []; const qbList: IMetricQuery[] = [];
const formulaList: IFormulaQuery[] = []; const formulaList: IFormulaQuery[] = [];
const promList: IPromQuery[] = []; const promList: IPromQuery[] = [];
const chQueryList: IChQuery[] = [];
// convert map[string]IMetricQuery to IMetricQuery[] // convert map[string]IMetricQuery to IMetricQuery[]
if (m) { if (m) {
@ -101,6 +105,13 @@ export const prepareStagedQuery = (
promList.push({ ...p[key], name: key }); promList.push({ ...p[key], name: key });
}); });
} }
// convert map[string]IChQuery to IChQuery[]
if (c) {
Object.keys(c).forEach((key) => {
console.log('c:', c[key]);
chQueryList.push({ ...c[key], name: key, rawQuery: c[key].query });
});
}
return { return {
queryType: t, queryType: t,
@ -109,7 +120,7 @@ export const prepareStagedQuery = (
formulas: formulaList, formulas: formulaList,
queryBuilder: qbList, queryBuilder: qbList,
}, },
clickHouse: [], clickHouse: chQueryList,
}; };
}; };

View File

@ -27,24 +27,34 @@ function ClickHouseQueryContainer({
toggleDisable, toggleDisable,
toggleDelete, toggleDelete,
}: IClickHouseQueryHandleChange): void => { }: IClickHouseQueryHandleChange): void => {
const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; // we must check if queryIndex is number type. because -
const currentIndexQuery = allQueries[queryIndex]; // ClickHouseQueryBuilder.handleQueryChange has a queryIndex
// parameter which supports both number and string formats.
// it is because, the dashboard side of query builder has queryIndex as number
// while the alert builder uses string format for query index (similar to backend)
// hence, this method is only applies when queryIndex is in number format.
if (rawQuery !== undefined) { if (typeof queryIndex === 'number') {
currentIndexQuery.rawQuery = rawQuery; const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME];
}
if (legend !== undefined) { const currentIndexQuery = allQueries[queryIndex];
currentIndexQuery.legend = legend;
}
if (toggleDisable) { if (rawQuery !== undefined) {
currentIndexQuery.disabled = !currentIndexQuery.disabled; currentIndexQuery.rawQuery = rawQuery;
}
if (legend !== undefined) {
currentIndexQuery.legend = legend;
}
if (toggleDisable) {
currentIndexQuery.disabled = !currentIndexQuery.disabled;
}
if (toggleDelete) {
allQueries.splice(queryIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
} }
if (toggleDelete) {
allQueries.splice(queryIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
}; };
const addQueryHandler = (): void => { const addQueryHandler = (): void => {
queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({ queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({

View File

@ -8,7 +8,7 @@ import { IClickHouseQueryHandleChange } from './types';
interface IClickHouseQueryBuilderProps { interface IClickHouseQueryBuilderProps {
queryData: IClickHouseQuery; queryData: IClickHouseQuery;
queryIndex: number; queryIndex: number | string;
handleQueryChange: (args: IClickHouseQueryHandleChange) => void; handleQueryChange: (args: IClickHouseQueryHandleChange) => void;
} }
@ -43,6 +43,9 @@ function ClickHouseQueryBuilder({
scrollbar: { scrollbar: {
alwaysConsumeMouseWheel: false, alwaysConsumeMouseWheel: false,
}, },
minimap: {
enabled: false,
},
}} }}
/> />
<Input <Input

View File

@ -1,7 +1,7 @@
import { IClickHouseQuery } from 'types/api/dashboard/getAll'; import { IClickHouseQuery } from 'types/api/dashboard/getAll';
export interface IClickHouseQueryHandleChange { export interface IClickHouseQueryHandleChange {
queryIndex: number; queryIndex: number | string;
rawQuery?: IClickHouseQuery['rawQuery']; rawQuery?: IClickHouseQuery['rawQuery'];
legend?: IClickHouseQuery['legend']; legend?: IClickHouseQuery['legend'];
toggleDisable?: IClickHouseQuery['disabled']; toggleDisable?: IClickHouseQuery['disabled'];

View File

@ -1,9 +1,8 @@
import CreateAlertRule from 'container/CreateAlertRule'; import CreateAlertRule from 'container/CreateAlertRule';
import React from 'react'; import React from 'react';
import { alertDefaults } from 'types/api/alerts/create';
function CreateAlertPage(): JSX.Element { function CreateAlertPage(): JSX.Element {
return <CreateAlertRule initialValue={alertDefaults} />; return <CreateAlertRule />;
} }
export default CreateAlertPage; export default CreateAlertPage;

View File

@ -0,0 +1,7 @@
// this list must exactly match with the backend
export enum AlertTypes {
NONE = 'NONE',
METRICS_BASED_ALERT = 'METRIC_BASED_ALERT',
LOGS_BASED_ALERT = 'LOGS_BASED_ALERT',
TRACES_BASED_ALERT = 'TRACES_BASED_ALERT',
}

View File

@ -1,4 +1,5 @@
import { import {
IClickHouseQuery,
IMetricsBuilderFormula, IMetricsBuilderFormula,
IMetricsBuilderQuery, IMetricsBuilderQuery,
IPromQLQuery, IPromQLQuery,
@ -9,17 +10,25 @@ import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
export interface ICompositeMetricQuery { export interface ICompositeMetricQuery {
builderQueries: IBuilderQueries; builderQueries: IBuilderQueries;
promQueries: IPromQueries; promQueries: IPromQueries;
chQueries: IChQueries;
queryType: EQueryType; queryType: EQueryType;
} }
export interface IPromQueries { export interface IChQueries {
[key: string]: IPromQuery; [key: string]: IChQuery;
}
export interface IChQuery extends IClickHouseQuery {
query: string;
} }
export interface IPromQuery extends IPromQLQuery { export interface IPromQuery extends IPromQLQuery {
stats?: ''; stats?: '';
} }
export interface IPromQueries {
[key: string]: IPromQuery;
}
export interface IBuilderQueries { export interface IBuilderQueries {
[key: string]: IBuilderQuery; [key: string]: IBuilderQuery;
} }

View File

@ -1,7 +1,5 @@
import { AlertDef } from 'types/api/alerts/def'; import { AlertDef } from 'types/api/alerts/def';
import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def';
export interface Props { export interface Props {
data: AlertDef; data: AlertDef;
} }
@ -10,39 +8,3 @@ export interface PayloadProps {
status: string; status: string;
data: string; data: string;
} }
export const alertDefaults: AlertDef = {
condition: {
compositeMetricQuery: {
builderQueries: {
A: {
queryName: 'A',
name: 'A',
formulaOnly: false,
metricName: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
aggregateOperator: 1,
expression: 'A',
disabled: false,
toggleDisable: false,
toggleDelete: false,
},
},
promQueries: {},
queryType: 1,
},
op: defaultCompareOp,
matchType: defaultMatchType,
},
labels: {
severity: 'warning',
},
annotations: {
description: 'A new alert',
},
evalWindow: defaultEvalWindow,
};

View File

@ -11,6 +11,7 @@ export const defaultCompareOp = '1';
export interface AlertDef { export interface AlertDef {
id?: number; id?: number;
alertType?: string;
alert?: string; alert?: string;
ruleType?: string; ruleType?: string;
condition: RuleCondition; condition: RuleCondition;