feat: Amol/webhook (#868)

webhook receiver enabled for alerts

Co-authored-by: Palash gupta <palash@signoz.io>
This commit is contained in:
Amol Umbark 2022-03-28 21:01:57 +05:30 committed by GitHub
parent e7ba5f9f33
commit 0efb901863
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 604 additions and 129 deletions

View File

@ -15,17 +15,20 @@ services:
retries: 3 retries: 3
alertmanager: alertmanager:
image: signoz/alertmanager:0.5.0 image: signoz/alertmanager:0.6.0
volumes: volumes:
- ./alertmanager.yml:/prometheus/alertmanager.yml # we no longer need the config file as query services delivers
# the required config now
# - ./alertmanager.yml:/prometheus/alertmanager.yml
- ./data/alertmanager:/data - ./data/alertmanager:/data
depends_on:
- query-service
command: command:
- '--config.file=/prometheus/alertmanager.yml' - '--queryService.url=http://query-service:8080'
- '--storage.path=/data' - '--storage.path=/data'
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:0.7.3 image: signoz/query-service:0.7.3
container_name: query-service container_name: query-service

View File

@ -0,0 +1,51 @@
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/createWebhook';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
let httpConfig = {};
if (props.username !== '' && props.password !== '') {
httpConfig = {
basic_auth: {
username: props.username,
password: props.password,
},
};
} else if (props.username === '' && props.password !== '') {
httpConfig = {
authorization: {
type: 'bearer',
credentials: props.password,
},
};
}
const response = await axios.post('/channels', {
name: props.name,
webhook_configs: [
{
send_resolved: true,
url: props.api_url,
http_config: httpConfig,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@ -0,0 +1,50 @@
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/editWebhook';
const editWebhook = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
let httpConfig = {};
if (props.username !== '' && props.password !== '') {
httpConfig = {
basic_auth: {
username: props.username,
password: props.password,
},
};
} else if (props.username === '' && props.password !== '') {
httpConfig = {
authorization: {
type: 'bearer',
credentials: props.password,
},
};
}
const response = await axios.put(`/channels/${props.id}`, {
name: props.name,
webhook_configs: [
{
send_resolved: true,
url: props.api_url,
http_config: httpConfig,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default editWebhook;

View File

@ -79,6 +79,7 @@ function Graph({
return 'rgba(231,233,237,0.8)'; return 'rgba(231,233,237,0.8)';
}, [currentTheme]); }, [currentTheme]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const buildChart = useCallback(() => { const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) { if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy(); lineChartRef.current.destroy();

View File

@ -30,7 +30,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
} catch (error) { } catch (error) {
notifications.error({ notifications.error({
message: 'Error', message: 'Error',
description: error instanceof Error ? error.toString() : 'Something went wrong', description:
error instanceof Error ? error.toString() : 'Something went wrong',
}); });
setLoading(false); setLoading(false);
} }

View File

@ -1,10 +1,22 @@
export interface SlackChannel { export interface Channel {
send_resolved: boolean; send_resolved?: boolean;
api_url: string;
channel: string;
title: string;
text: string;
name: string; name: string;
} }
export type ChannelType = 'slack' | 'email'; export interface SlackChannel extends Channel {
api_url?: string;
channel?: string;
title?: string;
text?: string;
}
export interface WebhookChannel extends Channel {
api_url?: string;
// basic auth
username?: string;
password?: string;
}
export type ChannelType = 'slack' | 'email' | 'webhook';
export const SlackType: ChannelType = 'slack';
export const WebhookType: ChannelType = 'webhook';

View File

@ -1,17 +1,26 @@
import { Form, notification } from 'antd'; import { Form, notification } from 'antd';
import createSlackApi from 'api/channels/createSlack'; import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels'; import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history'; import history from 'lib/history';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { ChannelType, SlackChannel } from './config'; import {
ChannelType,
SlackChannel,
SlackType,
WebhookChannel,
WebhookType,
} from './config';
function CreateAlertChannels({ function CreateAlertChannels({
preType = 'slack', preType = 'slack',
}: CreateAlertChannelsProps): JSX.Element { }: CreateAlertChannelsProps): JSX.Element {
const [formInstance] = Form.useForm(); const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({ const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
>({
text: ` {{ range .Alerts -}} text: ` {{ range .Alerts -}}
*Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }} *Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
@ -73,17 +82,93 @@ function CreateAlertChannels({
} }
setSavingState(false); setSavingState(false);
} catch (error) { } catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
});
setSavingState(false); setSavingState(false);
} }
}, [notifications, selectedConfig]); }, [notifications, selectedConfig]);
const onWebhookHandler = useCallback(async () => {
// initial api request without auth params
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
};
setSavingState(true);
try {
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
if (selectedConfig?.username !== '') {
// if username is not null then password must be passed
if (selectedConfig?.password !== '') {
request = {
...request,
username: selectedConfig.username,
password: selectedConfig.password,
};
} else {
notifications.error({
message: 'Error',
description: 'A Password must be provided with user name',
});
}
} else if (selectedConfig?.password !== '') {
// only password entered, set bearer token
request = {
...request,
username: '',
password: selectedConfig.password,
};
}
}
const response = await createWebhookApi(request);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Successfully created the channel',
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
}, 2000);
} else {
notifications.error({
message: 'Error',
description: response.error || 'Error while creating the channel',
});
}
} catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
});
}
setSavingState(false);
}, [notifications, selectedConfig]);
const onSaveHandler = useCallback( const onSaveHandler = useCallback(
async (value: ChannelType) => { async (value: ChannelType) => {
if (value === 'slack') { switch (value) {
onSlackHandler(); case SlackType:
onSlackHandler();
break;
case WebhookType:
onWebhookHandler();
break;
default:
notifications.error({
message: 'Error',
description: 'channel type selected is invalid',
});
} }
}, },
[onSlackHandler], [onSlackHandler, onWebhookHandler, notifications],
); );
return ( return (
@ -108,11 +193,7 @@ function CreateAlertChannels({
} }
interface CreateAlertChannelsProps { interface CreateAlertChannelsProps {
preType?: ChannelType; preType: ChannelType;
} }
CreateAlertChannels.defaultProps = {
preType: undefined,
};
export default CreateAlertChannels; export default CreateAlertChannels;

View File

@ -1,9 +1,13 @@
import { Form, notification } from 'antd'; import { Form, notification } from 'antd';
import editSlackApi from 'api/channels/editSlack'; import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { import {
ChannelType, ChannelType,
SlackChannel, SlackChannel,
SlackType,
WebhookChannel,
WebhookType,
} from 'container/CreateAlertChannels/config'; } from 'container/CreateAlertChannels/config';
import FormAlertChannels from 'container/FormAlertChannels'; import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history'; import history from 'lib/history';
@ -14,14 +18,18 @@ function EditAlertChannels({
initialValue, initialValue,
}: EditAlertChannelsProps): JSX.Element { }: EditAlertChannelsProps): JSX.Element {
const [formInstance] = Form.useForm(); const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({ const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
>({
...initialValue, ...initialValue,
}); });
const [savingState, setSavingState] = useState<boolean>(false); const [savingState, setSavingState] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification(); const [notifications, NotificationElement] = notification.useNotification();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [type, setType] = useState<ChannelType>('slack'); const [type, setType] = useState<ChannelType>(
initialValue?.type ? (initialValue.type as ChannelType) : SlackType,
);
const onTypeChangeHandler = useCallback((value: string) => { const onTypeChangeHandler = useCallback((value: string) => {
setType(value as ChannelType); setType(value as ChannelType);
@ -57,13 +65,62 @@ function EditAlertChannels({
setSavingState(false); setSavingState(false);
}, [selectedConfig, notifications, id]); }, [selectedConfig, notifications, id]);
const onWebhookEditHandler = useCallback(async () => {
setSavingState(true);
const { name, username, password } = selectedConfig;
const showError = (msg: string): void => {
notifications.error({
message: 'Error',
description: msg,
});
};
if (selectedConfig?.api_url === '') {
showError('Webhook URL is mandatory');
setSavingState(false);
return;
}
if (username && (!password || password === '')) {
showError('Please enter a password');
setSavingState(false);
return;
}
const response = await editWebhookApi({
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
username,
password,
id,
});
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Channels Edited Successfully',
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
}, 2000);
} else {
showError(response.error || 'error while updating the Channels');
}
setSavingState(false);
}, [selectedConfig, notifications, id]);
const onSaveHandler = useCallback( const onSaveHandler = useCallback(
(value: ChannelType) => { (value: ChannelType) => {
if (value === 'slack') { if (value === SlackType) {
onSlackEditHandler(); onSlackEditHandler();
} else if (value === WebhookType) {
onWebhookEditHandler();
} }
}, },
[onSlackEditHandler], [onSlackEditHandler, onWebhookEditHandler],
); );
const onTestHandler = useCallback(() => { const onTestHandler = useCallback(() => {

View File

@ -0,0 +1,59 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { WebhookChannel } from '../../CreateAlertChannels/config';
function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
return (
<>
<FormItem name="api_url" label="Webhook URL">
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
api_url: event.target.value,
}));
}}
/>
</FormItem>
<FormItem
name="username"
label="User Name (optional)"
help="Leave empty for bearer auth or when authentication is not necessary."
>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
username: event.target.value,
}));
}}
/>
</FormItem>
<FormItem
name="password"
label="Password (optional)"
help="Specify a password or bearer token"
>
<Input
type="password"
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
password: event.target.value,
}));
}}
/>
</FormItem>
</>
);
}
interface WebhookProps {
setSelectedConfig: React.Dispatch<
React.SetStateAction<Partial<WebhookChannel>>
>;
}
export default WebhookSettings;

