diff --git a/frontend/public/locales/en-GB/channels.json b/frontend/public/locales/en-GB/channels.json index 1378ba73b8..5e670cc536 100644 --- a/frontend/public/locales/en-GB/channels.json +++ b/frontend/public/locales/en-GB/channels.json @@ -12,9 +12,27 @@ "field_slack_description": "Description", "field_webhook_username": "User Name (optional)", "field_webhook_password": "Password (optional)", + "field_pager_routing_key": "Routing Key", + "field_pager_description": "Description", + "field_pager_severity": "Severity", + "field_pager_details": "Additional Information", + "field_pager_component": "Component", + "field_pager_group": "Group", + "field_pager_class": "Class", + "field_pager_client": "Client", + "field_pager_client_url": "Client URL", "placeholder_slack_description": "Description", + "placeholder_pager_description": "Description", + "help_pager_client": "Shows up as event source in Pagerduty", + "help_pager_client_url": "Shows up as event source link in Pagerduty", + "help_pager_class": "The class/type of the event", + "help_pager_details": "Specify a key-value format (must be a valid json)", + "help_pager_group": "A cluster or grouping of sources", + "help_pager_component": "The part or component of the affected system that is broke", + "help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'", "help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.", "help_webhook_password": "Specify a password or bearer token", + "help_pager_description": "Shows up as description in pagerduty", "channel_creation_done": "Successfully created the channel", "channel_creation_failed": "An unexpected error occurred while creating this channel", "channel_edit_done": "Channels Edited Successfully", diff --git a/frontend/public/locales/en/channels.json b/frontend/public/locales/en/channels.json index 1378ba73b8..5e670cc536 100644 --- a/frontend/public/locales/en/channels.json +++ b/frontend/public/locales/en/channels.json @@ -12,9 +12,27 @@ "field_slack_description": "Description", "field_webhook_username": "User Name (optional)", "field_webhook_password": "Password (optional)", + "field_pager_routing_key": "Routing Key", + "field_pager_description": "Description", + "field_pager_severity": "Severity", + "field_pager_details": "Additional Information", + "field_pager_component": "Component", + "field_pager_group": "Group", + "field_pager_class": "Class", + "field_pager_client": "Client", + "field_pager_client_url": "Client URL", "placeholder_slack_description": "Description", + "placeholder_pager_description": "Description", + "help_pager_client": "Shows up as event source in Pagerduty", + "help_pager_client_url": "Shows up as event source link in Pagerduty", + "help_pager_class": "The class/type of the event", + "help_pager_details": "Specify a key-value format (must be a valid json)", + "help_pager_group": "A cluster or grouping of sources", + "help_pager_component": "The part or component of the affected system that is broke", + "help_pager_severity": "Severity of the incident, must be one of: must be one of the following: 'critical', 'warning', 'error' or 'info'", "help_webhook_username": "Leave empty for bearer auth or when authentication is not necessary.", "help_webhook_password": "Specify a password or bearer token", + "help_pager_description": "Shows up as description in pagerduty", "channel_creation_done": "Successfully created the channel", "channel_creation_failed": "An unexpected error occurred while creating this channel", "channel_edit_done": "Channels Edited Successfully", diff --git a/frontend/src/api/channels/createPager.ts b/frontend/src/api/channels/createPager.ts new file mode 100644 index 0000000000..2747768cf1 --- /dev/null +++ b/frontend/src/api/channels/createPager.ts @@ -0,0 +1,42 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createPager'; + +const create = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/channels', { + name: props.name, + pagerduty_configs: [ + { + send_resolved: true, + routing_key: props.routing_key, + client: props.client, + client_url: props.client_url, + description: props.description, + severity: props.severity, + class: props.class, + component: props.component, + group: props.group, + details: { + ...props.detailsArray, + }, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/channels/editPager.ts b/frontend/src/api/channels/editPager.ts new file mode 100644 index 0000000000..a31d73dcdb --- /dev/null +++ b/frontend/src/api/channels/editPager.ts @@ -0,0 +1,42 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/editPager'; + +const editPager = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.put(`/channels/${props.id}`, { + name: props.name, + pagerduty_configs: [ + { + send_resolved: true, + routing_key: props.routing_key, + client: props.client, + client_url: props.client_url, + description: props.description, + severity: props.severity, + class: props.class, + component: props.component, + group: props.group, + details: { + ...props.detailsArray, + }, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default editPager; diff --git a/frontend/src/api/channels/testPager.ts b/frontend/src/api/channels/testPager.ts new file mode 100644 index 0000000000..717404649a --- /dev/null +++ b/frontend/src/api/channels/testPager.ts @@ -0,0 +1,42 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/channels/createPager'; + +const testPager = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.post('/testChannel', { + name: props.name, + pagerduty_configs: [ + { + send_resolved: true, + routing_key: props.routing_key, + client: props.client, + client_url: props.client_url, + description: props.description, + severity: props.severity, + class: props.class, + component: props.component, + group: props.group, + details: { + ...props.detailsArray, + }, + }, + ], + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default testPager; diff --git a/frontend/src/container/CreateAlertChannels/config.ts b/frontend/src/container/CreateAlertChannels/config.ts index f104a84076..6c89764637 100644 --- a/frontend/src/container/CreateAlertChannels/config.ts +++ b/frontend/src/container/CreateAlertChannels/config.ts @@ -1,6 +1,7 @@ export interface Channel { send_resolved?: boolean; name: string; + filter?: Partial>; } export interface SlackChannel extends Channel { @@ -17,6 +18,66 @@ export interface WebhookChannel extends Channel { password?: string; } -export type ChannelType = 'slack' | 'email' | 'webhook'; +// PagerChannel configures alert manager to send +// events to pagerduty +export interface PagerChannel extends Channel { + // ref: https://prometheus.io/docs/alerting/latest/configuration/#pagerduty_config + routing_key?: string; + // displays source of the event in pager duty + client?: string; + client_url?: string; + // A description of the incident + description?: string; + // Severity of the incident + severity?: string; + // The part or component of the affected system that is broken + component?: string; + // A cluster or grouping of sources + group?: string; + // The class/type of the event. + class?: string; + + details?: string; + detailsArray?: Record; +} +export const ValidatePagerChannel = (p: PagerChannel): string => { + if (!p) { + return 'Received unexpected input for this channel, please contact your administrator '; + } + + if (!p.name || p.name === '') { + return 'Name is mandatory for creating a channel'; + } + + if (!p.routing_key || p.routing_key === '') { + return 'Routing Key is mandatory for creating pagerduty channel'; + } + + // validate details json + try { + JSON.parse(p.details || '{}'); + } catch (e) { + return 'failed to parse additional information, please enter a valid json'; + } + + return ''; +}; + +export type ChannelType = 'slack' | 'email' | 'webhook' | 'pagerduty'; export const SlackType: ChannelType = 'slack'; export const WebhookType: ChannelType = 'webhook'; +export const PagerType: ChannelType = 'pagerduty'; + +// LabelFilterStatement will be used for preparing filter conditions / matchers +export interface LabelFilterStatement { + // ref: https://prometheus.io/docs/alerting/latest/configuration/#matcher + + // label name + name: string; + + // comparators supported by promql are =, !=, =~, or !~. = + comparator: string; + + // filter value + value: string; +} diff --git a/frontend/src/container/CreateAlertChannels/defaults.ts b/frontend/src/container/CreateAlertChannels/defaults.ts new file mode 100644 index 0000000000..ac15056703 --- /dev/null +++ b/frontend/src/container/CreateAlertChannels/defaults.ts @@ -0,0 +1,22 @@ +import { PagerChannel } from './config'; + +export const PagerInitialConfig: Partial = { + description: `{{ range .Alerts -}} + *Alert:* {{ if .Annotations.title }} {{ .Annotations.title }} {{ else }} {{ .Annotations.summary }} {{end}} {{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} + + *Description:* {{ .Annotations.description }} + + *Details:* + {{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }} + {{ end }} + {{ end }}`, + severity: '{{ (index .Alerts 0).Labels.severity }}', + client: 'SigNoz Alert Manager', + client_url: 'https://enter-signoz-host-n-port-here/alerts', + details: JSON.stringify({ + firing: `{{ template "pagerduty.default.instances" .Alerts.Firing }}`, + resolved: `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`, + num_firing: '{{ .Alerts.Firing | len }}', + num_resolved: '{{ .Alerts.Resolved | len }}', + }), +}; diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index dc3eee86a6..fb5ea8a3f6 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -1,6 +1,8 @@ import { Form, notification } from 'antd'; +import createPagerApi from 'api/channels/createPager'; import createSlackApi from 'api/channels/createSlack'; import createWebhookApi from 'api/channels/createWebhook'; +import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; @@ -11,11 +13,15 @@ import { useTranslation } from 'react-i18next'; import { ChannelType, + PagerChannel, + PagerType, SlackChannel, SlackType, + ValidatePagerChannel, WebhookChannel, WebhookType, } from './config'; +import { PagerInitialConfig } from './defaults'; function CreateAlertChannels({ preType = 'slack', @@ -26,7 +32,7 @@ function CreateAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ text: `{{ range .Alerts -}} *Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} @@ -55,9 +61,22 @@ function CreateAlertChannels({ const [notifications, NotificationElement] = notification.useNotification(); const [type, setType] = useState(preType); - const onTypeChangeHandler = useCallback((value: string) => { - setType(value as ChannelType); - }, []); + const onTypeChangeHandler = useCallback( + (value: string) => { + const currentType = type; + setType(value as ChannelType); + + if (value === PagerType && currentType !== value) { + // reset config to pager defaults + setSelectedConfig({ + name: selectedConfig?.name, + send_resolved: selectedConfig.send_resolved, + ...PagerInitialConfig, + }); + } + }, + [type, selectedConfig], + ); const prepareSlackRequest = useCallback(() => { return { @@ -71,8 +90,9 @@ function CreateAlertChannels({ }, [selectedConfig]); const onSlackHandler = useCallback(async () => { + setSavingState(true); + try { - setSavingState(true); const response = await createSlackApi(prepareSlackRequest()); if (response.statusCode === 200) { @@ -89,14 +109,13 @@ function CreateAlertChannels({ description: response.error || t('channel_creation_failed'), }); } - setSavingState(false); } catch (error) { notifications.error({ message: 'Error', description: t('channel_creation_failed'), }); - setSavingState(false); } + setSavingState(false); }, [prepareSlackRequest, t, notifications]); const prepareWebhookRequest = useCallback(() => { @@ -161,6 +180,65 @@ function CreateAlertChannels({ } setSavingState(false); }, [prepareWebhookRequest, t, notifications]); + + const preparePagerRequest = useCallback(() => { + const validationError = ValidatePagerChannel(selectedConfig as PagerChannel); + if (validationError !== '') { + notifications.error({ + message: 'Error', + description: validationError, + }); + return null; + } + + return { + name: selectedConfig?.name || '', + send_resolved: true, + routing_key: selectedConfig?.routing_key || '', + client: selectedConfig?.client || '', + client_url: selectedConfig?.client_url || '', + description: selectedConfig?.description || '', + severity: selectedConfig?.severity || '', + component: selectedConfig?.component || '', + group: selectedConfig?.group || '', + class: selectedConfig?.class || '', + details: selectedConfig.details || '', + detailsArray: JSON.parse(selectedConfig.details || '{}'), + }; + }, [selectedConfig, notifications]); + + const onPagerHandler = useCallback(async () => { + setSavingState(true); + const request = preparePagerRequest(); + + if (request) { + try { + const response = await createPagerApi(request); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_creation_done'), + }); + setTimeout(() => { + history.replace(ROUTES.SETTINGS); + }, 2000); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_creation_failed'), + }); + } + } catch (e) { + notifications.error({ + message: 'Error', + description: t('channel_creation_failed'), + }); + } + } + setSavingState(false); + }, [t, notifications, preparePagerRequest]); + const onSaveHandler = useCallback( async (value: ChannelType) => { switch (value) { @@ -170,6 +248,9 @@ function CreateAlertChannels({ case WebhookType: onWebhookHandler(); break; + case PagerType: + onPagerHandler(); + break; default: notifications.error({ message: 'Error', @@ -177,7 +258,7 @@ function CreateAlertChannels({ }); } }, - [onSlackHandler, t, onWebhookHandler, notifications], + [onSlackHandler, t, onPagerHandler, onWebhookHandler, notifications], ); const performChannelTest = useCallback( @@ -195,6 +276,10 @@ function CreateAlertChannels({ request = prepareSlackRequest(); response = await testSlackApi(request); break; + case PagerType: + request = preparePagerRequest(); + if (request) response = await testPagerApi(request); + break; default: notifications.error({ message: 'Error', @@ -204,7 +289,7 @@ function CreateAlertChannels({ return; } - if (response.statusCode === 200) { + if (response && response.statusCode === 200) { notifications.success({ message: 'Success', description: t('channel_test_done'), @@ -223,7 +308,13 @@ function CreateAlertChannels({ } setTestingState(false); }, - [prepareWebhookRequest, t, prepareSlackRequest, notifications], + [ + prepareWebhookRequest, + t, + preparePagerRequest, + prepareSlackRequest, + notifications, + ], ); const onTestHandler = useCallback( @@ -249,6 +340,7 @@ function CreateAlertChannels({ initialValue: { type, ...selectedConfig, + ...PagerInitialConfig, }, }} /> diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index b8db5a0e9b..ef8f6a0a2e 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -1,13 +1,18 @@ import { Form, notification } from 'antd'; +import editPagerApi from 'api/channels/editPager'; import editSlackApi from 'api/channels/editSlack'; import editWebhookApi from 'api/channels/editWebhook'; +import testPagerApi from 'api/channels/testPager'; import testSlackApi from 'api/channels/testSlack'; import testWebhookApi from 'api/channels/testWebhook'; import ROUTES from 'constants/routes'; import { ChannelType, + PagerChannel, + PagerType, SlackChannel, SlackType, + ValidatePagerChannel, WebhookChannel, WebhookType, } from 'container/CreateAlertChannels/config'; @@ -25,7 +30,7 @@ function EditAlertChannels({ const [formInstance] = Form.useForm(); const [selectedConfig, setSelectedConfig] = useState< - Partial + Partial >({ ...initialValue, }); @@ -138,15 +143,66 @@ function EditAlertChannels({ setSavingState(false); }, [prepareWebhookRequest, t, notifications, selectedConfig]); + const preparePagerRequest = useCallback(() => { + return { + name: selectedConfig.name || '', + routing_key: selectedConfig.routing_key, + client: selectedConfig.client, + client_url: selectedConfig.client_url, + description: selectedConfig.description, + severity: selectedConfig.severity, + component: selectedConfig.component, + class: selectedConfig.class, + group: selectedConfig.group, + details: selectedConfig.details, + detailsArray: JSON.parse(selectedConfig.details || '{}'), + id, + }; + }, [id, selectedConfig]); + + const onPagerEditHandler = useCallback(async () => { + setSavingState(true); + const validationError = ValidatePagerChannel(selectedConfig as PagerChannel); + + if (validationError !== '') { + notifications.error({ + message: 'Error', + description: validationError, + }); + setSavingState(false); + return; + } + const response = await editPagerApi(preparePagerRequest()); + + if (response.statusCode === 200) { + notifications.success({ + message: 'Success', + description: t('channel_edit_done'), + }); + + setTimeout(() => { + history.replace(ROUTES.SETTINGS); + }, 2000); + } else { + notifications.error({ + message: 'Error', + description: response.error || t('channel_edit_failed'), + }); + } + setSavingState(false); + }, [preparePagerRequest, notifications, selectedConfig, t]); + const onSaveHandler = useCallback( (value: ChannelType) => { if (value === SlackType) { onSlackEditHandler(); } else if (value === WebhookType) { onWebhookEditHandler(); + } else if (value === PagerType) { + onPagerEditHandler(); } }, - [onSlackEditHandler, onWebhookEditHandler], + [onSlackEditHandler, onWebhookEditHandler, onPagerEditHandler], ); const performChannelTest = useCallback( @@ -164,6 +220,10 @@ function EditAlertChannels({ request = prepareSlackRequest(); response = await testSlackApi(request); break; + case PagerType: + request = preparePagerRequest(); + if (request) response = await testPagerApi(request); + break; default: notifications.error({ message: 'Error', @@ -173,7 +233,7 @@ function EditAlertChannels({ return; } - if (response.statusCode === 200) { + if (response && response.statusCode === 200) { notifications.success({ message: 'Success', description: t('channel_test_done'), @@ -192,7 +252,13 @@ function EditAlertChannels({ } setTestingState(false); }, - [prepareWebhookRequest, t, prepareSlackRequest, notifications], + [ + t, + prepareWebhookRequest, + preparePagerRequest, + prepareSlackRequest, + notifications, + ], ); const onTestHandler = useCallback( @@ -216,7 +282,7 @@ function EditAlertChannels({ NotificationElement, title: t('page_title_edit'), initialValue, - nameDisable: true, + editing: true, }} /> ); diff --git a/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx b/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx new file mode 100644 index 0000000000..2d71c520ce --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/LabelFilter.tsx @@ -0,0 +1,64 @@ +import { Input, Select } from 'antd'; +import FormItem from 'antd/lib/form/FormItem'; +import { LabelFilterStatement } from 'container/CreateAlertChannels/config'; +import React from 'react'; + +const { Option } = Select; + +// LabelFilterForm supports filters or matchers on alert notifications +// presently un-used but will be introduced to the channel creation at some +// point +function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element { + return ( + + + + + { + setFilter((value) => { + const first: LabelFilterStatement = value[0] as LabelFilterStatement; + first.value = event.target.value; + return [first]; + }); + }} + /> + + + ); +} + +export interface LabelFilterProps { + setFilter: React.Dispatch< + React.SetStateAction>> + >; +} + +export default LabelFilterForm; diff --git a/frontend/src/container/FormAlertChannels/Settings/Pager.tsx b/frontend/src/container/FormAlertChannels/Settings/Pager.tsx new file mode 100644 index 0000000000..0e36613096 --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/Pager.tsx @@ -0,0 +1,155 @@ +import { Input } from 'antd'; +import FormItem from 'antd/lib/form/FormItem'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { PagerChannel } from '../../CreateAlertChannels/config'; + +const { TextArea } = Input; + +function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element { + const { t } = useTranslation('channels'); + return ( + <> + + { + setSelectedConfig((value) => ({ + ...value, + routing_key: event.target.value, + })); + }} + /> + + + +