diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1b436a32bb..35b2182350 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,11 +2,12 @@ name: build-pipeline on: pull_request: branches: + - develop - main - v* paths: - - 'pkg/**' - - 'frontend/**' + - "pkg/**" + - "frontend/**" jobs: get_filters: @@ -17,17 +18,17 @@ jobs: query-service: ${{ steps.filter.outputs.query-service }} flattener: ${{ steps.filter.outputs.flattener }} steps: - # For pull requests it's not necessary to checkout the code - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - frontend: - - 'frontend/**' - query-service: - - 'pkg/query-service/**' - flattener: - - 'pkg/processors/flattener/**' + # For pull requests it's not necessary to checkout the code + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + frontend: + - 'frontend/**' + query-service: + - 'pkg/query-service/**' + flattener: + - 'pkg/processors/flattener/**' build-frontend: runs-on: ubuntu-latest @@ -39,12 +40,11 @@ jobs: uses: actions/checkout@v2 - name: Install dependencies run: cd frontend && yarn install - - name: Run Prettier - run: cd frontend && npm run prettify - continue-on-error: true - name: Run ESLint run: cd frontend && npm run lint - continue-on-error: true + - name: TSC + run: yarn tsc + working-directory: ./frontend - name: Build frontend docker image shell: bash run: | diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 425dbde5a3..c5d8e6b630 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -17,19 +17,20 @@ services: retries: 3 alertmanager: - image: signoz/alertmanager:0.5.0 + image: signoz/alertmanager:0.6.0 volumes: - - ./alertmanager.yml:/prometheus/alertmanager.yml - ./data/alertmanager:/data command: - - '--config.file=/prometheus/alertmanager.yml' - - '--storage.path=/data' + - --queryService.url=http://query-service:8080 + - --storage.path=/data + depends_on: + - query-service deploy: restart_policy: condition: on-failure query-service: - image: signoz/query-service:0.7.3 + image: signoz/query-service:0.7.4 command: ["-config=/root/config/prometheus.yml"] ports: - "8080:8080" @@ -48,10 +49,10 @@ services: restart_policy: condition: on-failure depends_on: - - clickhouse + - clickhouse frontend: - image: signoz/frontend:0.7.3 + image: signoz/frontend:0.7.4 depends_on: - query-service ports: diff --git a/deploy/docker/clickhouse-setup/docker-compose.arm.yaml b/deploy/docker/clickhouse-setup/docker-compose.arm.yaml index 2d5a638fd9..0540fa2868 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.arm.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.arm.yaml @@ -15,16 +15,17 @@ services: retries: 3 alertmanager: - image: signoz/alertmanager:0.5.0 + image: signoz/alertmanager:0.6.0 volumes: - - ./alertmanager.yml:/prometheus/alertmanager.yml - ./data/alertmanager:/data + depends_on: + - query-service command: - - '--config.file=/prometheus/alertmanager.yml' - - '--storage.path=/data' + - --queryService.url=http://query-service:8080 + - --storage.path=/data query-service: - image: signoz/query-service:0.7.3 + image: signoz/query-service:0.7.4 container_name: query-service command: ["-config=/root/config/prometheus.yml"] volumes: @@ -44,7 +45,7 @@ services: condition: service_healthy frontend: - image: signoz/frontend:0.7.3 + image: signoz/frontend:0.7.4 container_name: frontend depends_on: - query-service @@ -66,7 +67,7 @@ services: # - "14268:14268" # Jaeger receiver # - "55678:55678" # OpenCensus receiver # - "55679:55679" # zpages extension - # - "55680:55680" # OTLP gRPC legacy port + # - "55680:55680" # OTLP gRPC legacy receiver # - "55681:55681" # OTLP HTTP legacy receiver mem_limit: 2000m restart: on-failure @@ -93,7 +94,7 @@ services: max-file: "3" command: ["all"] environment: - - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces + - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces load-hotrod: image: "grubykarol/locust:1.2.3-python3.9-alpine3.12" diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index ac69884ff6..b9c96c7bdb 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -15,19 +15,19 @@ services: retries: 3 alertmanager: - image: signoz/alertmanager:0.5.0 + image: signoz/alertmanager:0.6.0 volumes: - - ./alertmanager.yml:/prometheus/alertmanager.yml - ./data/alertmanager:/data + depends_on: + - query-service command: - - '--config.file=/prometheus/alertmanager.yml' - - '--storage.path=/data' + - --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 + image: signoz/query-service:0.7.4 container_name: query-service command: ["-config=/root/config/prometheus.yml"] volumes: @@ -40,14 +40,13 @@ services: - GODEBUG=netdns=go - TELEMETRY_ENABLED=true - DEPLOYMENT_TYPE=docker-standalone-amd - restart: on-failure depends_on: clickhouse: condition: service_healthy frontend: - image: signoz/frontend:0.7.3 + image: signoz/frontend:0.7.4 container_name: frontend depends_on: - query-service @@ -69,7 +68,7 @@ services: # - "14268:14268" # Jaeger receiver # - "55678:55678" # OpenCensus receiver # - "55679:55679" # zpages extension - # - "55680:55680" # OTLP gRPC legacy port + # - "55680:55680" # OTLP gRPC legacy receiver # - "55681:55681" # OTLP HTTP legacy receiver mem_limit: 2000m restart: on-failure diff --git a/frontend/package.json b/frontend/package.json index 75453c759e..28666512fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "author": "", "license": "ISC", "dependencies": { + "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.6.2", "@grafana/data": "^8.4.3", "@monaco-editor/react": "^4.3.1", diff --git a/frontend/src/api/alerts/getTriggered.ts b/frontend/src/api/alerts/getTriggered.ts new file mode 100644 index 0000000000..160b9a3b93 --- /dev/null +++ b/frontend/src/api/alerts/getTriggered.ts @@ -0,0 +1,29 @@ +import { AxiosAlertManagerInstance } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import convertObjectIntoParams from 'lib/query/convertObjectIntoParams'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/alerts/getTriggered'; + +const getTriggered = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const queryParams = convertObjectIntoParams(props); + + const response = await AxiosAlertManagerInstance.get( + `/alerts?${queryParams}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getTriggered; diff --git a/frontend/src/api/channels/createWebhook.ts b/frontend/src/api/channels/createWebhook.ts new file mode 100644 index 0000000000..9c3c52c943 --- /dev/null +++ b/frontend/src/api/channels/createWebhook.ts @@ -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 | 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; diff --git a/frontend/src/api/channels/editWebhook.ts b/frontend/src/api/channels/editWebhook.ts new file mode 100644 index 0000000000..a574633e4e --- /dev/null +++ b/frontend/src/api/channels/editWebhook.ts @@ -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 | 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; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index bdf23e6468..a8f668235d 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -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(); diff --git a/frontend/src/container/AllAlertChannels/Delete.tsx b/frontend/src/container/AllAlertChannels/Delete.tsx index 4501c916c8..85116fd922 100644 --- a/frontend/src/container/AllAlertChannels/Delete.tsx +++ b/frontend/src/container/AllAlertChannels/Delete.tsx @@ -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); } diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 84f7d237e2..5230ca5bae 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -27,6 +27,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element { } }, [isLoggedIn, isSignUpPage]); + useEffect(() => { + if (isLoggedIn && pathname === ROUTES.SIGN_UP) { + history.push(ROUTES.APPLICATION); + } + }, [isLoggedIn, pathname]); + return ( {!isSignUpPage && } diff --git a/frontend/src/container/CreateAlertChannels/config.ts b/frontend/src/container/CreateAlertChannels/config.ts index 364d367806..f104a84076 100644 --- a/frontend/src/container/CreateAlertChannels/config.ts +++ b/frontend/src/container/CreateAlertChannels/config.ts @@ -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'; diff --git a/frontend/src/container/CreateAlertChannels/index.tsx b/frontend/src/container/CreateAlertChannels/index.tsx index 8a3b3fe606..f999627154 100644 --- a/frontend/src/container/CreateAlertChannels/index.tsx +++ b/frontend/src/container/CreateAlertChannels/index.tsx @@ -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>({ + const [selectedConfig, setSelectedConfig] = useState< + Partial + >({ 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') { - onSlackHandler(); + 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; diff --git a/frontend/src/container/EditAlertChannels/index.tsx b/frontend/src/container/EditAlertChannels/index.tsx index 2d8ac3576d..e4aab19d31 100644 --- a/frontend/src/container/EditAlertChannels/index.tsx +++ b/frontend/src/container/EditAlertChannels/index.tsx @@ -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>({ + const [selectedConfig, setSelectedConfig] = useState< + Partial + >({ ...initialValue, }); const [savingState, setSavingState] = useState(false); const [notifications, NotificationElement] = notification.useNotification(); const { id } = useParams<{ id: string }>(); - const [type, setType] = useState('slack'); + const [type, setType] = useState( + 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(() => { diff --git a/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx b/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx new file mode 100644 index 0000000000..1c7748f795 --- /dev/null +++ b/frontend/src/container/FormAlertChannels/Settings/Webhook.tsx @@ -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 ( + <> + + { + setSelectedConfig((value) => ({ + ...value, + api_url: event.target.value, + })); + }} + /> + + + { + setSelectedConfig((value) => ({ + ...value, + username: event.target.value, + })); + }} + /> + + + { + setSelectedConfig((value) => ({ + ...value, + password: event.target.value, + })); + }} + /> + + + ); +} + +interface WebhookProps { + setSelectedConfig: React.Dispatch< + React.SetStateAction> + >; +} + +export default WebhookSettings; diff --git a/frontend/src/container/FormAlertChannels/index.tsx b/frontend/src/container/FormAlertChannels/index.tsx index 573be68d00..55aa6e238f 100644 --- a/frontend/src/container/FormAlertChannels/index.tsx +++ b/frontend/src/container/FormAlertChannels/index.tsx @@ -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 ; + case WebhookType: + return ; + default: + return null; + } + }; return ( <> {NotificationElement} @@ -52,14 +65,13 @@ function FormAlertChannels({ + - - {type === 'slack' && ( - - )} - + {renderSettings()} - ); + return Delete; } interface DispatchProps { diff --git a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx index fa2ff98991..e54431063f 100644 --- a/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx +++ b/frontend/src/container/ListOfDashboard/TableComponents/Name.tsx @@ -1,10 +1,10 @@ -import { Button } from 'antd'; import ROUTES from 'constants/routes'; import history from 'lib/history'; import React from 'react'; import { generatePath } from 'react-router-dom'; import { Data } from '..'; +import { TableLinkText } from './styles'; function Name(name: Data['name'], data: Data): JSX.Element { const onClickHandler = (): void => { @@ -17,11 +17,7 @@ function Name(name: Data['name'], data: Data): JSX.Element { ); }; - return ( - - ); + return {name}; } export default Name; diff --git a/frontend/src/container/ListOfDashboard/TableComponents/styles.ts b/frontend/src/container/ListOfDashboard/TableComponents/styles.ts new file mode 100644 index 0000000000..78c382700b --- /dev/null +++ b/frontend/src/container/ListOfDashboard/TableComponents/styles.ts @@ -0,0 +1,8 @@ +import { blue } from '@ant-design/colors'; +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const TableLinkText = styled(Typography.Text)` + color: ${blue.primary} !important; + cursor: pointer; +`; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 8828d748f2..492da5c846 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -157,7 +157,7 @@ function ListOfAllDashboard(): JSX.Element { diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx index 97aa24d180..916771eece 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/Query.tsx @@ -114,7 +114,7 @@ function Query({ diff --git a/frontend/src/container/Timeline/index.tsx b/frontend/src/container/Timeline/index.tsx index 1ecea1f59f..0bee6414a1 100644 --- a/frontend/src/container/Timeline/index.tsx +++ b/frontend/src/container/Timeline/index.tsx @@ -75,9 +75,10 @@ function Timeline({ {intervals && intervals.map((interval, index) => ( diff --git a/frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx b/frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx index df3b6ff012..425334e7ba 100644 --- a/frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx +++ b/frontend/src/container/TriggeredAlerts/TriggeredAlert.tsx @@ -1,4 +1,4 @@ -import getGroupApi from 'api/alerts/getGroup'; +import getTriggeredApi from 'api/alerts/getTriggered'; import useInterval from 'hooks/useInterval'; import React, { useState } from 'react'; import { Alerts } from 'types/api/alerts/getAll'; @@ -13,20 +13,22 @@ function TriggeredAlerts({ allAlerts }: TriggeredAlertsProps): JSX.Element { useInterval(() => { (async (): Promise => { - const response = await getGroupApi({ + const response = await getTriggeredApi({ active: true, inhibited: true, silenced: false, }); if (response.statusCode === 200 && response.payload !== null) { - const initialAlerts: Alerts[] = []; + // commented reduce() call as we no longer use /alerts/groups + // from alertmanager which needed re-grouping on client side + // const initialAlerts: Alerts[] = []; - const allAlerts: Alerts[] = response.payload.reduce((acc, cur) => { - return [...acc, ...cur.alerts]; - }, initialAlerts); + // const allAlerts: Alerts[] = response.payload.reduce((acc, cur) => { + // return [...acc, ...cur.alerts]; + // }, initialAlerts); - setInitialAlerts(allAlerts); + setInitialAlerts(response.payload); } })(); }, 30000); diff --git a/frontend/src/container/TriggeredAlerts/index.tsx b/frontend/src/container/TriggeredAlerts/index.tsx index a665adf6dd..88ebb2e138 100644 --- a/frontend/src/container/TriggeredAlerts/index.tsx +++ b/frontend/src/container/TriggeredAlerts/index.tsx @@ -1,9 +1,9 @@ -import getGroupApi from 'api/alerts/getGroup'; +import getTriggeredApi from 'api/alerts/getTriggered'; import Spinner from 'components/Spinner'; import { State } from 'hooks/useFetch'; import React, { useCallback, useEffect, useState } from 'react'; import { Alerts } from 'types/api/alerts/getAll'; -import { PayloadProps } from 'types/api/alerts/getGroups'; +import { PayloadProps } from 'types/api/alerts/getTriggered'; import TriggerComponent from './TriggeredAlert'; @@ -23,7 +23,7 @@ function TriggeredAlerts(): JSX.Element { loading: true, })); - const response = await getGroupApi({ + const response = await getTriggeredApi({ active: true, inhibited: true, silenced: false, @@ -65,13 +65,16 @@ function TriggeredAlerts(): JSX.Element { return ; } - const initialAlerts: Alerts[] = []; + // commented the reduce() call as we no longer use /alerts/groups + // API from alert manager, which returns a group for each receiver - const allAlerts: Alerts[] = groupState.payload.reduce((acc, curr) => { - return [...acc, ...curr.alerts]; - }, initialAlerts); + // const initialAlerts: Alerts[] = []; - return ; + // const allAlerts: Alerts[] = groupState.payload.reduce((acc, curr) => { + // return [...acc, ...curr.alerts]; + // }, initialAlerts); + + return ; } export default TriggeredAlerts; diff --git a/frontend/src/modules/Servicemap/ServiceMap.tsx b/frontend/src/modules/Servicemap/ServiceMap.tsx index 9d07892fa5..7408fe206d 100644 --- a/frontend/src/modules/Servicemap/ServiceMap.tsx +++ b/frontend/src/modules/Servicemap/ServiceMap.tsx @@ -1,12 +1,13 @@ /* eslint-disable */ //@ts-nocheck +import { Card } from 'antd'; import Spinner from 'components/Spinner'; import React, { useEffect, useRef } from 'react'; import { ForceGraph2D } from 'react-force-graph'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { getDetailedServiceMapItems, getServiceMapItems } from 'store/actions'; +import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions'; import { AppState } from 'store/reducers'; import styled from 'styled-components'; import { GlobalTime } from 'types/actions/globalTime'; @@ -31,9 +32,8 @@ const Container = styled.div` `; interface ServiceMapProps extends RouteComponentProps { - serviceMap: serviceMapStore; + serviceMap: ServiceMapStore; globalTime: GlobalTime; - getServiceMapItems: (time: GlobalTime) => void; getDetailedServiceMapItems: (time: GlobalTime) => void; } interface graphNode { @@ -53,29 +53,32 @@ export interface graphDataType { function ServiceMap(props: ServiceMapProps): JSX.Element { const fgRef = useRef(); - const { - getDetailedServiceMapItems, - getServiceMapItems, - globalTime, - serviceMap, - } = props; + const { getDetailedServiceMapItems, globalTime, serviceMap } = props; useEffect(() => { /* Call the apis only when the route is loaded. Check this issue: https://github.com/SigNoz/signoz/issues/110 */ - getServiceMapItems(globalTime); getDetailedServiceMapItems(globalTime); - }, [globalTime, getServiceMapItems, getDetailedServiceMapItems]); + }, [globalTime, getDetailedServiceMapItems]); useEffect(() => { fgRef.current && fgRef.current.d3Force('charge').strength(-400); }); - if (!serviceMap.items.length || !serviceMap.services.length) { + + if (serviceMap.loading) { return ; } + if (!serviceMap.loading && serviceMap.items.length === 0) { + return ( + + No Service Found + + ); + } + const zoomToService = (value: string): void => { fgRef && fgRef.current && @@ -149,7 +152,6 @@ const mapStateToProps = ( export default withRouter( connect(mapStateToProps, { - getServiceMapItems, getDetailedServiceMapItems, })(ServiceMap), ); diff --git a/frontend/src/pages/AlertChannelCreate/index.tsx b/frontend/src/pages/AlertChannelCreate/index.tsx index 49032941f2..59b2361cf8 100644 --- a/frontend/src/pages/AlertChannelCreate/index.tsx +++ b/frontend/src/pages/AlertChannelCreate/index.tsx @@ -20,7 +20,7 @@ function SettingsPage(): JSX.Element { }, { Component: (): JSX.Element => { - return ; + return ; }, name: 'Alert Channels', route: ROUTES.ALL_CHANNELS, diff --git a/frontend/src/pages/ChannelsEdit/index.tsx b/frontend/src/pages/ChannelsEdit/index.tsx index 60500cc04e..4048eda81c 100644 --- a/frontend/src/pages/ChannelsEdit/index.tsx +++ b/frontend/src/pages/ChannelsEdit/index.tsx @@ -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 ( { - return async (dispatch: Dispatch): Promise => { - dispatch({ - type: ActionTypes.getServiceMapItems, - payload: [], - }); - - const requestString = `/serviceMapDependencies?start=${globalTime.minTime}&end=${globalTime.maxTime}`; - - const response = await api.get(requestString); - - dispatch({ - type: ActionTypes.getServiceMapItems, - payload: response.data, - }); +export interface ServiceMapLoading { + type: ActionTypes.serviceMapLoading; + payload: { + loading: ServiceMapStore['loading']; }; -}; +} export const getDetailedServiceMapItems = (globalTime: GlobalTime) => { return async (dispatch: Dispatch): Promise => { - dispatch({ - type: ActionTypes.getServices, - payload: [], - }); - const requestString = `/services?start=${globalTime.minTime}&end=${globalTime.maxTime}`; - const response = await api.get(requestString); + const serviceMapDependencies = `/serviceMapDependencies?start=${globalTime.minTime}&end=${globalTime.maxTime}`; + + const [serviceMapDependenciesResponse, response] = await Promise.all([ + api.get(serviceMapDependencies), + api.get(requestString), + ]); dispatch({ type: ActionTypes.getServices, payload: response.data, }); + + dispatch({ + type: ActionTypes.getServiceMapItems, + payload: serviceMapDependenciesResponse.data, + }); + + dispatch({ + type: ActionTypes.serviceMapLoading, + payload: { + loading: false, + }, + }); }; }; diff --git a/frontend/src/store/actions/types.ts b/frontend/src/store/actions/types.ts index c15ea00286..702997d49b 100644 --- a/frontend/src/store/actions/types.ts +++ b/frontend/src/store/actions/types.ts @@ -1,14 +1,22 @@ -import { ServiceMapItemAction, ServicesAction } from './serviceMap'; +import { + ServiceMapItemAction, + ServiceMapLoading, + ServicesAction, +} from './serviceMap'; import { GetUsageDataAction } from './usage'; export enum ActionTypes { - updateTraceFilters = 'UPDATE_TRACES_FILTER', updateTimeInterval = 'UPDATE_TIME_INTERVAL', getServiceMapItems = 'GET_SERVICE_MAP_ITEMS', getServices = 'GET_SERVICES', getUsageData = 'GET_USAGE_DATE', fetchTraces = 'FETCH_TRACES', fetchTraceItem = 'FETCH_TRACE_ITEM', + serviceMapLoading = 'UPDATE_SERVICE_MAP_LOADING', } -export type Action = GetUsageDataAction | ServicesAction | ServiceMapItemAction; +export type Action = + | GetUsageDataAction + | ServicesAction + | ServiceMapItemAction + | ServiceMapLoading; diff --git a/frontend/src/store/reducers/serviceMap.ts b/frontend/src/store/reducers/serviceMap.ts index ef7cd21496..18ec21a9ec 100644 --- a/frontend/src/store/reducers/serviceMap.ts +++ b/frontend/src/store/reducers/serviceMap.ts @@ -3,6 +3,7 @@ import { Action, ActionTypes, ServiceMapStore } from 'store/actions'; const initialState: ServiceMapStore = { items: [], services: [], + loading: true, }; export const ServiceMapReducer = ( @@ -20,6 +21,12 @@ export const ServiceMapReducer = ( ...state, services: action.payload, }; + case ActionTypes.serviceMapLoading: { + return { + ...state, + loading: action.payload.loading, + }; + } default: return state; } diff --git a/frontend/src/types/api/alerts/getTriggered.ts b/frontend/src/types/api/alerts/getTriggered.ts new file mode 100644 index 0000000000..8b0e50a279 --- /dev/null +++ b/frontend/src/types/api/alerts/getTriggered.ts @@ -0,0 +1,10 @@ +import { Alerts } from './getAll'; + +export interface Props { + silenced: boolean; + inhibited: boolean; + active: boolean; + [key: string]: string | boolean; +} + +export type PayloadProps = Alerts[] | []; diff --git a/frontend/src/types/api/channels/createWebhook.ts b/frontend/src/types/api/channels/createWebhook.ts new file mode 100644 index 0000000000..295bb55f90 --- /dev/null +++ b/frontend/src/types/api/channels/createWebhook.ts @@ -0,0 +1,8 @@ +import { WebhookChannel } from 'container/CreateAlertChannels/config'; + +export type Props = WebhookChannel; + +export interface PayloadProps { + data: string; + status: string; +} diff --git a/frontend/src/types/api/channels/editWebhook.ts b/frontend/src/types/api/channels/editWebhook.ts new file mode 100644 index 0000000000..475bdb057c --- /dev/null +++ b/frontend/src/types/api/channels/editWebhook.ts @@ -0,0 +1,10 @@ +import { WebhookChannel } from 'container/CreateAlertChannels/config'; + +export interface Props extends WebhookChannel { + id: string; +} + +export interface PayloadProps { + data: string; + status: string; +} diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index b211bcaf94..de49aa0e49 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -13,6 +13,7 @@ import ( "net/http" "net/url" "os" + "regexp" "sort" "strconv" "strings" @@ -44,6 +45,7 @@ import ( "github.com/prometheus/prometheus/util/strutil" "go.signoz.io/query-service/constants" + am "go.signoz.io/query-service/integrations/alertManager" "go.signoz.io/query-service/model" "go.uber.org/zap" ) @@ -74,6 +76,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 +91,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 +657,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 +729,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 +764,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 +797,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 +835,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 +851,7 @@ 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() @@ -2644,7 +2602,6 @@ func (r *ClickHouseReader) GetDisks(ctx context.Context) (*[]model.DiskItem, *mo fmt.Errorf("error while getting disks. Err=%v", err)} } - zap.S().Infof("Got response: %+v\n", diskItems) return &diskItems, nil @@ -2652,29 +2609,33 @@ func (r *ClickHouseReader) GetDisks(ctx context.Context) (*[]model.DiskItem, *mo func (r *ClickHouseReader) GetTTL(ctx context.Context, ttlParams *model.GetTTLParams) (*model.GetTTLResponseItem, *model.ApiError) { - parseTTL := func(queryResp string) int { - values := strings.Split(queryResp, " ") - N := len(values) - ttlIdx := -1 + parseTTL := func(queryResp string) (int, int) { - for i := 0; i < N; i++ { - if strings.Contains(values[i], "toIntervalSecond") { - ttlIdx = i - break + zap.S().Debugf("Parsing TTL from: %s", queryResp) + deleteTTLExp := regexp.MustCompile(`toIntervalSecond\(([0-9]*)\)`) + moveTTLExp := regexp.MustCompile(`toIntervalSecond\(([0-9]*)\) TO VOLUME`) + + var delTTL, moveTTL int = -1, -1 + + m := deleteTTLExp.FindStringSubmatch(queryResp) + if len(m) > 1 { + seconds_int, err := strconv.Atoi(m[1]) + if err != nil { + return -1, -1 } - } - if ttlIdx == -1 { - return ttlIdx + delTTL = seconds_int / 3600 } - output := strings.SplitN(values[ttlIdx], "(", 2) - timePart := strings.Trim(output[1], ")") - seconds_int, err := strconv.Atoi(timePart) - if err != nil { - return -1 + m = moveTTLExp.FindStringSubmatch(queryResp) + if len(m) > 1 { + seconds_int, err := strconv.Atoi(m[1]) + if err != nil { + return -1, -1 + } + moveTTL = seconds_int / 3600 } - ttl_hrs := seconds_int / 3600 - return ttl_hrs + + return delTTL, moveTTL } getMetricsTTL := func() (*model.DBResponseTTL, *model.ApiError) { @@ -2713,7 +2674,8 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, ttlParams *model.GetTTLPa return nil, err } - return &model.GetTTLResponseItem{TracesTime: parseTTL(dbResp.EngineFull)}, nil + delTTL, moveTTL := parseTTL(dbResp.EngineFull) + return &model.GetTTLResponseItem{TracesTime: delTTL, TracesMoveTime: moveTTL}, nil case constants.MetricsTTL: dbResp, err := getMetricsTTL() @@ -2721,7 +2683,9 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, ttlParams *model.GetTTLPa return nil, err } - return &model.GetTTLResponseItem{MetricsTime: parseTTL(dbResp.EngineFull)}, nil + delTTL, moveTTL := parseTTL(dbResp.EngineFull) + return &model.GetTTLResponseItem{MetricsTime: delTTL, MetricsMoveTime: moveTTL}, nil + } db1, err := getTracesTTL() if err != nil { @@ -2732,9 +2696,15 @@ func (r *ClickHouseReader) GetTTL(ctx context.Context, ttlParams *model.GetTTLPa if err != nil { return nil, err } + tracesDelTTL, tracesMoveTTL := parseTTL(db1.EngineFull) + metricsDelTTL, metricsMoveTTL := parseTTL(db2.EngineFull) - return &model.GetTTLResponseItem{TracesTime: parseTTL(db1.EngineFull), MetricsTime: parseTTL(db2.EngineFull)}, nil - + return &model.GetTTLResponseItem{ + TracesTime: tracesDelTTL, + TracesMoveTime: tracesMoveTTL, + MetricsTime: metricsDelTTL, + MetricsMoveTime: metricsMoveTTL, + }, nil } func (r *ClickHouseReader) GetErrors(ctx context.Context, queryParams *model.GetErrorsParams) (*[]model.Error, *model.ApiError) { diff --git a/pkg/query-service/app/druidReader/reader.go b/pkg/query-service/app/druidReader/reader.go index 2444b4ce1b..1c077466a6 100644 --- a/pkg/query-service/app/druidReader/reader.go +++ b/pkg/query-service/app/druidReader/reader.go @@ -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")} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 4aab993e57..771637935a 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -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) diff --git a/pkg/query-service/app/interface.go b/pkg/query-service/app/interface.go index 0f8409dcf4..8a979090d1 100644 --- a/pkg/query-service/app/interface.go +++ b/pkg/query-service/app/interface.go @@ -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) diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index 3e3abc6c79..ae9abc5204 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -914,7 +914,7 @@ func parseTTLParams(r *http.Request) (*model.TTLParams, error) { if err != nil { return nil, fmt.Errorf("Not a valid toCold TTL duration %v", toColdDuration) } - if toColdParsed.Seconds() >= durationParsed.Seconds() { + if toColdParsed.Seconds() != 0 && toColdParsed.Seconds() >= durationParsed.Seconds() { return nil, fmt.Errorf("Delete TTL should be greater than cold storage move TTL.") } } diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 0e19f83f93..6937d1709b 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -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 +} \ No newline at end of file diff --git a/pkg/query-service/godruid/queries.go b/pkg/query-service/godruid/queries.go index 5fb518f908..ffdeee002d 100644 --- a/pkg/query-service/godruid/queries.go +++ b/pkg/query-service/godruid/queries.go @@ -62,7 +62,7 @@ type QueryScan struct { Limit int64 `json:"limit,omitempty"` Offset int64 `json:"offset,omitempty"` BatchSize int64 `json:"batchSize,omitempty"` - Order string `json:"order",omitempty` + Order string `json:"order,omitempty"` ResultFormat string `json:"resultFormat"` Context map[string]interface{} `json:"context,omitempty"` @@ -189,7 +189,7 @@ type TimeBoundaryItem struct { type TimeBoundary struct { MinTime string `json:"minTime"` - MaxTime string `json:"minTime"` + MaxTime string `json:"maxTime"` } func (q *QueryTimeBoundary) setup() { q.QueryType = "timeBoundary" } diff --git a/pkg/query-service/integrations/alertManager/manager.go b/pkg/query-service/integrations/alertManager/manager.go new file mode 100644 index 0000000000..f0e8b024d1 --- /dev/null +++ b/pkg/query-service/integrations/alertManager/manager.go @@ -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 +} + + + diff --git a/pkg/query-service/integrations/alertManager/model.go b/pkg/query-service/integrations/alertManager/model.go new file mode 100644 index 0000000000..705b0492fd --- /dev/null +++ b/pkg/query-service/integrations/alertManager/model.go @@ -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"` +} \ No newline at end of file diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 5b72ad4302..8d4bd4b766 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -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"` @@ -304,8 +283,10 @@ type DBResponseTTL struct { } type GetTTLResponseItem struct { - MetricsTime int `json:"metrics_ttl_duration_hrs"` - TracesTime int `json:"traces_ttl_duration_hrs"` + MetricsTime int `json:"metrics_ttl_duration_hrs,omitempty"` + MetricsMoveTime int `json:"metrics_move_ttl_duration_hrs,omitempty"` + TracesTime int `json:"traces_ttl_duration_hrs,omitempty"` + TracesMoveTime int `json:"traces_move_ttl_duration_hrs,omitempty"` } type DBResponseMinMaxDuration struct { diff --git a/pkg/query-service/tests/cold_storage_test.go b/pkg/query-service/tests/cold_storage_test.go index f748db30dc..8159805a56 100644 --- a/pkg/query-service/tests/cold_storage_test.go +++ b/pkg/query-service/tests/cold_storage_test.go @@ -1,6 +1,7 @@ package tests import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -8,6 +9,7 @@ import ( "time" "github.com/stretchr/testify/require" + "go.signoz.io/query-service/model" ) const ( @@ -102,6 +104,76 @@ func TestSetTTL(t *testing.T) { fmt.Printf("=== Found %d objects in Minio\n", count) } +func getTTL(t *testing.T, table string) *model.GetTTLResponseItem { + req := endpoint + fmt.Sprintf("/api/v1/settings/ttl?type=%s", table) + if len(table) == 0 { + req = endpoint + "/api/v1/settings/ttl" + } + + resp, err := client.Get(req) + require.NoError(t, err) + + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + + res := &model.GetTTLResponseItem{} + require.NoError(t, json.Unmarshal(b, res)) + return res +} + +func TestGetTTL(t *testing.T) { + r, err := setTTL("traces", "s3", "3600s", "7200s") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + resp := getTTL(t, "traces") + require.Equal(t, 1, resp.TracesMoveTime) + require.Equal(t, 2, resp.TracesTime) + + r, err = setTTL("metrics", "s3", "3600s", "7200s") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + resp = getTTL(t, "metrics") + require.Equal(t, 1, resp.MetricsMoveTime) + require.Equal(t, 2, resp.MetricsTime) + + r, err = setTTL("traces", "s3", "36000s", "72000s") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + resp = getTTL(t, "") + require.Equal(t, 10, resp.TracesMoveTime) + require.Equal(t, 20, resp.TracesTime) + require.Equal(t, 1, resp.MetricsMoveTime) + require.Equal(t, 2, resp.MetricsTime) + + r, err = setTTL("metrics", "s3", "15h", "50h") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + resp = getTTL(t, "") + require.Equal(t, 10, resp.TracesMoveTime) + require.Equal(t, 20, resp.TracesTime) + require.Equal(t, 15, resp.MetricsMoveTime) + require.Equal(t, 50, resp.MetricsTime) + + r, err = setTTL("metrics", "s3", "0s", "0s") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + r, err = setTTL("traces", "s3", "0s", "0s") + require.NoError(t, err) + require.Contains(t, string(r), "successfully set up") + + resp = getTTL(t, "") + require.Equal(t, 0, resp.TracesMoveTime) + require.Equal(t, 0, resp.TracesTime) + require.Equal(t, 0, resp.MetricsMoveTime) + require.Equal(t, 0, resp.MetricsTime) +} + func TestMain(m *testing.M) { if err := startCluster(); err != nil { fmt.Println(err) diff --git a/pkg/query-service/tests/test-deploy/data/signoz.db b/pkg/query-service/tests/test-deploy/data/signoz.db new file mode 100644 index 0000000000..c19319ab34 Binary files /dev/null and b/pkg/query-service/tests/test-deploy/data/signoz.db differ diff --git a/pkg/query-service/tests/test-deploy/docker-compose.arm.yaml b/pkg/query-service/tests/test-deploy/docker-compose.arm.yaml index d231ab656b..0447c26a35 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.arm.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.arm.yaml @@ -5,7 +5,6 @@ services: image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm volumes: - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml - - ./data/clickhouse/:/var/lib/clickhouse/ healthcheck: # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"] @@ -14,13 +13,12 @@ services: retries: 3 alertmanager: - image: signoz/alertmanager:0.5.0 - volumes: - - ./alertmanager.yml:/prometheus/alertmanager.yml - - ./data/alertmanager:/data + image: signoz/alertmanager:0.6.0 + depends_on: + - query-service command: - - '--config.file=/prometheus/alertmanager.yml' - - '--storage.path=/data' + - --queryService.url=http://query-service:8080 + - --storage.path=/data query-service: image: signoz/query-service:latest @@ -29,9 +27,9 @@ services: volumes: - ./prometheus.yml:/root/config/prometheus.yml - ../dashboards:/root/config/dashboards - - ./data/signoz/:/var/lib/signoz/ + - ./data:/var/lib/signoz ports: - - "8180:8080" + - "8180:8080" environment: - ClickHouseUrl=tcp://clickhouse:9000 - STORAGE=clickhouse @@ -72,7 +70,7 @@ services: max-file: "3" command: ["all"] environment: - - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces + - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces load-hotrod: image: "grubykarol/locust:1.2.3-python3.9-alpine3.12" @@ -87,4 +85,4 @@ services: QUIET_MODE: "${QUIET_MODE:-false}" LOCUST_OPTS: "--headless -u 10 -r 1" volumes: - - ../common/locust-scripts:/locust + - ../../../../deploy/docker/common/locust-scripts:/locust diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index 2dbafff539..6edd31b63a 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -13,22 +13,25 @@ services: retries: 3 alertmanager: - image: signoz/alertmanager:0.5.0 - volumes: - - ./alertmanager.yml:/prometheus/alertmanager.yml + image: signoz/alertmanager:0.6.0 + depends_on: + - query-service command: - - '--config.file=/prometheus/alertmanager.yml' - - '--storage.path=/data' + - --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:latest + container_name: query-service command: ["-config=/root/config/prometheus.yml"] volumes: - ./prometheus.yml:/root/config/prometheus.yml - ../dashboards:/root/config/dashboards + - ./data:/var/lib/signoz + ports: + - "8180:8080" environment: - ClickHouseUrl=tcp://clickhouse:9000 - STORAGE=clickhouse @@ -37,13 +40,6 @@ services: depends_on: clickhouse: condition: service_healthy - ports: - - "8180:8080" - volumes: - - type: bind - source: ./data - target: /var/lib/signoz - read_only: false otel-collector: image: signoz/otelcontribcol:0.43.0