View File

@ -5,11 +5,14 @@ import ROUTES from 'constants/routes';
import { import {
ChannelType, ChannelType,
SlackChannel, SlackChannel,
SlackType,
WebhookType,
} from 'container/CreateAlertChannels/config'; } from 'container/CreateAlertChannels/config';
import history from 'lib/history'; import history from 'lib/history';
import React from 'react'; import React from 'react';
import SlackSettings from './Settings/Slack'; import SlackSettings from './Settings/Slack';
import WebhookSettings from './Settings/Webhook';
import { Button } from './styles'; import { Button } from './styles';
const { Option } = Select; const { Option } = Select;
@ -28,6 +31,16 @@ function FormAlertChannels({
initialValue, initialValue,
nameDisable = false, nameDisable = false,
}: FormAlertChannelsProps): JSX.Element { }: FormAlertChannelsProps): JSX.Element {
const renderSettings = (): React.ReactElement | null => {
switch (type) {
case SlackType:
return <SlackSettings setSelectedConfig={setSelectedConfig} />;
case WebhookType:
return <WebhookSettings setSelectedConfig={setSelectedConfig} />;
default:
return null;
}
};
return ( return (
<> <>
{NotificationElement} {NotificationElement}
@ -52,14 +65,13 @@ function FormAlertChannels({
<Option value="slack" key="slack"> <Option value="slack" key="slack">
Slack Slack
</Option> </Option>
<Option value="webhook" key="webhook">
Webhook
</Option>
</Select> </Select>
</FormItem> </FormItem>
<FormItem> <FormItem>{renderSettings()}</FormItem>
{type === 'slack' && (
<SlackSettings setSelectedConfig={setSelectedConfig} />
)}
</FormItem>
<FormItem> <FormItem>
<Button <Button

View File

@ -75,9 +75,10 @@ function Timeline({
{intervals && {intervals &&
intervals.map((interval, index) => ( intervals.map((interval, index) => (
<TimelineInterval <TimelineInterval
transform={`translate(${TimelineHSpacing + transform={`translate(${
TimelineHSpacing +
(interval.percentage * (width - 2 * TimelineHSpacing)) / 100 (interval.percentage * (width - 2 * TimelineHSpacing)) / 100
},0)`} },0)`}
key={`${interval.label + interval.percentage + index}`} key={`${interval.label + interval.percentage + index}`}
> >
<text y={13} fill={isDarkMode ? 'white' : 'black'}> <text y={13} fill={isDarkMode ? 'white' : 'black'}>

View File

@ -20,7 +20,7 @@ function SettingsPage(): JSX.Element {
}, },
{ {
Component: (): JSX.Element => { Component: (): JSX.Element => {
return <CreateAlertChannels />; return <CreateAlertChannels preType="slack" />;
}, },
name: 'Alert Channels', name: 'Alert Channels',
route: ROUTES.ALL_CHANNELS, route: ROUTES.ALL_CHANNELS,

View File

@ -1,7 +1,12 @@
import { Typography } from 'antd'; import { Typography } from 'antd';
import get from 'api/channels/get'; import get from 'api/channels/get';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { SlackChannel } from 'container/CreateAlertChannels/config'; import {
SlackChannel,
SlackType,
WebhookChannel,
WebhookType,
} from 'container/CreateAlertChannels/config';
import EditAlertChannels from 'container/EditAlertChannels'; import EditAlertChannels from 'container/EditAlertChannels';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import React from 'react'; import React from 'react';
@ -29,15 +34,36 @@ function ChannelsEdit(): JSX.Element {
const { data } = payload; const { data } = payload;
const value = JSON.parse(data); const value = JSON.parse(data);
let type = '';
let channel: SlackChannel & WebhookChannel = { name: '' };
const channel: SlackChannel = value.slack_configs[0]; if (value && 'slack_configs' in value) {
const slackConfig = value.slack_configs[0];
channel = slackConfig;
type = SlackType;
} else if (value && 'webhook_configs' in value) {
const webhookConfig = value.webhook_configs[0];
channel = webhookConfig;
channel.api_url = webhookConfig.url;
if ('http_config' in webhookConfig) {
const httpConfig = webhookConfig.http_config;
if ('basic_auth' in httpConfig) {
channel.username = webhookConfig.http_config?.basic_auth?.username;
channel.password = webhookConfig.http_config?.basic_auth?.password;
} else if ('authorization' in httpConfig) {
channel.password = webhookConfig.http_config?.authorization?.credentials;
}
}
type = WebhookType;
}
console.log('channel:', channel);
return ( return (
<EditAlertChannels <EditAlertChannels
{...{ {...{
initialValue: { initialValue: {
...channel, ...channel,
type: 'slack', type,
name: value.name, name: value.name,
}, },
}} }}

View File

@ -0,0 +1,8 @@
import { WebhookChannel } from 'container/CreateAlertChannels/config';
export type Props = WebhookChannel;
export interface PayloadProps {
data: string;
status: string;
}

View File

@ -0,0 +1,10 @@
import { WebhookChannel } from 'container/CreateAlertChannels/config';
export interface Props extends WebhookChannel {
id: string;
}
export interface PayloadProps {
data: string;
status: string;
}

View File

@ -45,6 +45,7 @@ import (
"go.signoz.io/query-service/constants" "go.signoz.io/query-service/constants"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
am "go.signoz.io/query-service/integrations/alertManager"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -74,6 +75,7 @@ type ClickHouseReader struct {
remoteStorage *remote.Storage remoteStorage *remote.Storage
ruleManager *rules.Manager ruleManager *rules.Manager
promConfig *config.Config promConfig *config.Config
alertManager am.Manager
} }
// NewTraceReader returns a TraceReader for the database // NewTraceReader returns a TraceReader for the database
@ -88,9 +90,12 @@ func NewReader(localDB *sqlx.DB) *ClickHouseReader {
os.Exit(1) os.Exit(1)
} }
alertManager := am.New("")
return &ClickHouseReader{ return &ClickHouseReader{
db: db, db: db,
localDB: localDB, localDB: localDB,
alertManager: alertManager,
operationsTable: options.primary.OperationsTable, operationsTable: options.primary.OperationsTable,
indexTable: options.primary.IndexTable, indexTable: options.primary.IndexTable,
errorTable: options.primary.ErrorTable, errorTable: options.primary.ErrorTable,
@ -651,7 +656,7 @@ func (r *ClickHouseReader) LoadRule(rule model.RuleResponseItem) *model.ApiError
func (r *ClickHouseReader) LoadChannel(channel *model.ChannelItem) *model.ApiError { func (r *ClickHouseReader) LoadChannel(channel *model.ChannelItem) *model.ApiError {
receiver := &model.Receiver{} receiver := &am.Receiver{}
if err := json.Unmarshal([]byte(channel.Data), receiver); err != nil { // Parse []byte to go struct pointer if err := json.Unmarshal([]byte(channel.Data), receiver); err != nil { // Parse []byte to go struct pointer
return &model.ApiError{Typ: model.ErrorBadData, Err: err} return &model.ApiError{Typ: model.ErrorBadData, Err: err}
} }
@ -723,32 +728,10 @@ func (r *ClickHouseReader) DeleteChannel(id string) *model.ApiError {
} }
} }
values := map[string]string{"name": channelToDelete.Name} apiError := r.alertManager.DeleteRoute(channelToDelete.Name)
jsonValue, _ := json.Marshal(values) if apiError != nil {
req, err := http.NewRequest(http.MethodDelete, constants.GetAlertManagerApiPrefix()+"v1/receivers", bytes.NewBuffer(jsonValue))
if err != nil {
zap.S().Errorf("Error in creating new delete request to alertmanager/v1/receivers\n", err)
tx.Rollback() tx.Rollback()
return &model.ApiError{Typ: model.ErrorInternal, Err: err} return apiError
}
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
zap.S().Errorf("Error in delete API call to alertmanager/v1/receivers\n", err)
tx.Rollback()
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if response.StatusCode > 299 {
err := fmt.Errorf("Error in getting 2xx response in API call to delete alertmanager/v1/receivers\n", response.Status)
zap.S().Error(err)
tx.Rollback()
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
} }
err = tx.Commit() err = tx.Commit()
@ -780,7 +763,7 @@ func (r *ClickHouseReader) GetChannels() (*[]model.ChannelItem, *model.ApiError)
} }
func getChannelType(receiver *model.Receiver) string { func getChannelType(receiver *am.Receiver) string {
if receiver.EmailConfigs != nil { if receiver.EmailConfigs != nil {
return "email" return "email"
@ -813,7 +796,7 @@ func getChannelType(receiver *model.Receiver) string {
return "" return ""
} }
func (r *ClickHouseReader) EditChannel(receiver *model.Receiver, id string) (*model.Receiver, *model.ApiError) { func (r *ClickHouseReader) EditChannel(receiver *am.Receiver, id string) (*am.Receiver, *model.ApiError) {
idInt, _ := strconv.Atoi(id) idInt, _ := strconv.Atoi(id)
@ -851,30 +834,10 @@ func (r *ClickHouseReader) EditChannel(receiver *model.Receiver, id string) (*mo
} }
} }
req, err := http.NewRequest(http.MethodPut, constants.GetAlertManagerApiPrefix()+"v1/receivers", bytes.NewBuffer(receiverString)) apiError := r.alertManager.EditRoute(receiver)
if apiError != nil {
if err != nil {
zap.S().Errorf("Error in creating new update request to alertmanager/v1/receivers\n", err)
tx.Rollback() tx.Rollback()
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} return nil, apiError
}
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
zap.S().Errorf("Error in update API call to alertmanager/v1/receivers\n", err)
tx.Rollback()
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if response.StatusCode > 299 {
err := fmt.Errorf("Error in getting 2xx response in API call to alertmanager/v1/receivers\n", response.Status)
zap.S().Error(err)
tx.Rollback()
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
} }
err = tx.Commit() err = tx.Commit()
@ -887,7 +850,8 @@ func (r *ClickHouseReader) EditChannel(receiver *model.Receiver, id string) (*mo
} }
func (r *ClickHouseReader) CreateChannel(receiver *model.Receiver) (*model.Receiver, *model.ApiError) {
func (r *ClickHouseReader) CreateChannel(receiver *am.Receiver) (*am.Receiver, *model.ApiError) {
tx, err := r.localDB.Begin() tx, err := r.localDB.Begin()
if err != nil { if err != nil {
@ -896,6 +860,8 @@ func (r *ClickHouseReader) CreateChannel(receiver *model.Receiver) (*model.Recei
channel_type := getChannelType(receiver) channel_type := getChannelType(receiver)
receiverString, _ := json.Marshal(receiver) receiverString, _ := json.Marshal(receiver)
// todo: check if the channel name already exists, raise an error if so
{ {
stmt, err := tx.Prepare(`INSERT INTO notification_channels (created_at, updated_at, name, type, data) VALUES($1,$2,$3,$4,$5);`) stmt, err := tx.Prepare(`INSERT INTO notification_channels (created_at, updated_at, name, type, data) VALUES($1,$2,$3,$4,$5);`)
@ -913,20 +879,12 @@ func (r *ClickHouseReader) CreateChannel(receiver *model.Receiver) (*model.Recei
} }
} }
response, err := http.Post(constants.GetAlertManagerApiPrefix()+"v1/receivers", "application/json", bytes.NewBuffer(receiverString)) apiError := r.alertManager.AddRoute(receiver)
if apiError != nil {
if err != nil {
zap.S().Errorf("Error in getting response of API call to alertmanager/v1/receivers\n", err)
tx.Rollback() tx.Rollback()
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err} return nil, apiError
} }
if response.StatusCode > 299 {
err := fmt.Errorf("Error in getting 2xx response in API call to alertmanager/v1/receivers\n", response.Status)
zap.S().Error(err)
tx.Rollback()
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
err = tx.Commit() err = tx.Commit()
if err != nil { if err != nil {
zap.S().Errorf("Error in commiting transaction for INSERT to notification_channels\n", err) zap.S().Errorf("Error in commiting transaction for INSERT to notification_channels\n", err)

View File

@ -11,6 +11,7 @@ import (
"go.signoz.io/query-service/druidQuery" "go.signoz.io/query-service/druidQuery"
"go.signoz.io/query-service/godruid" "go.signoz.io/query-service/godruid"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
am "go.signoz.io/query-service/integrations/alertManager"
) )
type DruidReader struct { type DruidReader struct {
@ -65,12 +66,12 @@ func (druid *DruidReader) GetChannel(id string) (*model.ChannelItem, *model.ApiE
func (druid *DruidReader) GetChannels() (*[]model.ChannelItem, *model.ApiError) { func (druid *DruidReader) GetChannels() (*[]model.ChannelItem, *model.ApiError) {
return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")} return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")}
} }
func (druid *DruidReader) CreateChannel(receiver *model.Receiver) (*model.Receiver, *model.ApiError) { func (druid *DruidReader) CreateChannel(receiver *am.Receiver) (*am.Receiver, *model.ApiError) {
return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")} return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")}
} }
func (druid *DruidReader) EditChannel(receiver *model.Receiver, id string) (*model.Receiver, *model.ApiError) { func (druid *DruidReader) EditChannel(receiver *am.Receiver, id string) (*am.Receiver, *model.ApiError) {
return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")} return nil, &model.ApiError{model.ErrorNotImplemented, fmt.Errorf("Druid does not support notification channel for alerts")}

View File

@ -16,6 +16,7 @@ import (
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
"go.signoz.io/query-service/telemetry" "go.signoz.io/query-service/telemetry"
"go.signoz.io/query-service/version" "go.signoz.io/query-service/version"
am "go.signoz.io/query-service/integrations/alertManager"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -467,7 +468,7 @@ func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) {
return return
} }
receiver := &model.Receiver{} receiver := &am.Receiver{}
if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer
zap.S().Errorf("Error in parsing req body of editChannel API\n", err) zap.S().Errorf("Error in parsing req body of editChannel API\n", err)
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
@ -495,7 +496,7 @@ func (aH *APIHandler) createChannel(w http.ResponseWriter, r *http.Request) {
return return
} }
receiver := &model.Receiver{} receiver := &am.Receiver{}
if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer if err := json.Unmarshal(body, receiver); err != nil { // Parse []byte to go struct pointer
zap.S().Errorf("Error in parsing req body of createChannel API\n", err) zap.S().Errorf("Error in parsing req body of createChannel API\n", err)
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)

View File

@ -6,14 +6,15 @@ import (
"github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/util/stats" "github.com/prometheus/prometheus/util/stats"
"go.signoz.io/query-service/model" "go.signoz.io/query-service/model"
am "go.signoz.io/query-service/integrations/alertManager"
) )
type Reader interface { type Reader interface {
GetChannel(id string) (*model.ChannelItem, *model.ApiError) GetChannel(id string) (*model.ChannelItem, *model.ApiError)
GetChannels() (*[]model.ChannelItem, *model.ApiError) GetChannels() (*[]model.ChannelItem, *model.ApiError)
DeleteChannel(id string) *model.ApiError DeleteChannel(id string) *model.ApiError
CreateChannel(receiver *model.Receiver) (*model.Receiver, *model.ApiError) CreateChannel(receiver *am.Receiver) (*am.Receiver, *model.ApiError)
EditChannel(receiver *model.Receiver, id string) (*model.Receiver, *model.ApiError) EditChannel(receiver *am.Receiver, id string) (*am.Receiver, *model.ApiError)
GetRule(id string) (*model.RuleResponseItem, *model.ApiError) GetRule(id string) (*model.RuleResponseItem, *model.ApiError)
ListRulesFromProm() (*model.AlertDiscovery, *model.ApiError) ListRulesFromProm() (*model.AlertDiscovery, *model.ApiError)

View File

@ -30,7 +30,10 @@ func GetAlertManagerApiPrefix() string {
return "http://alertmanager:9093/api/" return "http://alertmanager:9093/api/"
} }
const RELATIONAL_DATASOURCE_PATH = "/var/lib/signoz/signoz.db" // Alert manager channel subpath
var AmChannelApiPath = GetOrDefaultEnv("ALERTMANAGER_API_CHANNEL_PATH", "v1/routes")
var RELATIONAL_DATASOURCE_PATH = GetOrDefaultEnv("SIGNOZ_LOCAL_DB_PATH", "/var/lib/signoz/signoz.db")
const ( const (
ServiceName = "serviceName" ServiceName = "serviceName"
@ -43,3 +46,12 @@ const (
OperationDB = "name" OperationDB = "name"
OperationRequest = "operation" OperationRequest = "operation"
) )
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)
if len(v) == 0 {
return fallback
}
return v
}

View File

@ -0,0 +1,129 @@
package alertManager
// Wrapper to connect and process alert manager functions
import (
"fmt"
"encoding/json"
"bytes"
"net/http"
"go.uber.org/zap"
"go.signoz.io/query-service/constants"
"go.signoz.io/query-service/model"
)
const contentType = "application/json"
type Manager interface {
AddRoute(receiver *Receiver) *model.ApiError
EditRoute(receiver *Receiver) *model.ApiError
DeleteRoute(name string) *model.ApiError
}
func New(url string) Manager{
if url == ""{
url = constants.GetAlertManagerApiPrefix()
}
return &manager {
url: url,
}
}
type manager struct {
url string
}
func prepareAmChannelApiURL() string {
basePath := constants.GetAlertManagerApiPrefix()
AmChannelApiPath := constants.AmChannelApiPath
if len(AmChannelApiPath) > 0 && rune(AmChannelApiPath[0]) == rune('/') {
AmChannelApiPath = AmChannelApiPath[1:]
}
return fmt.Sprintf("%s%s", basePath, AmChannelApiPath)
}
func (m *manager) AddRoute(receiver *Receiver) (*model.ApiError) {
receiverString, _ := json.Marshal(receiver)
amURL := prepareAmChannelApiURL()
response, err := http.Post(amURL, contentType, bytes.NewBuffer(receiverString))
if err != nil {
zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(POST %s)\n", amURL), err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if response.StatusCode > 299 {
err := fmt.Errorf(fmt.Sprintf("Error in getting 2xx response in API call to alertmanager(POST %s)\n", amURL), response.Status)
zap.S().Error(err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
return nil
}
func (m *manager) EditRoute(receiver *Receiver) *model.ApiError {
receiverString, _ := json.Marshal(receiver)
amURL := prepareAmChannelApiURL()
req, err := http.NewRequest(http.MethodPut, amURL, bytes.NewBuffer(receiverString))
if err != nil {
zap.S().Errorf(fmt.Sprintf("Error creating new update request for API call to alertmanager(PUT %s)\n", amURL), err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
req.Header.Add("Content-Type", contentType)
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(PUT %s)\n", amURL), err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if response.StatusCode > 299 {
err := fmt.Errorf(fmt.Sprintf("Error in getting 2xx response in PUT API call to alertmanager(PUT %s)\n", amURL), response.Status)
zap.S().Error(err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
return nil
}
func (m *manager) DeleteRoute(name string) *model.ApiError {
values := map[string]string{"name": name}
requestData, _ := json.Marshal(values)
amURL := prepareAmChannelApiURL()
req, err := http.NewRequest(http.MethodDelete, amURL, bytes.NewBuffer(requestData))
if err != nil {
zap.S().Errorf("Error in creating new delete request to alertmanager/v1/receivers\n", err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
req.Header.Add("Content-Type", contentType)
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
zap.S().Errorf(fmt.Sprintf("Error in getting response of API call to alertmanager(DELETE %s)\n", amURL), err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
if response.StatusCode > 299 {
err := fmt.Errorf(fmt.Sprintf("Error in getting 2xx response in PUT API call to alertmanager(DELETE %s)\n", amURL), response.Status)
zap.S().Error(err)
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
}
return nil
}

View File

@ -0,0 +1,22 @@
package alertManager
// Receiver configuration provides configuration on how to contact a receiver.
type Receiver struct {
// A unique identifier for this receiver.
Name string `yaml:"name" json:"name"`
EmailConfigs interface{} `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
PagerdutyConfigs interface{} `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs interface{} `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs interface{} `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
OpsGenieConfigs interface{} `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs interface{} `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
PushoverConfigs interface{} `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs interface{} `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
SNSConfigs interface{} `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"`
}
type ReceiverResponse struct {
Status string `json:"status"`
Data Receiver `json:"data"`
}

View File

@ -51,27 +51,6 @@ type ChannelItem struct {
Data string `json:"data" db:"data"` Data string `json:"data" db:"data"`
} }
// Receiver configuration provides configuration on how to contact a receiver.
type Receiver struct {
// A unique identifier for this receiver.
Name string `yaml:"name" json:"name"`
EmailConfigs interface{} `yaml:"email_configs,omitempty" json:"email_configs,omitempty"`
PagerdutyConfigs interface{} `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"`
SlackConfigs interface{} `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"`
WebhookConfigs interface{} `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"`
OpsGenieConfigs interface{} `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"`
WechatConfigs interface{} `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"`
PushoverConfigs interface{} `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"`
VictorOpsConfigs interface{} `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"`
SNSConfigs interface{} `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"`
}
type ReceiverResponse struct {
Status string `json:"status"`
Data Receiver `json:"data"`
}
// AlertDiscovery has info for all active alerts. // AlertDiscovery has info for all active alerts.
type AlertDiscovery struct { type AlertDiscovery struct {
Alerts []*AlertingRuleResponse `json:"rules"` Alerts []*AlertingRuleResponse `json:"rules"`