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",
"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</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",
"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</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 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>(
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 (
<Row wrap={false}>
<SelectAlertType onSelect={onSelectType} />
</Row>
);
}
return (
<FormAlertRules
alertType={alertType}
formInstance={formInstance}
initialValue={initialValue}
initialValue={initValues}
ruleId={0}
/>
);
}
interface CreateRulesProps {
initialValue: AlertDef;
}
export default CreateRules;

View File

@ -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 (
<FormAlertRules
alertType={
initialValue.alertType
? (initialValue.alertType as AlertTypes)
: AlertTypes.METRICS_BASED_ALERT
}
formInstance={formInstance}
initialValue={initialValue}
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 { 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 (
<ChartContainer>
{headline}
{(queryResponse?.data?.error || queryResponse?.isError) && (
{(queryResponse?.isError || queryResponse?.error) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse?.data?.error ||
queryResponse?.error ||
{(queryResponse?.error as QueryResponseError).message ||
t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{queryResponse.isLoading && <Spinner size="large" tip="Loading..." />}
{chartDataSet && !queryResponse.isError && (
<GridGraphComponent
title={name}
@ -118,6 +142,7 @@ ChartPreview.defaultProps = {
selectedInterval: '5min',
headline: undefined,
threshold: undefined,
userQueryKey: '',
};
export default ChartPreview;

View File

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

View File

@ -1,5 +1,5 @@
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 MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
import {
@ -8,13 +8,16 @@ import {
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
IChQueries,
IFormulaQueries,
IMetricQueries,
IPromQueries,
} from 'types/api/alerts/compositeQuery';
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection';
import { FormContainer, QueryButton, StepHeading } from './styles';
import { toIMetricsBuilderQuery } from './utils';
@ -29,6 +32,10 @@ function QuerySection({
setFormulaQueries,
promQueries,
setPromQueries,
chQueries,
setChQueries,
alertType,
runQuery,
}: QuerySectionProps): JSX.Element {
// init namespace for translations
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));
};
@ -196,6 +217,10 @@ function QuerySection({
);
};
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
@ -258,23 +283,84 @@ function QuerySection({
</div>
);
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>
const handleRunQuery = (): void => {
runQuery();
};
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
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER.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()} />
<TabPane tab={t('tab_chquery')} key={EQueryType.CLICKHOUSE.toString()} />
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
</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>
</>
);
@ -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;

View File

@ -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 (
<>
<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 (
<StyledMainContainer>
<Row>
@ -121,6 +178,7 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
</Row>
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
{queryType === EQueryType.PROM && renderGuideForPQL()}
{queryType === EQueryType.CLICKHOUSE && renderGuideForCH()}
</StyledMainContainer>
);
}

View File

@ -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<string>();
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(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<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 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
// 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 (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
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()}
<QuerySection
queryCategory={queryCategory}
setQueryCategory={onQueryCategoryChange}
@ -401,6 +493,10 @@ function FormAlertRules({
setFormulaQueries={setFormulaQueries}
promQueries={promQueries}
setPromQueries={setPromQueries}
chQueries={chQueries}
setChQueries={setChQueries}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={onRunQuery}
/>
<RuleOptions
@ -446,7 +542,12 @@ function FormAlertRules({
);
}
FormAlertRules.defaultProps = {
alertType: AlertTypes.METRICS_BASED_ALERT,
};
interface FormAlertRuleProps {
alertType?: AlertTypes;
formInstance: FormInstance;
initialValue: AlertDef;
ruleId: number;

View File

@ -1,6 +1,8 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import {
IBuilderQueries,
IChQueries,
IChQuery,
IFormulaQueries,
IFormulaQuery,
IMetricQueries,
@ -76,10 +78,12 @@ export const prepareStagedQuery = (
m: IMetricQueries,
f: IFormulaQueries,
p: IPromQueries,
c: IChQueries,
): IStagedQuery => {
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,
};
};

View File

@ -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({

View File

@ -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,
},
}}
/>
<Input

View File

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

View File

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

View File

@ -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,
};

View File

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