diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index cae309fd45..fa2d9516fe 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -28,6 +28,7 @@ "condition_required": "at least one metric condition is required", "alertname_required": "alert name is required", "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_createrule": "Create Rule", "button_returntorules": "Return to rules", @@ -55,6 +56,7 @@ "button_formula": "Formula", "tab_qb": "Query Builder", "tab_promql": "PromQL", + "tab_chquery": "ClickHouse Query", "title_confirm": "Confirm", "button_ok": "Yes", "button_cancel": "No", @@ -88,5 +90,21 @@ "user_guide_pql_step3": "Step 3 -Alert Configuration", "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_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 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." } \ No newline at end of file diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index cae309fd45..fa2d9516fe 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -28,6 +28,7 @@ "condition_required": "at least one metric condition is required", "alertname_required": "alert name is required", "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_createrule": "Create Rule", "button_returntorules": "Return to rules", @@ -55,6 +56,7 @@ "button_formula": "Formula", "tab_qb": "Query Builder", "tab_promql": "PromQL", + "tab_chquery": "ClickHouse Query", "title_confirm": "Confirm", "button_ok": "Yes", "button_cancel": "No", @@ -88,5 +90,21 @@ "user_guide_pql_step3": "Step 3 -Alert Configuration", "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_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 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." } \ No newline at end of file diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx new file mode 100644 index 0000000000..e892f3ac22 --- /dev/null +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx @@ -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) => ( + { + onSelect(o.selection); + }} + > + {o.description} + + ))} + + ); + }; + return ( + +

{t('choose_alert_type')}

+ {renderOptions()} +
+ ); +} + +interface SelectAlertTypeProps { + onSelect: (typ: AlertTypes) => void; +} + +export default SelectAlertType; diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts b/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts new file mode 100644 index 0000000000..9cbde02b5f --- /dev/null +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts @@ -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; + } +`; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts new file mode 100644 index 0000000000..0172fcdff2 --- /dev/null +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -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, +}; diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index f527fbbdf1..101b0cae1b 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -1,22 +1,53 @@ -import { Form } from 'antd'; +import { Form, Row } from 'antd'; import FormAlertRules from 'container/FormAlertRules'; -import React from 'react'; -import { AlertDef } from 'types/api/alerts/def'; +import React, { useState } from 'react'; +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.METRICS_BASED_ALERT, + ); 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 ( + + + + ); + } return ( ); } -interface CreateRulesProps { - initialValue: AlertDef; -} - export default CreateRules; diff --git a/frontend/src/container/EditRules/index.tsx b/frontend/src/container/EditRules/index.tsx index cf4a02e717..89c9e66410 100644 --- a/frontend/src/container/EditRules/index.tsx +++ b/frontend/src/container/EditRules/index.tsx @@ -1,6 +1,7 @@ import { Form } from 'antd'; import FormAlertRules from 'container/FormAlertRules'; import React from 'react'; +import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertDef } from 'types/api/alerts/def'; function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element { @@ -8,6 +9,11 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element { return ( { + 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 ( + + ); +} + +interface ChQuerySectionProps { + chQueries: IChQueries; + setChQueries: (q: IChQueries) => void; +} + +export default ChQuerySection; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 6243c1d4d4..e7ee323dce 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -1,11 +1,12 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { StaticLineProps } from 'components/Graph'; +import Spinner from 'components/Spinner'; import GridGraphComponent from 'container/GridGraphComponent'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; import getChartData from 'lib/getChartData'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults'; @@ -22,6 +23,10 @@ export interface ChartPreviewProps { selectedInterval?: Time; headline?: JSX.Element; threshold?: number | undefined; + userQueryKey?: string; +} +interface QueryResponseError { + message?: string; } function ChartPreview({ @@ -32,6 +37,7 @@ function ChartPreview({ selectedInterval = '5min', headline, threshold, + userQueryKey, }: ChartPreviewProps): JSX.Element | null { const { t } = useTranslation('alerts'); const staticLine: StaticLineProps | undefined = @@ -46,9 +52,34 @@ function ChartPreview({ } : 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({ - queryKey: ['chartPreview', queryKey, selectedInterval], + queryKey: [ + 'chartPreview', + userQueryKey || JSON.stringify(query), + selectedInterval, + ], queryFn: () => GetMetricQueryRange({ query: query || { @@ -64,14 +95,8 @@ function ChartPreview({ graphType, selectedTime, }), - enabled: - query != null && - ((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 !== '')), + retry: false, + enabled: canQuery, }); const chartDataSet = queryResponse.isError @@ -89,15 +114,14 @@ function ChartPreview({ return ( {headline} - {(queryResponse?.data?.error || queryResponse?.isError) && ( + {(queryResponse?.isError || queryResponse?.error) && ( {' '} - {queryResponse?.data?.error || - queryResponse?.error || + {(queryResponse?.error as QueryResponseError).message || t('preview_chart_unexpected_error')} )} - + {queryResponse.isLoading && } {chartDataSet && !queryResponse.isError && ( { + return ; + }; + const renderFormulaButton = (): JSX.Element => { return ( }> @@ -258,23 +283,84 @@ function QuerySection({ ); }; - return ( - <> - {t('alert_form_step1')} - -
+ + const handleRunQuery = (): void => { + runQuery(); + }; + + const renderTabs = (typ: AlertTypes): JSX.Element | null => { + switch (typ) { + case AlertTypes.TRACES_BASED_ALERT: + case AlertTypes.LOGS_BASED_ALERT: + return ( + + {queryCategory === EQueryType.CLICKHOUSE && ( + + )} + + } + > + + + + ); + case AlertTypes.METRICS_BASED_ALERT: + default: + return ( + {queryCategory === EQueryType.CLICKHOUSE && ( + + )} + + } > + -
- {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 ( + <> + {t('alert_form_step1')} + +
{renderTabs(alertType)}
+ {renderQuerySection(queryCategory)}
); @@ -289,6 +375,10 @@ interface QuerySectionProps { setFormulaQueries: (b: IFormulaQueries) => void; promQueries: IPromQueries; setPromQueries: (p: IPromQueries) => void; + chQueries: IChQueries; + setChQueries: (q: IChQueries) => void; + alertType: AlertTypes; + runQuery: () => void; } export default QuerySection; diff --git a/frontend/src/container/FormAlertRules/UserGuide/index.tsx b/frontend/src/container/FormAlertRules/UserGuide/index.tsx index 1cf5dac163..d24ac82cb2 100644 --- a/frontend/src/container/FormAlertRules/UserGuide/index.tsx +++ b/frontend/src/container/FormAlertRules/UserGuide/index.tsx @@ -1,7 +1,7 @@ import { Col, Row, Typography } from 'antd'; import TextToolTip from 'components/TextToolTip'; import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { EQueryType } from 'types/common/dashboard'; import { @@ -106,6 +106,63 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element { ); }; + const renderStep1CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step1')} + + + , + ]} + /> + + {t('user_guide_ch_step1b')} + + + ); + }; + const renderStep2CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step2')} + + {t('user_guide_ch_step2a')} + {t('user_guide_ch_step2b')} + + + ); + }; + + const renderStep3CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step3')} + + {t('user_guide_ch_step3a')} + {t('user_guide_ch_step3b')} + + + ); + }; + + const renderGuideForCH = (): JSX.Element => { + return ( + <> + {renderStep1CH()} + {renderStep2CH()} + {renderStep3CH()} + + ); + }; return ( @@ -121,6 +178,7 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element { {queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()} {queryType === EQueryType.PROM && renderGuideForPQL()} + {queryType === EQueryType.CLICKHOUSE && renderGuideForCH()} ); } diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 022e913f8e..a0791a4aa1 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -9,7 +9,9 @@ import history from 'lib/history'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; +import { AlertTypes } from 'types/api/alerts/alertTypes'; import { + IChQueries, IFormulaQueries, IMetricQueries, IPromQueries, @@ -45,6 +47,7 @@ import { } from './utils'; function FormAlertRules({ + alertType, formInstance, initialValue, ruleId, @@ -57,6 +60,10 @@ function FormAlertRules({ 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(); + // alertDef holds the form values to be posted const [alertDef, setAlertDef] = useState(initialValue); @@ -82,9 +89,31 @@ function FormAlertRules({ ...initQuery?.promQueries, }); - // staged query is used to display chart preview + // local state to handle promql queries + const [chQueries, setChQueries] = useState({ + ...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(); - 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(); + + // 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 // other queries based on server data. @@ -101,14 +130,26 @@ function FormAlertRules({ const fq = toFormulaQueries(initQuery?.builderQueries); // 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 chq = initQuery?.chQueries; setQueryCategory(typ); setMetricQueries(mq); setFormulaQueries(fq); setPromQueries(pq); setStagedQuery(sq); + + // also set manually staged query + setManualStagedQuery(sq); + + setChQueries(chq); setAlertDef(initialValue); }, [initialValue]); @@ -121,9 +162,15 @@ function FormAlertRules({ metricQueries, formulaQueries, promQueries, + chQueries, ); 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(() => { history.replace(ROUTES.LIST_ALL_ALERT); @@ -169,6 +216,31 @@ function FormAlertRules({ return retval; }, [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 => { let retval = true; if (queryCategory !== EQueryType.QUERY_BUILDER) return true; @@ -224,12 +296,17 @@ function FormAlertRules({ return false; } + if (!validateChQueryParams()) { + return false; + } + return validateQBParams(); - }, [t, validateQBParams, alertDef, validatePromParams]); + }, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]); const preparePostData = (): AlertDef => { const postableAlert: AlertDef = { ...alertDef, + alertType, source: window?.location.toString(), ruleType: queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule', @@ -238,6 +315,7 @@ function FormAlertRules({ compositeMetricQuery: { builderQueries: prepareBuilderQueries(metricQueries, formulaQueries), promQueries, + chQueries, queryType: queryCategory, }, }, @@ -251,6 +329,8 @@ function FormAlertRules({ metricQueries, formulaQueries, promQueries, + chQueries, + alertType, ]); const saveRule = useCallback(async () => { @@ -380,6 +460,17 @@ function FormAlertRules({ ); }; + const renderChQueryChartPreview = (): JSX.Element => { + return ( + } + name="Chart Preview" + threshold={alertDef.condition?.target} + query={manualStagedQuery} + userQueryKey={runQueryId} + /> + ); + }; return ( <> {Element} @@ -392,6 +483,7 @@ function FormAlertRules({ > {queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()} {queryCategory === EQueryType.PROM && renderPromChartPreview()} + {queryCategory === EQueryType.CLICKHOUSE && renderChQueryChartPreview()} { const qbList: IMetricQuery[] = []; const formulaList: IFormulaQuery[] = []; const promList: IPromQuery[] = []; + const chQueryList: IChQuery[] = []; // convert map[string]IMetricQuery to IMetricQuery[] if (m) { @@ -101,6 +105,13 @@ export const prepareStagedQuery = ( 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 { queryType: t, @@ -109,7 +120,7 @@ export const prepareStagedQuery = ( formulas: formulaList, queryBuilder: qbList, }, - clickHouse: [], + clickHouse: chQueryList, }; }; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx index b2712c86ae..95b0fa8bfd 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx @@ -27,24 +27,34 @@ function ClickHouseQueryContainer({ toggleDisable, toggleDelete, }: IClickHouseQueryHandleChange): void => { - const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; - const currentIndexQuery = allQueries[queryIndex]; + // we must check if queryIndex is number type. because - + // 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) { - currentIndexQuery.rawQuery = rawQuery; - } + if (typeof queryIndex === 'number') { + const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; - if (legend !== undefined) { - currentIndexQuery.legend = legend; - } + const currentIndexQuery = allQueries[queryIndex]; - if (toggleDisable) { - currentIndexQuery.disabled = !currentIndexQuery.disabled; + if (rawQuery !== undefined) { + 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 => { queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({ diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx index 7e9a3df0c2..0ace45fdc8 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx @@ -8,7 +8,7 @@ import { IClickHouseQueryHandleChange } from './types'; interface IClickHouseQueryBuilderProps { queryData: IClickHouseQuery; - queryIndex: number; + queryIndex: number | string; handleQueryChange: (args: IClickHouseQueryHandleChange) => void; } @@ -43,6 +43,9 @@ function ClickHouseQueryBuilder({ scrollbar: { alwaysConsumeMouseWheel: false, }, + minimap: { + enabled: false, + }, }} /> ; + return ; } export default CreateAlertPage; diff --git a/frontend/src/types/api/alerts/alertTypes.ts b/frontend/src/types/api/alerts/alertTypes.ts new file mode 100644 index 0000000000..ed80bb5168 --- /dev/null +++ b/frontend/src/types/api/alerts/alertTypes.ts @@ -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', +} diff --git a/frontend/src/types/api/alerts/compositeQuery.ts b/frontend/src/types/api/alerts/compositeQuery.ts index 42c0c5a753..864e4aa163 100644 --- a/frontend/src/types/api/alerts/compositeQuery.ts +++ b/frontend/src/types/api/alerts/compositeQuery.ts @@ -1,4 +1,5 @@ import { + IClickHouseQuery, IMetricsBuilderFormula, IMetricsBuilderQuery, IPromQLQuery, @@ -9,17 +10,25 @@ import { EAggregateOperator, EQueryType } from 'types/common/dashboard'; export interface ICompositeMetricQuery { builderQueries: IBuilderQueries; promQueries: IPromQueries; + chQueries: IChQueries; queryType: EQueryType; } -export interface IPromQueries { - [key: string]: IPromQuery; +export interface IChQueries { + [key: string]: IChQuery; +} + +export interface IChQuery extends IClickHouseQuery { + query: string; } export interface IPromQuery extends IPromQLQuery { stats?: ''; } +export interface IPromQueries { + [key: string]: IPromQuery; +} export interface IBuilderQueries { [key: string]: IBuilderQuery; } diff --git a/frontend/src/types/api/alerts/create.ts b/frontend/src/types/api/alerts/create.ts index 6f179af79a..41a4953c48 100644 --- a/frontend/src/types/api/alerts/create.ts +++ b/frontend/src/types/api/alerts/create.ts @@ -1,7 +1,5 @@ import { AlertDef } from 'types/api/alerts/def'; -import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def'; - export interface Props { data: AlertDef; } @@ -10,39 +8,3 @@ export interface PayloadProps { status: 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, -}; diff --git a/frontend/src/types/api/alerts/def.ts b/frontend/src/types/api/alerts/def.ts index f417678ee1..65b3e64af4 100644 --- a/frontend/src/types/api/alerts/def.ts +++ b/frontend/src/types/api/alerts/def.ts @@ -11,6 +11,7 @@ export const defaultCompareOp = '1'; export interface AlertDef { id?: number; + alertType?: string; alert?: string; ruleType?: string; condition: RuleCondition;