mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 17:49:02 +08:00
feat: Amol/webhook (#868)
webhook receiver enabled for alerts Co-authored-by: Palash gupta <palash@signoz.io>
This commit is contained in:
parent
e7ba5f9f33
commit
0efb901863
@ -15,17 +15,20 @@ services:
|
||||
retries: 3
|
||||
|
||||
alertmanager:
|
||||
image: signoz/alertmanager:0.5.0
|
||||
image: signoz/alertmanager:0.6.0
|
||||
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
|
||||
depends_on:
|
||||
- query-service
|
||||
command:
|
||||
- '--config.file=/prometheus/alertmanager.yml'
|
||||
- '--queryService.url=http://query-service:8080'
|
||||
- '--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`
|
||||
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.7.3
|
||||
container_name: query-service
|
||||
|
51
frontend/src/api/channels/createWebhook.ts
Normal file
51
frontend/src/api/channels/createWebhook.ts
Normal 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;
|
50
frontend/src/api/channels/editWebhook.ts
Normal file
50
frontend/src/api/channels/editWebhook.ts
Normal 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;
|
@ -79,6 +79,7 @@ function Graph({
|
||||
return 'rgba(231,233,237,0.8)';
|
||||
}, [currentTheme]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const buildChart = useCallback(() => {
|
||||
if (lineChartRef.current !== undefined) {
|
||||
lineChartRef.current.destroy();
|
||||
|
@ -30,7 +30,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: error instanceof Error ? error.toString() : 'Something went wrong',
|
||||
description:
|
||||
error instanceof Error ? error.toString() : 'Something went wrong',
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
|
@ -1,10 +1,22 @@
|
||||
export interface SlackChannel {
|
||||
send_resolved: boolean;
|
||||
api_url: string;
|
||||
channel: string;
|
||||
title: string;
|
||||
text: string;
|
||||
export interface Channel {
|
||||
send_resolved?: boolean;
|
||||
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';
|
||||
|
@ -1,17 +1,26 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import createSlackApi from 'api/channels/createSlack';
|
||||
import createWebhookApi from 'api/channels/createWebhook';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { ChannelType, SlackChannel } from './config';
|
||||
import {
|
||||
ChannelType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
WebhookChannel,
|
||||
WebhookType,
|
||||
} from './config';
|
||||
|
||||
function CreateAlertChannels({
|
||||
preType = 'slack',
|
||||
}: CreateAlertChannelsProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
|
||||
const [selectedConfig, setSelectedConfig] = useState<
|
||||
Partial<SlackChannel & WebhookChannel>
|
||||
>({
|
||||
text: ` {{ range .Alerts -}}
|
||||
*Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
|
||||
|
||||
@ -73,17 +82,93 @@ function CreateAlertChannels({
|
||||
}
|
||||
setSavingState(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
'An unexpected error occurred while creating this channel, please try again',
|
||||
});
|
||||
setSavingState(false);
|
||||
}
|
||||
}, [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(
|
||||
async (value: ChannelType) => {
|
||||
if (value === 'slack') {
|
||||
switch (value) {
|
||||
case SlackType:
|
||||
onSlackHandler();
|
||||
break;
|
||||
case WebhookType:
|
||||
onWebhookHandler();
|
||||
break;
|
||||
default:
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: 'channel type selected is invalid',
|
||||
});
|
||||
}
|
||||
},
|
||||
[onSlackHandler],
|
||||
[onSlackHandler, onWebhookHandler, notifications],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -108,11 +193,7 @@ function CreateAlertChannels({
|
||||
}
|
||||
|
||||
interface CreateAlertChannelsProps {
|
||||
preType?: ChannelType;
|
||||
preType: ChannelType;
|
||||
}
|
||||
|
||||
CreateAlertChannels.defaultProps = {
|
||||
preType: undefined,
|
||||
};
|
||||
|
||||
export default CreateAlertChannels;
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { Form, notification } from 'antd';
|
||||
import editSlackApi from 'api/channels/editSlack';
|
||||
import editWebhookApi from 'api/channels/editWebhook';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
WebhookChannel,
|
||||
WebhookType,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import FormAlertChannels from 'container/FormAlertChannels';
|
||||
import history from 'lib/history';
|
||||
@ -14,14 +18,18 @@ function EditAlertChannels({
|
||||
initialValue,
|
||||
}: EditAlertChannelsProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
|
||||
const [selectedConfig, setSelectedConfig] = useState<
|
||||
Partial<SlackChannel & WebhookChannel>
|
||||
>({
|
||||
...initialValue,
|
||||
});
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [notifications, NotificationElement] = notification.useNotification();
|
||||
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) => {
|
||||
setType(value as ChannelType);
|
||||
@ -57,13 +65,62 @@ function EditAlertChannels({
|
||||
setSavingState(false);
|
||||
}, [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(
|
||||
(value: ChannelType) => {
|
||||
if (value === 'slack') {
|
||||
if (value === SlackType) {
|
||||
onSlackEditHandler();
|
||||
} else if (value === WebhookType) {
|
||||
onWebhookEditHandler();
|
||||
}
|
||||
},
|
||||
[onSlackEditHandler],
|
||||
[onSlackEditHandler, onWebhookEditHandler],
|
||||
);
|
||||
|
||||
const onTestHandler = useCallback(() => {
|
||||
|
@ -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;
|
@ -5,11 +5,14 @@ import ROUTES from 'constants/routes';
|
||||
import {
|
||||
ChannelType,
|
||||
SlackChannel,
|
||||
SlackType,
|
||||
WebhookType,
|
||||
} from 'container/CreateAlertChannels/config';
|
||||
import history from 'lib/history';
|
||||
import React from 'react';
|
||||
|
||||
import SlackSettings from './Settings/Slack';
|
||||
import WebhookSettings from './Settings/Webhook';
|
||||
import { Button } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
@ -28,6 +31,16 @@ function FormAlertChannels({
|
||||
initialValue,
|
||||
nameDisable = false,
|
||||
}: 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 (
|
||||
<>
|
||||
{NotificationElement}
|
||||
@ -52,14 +65,13 @@ function FormAlertChannels({
|
||||
<Option value="slack" key="slack">
|
||||
Slack
|
||||
</Option>
|
||||
<Option value="webhook" key="webhook">
|
||||
Webhook
|
||||
</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
{type === 'slack' && (
|
||||
<SlackSettings setSelectedConfig={setSelectedConfig} />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>{renderSettings()}</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Button
|
||||
|
@ -75,7 +75,8 @@ function Timeline({
|
||||
{intervals &&
|
||||
intervals.map((interval, index) => (
|
||||
<TimelineInterval
|
||||
transform={`translate(${TimelineHSpacing +
|
||||
transform={`translate(${
|
||||
TimelineHSpacing +
|
||||
(interval.percentage * (width - 2 * TimelineHSpacing)) / 100
|
||||
},0)`}
|
||||
key={`${interval.label + interval.percentage + index}`}
|
||||
|
@ -20,7 +20,7 @@ function SettingsPage(): JSX.Element {
|
||||
},
|
||||
{
|
||||
Component: (): JSX.Element => {
|
||||
return <CreateAlertChannels />;
|
||||
return <CreateAlertChannels preType="slack" />;
|
||||
},
|
||||
name: 'Alert Channels',
|
||||
route: ROUTES.ALL_CHANNELS,
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { Typography } from 'antd';
|
||||
import get from 'api/channels/get';
|
||||
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 useFetch from 'hooks/useFetch';
|
||||
import React from 'react';
|
||||
@ -29,15 +34,36 @@ function ChannelsEdit(): JSX.Element {
|
||||
const { data } = payload;
|
||||
|
||||
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 (
|
||||
<EditAlertChannels
|
||||
{...{
|
||||
initialValue: {
|
||||
...channel,
|
||||
type: 'slack',
|
||||
type,
|
||||
name: value.name,
|
||||
},
|
||||
}}
|
||||
|
8
frontend/src/types/api/channels/createWebhook.ts
Normal file
8
frontend/src/types/api/channels/createWebhook.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { WebhookChannel } from 'container/CreateAlertChannels/config';
|
||||
|
||||
export type Props = WebhookChannel;
|
||||
|
||||
export interface PayloadProps {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
10
frontend/src/types/api/channels/editWebhook.ts
Normal file
10
frontend/src/types/api/channels/editWebhook.ts
Normal 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;
|
||||
}
|
@ -45,6 +45,7 @@ import (
|
||||
|
||||
"go.signoz.io/query-service/constants"
|
||||
"go.signoz.io/query-service/model"
|
||||
am "go.signoz.io/query-service/integrations/alertManager"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -74,6 +75,7 @@ type ClickHouseReader struct {
|
||||
remoteStorage *remote.Storage
|
||||
ruleManager *rules.Manager
|
||||
promConfig *config.Config
|
||||
alertManager am.Manager
|
||||
}
|
||||
|
||||
// NewTraceReader returns a TraceReader for the database
|
||||
@ -88,9 +90,12 @@ func NewReader(localDB *sqlx.DB) *ClickHouseReader {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
alertManager := am.New("")
|
||||
|
||||
return &ClickHouseReader{
|
||||
db: db,
|
||||
localDB: localDB,
|
||||
alertManager: alertManager,
|
||||
operationsTable: options.primary.OperationsTable,
|
||||
indexTable: options.primary.IndexTable,
|
||||
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 {
|
||||
|
||||
receiver := &model.Receiver{}
|
||||
receiver := &am.Receiver{}
|
||||
if err := json.Unmarshal([]byte(channel.Data), receiver); err != nil { // Parse []byte to go struct pointer
|
||||
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}
|
||||
jsonValue, _ := json.Marshal(values)
|
||||
|
||||
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)
|
||||
apiError := r.alertManager.DeleteRoute(channelToDelete.Name)
|
||||
if apiError != nil {
|
||||
tx.Rollback()
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
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}
|
||||
return apiError
|
||||
}
|
||||
|
||||
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 {
|
||||
return "email"
|
||||
@ -813,7 +796,7 @@ func getChannelType(receiver *model.Receiver) string {
|
||||
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)
|
||||
|
||||
@ -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))
|
||||
|
||||
if err != nil {
|
||||
zap.S().Errorf("Error in creating new update request to alertmanager/v1/receivers\n", err)
|
||||
apiError := r.alertManager.EditRoute(receiver)
|
||||
if apiError != nil {
|
||||
tx.Rollback()
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
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}
|
||||
return nil, apiError
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
@ -897,6 +861,8 @@ func (r *ClickHouseReader) CreateChannel(receiver *model.Receiver) (*model.Recei
|
||||
channel_type := getChannelType(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);`)
|
||||
if err != nil {
|
||||
@ -913,18 +879,10 @@ func (r *ClickHouseReader) CreateChannel(receiver *model.Receiver) (*model.Recei
|
||||
}
|
||||
}
|
||||
|
||||
response, err := http.Post(constants.GetAlertManagerApiPrefix()+"v1/receivers", "application/json", bytes.NewBuffer(receiverString))
|
||||
|
||||
if err != nil {
|
||||
zap.S().Errorf("Error in getting response of API call to alertmanager/v1/receivers\n", err)
|
||||
apiError := r.alertManager.AddRoute(receiver)
|
||||
if apiError != nil {
|
||||
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}
|
||||
return nil, apiError
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"go.signoz.io/query-service/druidQuery"
|
||||
"go.signoz.io/query-service/godruid"
|
||||
"go.signoz.io/query-service/model"
|
||||
am "go.signoz.io/query-service/integrations/alertManager"
|
||||
)
|
||||
|
||||
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) {
|
||||
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")}
|
||||
|
||||
}
|
||||
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")}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"go.signoz.io/query-service/model"
|
||||
"go.signoz.io/query-service/telemetry"
|
||||
"go.signoz.io/query-service/version"
|
||||
am "go.signoz.io/query-service/integrations/alertManager"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -467,7 +468,7 @@ func (aH *APIHandler) editChannel(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
receiver := &model.Receiver{}
|
||||
receiver := &am.Receiver{}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
receiver := &model.Receiver{}
|
||||
receiver := &am.Receiver{}
|
||||
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)
|
||||
aH.respondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
|
@ -6,14 +6,15 @@ import (
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/util/stats"
|
||||
"go.signoz.io/query-service/model"
|
||||
am "go.signoz.io/query-service/integrations/alertManager"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
GetChannel(id string) (*model.ChannelItem, *model.ApiError)
|
||||
GetChannels() (*[]model.ChannelItem, *model.ApiError)
|
||||
DeleteChannel(id string) *model.ApiError
|
||||
CreateChannel(receiver *model.Receiver) (*model.Receiver, *model.ApiError)
|
||||
EditChannel(receiver *model.Receiver, id string) (*model.Receiver, *model.ApiError)
|
||||
CreateChannel(receiver *am.Receiver) (*am.Receiver, *model.ApiError)
|
||||
EditChannel(receiver *am.Receiver, id string) (*am.Receiver, *model.ApiError)
|
||||
|
||||
GetRule(id string) (*model.RuleResponseItem, *model.ApiError)
|
||||
ListRulesFromProm() (*model.AlertDiscovery, *model.ApiError)
|
||||
|
@ -30,7 +30,10 @@ func GetAlertManagerApiPrefix() string {
|
||||
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 (
|
||||
ServiceName = "serviceName"
|
||||
@ -43,3 +46,12 @@ const (
|
||||
OperationDB = "name"
|
||||
OperationRequest = "operation"
|
||||
)
|
||||
|
||||
|
||||
func GetOrDefaultEnv(key string, fallback string) string {
|
||||
v := os.Getenv(key)
|
||||
if len(v) == 0 {
|
||||
return fallback
|
||||
}
|
||||
return v
|
||||
}
|
129
pkg/query-service/integrations/alertManager/manager.go
Normal file
129
pkg/query-service/integrations/alertManager/manager.go
Normal 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
|
||||
}
|
||||
|
||||
|
||||
|
22
pkg/query-service/integrations/alertManager/model.go
Normal file
22
pkg/query-service/integrations/alertManager/model.go
Normal 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"`
|
||||
}
|
@ -51,27 +51,6 @@ type ChannelItem struct {
|
||||
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.
|
||||
type AlertDiscovery struct {
|
||||
Alerts []*AlertingRuleResponse `json:"rules"`
|
||||
|
Loading…
x
Reference in New Issue
Block a user