From b8c58a9812844443e2f0b6af8534f062d7a5dc81 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Fri, 18 Nov 2022 19:04:40 +0530 Subject: [PATCH 01/27] chore: removed unnessesary eslint check (#1668) Co-authored-by: Srikanth Chekuri --- frontend/i18-generate-hash.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/i18-generate-hash.js b/frontend/i18-generate-hash.js index 97476e40af..cbc03f9025 100644 --- a/frontend/i18-generate-hash.js +++ b/frontend/i18-generate-hash.js @@ -1,24 +1,20 @@ -/* eslint-disable */ -// @ts-ignore -// @ts-nocheck - const crypto = require('crypto'); const fs = require('fs'); const glob = require('glob'); function generateChecksum(str, algorithm, encoding) { - return crypto - .createHash(algorithm || 'md5') - .update(str, 'utf8') - .digest(encoding || 'hex'); + return crypto + .createHash(algorithm || 'md5') + .update(str, 'utf8') + .digest(encoding || 'hex'); } const result = {}; -glob.sync(`public/locales/**/*.json`).forEach(path => { - const [_, lang] = path.split('public/locales'); - const content = fs.readFileSync(path, { encoding: 'utf-8' }); - result[lang.replace('.json', '')] = generateChecksum(content); +glob.sync(`public/locales/**/*.json`).forEach((path) => { + const [_, lang] = path.split('public/locales'); + const content = fs.readFileSync(path, { encoding: 'utf-8' }); + result[lang.replace('.json', '')] = generateChecksum(content); }); fs.writeFileSync('./i18n-translations-hash.json', JSON.stringify(result)); From db105af89f86c735e59e580062ae3844f68f154f Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 21 Nov 2022 13:39:54 +0530 Subject: [PATCH 02/27] refactor: some of the styles are removed and used native antd components (#1730) --- frontend/src/container/MetricsTable/index.tsx | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx index bfc97ba9e2..3042e94d1a 100644 --- a/frontend/src/container/MetricsTable/index.tsx +++ b/frontend/src/container/MetricsTable/index.tsx @@ -1,6 +1,6 @@ import { blue } from '@ant-design/colors'; import { SearchOutlined } from '@ant-design/icons'; -import { Button, Input, Space, Table } from 'antd'; +import { Button, Card, Input, Space, Table } from 'antd'; import type { ColumnsType, ColumnType } from 'antd/es/table'; import type { FilterConfirmProps } from 'antd/es/table/interface'; import localStorageGet from 'api/browser/localstorage/get'; @@ -48,37 +48,27 @@ function Metrics(): JSX.Element { const filterDropdown = useCallback( ({ setSelectedKeys, selectedKeys, confirm }) => ( -
- - setSelectedKeys(e.target.value ? [e.target.value] : []) - } - allowClear - onPressEnter={(): void => handleSearch(confirm)} - style={{ - marginBottom: 8, - }} - /> - + + + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + allowClear + onPressEnter={(): void => handleSearch(confirm)} + /> -
+ ), [], ); From 4a244ad7b205141fc6cef375018072b11f73bba6 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 21 Nov 2022 16:44:41 +0530 Subject: [PATCH 03/27] feat: onClick is updated (#1732) Co-authored-by: Pranay Prateek --- frontend/src/container/Trace/TraceTable/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index c532ff2683..d66dd37592 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -3,6 +3,7 @@ import Table, { ColumnsType } from 'antd/lib/table'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; +import history from 'lib/history'; import omit from 'lodash-es/omit'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; @@ -194,7 +195,11 @@ function TraceTable(): JSX.Element { onClick: (event): void => { event.preventDefault(); event.stopPropagation(); - window.open(getLink(record)); + if (event.metaKey || event.ctrlKey) { + window.open(getLink(record), '_blank'); + } else { + history.push(getLink(record)); + } }, })} pagination={{ From 90a6313423a8bc69d04943793ac46c39bb4189ea Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 21 Nov 2022 21:03:33 +0530 Subject: [PATCH 04/27] feat: value graph is updated (#1733) --- frontend/src/components/ValueGraph/styles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ValueGraph/styles.ts b/frontend/src/components/ValueGraph/styles.ts index af9f9c7ad0..74a412fec3 100644 --- a/frontend/src/components/ValueGraph/styles.ts +++ b/frontend/src/components/ValueGraph/styles.ts @@ -2,5 +2,6 @@ import { Typography } from 'antd'; import styled from 'styled-components'; export const Value = styled(Typography)` - font-size: 3rem; + font-size: 2.5vw; + text-align: center; `; From 87502baabfea6c1b8e5185f9682c247c18dc6089 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Tue, 22 Nov 2022 12:08:51 +0530 Subject: [PATCH 05/27] feat: filter is added in exceptions page (#1731) * feat: filter is added Co-authored-by: Pranay Prateek Co-authored-by: Vishal Sharma --- frontend/src/container/AllError/index.tsx | 94 ++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/frontend/src/container/AllError/index.tsx b/frontend/src/container/AllError/index.tsx index 253af7dfe1..6db945b9bc 100644 --- a/frontend/src/container/AllError/index.tsx +++ b/frontend/src/container/AllError/index.tsx @@ -1,12 +1,25 @@ -import { notification, Table, TableProps, Tooltip, Typography } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { + Button, + Card, + Input, + notification, + Space, + Table, + TableProps, + Tooltip, + Typography, +} from 'antd'; +import { ColumnType } from 'antd/es/table'; import { ColumnsType } from 'antd/lib/table'; +import { FilterConfirmProps } from 'antd/lib/table/interface'; import getAll from 'api/errors/getAll'; import getErrorCounts from 'api/errors/getErrorCounts'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import createQueryParams from 'lib/createQueryParams'; import history from 'lib/history'; -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueries } from 'react-query'; import { useSelector } from 'react-redux'; @@ -93,11 +106,87 @@ function AllErrors(): JSX.Element { {dayjs(value).format('DD/MM/YYYY HH:mm:ss A')} ); + const filterIcon = useCallback(() => , []); + + const handleSearch = ( + confirm: (param?: FilterConfirmProps) => void, + ): VoidFunction => (): void => { + confirm(); + }; + + const filterDropdownWrapper = useCallback( + ({ setSelectedKeys, selectedKeys, confirm, placeholder }) => { + return ( + + + + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + allowClear + onPressEnter={handleSearch(confirm)} + /> + + + + ); + }, + [], + ); + + const onExceptionTypeFilter = useCallback( + (value, record: Exception): boolean => { + if (record.exceptionType && typeof value === 'string') { + return record.exceptionType.toLowerCase().includes(value.toLowerCase()); + } + return false; + }, + [], + ); + + const onApplicationTypeFilter = useCallback( + (value, record: Exception): boolean => { + if (record.serviceName && typeof value === 'string') { + return record.serviceName.toLowerCase().includes(value.toLowerCase()); + } + return false; + }, + [], + ); + + const getFilter = useCallback( + ( + onFilter: ColumnType['onFilter'], + placeholder: string, + ): ColumnType => ({ + onFilter, + filterIcon, + filterDropdown: ({ confirm, selectedKeys, setSelectedKeys }): JSX.Element => + filterDropdownWrapper({ + setSelectedKeys, + selectedKeys, + confirm, + placeholder, + }), + }), + [filterIcon, filterDropdownWrapper], + ); + const columns: ColumnsType = [ { title: 'Exception Type', dataIndex: 'exceptionType', key: 'exceptionType', + ...getFilter(onExceptionTypeFilter, 'Search By Exception'), render: (value, record): JSX.Element => ( value}> Date: Tue, 22 Nov 2022 13:13:10 +0530 Subject: [PATCH 06/27] fix: getNanoTimestamp function and cache fix (#1737) --- frontend/src/container/AllError/utils.ts | 2 +- frontend/src/container/TopNav/Breadcrumbs/index.tsx | 2 +- frontend/src/pages/ErrorDetails/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/AllError/utils.ts b/frontend/src/container/AllError/utils.ts index 239d404b1c..aae242ffa0 100644 --- a/frontend/src/container/AllError/utils.ts +++ b/frontend/src/container/AllError/utils.ts @@ -77,7 +77,7 @@ export const getDefaultOrder = ( export const getNanoSeconds = (date: string): string => { return ( Math.floor(new Date(date).getTime() / 1e3).toString() + - Timestamp.fromString(date).getNano().toString() + String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0') ); }; diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index bc839d407c..a4ada700d5 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -15,7 +15,7 @@ const breadcrumbNameMap = { [ROUTES.VERSION]: 'Status', [ROUTES.ORG_SETTINGS]: 'Organization Settings', [ROUTES.MY_SETTINGS]: 'My Settings', - [ROUTES.ERROR_DETAIL]: 'Errors', + [ROUTES.ERROR_DETAIL]: 'Exceptions', [ROUTES.LIST_ALL_ALERT]: 'Alerts', [ROUTES.ALL_DASHBOARD]: 'Dashboard', [ROUTES.LOGS]: 'Logs', diff --git a/frontend/src/pages/ErrorDetails/index.tsx b/frontend/src/pages/ErrorDetails/index.tsx index 348391b741..94bd8e248e 100644 --- a/frontend/src/pages/ErrorDetails/index.tsx +++ b/frontend/src/pages/ErrorDetails/index.tsx @@ -48,7 +48,7 @@ function ErrorDetails(): JSX.Element { }, ); - const { data, status } = useQuery([maxTime, minTime, groupId], { + const { data, status } = useQuery([maxTime, minTime, groupId, errorId], { queryFn: () => getByErrorType({ groupID: groupId || '', From 4c0d5737603b0c5242aac77415516cb3b7036c11 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Wed, 23 Nov 2022 13:42:36 +0530 Subject: [PATCH 07/27] fix: Logs issues are fixed (#1727) * feat: logs is updated * chore: width:100% is removed * chore: position of filter is updated * chore: min time and max time are now tracked from global state Co-authored-by: Pranay Prateek --- frontend/src/container/LogControls/styles.ts | 1 - .../src/container/LogLiveTail/OptionIcon.tsx | 26 --- frontend/src/container/LogLiveTail/config.ts | 26 +++ frontend/src/container/LogLiveTail/index.tsx | 126 ++++------ frontend/src/container/LogLiveTail/styles.ts | 13 ++ frontend/src/container/Logs/index.tsx | 66 ------ .../src/container/LogsAggregate/index.tsx | 53 +++-- frontend/src/container/LogsFilters/index.tsx | 5 +- .../QueryBuilder/QueryBuilder.tsx | 89 ++++---- .../SearchFields/QueryBuilder/styles.ts | 17 ++ .../SearchFields/Suggestions.tsx | 2 +- .../LogsSearchFilter/SearchFields/index.tsx | 17 +- .../LogsSearchFilter/SearchFields/styles.tsx | 5 +- .../src/container/LogsSearchFilter/index.tsx | 216 ++++++++---------- .../src/container/LogsSearchFilter/styles.ts | 12 +- .../LogsSearchFilter/useSearchParser.ts | 10 +- frontend/src/container/LogsTable/index.tsx | 58 +---- frontend/src/pages/Logs/index.tsx | 74 +++++- frontend/src/pages/Logs/styles.ts | 10 + frontend/src/store/reducers/logs.ts | 13 +- 20 files changed, 395 insertions(+), 444 deletions(-) delete mode 100644 frontend/src/container/LogLiveTail/OptionIcon.tsx create mode 100644 frontend/src/container/LogLiveTail/config.ts delete mode 100644 frontend/src/container/Logs/index.tsx create mode 100644 frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/styles.ts create mode 100644 frontend/src/pages/Logs/styles.ts diff --git a/frontend/src/container/LogControls/styles.ts b/frontend/src/container/LogControls/styles.ts index 304b443c54..f91bc43363 100644 --- a/frontend/src/container/LogControls/styles.ts +++ b/frontend/src/container/LogControls/styles.ts @@ -5,5 +5,4 @@ export const Container = styled.div` align-items: center; justify-content: flex-end; gap: 0.5rem; - margin-bottom: 0.5rem; `; diff --git a/frontend/src/container/LogLiveTail/OptionIcon.tsx b/frontend/src/container/LogLiveTail/OptionIcon.tsx deleted file mode 100644 index 9a6f935ed9..0000000000 --- a/frontend/src/container/LogLiveTail/OptionIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; - -interface OptionIconProps { - isDarkMode: boolean; -} -function OptionIcon({ isDarkMode }: OptionIconProps): JSX.Element { - return ( - - - - ); -} - -export default OptionIcon; diff --git a/frontend/src/container/LogLiveTail/config.ts b/frontend/src/container/LogLiveTail/config.ts new file mode 100644 index 0000000000..27ee168205 --- /dev/null +++ b/frontend/src/container/LogLiveTail/config.ts @@ -0,0 +1,26 @@ +export const TIME_PICKER_OPTIONS = [ + { + value: 5, + label: '5m', + }, + { + value: 15, + label: '15m', + }, + { + value: 30, + label: '30m', + }, + { + value: 60, + label: '1hr', + }, + { + value: 360, + label: '6hrs', + }, + { + value: 720, + label: '12hrs', + }, +]; diff --git a/frontend/src/container/LogLiveTail/index.tsx b/frontend/src/container/LogLiveTail/index.tsx index 315c629828..1c48b4d8ab 100644 --- a/frontend/src/container/LogLiveTail/index.tsx +++ b/frontend/src/container/LogLiveTail/index.tsx @@ -1,7 +1,10 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { green } from '@ant-design/colors'; -import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons'; -import { Button, Popover, Row, Select } from 'antd'; +import { + MoreOutlined, + PauseOutlined, + PlayCircleOutlined, +} from '@ant-design/icons'; +import { Button, Popover, Select, Space } from 'antd'; import { LiveTail } from 'api/logs/livetail'; import dayjs from 'dayjs'; import { throttle } from 'lodash-es'; @@ -18,38 +21,11 @@ import { TLogsLiveTailState } from 'types/api/logs/liveTail'; import AppReducer from 'types/reducer/app'; import { ILogsReducer } from 'types/reducer/logs'; -import OptionIcon from './OptionIcon'; -import { TimePickerCard, TimePickerSelect } from './styles'; +import { TIME_PICKER_OPTIONS } from './config'; +import { StopContainer, TimePickerCard, TimePickerSelect } from './styles'; const { Option } = Select; -const TIME_PICKER_OPTIONS = [ - { - value: 5, - label: '5m', - }, - { - value: 15, - label: '15m', - }, - { - value: 30, - label: '30m', - }, - { - value: 60, - label: '1hr', - }, - { - value: 360, - label: '6hrs', - }, - { - value: 720, - label: '12hrs', - }, -]; - function LogLiveTail(): JSX.Element { const { liveTail, @@ -75,14 +51,12 @@ function LogLiveTail(): JSX.Element { type: PUSH_LIVE_TAIL_EVENT, payload: batchedEventsRef.current.reverse(), }); - // console.log('DISPATCH', batchedEventsRef.current.length); batchedEventsRef.current = []; }, 1500), [], ); const batchLiveLog = (e: { data: string }): void => { - // console.log('EVENT BATCHED'); batchedEventsRef.current.push(JSON.parse(e.data as string) as never); pushLiveLog(); }; @@ -123,6 +97,7 @@ function LogLiveTail(): JSX.Element { if (liveTail === 'STOPPED') { liveTailSourceRef.current = null; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [liveTail]); const handleLiveTailStart = (): void => { @@ -155,47 +130,39 @@ function LogLiveTail(): JSX.Element { ), [dispatch, liveTail, liveTailStartRange], ); + return ( - -
- {liveTail === 'PLAYING' ? ( - - ) : ( - - )} - {liveTail !== 'STOPPED' && ( - - )} -
+ + {liveTail === 'PLAYING' ? ( + + ) : ( + + )} + + {liveTail !== 'STOPPED' && ( + + )} - - - + -
+
); } diff --git a/frontend/src/container/LogLiveTail/styles.ts b/frontend/src/container/LogLiveTail/styles.ts index 4e8691dbd0..66081585b3 100644 --- a/frontend/src/container/LogLiveTail/styles.ts +++ b/frontend/src/container/LogLiveTail/styles.ts @@ -3,6 +3,7 @@ import styled from 'styled-components'; export const TimePickerCard = styled(Card)` .ant-card-body { + display: flex; padding: 0; } `; @@ -10,3 +11,15 @@ export const TimePickerCard = styled(Card)` export const TimePickerSelect = styled(Select)` min-width: 100px; `; + +interface Props { + isDarkMode: boolean; +} + +export const StopContainer = styled.div` + height: 0.8rem; + width: 0.8rem; + border-radius: 0.1rem; + background-color: ${({ isDarkMode }): string => + isDarkMode ? '#fff' : '#000'}; +`; diff --git a/frontend/src/container/Logs/index.tsx b/frontend/src/container/Logs/index.tsx deleted file mode 100644 index 9139d7c9e2..0000000000 --- a/frontend/src/container/Logs/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Divider, Row } from 'antd'; -import LogControls from 'container/LogControls'; -import LogDetailedView from 'container/LogDetailedView'; -import LogLiveTail from 'container/LogLiveTail'; -import LogsAggregate from 'container/LogsAggregate'; -import LogsFilters from 'container/LogsFilters'; -import SearchFilter from 'container/LogsSearchFilter'; -import LogsTable from 'container/LogsTable'; -import useUrlQuery from 'hooks/useUrlQuery'; -import React, { memo, useEffect } from 'react'; -import { connect, useDispatch } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GetLogsFields } from 'store/actions/logs/getFields'; -import AppActions from 'types/actions'; -import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs'; - -function Logs({ getLogsFields }: LogsProps): JSX.Element { - const urlQuery = useUrlQuery(); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch({ - type: SET_SEARCH_QUERY_STRING, - payload: urlQuery.get('q'), - }); - }, [dispatch, urlQuery]); - - useEffect(() => { - getLogsFields(); - }, [getLogsFields]); - - return ( -
- - - - - - - - - - - - - - -
- ); -} - -type LogsProps = DispatchProps; - -interface DispatchProps { - getLogsFields: () => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getLogsFields: bindActionCreators(GetLogsFields, dispatch), -}); - -export default connect(null, mapDispatchToProps)(memo(Logs)); diff --git a/frontend/src/container/LogsAggregate/index.tsx b/frontend/src/container/LogsAggregate/index.tsx index 5b7c330135..737164f9fd 100644 --- a/frontend/src/container/LogsAggregate/index.tsx +++ b/frontend/src/container/LogsAggregate/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { blue } from '@ant-design/colors'; import Graph from 'components/Graph'; import Spinner from 'components/Spinner'; @@ -16,9 +15,6 @@ import { ILogsReducer } from 'types/reducer/logs'; import { Container } from './styles'; -interface LogsAggregateProps { - getLogsAggregate: (arg0: Parameters[0]) => void; -} function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { const { searchFilter: { queryString }, @@ -42,18 +38,18 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { clearInterval(reFetchIntervalRef.current); } reFetchIntervalRef.current = null; - getLogsAggregate({ - timestampStart: minTime, - timestampEnd: maxTime, - step: getStep({ - start: minTime, - end: maxTime, - inputFormat: 'ns', - }), - q: queryString, - ...(idStart ? { idGt: idStart } : {}), - ...(idEnd ? { idLt: idEnd } : {}), - }); + // getLogsAggregate({ + // timestampStart: minTime, + // timestampEnd: maxTime, + // step: getStep({ + // start: minTime, + // end: maxTime, + // inputFormat: 'ns', + // }), + // q: queryString, + // ...(idStart ? { idGt: idStart } : {}), + // ...(idEnd ? { idLt: idEnd } : {}), + // }); break; } @@ -89,18 +85,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { break; } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [getLogsAggregate, maxTime, minTime, liveTail]); - const data = { - labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)), - datasets: [ - { - data: logsAggregate.map((s) => s.value), - backgroundColor: blue[4], - }, - ], - }; - return ( {isLoadingAggregate ? ( @@ -108,7 +95,15 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { ) : ( new Date(s.timestamp / 1000000)), + datasets: [ + { + data: logsAggregate.map((s) => s.value), + backgroundColor: blue[4], + }, + ], + }} type="bar" containerHeight="100%" animate={false} @@ -118,6 +113,10 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { ); } +interface LogsAggregateProps { + getLogsAggregate: (arg0: Parameters[0]) => void; +} + interface DispatchProps { getLogsAggregate: ( props: Parameters[0], diff --git a/frontend/src/container/LogsFilters/index.tsx b/frontend/src/container/LogsFilters/index.tsx index 28480edccc..42d8bc500e 100644 --- a/frontend/src/container/LogsFilters/index.tsx +++ b/frontend/src/container/LogsFilters/index.tsx @@ -21,9 +21,6 @@ import { CategoryContainer, Container, FieldContainer } from './styles'; const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id']; -interface LogsFiltersProps { - getLogsFields: () => void; -} function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element { const { fields: { interesting, selected }, @@ -150,4 +147,6 @@ const mapDispatchToProps = ( getLogsFields: bindActionCreators(GetLogsFields, dispatch), }); +type LogsFiltersProps = DispatchProps; + export default connect(null, mapDispatchToProps)(memo(LogsFilters)); diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 0723d2378e..6744c234da 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -4,7 +4,7 @@ /* eslint-disable no-param-reassign */ /* eslint-disable react/no-array-index-key */ /* eslint-disable react-hooks/exhaustive-deps */ -import { CloseOutlined } from '@ant-design/icons'; +import { CloseOutlined, CloseSquareOutlined } from '@ant-design/icons'; import { Button, Input, Select } from 'antd'; import CategoryHeading from 'components/Logs/CategoryHeading'; import { @@ -19,12 +19,46 @@ import { AppState } from 'store/reducers'; import { ILogsReducer } from 'types/reducer/logs'; import { v4 } from 'uuid'; +import { SearchFieldsProps } from '..'; import FieldKey from '../FieldKey'; import { QueryConditionContainer, QueryFieldContainer } from '../styles'; import { createParsedQueryStructure } from '../utils'; +import { Container, QueryWrapper } from './styles'; import { hashCode, parseQuery } from './utils'; const { Option } = Select; + +function QueryConditionField({ + query, + queryIndex, + onUpdate, + style, +}: QueryConditionFieldProps): JSX.Element { + return ( + + + + ); +} + +QueryConditionField.defaultProps = { + style: undefined, +}; + interface QueryFieldProps { query: Query; queryIndex: number; @@ -140,41 +174,15 @@ interface QueryConditionFieldProps { query: { value: string | string[]; type: string }[]; queryIndex: number; onUpdate: (arg0: unknown, arg1: number) => void; -} -function QueryConditionField({ - query, - queryIndex, - onUpdate, -}: QueryConditionFieldProps): JSX.Element { - return ( - - - - ); + style?: React.CSSProperties; } export type Query = { value: string | string[]; type: string }[]; function QueryBuilder({ updateParsedQuery, -}: { - updateParsedQuery: (arg0: unknown) => void; -}): JSX.Element { + onDropDownToggleHandler, +}: SearchFieldsProps): JSX.Element { const { searchFilter: { parsedQuery }, } = useSelector((store) => store.logs); @@ -233,19 +241,16 @@ function QueryBuilder({ /> ); }); + return ( -
- LOG QUERY BUILDER -
- {QueryUI()} -
-
+ <> + + LOG QUERY BUILDER + + + + {QueryUI()} + ); } diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/styles.ts b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/styles.ts new file mode 100644 index 0000000000..211a5b6407 --- /dev/null +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +interface Props { + isMargin: boolean; +} +export const Container = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')}; +`; + +export const QueryWrapper = styled.div` + display: grid; + grid-template-columns: 80px 1fr; + margin: 0.5rem 0px; +`; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx index c80b996e48..838d790954 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/Suggestions.tsx @@ -1,6 +1,6 @@ import { Button } from 'antd'; import CategoryHeading from 'components/Logs/CategoryHeading'; -import { map } from 'lodash-es'; +import map from 'lodash-es/map'; import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx index 888257279f..9080c454ab 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/index.tsx @@ -2,14 +2,23 @@ import React from 'react'; import QueryBuilder from './QueryBuilder/QueryBuilder'; import Suggestions from './Suggestions'; +import { QueryFields } from './utils'; -interface SearchFieldsProps { - updateParsedQuery: () => void; +export interface SearchFieldsProps { + updateParsedQuery: (query: QueryFields[]) => void; + onDropDownToggleHandler: (value: boolean) => VoidFunction; } -function SearchFields({ updateParsedQuery }: SearchFieldsProps): JSX.Element { + +function SearchFields({ + updateParsedQuery, + onDropDownToggleHandler, +}: SearchFieldsProps): JSX.Element { return ( <> - + ); diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx index 3ec67d2fd7..1f06e924ca 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx @@ -9,12 +9,13 @@ export const QueryFieldContainer = styled.div` align-items: center; border-radius: 0.25rem; gap: 1rem; + width: 100%; &:hover { background: ${blue[6]}; } `; export const QueryConditionContainer = styled.div` - padding: 0.25rem 0rem; - margin: 0.1rem 0; + display: flex; + flex-direction: row; `; diff --git a/frontend/src/container/LogsSearchFilter/index.tsx b/frontend/src/container/LogsSearchFilter/index.tsx index 6c3d43d874..5af48b17c1 100644 --- a/frontend/src/container/LogsSearchFilter/index.tsx +++ b/frontend/src/container/LogsSearchFilter/index.tsx @@ -1,11 +1,8 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { CloseSquareOutlined } from '@ant-design/icons'; -import { Button, Input } from 'antd'; -import useClickOutside from 'hooks/useClickOutside'; +import { Input, InputRef, Popover } from 'antd'; +import useUrlQuery from 'hooks/useUrlQuery'; import getStep from 'lib/getStep'; -import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { connect, useDispatch, useSelector } from 'react-redux'; -import { useLocation } from 'react-use'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { getLogs } from 'store/actions/logs/getLogs'; @@ -17,17 +14,9 @@ import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; import SearchFields from './SearchFields'; -import { DropDownContainer } from './styles'; +import { Container, DropDownContainer } from './styles'; import { useSearchParser } from './useSearchParser'; -const { Search } = Input; - -interface SearchFilterProps { - getLogs: (props: Parameters[0]) => ReturnType; - getLogsAggregate: ( - props: Parameters[0], - ) => ReturnType; -} function SearchFilter({ getLogs, getLogsAggregate, @@ -38,6 +27,14 @@ function SearchFilter({ updateQueryString, } = useSearchParser(); const [showDropDown, setShowDropDown] = useState(false); + const searchRef = useRef(null); + + const onDropDownToggleHandler = useCallback( + (value: boolean) => (): void => { + setShowDropDown(value); + }, + [], + ); const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector< AppState, @@ -48,117 +45,104 @@ function SearchFilter({ (state) => state.globalTime, ); - const searchComponentRef = useRef(null); + const dispatch = useDispatch>(); - useClickOutside(searchComponentRef, (e: HTMLElement) => { - // using this hack as overlay span is voilating this condition - if ( - e.nodeName === 'svg' || - e.nodeName === 'path' || - e.nodeName === 'span' || - e.nodeName === 'button' - ) { - return; - } + const handleSearch = useCallback( + (customQuery) => { + if (liveTail === 'PLAYING') { + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: 'PAUSED', + }); + setTimeout( + () => + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: liveTail, + }), + 0, + ); + } else { + getLogs({ + q: customQuery, + limit: logLinesPerPage, + orderBy: 'timestamp', + order: 'desc', + timestampStart: minTime, + timestampEnd: maxTime, + ...(idStart ? { idGt: idStart } : {}), + ...(idEnd ? { idLt: idEnd } : {}), + }); - if ( - e.nodeName === 'DIV' && - ![ - 'ant-empty-image', - 'ant-select-item', - 'ant-col', - 'ant-select-item-option-content', - 'ant-select-item-option-active', - ].find((p) => p.indexOf(e.className) !== -1) && - !(e.ariaSelected === 'true') && - showDropDown - ) { - setShowDropDown(false); - } - }); - const { search } = useLocation(); - const dispatch = useDispatch(); - const handleSearch = (customQuery = ''): void => { - if (liveTail === 'PLAYING') { - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: 'PAUSED', - }); - setTimeout( - () => - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: liveTail, + getLogsAggregate({ + timestampStart: minTime, + timestampEnd: maxTime, + step: getStep({ + start: minTime, + end: maxTime, + inputFormat: 'ns', }), - 0, - ); - } else { - getLogs({ - q: customQuery || queryString, - limit: logLinesPerPage, - orderBy: 'timestamp', - order: 'desc', - timestampStart: minTime, - timestampEnd: maxTime, - ...(idStart ? { idGt: idStart } : {}), - ...(idEnd ? { idLt: idEnd } : {}), - }); + q: customQuery, + }); + } + }, + [ + dispatch, + getLogs, + getLogsAggregate, + idEnd, + idStart, + liveTail, + logLinesPerPage, + maxTime, + minTime, + ], + ); - getLogsAggregate({ - timestampStart: minTime, - timestampEnd: maxTime, - step: getStep({ - start: minTime, - end: maxTime, - inputFormat: 'ns', - }), - q: customQuery || queryString, - }); - } - setShowDropDown(false); - }; - - const urlQuery = useMemo(() => { - return new URLSearchParams(search); - }, [search]); + const urlQuery = useUrlQuery(); + const urlQueryString = urlQuery.get('q'); useEffect(() => { - const urlQueryString = urlQuery.get('q'); - if (urlQueryString !== null) handleSearch(urlQueryString); - }, []); + handleSearch(urlQueryString || ''); + }, [handleSearch, urlQueryString]); return ( -
- setShowDropDown(true)} - value={queryString} - onChange={(e): void => { - updateQueryString(e.target.value); - }} - onSearch={handleSearch} - /> -
- {showDropDown && ( + + - - + - )} -
-
+ } + trigger="click" + overlayInnerStyle={{ + width: `${searchRef?.current?.input?.offsetWidth || 0}px`, + }} + visible={showDropDown} + destroyTooltipOnHide + onVisibleChange={(value): void => { + onDropDownToggleHandler(value)(); + }} + > + { + updateQueryString(e.target.value); + }} + allowClear + onSearch={handleSearch} + /> + +
); } + interface DispatchProps { getLogs: ( props: Parameters[0], @@ -168,6 +152,8 @@ interface DispatchProps { ) => (dispatch: Dispatch) => void; } +type SearchFilterProps = DispatchProps; + const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ @@ -175,4 +161,4 @@ const mapDispatchToProps = ( getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), }); -export default connect(null, mapDispatchToProps)(memo(SearchFilter)); +export default connect(null, mapDispatchToProps)(SearchFilter); diff --git a/frontend/src/container/LogsSearchFilter/styles.ts b/frontend/src/container/LogsSearchFilter/styles.ts index 640881c3fa..7b379b08da 100644 --- a/frontend/src/container/LogsSearchFilter/styles.ts +++ b/frontend/src/container/LogsSearchFilter/styles.ts @@ -2,11 +2,13 @@ import { Card } from 'antd'; import styled from 'styled-components'; export const DropDownContainer = styled(Card)` - top: 0.5rem; - position: absolute; - width: 100%; - z-index: 1; .ant-card-body { - padding: 0.8rem; + width: 100%; } `; + +export const Container = styled.div` + width: 100%; + flex: 1; + position: relative; +`; diff --git a/frontend/src/container/LogsSearchFilter/useSearchParser.ts b/frontend/src/container/LogsSearchFilter/useSearchParser.ts index 50c4cc1e35..7bb43eeea0 100644 --- a/frontend/src/container/LogsSearchFilter/useSearchParser.ts +++ b/frontend/src/container/LogsSearchFilter/useSearchParser.ts @@ -23,10 +23,12 @@ export function useSearchParser(): { const updateQueryString = useCallback( (updatedQueryString) => { - history.push({ - pathname: history.location.pathname, - search: updatedQueryString ? `?q=${updatedQueryString}` : '', - }); + if (updatedQueryString) { + history.push({ + pathname: history.location.pathname, + search: updatedQueryString ? `?q=${updatedQueryString}` : '', + }); + } dispatch({ type: SET_SEARCH_QUERY_STRING, diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 7997fac91f..68758c59fe 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -3,48 +3,18 @@ import { Typography } from 'antd'; import LogItem from 'components/Logs/LogItem'; import Spinner from 'components/Spinner'; import { map } from 'lodash-es'; -import React, { memo, useEffect } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { getLogs } from 'store/actions/logs/getLogs'; +import React, { memo } from 'react'; +import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; import { Container, Heading } from './styles'; -function LogsTable({ getLogs }: LogsTableProps): JSX.Element { - const { - searchFilter: { queryString }, - logs, - logLinesPerPage, - idEnd, - idStart, - isLoading, - liveTail, - } = useSelector((state) => state.logs); - - const { maxTime, minTime } = useSelector( - (state) => state.globalTime, +function LogsTable(): JSX.Element { + const { logs, isLoading, liveTail } = useSelector( + (state) => state.logs, ); - useEffect(() => { - if (liveTail === 'STOPPED') - getLogs({ - q: queryString, - limit: logLinesPerPage, - orderBy: 'timestamp', - order: 'desc', - timestampStart: minTime, - timestampEnd: maxTime, - ...(idStart ? { idGt: idStart } : {}), - ...(idEnd ? { idLt: idEnd } : {}), - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getLogs, idEnd, idStart, logLinesPerPage, maxTime, minTime, liveTail]); - if (isLoading) { return ; } @@ -72,20 +42,4 @@ function LogsTable({ getLogs }: LogsTableProps): JSX.Element { ); } -interface DispatchProps { - getLogs: ( - props: Parameters[0], - ) => (dispatch: Dispatch) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getLogs: bindActionCreators(getLogs, dispatch), -}); - -interface LogsTableProps { - getLogs: (props: Parameters[0]) => ReturnType; -} - -export default connect(null, mapDispatchToProps)(memo(LogsTable)); +export default memo(LogsTable); diff --git a/frontend/src/pages/Logs/index.tsx b/frontend/src/pages/Logs/index.tsx index b11afff009..2799295146 100644 --- a/frontend/src/pages/Logs/index.tsx +++ b/frontend/src/pages/Logs/index.tsx @@ -1,8 +1,72 @@ -import Logs from 'container/Logs'; -import React from 'react'; +import { Divider, Row } from 'antd'; +import LogControls from 'container/LogControls'; +import LogDetailedView from 'container/LogDetailedView'; +import LogLiveTail from 'container/LogLiveTail'; +import LogsAggregate from 'container/LogsAggregate'; +import LogsFilters from 'container/LogsFilters'; +import LogsSearchFilter from 'container/LogsSearchFilter'; +import LogsTable from 'container/LogsTable'; +import useUrlQuery from 'hooks/useUrlQuery'; +import React, { memo, useEffect } from 'react'; +import { connect, useDispatch } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { GetLogsFields } from 'store/actions/logs/getFields'; +import AppActions from 'types/actions'; +import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs'; -function LogsHome(): JSX.Element { - return ; +import SpaceContainer from './styles'; + +function Logs({ getLogsFields }: LogsProps): JSX.Element { + const urlQuery = useUrlQuery(); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch({ + type: SET_SEARCH_QUERY_STRING, + payload: urlQuery.get('q'), + }); + }, [dispatch, urlQuery]); + + useEffect(() => { + getLogsFields(); + }, [getLogsFields]); + + return ( + <> + } + align="center" + direction="horizontal" + > + + + + + + + + + + + + + + + ); } -export default LogsHome; +type LogsProps = DispatchProps; + +interface DispatchProps { + getLogsFields: () => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getLogsFields: bindActionCreators(GetLogsFields, dispatch), +}); + +export default connect(null, mapDispatchToProps)(memo(Logs)); diff --git a/frontend/src/pages/Logs/styles.ts b/frontend/src/pages/Logs/styles.ts new file mode 100644 index 0000000000..d0675ccf97 --- /dev/null +++ b/frontend/src/pages/Logs/styles.ts @@ -0,0 +1,10 @@ +import { Space } from 'antd'; +import styled from 'styled-components'; + +const SpaceContainer = styled(Space)` + .ant-space-item:nth-child(1) { + width: 100%; + } +`; + +export default SpaceContainer; diff --git a/frontend/src/store/reducers/logs.ts b/frontend/src/store/reducers/logs.ts index b082a8c022..a170de66e3 100644 --- a/frontend/src/store/reducers/logs.ts +++ b/frontend/src/store/reducers/logs.ts @@ -99,15 +99,14 @@ export const LogsReducer = ( } case ADD_SEARCH_FIELD_QUERY_STRING: { - const updatedQueryString = - state.searchFilter.queryString || - `${ - state.searchFilter.queryString && state.searchFilter.queryString.length > 0 - ? ' and ' - : '' - }${action.payload}`; + const updatedQueryString = `${state?.searchFilter?.queryString || ''}${ + state.searchFilter.queryString && state.searchFilter.queryString.length > 0 + ? ' and ' + : '' + }${action.payload}`; const updatedParsedQuery = parseQuery(updatedQueryString); + console.log({ updatedParsedQuery, updatedQueryString, action }); return { ...state, searchFilter: { From d5bd9914177563c33adb5e4974eb64e3121046f8 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Wed, 23 Nov 2022 16:25:02 +0530 Subject: [PATCH 08/27] fix: onApply data is updated (#1655) --- .../container/Licenses/ApplyLicenseForm.tsx | 37 ++++++++++++++++++- .../src/container/Licenses/ListLicenses.tsx | 1 - frontend/src/container/Licenses/index.tsx | 26 +++++++------ .../{applyFormStyles.ts => styles.ts} | 0 4 files changed, 49 insertions(+), 15 deletions(-) rename frontend/src/container/Licenses/{applyFormStyles.ts => styles.ts} (100%) diff --git a/frontend/src/container/Licenses/ApplyLicenseForm.tsx b/frontend/src/container/Licenses/ApplyLicenseForm.tsx index 898ae6d78c..858bf38d54 100644 --- a/frontend/src/container/Licenses/ApplyLicenseForm.tsx +++ b/frontend/src/container/Licenses/ApplyLicenseForm.tsx @@ -1,15 +1,30 @@ import { Button, Input, notification } from 'antd'; import FormItem from 'antd/lib/form/FormItem'; +import getFeaturesFlags from 'api/features/getFeatureFlags'; import apply from 'api/licenses/apply'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { QueryObserverResult, RefetchOptions, useQuery } from 'react-query'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AppAction, UPDATE_FEATURE_FLAGS } from 'types/actions/app'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps } from 'types/api/licenses/getAll'; -import { ApplyForm, ApplyFormContainer, LicenseInput } from './applyFormStyles'; +import { ApplyForm, ApplyFormContainer, LicenseInput } from './styles'; -function ApplyLicenseForm(): JSX.Element { +function ApplyLicenseForm({ + licenseRefetch, +}: ApplyLicenseFormProps): JSX.Element { const { t } = useTranslation(['licenses']); const [key, setKey] = useState(''); const [loading, setLoading] = useState(false); + const dispatch = useDispatch>(); + const { refetch } = useQuery({ + queryFn: getFeaturesFlags, + queryKey: 'getFeatureFlags', + enabled: false, + }); const onFinish = async (values: unknown | { key: string }): Promise => { const params = values as { key: string }; @@ -28,6 +43,16 @@ function ApplyLicenseForm(): JSX.Element { }); if (response.statusCode === 200) { + const [featureFlagsResponse] = await Promise.all([ + refetch(), + licenseRefetch(), + ]); + if (featureFlagsResponse.data?.payload) { + dispatch({ + type: UPDATE_FEATURE_FLAGS, + payload: featureFlagsResponse.data.payload, + }); + } notification.success({ message: 'Success', description: t('license_applied'), @@ -74,4 +99,12 @@ function ApplyLicenseForm(): JSX.Element { ); } +interface ApplyLicenseFormProps { + licenseRefetch: ( + options?: RefetchOptions, + ) => Promise< + QueryObserverResult | ErrorResponse, unknown> + >; +} + export default ApplyLicenseForm; diff --git a/frontend/src/container/Licenses/ListLicenses.tsx b/frontend/src/container/Licenses/ListLicenses.tsx index ba19fe9179..d9a994cfbf 100644 --- a/frontend/src/container/Licenses/ListLicenses.tsx +++ b/frontend/src/container/Licenses/ListLicenses.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/display-name */ import { Table } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import React from 'react'; diff --git a/frontend/src/container/Licenses/index.tsx b/frontend/src/container/Licenses/index.tsx index 04b20b9927..b326a5b0e7 100644 --- a/frontend/src/container/Licenses/index.tsx +++ b/frontend/src/container/Licenses/index.tsx @@ -1,9 +1,9 @@ import { Tabs, Typography } from 'antd'; import getAll from 'api/licenses/getAll'; import Spinner from 'components/Spinner'; -import useFetch from 'hooks/useFetch'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useQuery } from 'react-query'; import ApplyLicenseForm from './ApplyLicenseForm'; import ListLicenses from './ListLicenses'; @@ -12,29 +12,31 @@ const { TabPane } = Tabs; function Licenses(): JSX.Element { const { t } = useTranslation(['licenses']); - const { loading, payload, error, errorMessage } = useFetch(getAll); + const { data, isError, isLoading, refetch } = useQuery({ + queryFn: getAll, + queryKey: 'getAllLicenses', + }); - if (error) { - return {errorMessage}; + if (isError || data?.error) { + return {data?.error}; } - if (loading || payload === undefined) { + if (isLoading || data?.payload === undefined) { return ; } + const allValidLicense = + data?.payload?.filter((license) => license.isCurrent) || []; + return ( - - l.isCurrent === true) : []} - /> + + - l.isCurrent === false) : []} - /> + ); diff --git a/frontend/src/container/Licenses/applyFormStyles.ts b/frontend/src/container/Licenses/styles.ts similarity index 100% rename from frontend/src/container/Licenses/applyFormStyles.ts rename to frontend/src/container/Licenses/styles.ts From 7ebc94c2732e2aced8a6861dae67fc9663d36fe5 Mon Sep 17 00:00:00 2001 From: Ankit Nayan Date: Wed, 23 Nov 2022 16:44:47 +0530 Subject: [PATCH 09/27] display message updated (#1744) * display message updated * chore: display message changed --- frontend/public/locales/en-GB/licenses.json | 4 ++-- frontend/public/locales/en/licenses.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/public/locales/en-GB/licenses.json b/frontend/public/locales/en-GB/licenses.json index 5d46685f9d..ed7c3eea47 100644 --- a/frontend/public/locales/en-GB/licenses.json +++ b/frontend/public/locales/en-GB/licenses.json @@ -9,5 +9,5 @@ "tab_license_history": "History", "loading_licenses": "Loading licenses...", "enter_license_key": "Please enter a license key", - "license_applied": "License applied successfully, please refresh the page to see changes." -} \ No newline at end of file + "license_applied": "License applied successfully" +} diff --git a/frontend/public/locales/en/licenses.json b/frontend/public/locales/en/licenses.json index 5d46685f9d..ed7c3eea47 100644 --- a/frontend/public/locales/en/licenses.json +++ b/frontend/public/locales/en/licenses.json @@ -9,5 +9,5 @@ "tab_license_history": "History", "loading_licenses": "Loading licenses...", "enter_license_key": "Please enter a license key", - "license_applied": "License applied successfully, please refresh the page to see changes." -} \ No newline at end of file + "license_applied": "License applied successfully" +} From 88af45691569450e7e360dbc125d9664363397a6 Mon Sep 17 00:00:00 2001 From: Ankit Nayan Date: Wed, 23 Nov 2022 16:57:49 +0530 Subject: [PATCH 10/27] chore: detect first registration --- pkg/query-service/auth/auth.go | 4 ++-- pkg/query-service/dao/interface.go | 2 +- pkg/query-service/dao/sqlite/rbac.go | 12 +++++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go index ccb994b126..082c9f36de 100644 --- a/pkg/query-service/auth/auth.go +++ b/pkg/query-service/auth/auth.go @@ -259,7 +259,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User, OrgId: org.Id, } - return dao.DB().CreateUser(ctx, user) + return dao.DB().CreateUser(ctx, user, true) } // RegisterInvitedUser handles registering a invited user @@ -338,7 +338,7 @@ func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword b } // TODO(Ahsan): Ideally create user and delete invitation should happen in a txn. - user, apiErr = dao.DB().CreateUser(ctx, user) + user, apiErr = dao.DB().CreateUser(ctx, user, false) if apiErr != nil { zap.S().Debugf("CreateUser failed, err: %v\n", apiErr.Err) return nil, apiErr diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index 40ba17988e..a9a41d755c 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -37,7 +37,7 @@ type Mutations interface { CreateInviteEntry(ctx context.Context, req *model.InvitationObject) *model.ApiError DeleteInvitation(ctx context.Context, email string) *model.ApiError - CreateUser(ctx context.Context, user *model.User) (*model.User, *model.ApiError) + CreateUser(ctx context.Context, user *model.User, isFirstUser bool) (*model.User, *model.ApiError) EditUser(ctx context.Context, update *model.User) (*model.User, *model.ApiError) DeleteUser(ctx context.Context, id string) *model.ApiError diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go index d803f03cdd..81b2a79cf4 100644 --- a/pkg/query-service/dao/sqlite/rbac.go +++ b/pkg/query-service/dao/sqlite/rbac.go @@ -176,7 +176,7 @@ func (mds *ModelDaoSqlite) DeleteOrg(ctx context.Context, id string) *model.ApiE } func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, - user *model.User) (*model.User, *model.ApiError) { + user *model.User, isFirstUser bool) (*model.User, *model.ApiError) { _, err := mds.db.ExecContext(ctx, `INSERT INTO users (id, name, email, password, created_at, profile_picture_url, group_id, org_id) @@ -190,9 +190,15 @@ func (mds *ModelDaoSqlite) CreateUser(ctx context.Context, } data := map[string]interface{}{ - "name": user.Name, - "email": user.Email, + "name": user.Name, + "email": user.Email, + "firstRegistration": false, } + + if isFirstUser { + data["firstRegistration"] = true + } + telemetry.GetInstance().IdentifyUser(user) telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER, data) From 00863e54deda1d0fa81a63725f1afc3967a9c45b Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Wed, 23 Nov 2022 18:49:03 +0530 Subject: [PATCH 11/27] feat: added ch query support (#1735) * feat: added ch query support * fix: added new vars to resolve alert query format issue * fix: replaced timestamp vars in metric query range Co-authored-by: Pranay Prateek Co-authored-by: Srikanth Chekuri --- pkg/query-service/app/http_handler.go | 6 + pkg/query-service/rules/apiParams.go | 3 +- pkg/query-service/rules/thresholdRule.go | 144 +++++++++++++++--- pkg/query-service/utils/queryTemplate/vars.go | 24 +++ 4 files changed, 151 insertions(+), 26 deletions(-) create mode 100644 pkg/query-service/utils/queryTemplate/vars.go diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 18d2743924..fcf25b93f0 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -24,6 +24,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/parser" "go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/constants" + querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate" "go.signoz.io/signoz/pkg/query-service/dao" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" @@ -652,11 +653,16 @@ func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request return } var query bytes.Buffer + + // replace go template variables + querytemplate.AssignReservedVars(metricsQueryRangeParams) + err = tmpl.Execute(&query, metricsQueryRangeParams.Variables) if err != nil { RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) return } + queries[name] = query.String() } seriesList, err, errQuriesByName = execClickHouseQueries(queries) diff --git a/pkg/query-service/rules/apiParams.go b/pkg/query-service/rules/apiParams.go index bf4c41a17a..aad672c99a 100644 --- a/pkg/query-service/rules/apiParams.go +++ b/pkg/query-service/rules/apiParams.go @@ -32,6 +32,7 @@ func newApiErrorBadData(err error) *model.ApiError { // PostableRule is used to create alerting rule from HTTP api type PostableRule struct { Alert string `yaml:"alert,omitempty" json:"alert,omitempty"` + AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` @@ -92,7 +93,7 @@ func parseIntoRule(initRule PostableRule, content []byte, kind string) (*Postabl CompositeMetricQuery: &model.CompositeMetricQuery{ QueryType: model.PROM, PromQueries: map[string]*model.PromQuery{ - "A": &model.PromQuery{ + "A": { Query: rule.Expr, }, }, diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index 0ce8d9317b..3d08f70d35 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -1,12 +1,14 @@ package rules import ( + "bytes" "context" "fmt" "math" "reflect" "sort" "sync" + "text/template" "time" "go.uber.org/zap" @@ -16,6 +18,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/constants" qsmodel "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/utils/labels" + querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate" "go.signoz.io/signoz/pkg/query-service/utils/times" "go.signoz.io/signoz/pkg/query-service/utils/timestamp" "go.signoz.io/signoz/pkg/query-service/utils/value" @@ -320,6 +323,7 @@ func (r *ThresholdRule) CheckCondition(v float64) bool { return false } + zap.S().Debugf("target:", v, *r.ruleCondition.Target) switch r.ruleCondition.CompareOp { case ValueIsEq: return v == *r.ruleCondition.Target @@ -336,17 +340,21 @@ func (r *ThresholdRule) CheckCondition(v float64) bool { func (r *ThresholdRule) prepareQueryRange(ts time.Time) *qsmodel.QueryRangeParamsV2 { // todo(amol): add 30 seconds to evalWindow for rate calc - tsEnd := ts.UnixNano() / int64(time.Millisecond) - tsStart := ts.Add(-time.Duration(r.evalWindow)).UnixNano() / int64(time.Millisecond) - // for k, v := range r.ruleCondition.CompositeMetricQuery.BuilderQueries { - // v.ReduceTo = qsmodel.RMAX - // r.ruleCondition.CompositeMetricQuery.BuilderQueries[k] = v - // } + if r.ruleCondition.QueryType() == qsmodel.CLICKHOUSE { + return &qsmodel.QueryRangeParamsV2{ + Start: ts.UnixMilli(), + End: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), + Step: 30, + CompositeMetricQuery: r.ruleCondition.CompositeMetricQuery, + Variables: make(map[string]interface{}, 0), + } + } + // default mode return &qsmodel.QueryRangeParamsV2{ - Start: tsStart, - End: tsEnd, + Start: ts.UnixMilli(), + End: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), Step: 30, CompositeMetricQuery: r.ruleCondition.CompositeMetricQuery, } @@ -384,6 +392,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer // but we dont know when the rates are being used // so we always pick timeframe - 30 seconds interval // and skip the first record for a given label combo + // NOTE: this is not applicable for raw queries skipFirstRecord := make(map[uint64]bool, 0) defer rows.Close() @@ -406,7 +415,7 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer case *time.Time: timval := *v - if colName == "ts" { + if colName == "ts" || colName == "interval" { sample.Point.T = timval.Unix() } else { lbls.Set(colName, timval.Format("2006-01-02 15:04:05")) @@ -478,12 +487,24 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer } } else { - if exists, _ := skipFirstRecord[labelHash]; exists { - resultMap[labelHash] = sample + if r.Condition().QueryType() == qsmodel.QUERY_BUILDER { + // for query builder, time series data + // we skip the first record to support rate cases correctly + // improvement(amol): explore approaches to limit this only for + // rate uses cases + if exists, _ := skipFirstRecord[labelHash]; exists { + resultMap[labelHash] = sample + } else { + // looks like the first record for this label combo, skip it + skipFirstRecord[labelHash] = true + } } else { - // looks like the first record for this label combo, skip it - skipFirstRecord[labelHash] = true + // for clickhouse raw queries, all records are considered + // improvement(amol): think about supporting rate queries + // written by user. may have to skip a record, similar to qb case(above) + resultMap[labelHash] = sample } + } } zap.S().Debugf("ruleid:", r.ID(), "\t resultmap(potential alerts):", len(resultMap)) @@ -499,32 +520,105 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer return result, nil } +func (r *ThresholdRule) prepareBuilderQueries(ts time.Time) (map[string]string, error) { + params := r.prepareQueryRange(ts) + runQueries := metrics.PrepareBuilderMetricQueries(params, constants.SIGNOZ_TIMESERIES_TABLENAME) + + return runQueries.Queries, runQueries.Err +} + +func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]string, error) { + queries := make(map[string]string) + + if r.ruleCondition == nil { + return nil, fmt.Errorf("rule condition is empty") + } + + if r.ruleCondition.QueryType() != qsmodel.CLICKHOUSE { + zap.S().Debugf("ruleid:", r.ID(), "\t msg: unsupported query type in prepareClickhouseQueries()") + return nil, fmt.Errorf("failed to prepare clickhouse queries") + } + + params := r.prepareQueryRange(ts) + + // replace reserved go template variables + querytemplate.AssignReservedVars(params) + + for name, chQuery := range r.ruleCondition.CompositeMetricQuery.ClickHouseQueries { + if chQuery.Disabled { + continue + } + tmpl := template.New("clickhouse-query") + tmpl, err := tmpl.Parse(chQuery.Query) + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to parse clickhouse query to populate vars", err) + r.SetHealth(HealthBad) + return nil, err + } + var query bytes.Buffer + err = tmpl.Execute(&query, params.Variables) + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to populate clickhouse query", err) + r.SetHealth(HealthBad) + return nil, err + } + zap.S().Debugf("ruleid:", r.ID(), "\t query:", query.String()) + queries[name] = query.String() + } + return queries, nil +} + // query looks if alert condition is being // satisfied and returns the signals func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch clickhouse.Conn) (Vector, error) { - params := r.prepareQueryRange(ts) - - runQueries := metrics.PrepareBuilderMetricQueries(params, constants.SIGNOZ_TIMESERIES_TABLENAME) - if runQueries.Err != nil { - return nil, fmt.Errorf("failed to prepare metric queries: %v", runQueries.Err) + if r.ruleCondition == nil || r.ruleCondition.CompositeMetricQuery == nil { + r.SetHealth(HealthBad) + return nil, fmt.Errorf("invalid rule condition") } - if len(runQueries.Queries) == 0 { + // var to hold target query to be executed + queries := make(map[string]string) + var err error + + // fetch the target query based on query type + if r.ruleCondition.QueryType() == qsmodel.QUERY_BUILDER { + + queries, err = r.prepareBuilderQueries(ts) + + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to prepare metric queries", zap.Error(err)) + return nil, fmt.Errorf("failed to prepare metric queries") + } + + } else if r.ruleCondition.QueryType() == qsmodel.CLICKHOUSE { + + queries, err = r.prepareClickhouseQueries(ts) + + if err != nil { + zap.S().Errorf("ruleid:", r.ID(), "\t msg: failed to prepare clickhouse queries", zap.Error(err)) + return nil, fmt.Errorf("failed to prepare clickhouse queries") + } + + } else { + return nil, fmt.Errorf("unexpected rule condition - query type is empty") + } + + if len(queries) == 0 { return nil, fmt.Errorf("no queries could be built with the rule config") } - zap.S().Debugf("ruleid:", r.ID(), "\t runQueries:", runQueries.Queries) + zap.S().Debugf("ruleid:", r.ID(), "\t runQueries:", queries) // find target query label - if query, ok := runQueries.Queries["F1"]; ok { + if query, ok := queries["F1"]; ok { // found a formula query, run with it return r.runChQuery(ctx, ch, query) } // no formula in rule condition, now look for // query label with max ascii val - keys := make([]string, 0, len(runQueries.Queries)) - for k := range runQueries.Queries { + keys := make([]string, 0, len(queries)) + for k := range queries { keys = append(keys, k) } sort.Strings(keys) @@ -533,11 +627,11 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch c zap.S().Debugf("ruleId: ", r.ID(), "\t result query label:", queryLabel) - if queryString, ok := runQueries.Queries[queryLabel]; ok { + if queryString, ok := queries[queryLabel]; ok { return r.runChQuery(ctx, ch, queryString) } - zap.S().Errorf("ruleId: ", r.ID(), "\t invalid query label:", queryLabel, "\t queries:", runQueries.Queries) + zap.S().Errorf("ruleId: ", r.ID(), "\t invalid query label:", queryLabel, "\t queries:", queries) return nil, fmt.Errorf("this is unexpected, invalid query label") } diff --git a/pkg/query-service/utils/queryTemplate/vars.go b/pkg/query-service/utils/queryTemplate/vars.go new file mode 100644 index 0000000000..093977aa01 --- /dev/null +++ b/pkg/query-service/utils/queryTemplate/vars.go @@ -0,0 +1,24 @@ +package querytemplate + +import ( + "fmt" + + "go.signoz.io/signoz/pkg/query-service/model" +) + +// AssignReservedVars assigns values for go template vars. assumes that +// model.QueryRangeParamsV2.Start and End are Unix Nano timestamps +func AssignReservedVars(metricsQueryRangeParams *model.QueryRangeParamsV2) { + metricsQueryRangeParams.Variables["start_timestamp"] = metricsQueryRangeParams.Start / 1000 + metricsQueryRangeParams.Variables["end_timestamp"] = metricsQueryRangeParams.End / 1000 + + metricsQueryRangeParams.Variables["start_timestamp_ms"] = metricsQueryRangeParams.Start + metricsQueryRangeParams.Variables["end_timestamp_ms"] = metricsQueryRangeParams.End + + metricsQueryRangeParams.Variables["start_timestamp_nano"] = metricsQueryRangeParams.Start * 1e6 + metricsQueryRangeParams.Variables["end_timestamp_nano"] = metricsQueryRangeParams.End * 1e6 + + metricsQueryRangeParams.Variables["start_datetime"] = fmt.Sprintf("toDateTime(%d)", metricsQueryRangeParams.Start/1000) + metricsQueryRangeParams.Variables["end_datetime"] = fmt.Sprintf("toDateTime(%d)", metricsQueryRangeParams.End/1000) + +} From 4727dbc9f067a9fb94da2ef00f67ab3f9389e524 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 24 Nov 2022 00:08:56 +0530 Subject: [PATCH 12/27] fix: if invalid switch is disabled (#1656) Co-authored-by: Ankit Nayan --- .../AuthDomains/Switch/index.tsx | 4 ++-- .../AuthDomains/index.tsx | 9 +-------- .../OrganizationSettings/AuthDomains/utils.ts | 19 +++++++++++++------ frontend/src/types/api/SAML/listDomain.ts | 12 +++++++----- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/frontend/src/container/OrganizationSettings/AuthDomains/Switch/index.tsx b/frontend/src/container/OrganizationSettings/AuthDomains/Switch/index.tsx index b305517429..fc24014830 100644 --- a/frontend/src/container/OrganizationSettings/AuthDomains/Switch/index.tsx +++ b/frontend/src/container/OrganizationSettings/AuthDomains/Switch/index.tsx @@ -25,7 +25,7 @@ function SwitchComponent({ setIsLoading(false); }; - const isInValidCertificate = useMemo( + const isInValidVerificate = useMemo( () => !getIsValidCertificate(record?.samlConfig), [record], ); @@ -33,7 +33,7 @@ function SwitchComponent({ return ( diff --git a/frontend/src/container/OrganizationSettings/AuthDomains/index.tsx b/frontend/src/container/OrganizationSettings/AuthDomains/index.tsx index 221ba963dc..3ae10eb859 100644 --- a/frontend/src/container/OrganizationSettings/AuthDomains/index.tsx +++ b/frontend/src/container/OrganizationSettings/AuthDomains/index.tsx @@ -1,5 +1,5 @@ import { LockTwoTone } from '@ant-design/icons'; -import { Button, Modal, notification, Space, Table, Typography } from 'antd'; +import { Button, Modal, notification, Space, Table } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import deleteDomain from 'api/SAML/deleteDomain'; import listAllDomain from 'api/SAML/listAllDomain'; @@ -20,7 +20,6 @@ import AddDomain from './AddDomain'; import Create from './Create'; import EditSaml from './Edit'; import SwitchComponent from './Switch'; -import { getIsValidCertificate } from './utils'; function AuthDomains(): JSX.Element { const { t } = useTranslation(['common', 'organizationsettings']); @@ -196,12 +195,6 @@ function AuthDomains(): JSX.Element { ); } - const isValidCertificate = getIsValidCertificate(record.samlConfig); - - if (!isValidCertificate) { - return Configure SSO  ; - } - return ( + )} + + } + > + + + + ); + case AlertTypes.METRICS_BASED_ALERT: + default: + return ( + {queryCategory === EQueryType.CLICKHOUSE && ( + + )} + + } > + - - {queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()} + ); + } + }; + const renderQuerySection = (c: EQueryType): JSX.Element | null => { + switch (c) { + case EQueryType.PROM: + return renderPromqlUI(); + case EQueryType.CLICKHOUSE: + return renderChQueryUI(); + case EQueryType.QUERY_BUILDER: + return renderMetricUI(); + default: + return null; + } + }; + return ( + <> + {t('alert_form_step1')} + +
{renderTabs(alertType)}
+ {renderQuerySection(queryCategory)}
); @@ -289,6 +375,10 @@ interface QuerySectionProps { setFormulaQueries: (b: IFormulaQueries) => void; promQueries: IPromQueries; setPromQueries: (p: IPromQueries) => void; + chQueries: IChQueries; + setChQueries: (q: IChQueries) => void; + alertType: AlertTypes; + runQuery: () => void; } export default QuerySection; diff --git a/frontend/src/container/FormAlertRules/UserGuide/index.tsx b/frontend/src/container/FormAlertRules/UserGuide/index.tsx index 1cf5dac163..d24ac82cb2 100644 --- a/frontend/src/container/FormAlertRules/UserGuide/index.tsx +++ b/frontend/src/container/FormAlertRules/UserGuide/index.tsx @@ -1,7 +1,7 @@ import { Col, Row, Typography } from 'antd'; import TextToolTip from 'components/TextToolTip'; import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { Trans, useTranslation } from 'react-i18next'; import { EQueryType } from 'types/common/dashboard'; import { @@ -106,6 +106,63 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element { ); }; + const renderStep1CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step1')} + + + , + ]} + /> + + {t('user_guide_ch_step1b')} + + + ); + }; + const renderStep2CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step2')} + + {t('user_guide_ch_step2a')} + {t('user_guide_ch_step2b')} + + + ); + }; + + const renderStep3CH = (): JSX.Element => { + return ( + <> + {t('user_guide_ch_step3')} + + {t('user_guide_ch_step3a')} + {t('user_guide_ch_step3b')} + + + ); + }; + + const renderGuideForCH = (): JSX.Element => { + return ( + <> + {renderStep1CH()} + {renderStep2CH()} + {renderStep3CH()} + + ); + }; return ( @@ -121,6 +178,7 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element { {queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()} {queryType === EQueryType.PROM && renderGuideForPQL()} + {queryType === EQueryType.CLICKHOUSE && renderGuideForCH()} ); } diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 022e913f8e..a0791a4aa1 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -9,7 +9,9 @@ import history from 'lib/history'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQueryClient } from 'react-query'; +import { AlertTypes } from 'types/api/alerts/alertTypes'; import { + IChQueries, IFormulaQueries, IMetricQueries, IPromQueries, @@ -45,6 +47,7 @@ import { } from './utils'; function FormAlertRules({ + alertType, formInstance, initialValue, ruleId, @@ -57,6 +60,10 @@ function FormAlertRules({ const [loading, setLoading] = useState(false); + // queryRunId helps to override of query caching for clickhouse query + // tab. A random string will be assigned for each execution + const [runQueryId, setRunQueryId] = useState(); + // alertDef holds the form values to be posted const [alertDef, setAlertDef] = useState(initialValue); @@ -82,9 +89,31 @@ function FormAlertRules({ ...initQuery?.promQueries, }); - // staged query is used to display chart preview + // local state to handle promql queries + const [chQueries, setChQueries] = useState({ + ...initQuery?.chQueries, + }); + + // staged query is used to display chart preview. the query gets + // auto refreshed when any of the params in query section change. + // though this is the source of chart data, the final query used + // by chart will be either debouncedStagedQuery or manualStagedQuery + // depending on the run option (auto-run or use of run query button) const [stagedQuery, setStagedQuery] = useState(); - const debouncedStagedQuery = useDebounce(stagedQuery, 1000); + + // manualStagedQuery requires manual staging of query + // when user clicks run query button. Useful for clickhouse tab where + // run query button is provided. + const [manualStagedQuery, setManualStagedQuery] = useState(); + + // delay to reduce load on backend api with auto-run query. only for clickhouse + // queries we have manual run, hence both debounce and debounceStagedQuery are not required + const debounceDelay = queryCategory !== EQueryType.CLICKHOUSE ? 1000 : 0; + + // debounce query to delay backend api call and chart update. + // used in query builder and promql tabs to enable auto-refresh + // of chart on user edit + const debouncedStagedQuery = useDebounce(stagedQuery, debounceDelay); // this use effect initiates staged query and // other queries based on server data. @@ -101,14 +130,26 @@ function FormAlertRules({ const fq = toFormulaQueries(initQuery?.builderQueries); // prepare staged query - const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries); + const sq = prepareStagedQuery( + typ, + mq, + fq, + initQuery?.promQueries, + initQuery?.chQueries, + ); const pq = initQuery?.promQueries; + const chq = initQuery?.chQueries; setQueryCategory(typ); setMetricQueries(mq); setFormulaQueries(fq); setPromQueries(pq); setStagedQuery(sq); + + // also set manually staged query + setManualStagedQuery(sq); + + setChQueries(chq); setAlertDef(initialValue); }, [initialValue]); @@ -121,9 +162,15 @@ function FormAlertRules({ metricQueries, formulaQueries, promQueries, + chQueries, ); setStagedQuery(sq); - }, [queryCategory, metricQueries, formulaQueries, promQueries]); + }, [queryCategory, chQueries, metricQueries, formulaQueries, promQueries]); + + const onRunQuery = (): void => { + setRunQueryId(Math.random().toString(36).substring(2, 15)); + setManualStagedQuery(stagedQuery); + }; const onCancelHandler = useCallback(() => { history.replace(ROUTES.LIST_ALL_ALERT); @@ -169,6 +216,31 @@ function FormAlertRules({ return retval; }, [t, promQueries, queryCategory]); + const validateChQueryParams = useCallback((): boolean => { + let retval = true; + if (queryCategory !== EQueryType.CLICKHOUSE) return retval; + + if (!chQueries || Object.keys(chQueries).length === 0) { + notification.error({ + message: 'Error', + description: t('chquery_required'), + }); + return false; + } + + Object.keys(chQueries).forEach((key) => { + if (chQueries[key].rawQuery === '') { + notification.error({ + message: 'Error', + description: t('chquery_required'), + }); + retval = false; + } + }); + + return retval; + }, [t, chQueries, queryCategory]); + const validateQBParams = useCallback((): boolean => { let retval = true; if (queryCategory !== EQueryType.QUERY_BUILDER) return true; @@ -224,12 +296,17 @@ function FormAlertRules({ return false; } + if (!validateChQueryParams()) { + return false; + } + return validateQBParams(); - }, [t, validateQBParams, alertDef, validatePromParams]); + }, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]); const preparePostData = (): AlertDef => { const postableAlert: AlertDef = { ...alertDef, + alertType, source: window?.location.toString(), ruleType: queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule', @@ -238,6 +315,7 @@ function FormAlertRules({ compositeMetricQuery: { builderQueries: prepareBuilderQueries(metricQueries, formulaQueries), promQueries, + chQueries, queryType: queryCategory, }, }, @@ -251,6 +329,8 @@ function FormAlertRules({ metricQueries, formulaQueries, promQueries, + chQueries, + alertType, ]); const saveRule = useCallback(async () => { @@ -380,6 +460,17 @@ function FormAlertRules({ ); }; + const renderChQueryChartPreview = (): JSX.Element => { + return ( + } + name="Chart Preview" + threshold={alertDef.condition?.target} + query={manualStagedQuery} + userQueryKey={runQueryId} + /> + ); + }; return ( <> {Element} @@ -392,6 +483,7 @@ function FormAlertRules({ > {queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()} {queryCategory === EQueryType.PROM && renderPromChartPreview()} + {queryCategory === EQueryType.CLICKHOUSE && renderChQueryChartPreview()} { const qbList: IMetricQuery[] = []; const formulaList: IFormulaQuery[] = []; const promList: IPromQuery[] = []; + const chQueryList: IChQuery[] = []; // convert map[string]IMetricQuery to IMetricQuery[] if (m) { @@ -101,6 +105,13 @@ export const prepareStagedQuery = ( promList.push({ ...p[key], name: key }); }); } + // convert map[string]IChQuery to IChQuery[] + if (c) { + Object.keys(c).forEach((key) => { + console.log('c:', c[key]); + chQueryList.push({ ...c[key], name: key, rawQuery: c[key].query }); + }); + } return { queryType: t, @@ -109,7 +120,7 @@ export const prepareStagedQuery = ( formulas: formulaList, queryBuilder: qbList, }, - clickHouse: [], + clickHouse: chQueryList, }; }; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx index b2712c86ae..95b0fa8bfd 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/index.tsx @@ -27,24 +27,34 @@ function ClickHouseQueryContainer({ toggleDisable, toggleDelete, }: IClickHouseQueryHandleChange): void => { - const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; - const currentIndexQuery = allQueries[queryIndex]; + // we must check if queryIndex is number type. because - + // ClickHouseQueryBuilder.handleQueryChange has a queryIndex + // parameter which supports both number and string formats. + // it is because, the dashboard side of query builder has queryIndex as number + // while the alert builder uses string format for query index (similar to backend) + // hence, this method is only applies when queryIndex is in number format. - if (rawQuery !== undefined) { - currentIndexQuery.rawQuery = rawQuery; - } + if (typeof queryIndex === 'number') { + const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]; - if (legend !== undefined) { - currentIndexQuery.legend = legend; - } + const currentIndexQuery = allQueries[queryIndex]; - if (toggleDisable) { - currentIndexQuery.disabled = !currentIndexQuery.disabled; + if (rawQuery !== undefined) { + currentIndexQuery.rawQuery = rawQuery; + } + + if (legend !== undefined) { + currentIndexQuery.legend = legend; + } + + if (toggleDisable) { + currentIndexQuery.disabled = !currentIndexQuery.disabled; + } + if (toggleDelete) { + allQueries.splice(queryIndex, 1); + } + updateQueryData({ updatedQuery: { ...queryData } }); } - if (toggleDelete) { - allQueries.splice(queryIndex, 1); - } - updateQueryData({ updatedQuery: { ...queryData } }); }; const addQueryHandler = (): void => { queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({ diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx index 7e9a3df0c2..0ace45fdc8 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx @@ -8,7 +8,7 @@ import { IClickHouseQueryHandleChange } from './types'; interface IClickHouseQueryBuilderProps { queryData: IClickHouseQuery; - queryIndex: number; + queryIndex: number | string; handleQueryChange: (args: IClickHouseQueryHandleChange) => void; } @@ -43,6 +43,9 @@ function ClickHouseQueryBuilder({ scrollbar: { alwaysConsumeMouseWheel: false, }, + minimap: { + enabled: false, + }, }} /> ; + return ; } export default CreateAlertPage; diff --git a/frontend/src/types/api/alerts/alertTypes.ts b/frontend/src/types/api/alerts/alertTypes.ts new file mode 100644 index 0000000000..ed80bb5168 --- /dev/null +++ b/frontend/src/types/api/alerts/alertTypes.ts @@ -0,0 +1,7 @@ +// this list must exactly match with the backend +export enum AlertTypes { + NONE = 'NONE', + METRICS_BASED_ALERT = 'METRIC_BASED_ALERT', + LOGS_BASED_ALERT = 'LOGS_BASED_ALERT', + TRACES_BASED_ALERT = 'TRACES_BASED_ALERT', +} diff --git a/frontend/src/types/api/alerts/compositeQuery.ts b/frontend/src/types/api/alerts/compositeQuery.ts index 42c0c5a753..864e4aa163 100644 --- a/frontend/src/types/api/alerts/compositeQuery.ts +++ b/frontend/src/types/api/alerts/compositeQuery.ts @@ -1,4 +1,5 @@ import { + IClickHouseQuery, IMetricsBuilderFormula, IMetricsBuilderQuery, IPromQLQuery, @@ -9,17 +10,25 @@ import { EAggregateOperator, EQueryType } from 'types/common/dashboard'; export interface ICompositeMetricQuery { builderQueries: IBuilderQueries; promQueries: IPromQueries; + chQueries: IChQueries; queryType: EQueryType; } -export interface IPromQueries { - [key: string]: IPromQuery; +export interface IChQueries { + [key: string]: IChQuery; +} + +export interface IChQuery extends IClickHouseQuery { + query: string; } export interface IPromQuery extends IPromQLQuery { stats?: ''; } +export interface IPromQueries { + [key: string]: IPromQuery; +} export interface IBuilderQueries { [key: string]: IBuilderQuery; } diff --git a/frontend/src/types/api/alerts/create.ts b/frontend/src/types/api/alerts/create.ts index 6f179af79a..41a4953c48 100644 --- a/frontend/src/types/api/alerts/create.ts +++ b/frontend/src/types/api/alerts/create.ts @@ -1,7 +1,5 @@ import { AlertDef } from 'types/api/alerts/def'; -import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def'; - export interface Props { data: AlertDef; } @@ -10,39 +8,3 @@ export interface PayloadProps { status: string; data: string; } - -export const alertDefaults: AlertDef = { - condition: { - compositeMetricQuery: { - builderQueries: { - A: { - queryName: 'A', - name: 'A', - formulaOnly: false, - metricName: '', - tagFilters: { - op: 'AND', - items: [], - }, - groupBy: [], - aggregateOperator: 1, - expression: 'A', - disabled: false, - toggleDisable: false, - toggleDelete: false, - }, - }, - promQueries: {}, - queryType: 1, - }, - op: defaultCompareOp, - matchType: defaultMatchType, - }, - labels: { - severity: 'warning', - }, - annotations: { - description: 'A new alert', - }, - evalWindow: defaultEvalWindow, -}; diff --git a/frontend/src/types/api/alerts/def.ts b/frontend/src/types/api/alerts/def.ts index f417678ee1..65b3e64af4 100644 --- a/frontend/src/types/api/alerts/def.ts +++ b/frontend/src/types/api/alerts/def.ts @@ -11,6 +11,7 @@ export const defaultCompareOp = '1'; export interface AlertDef { id?: number; + alertType?: string; alert?: string; ruleType?: string; condition: RuleCondition; From b0ec61988100266b3ccd23e4644a41605cea85fd Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Thu, 24 Nov 2022 16:25:26 +0530 Subject: [PATCH 14/27] fix: trace table pagination (#1749) * fix: trace table pagination * chore: refactor * chore: refactor Co-authored-by: Palash Gupta --- .../src/container/Trace/TraceTable/index.tsx | 18 ++++++------------ .../src/container/Trace/TraceTable/util.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 frontend/src/container/Trace/TraceTable/util.ts diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index d66dd37592..aa9fc1cf6b 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -1,6 +1,10 @@ import { TableProps, Tag, Typography } from 'antd'; import Table, { ColumnsType } from 'antd/lib/table'; import ROUTES from 'constants/routes'; +import { + getSpanOrder, + getSpanOrderParam, +} from 'container/Trace/TraceTable/util'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import history from 'lib/history'; @@ -111,16 +115,6 @@ function TraceTable(): JSX.Element { }, ]; - const getSortKey = (key: string): string => { - if (key === 'durationNano') { - return 'duration'; - } - if (key === 'timestamp') { - return 'timestamp'; - } - return ''; - }; - const onChangeHandler: TableProps['onChange'] = ( props, _, @@ -129,8 +123,8 @@ function TraceTable(): JSX.Element { if (!Array.isArray(sort)) { const { order = spansAggregateOrder } = sort; if (props.current && props.pageSize) { - const spanOrder = order === 'ascend' ? 'ascending' : 'descending'; - const orderParam = getSortKey(sort.field as string); + const spanOrder = getSpanOrder(order || ''); + const orderParam = getSpanOrderParam(sort.field as string); dispatch({ type: UPDATE_SPAN_ORDER, diff --git a/frontend/src/container/Trace/TraceTable/util.ts b/frontend/src/container/Trace/TraceTable/util.ts new file mode 100644 index 0000000000..08276636c1 --- /dev/null +++ b/frontend/src/container/Trace/TraceTable/util.ts @@ -0,0 +1,19 @@ +export const getSpanOrderParam = (key: string): string => { + if (key === 'durationNano') { + return 'duration'; + } + if (key === 'timestamp') { + return 'timestamp'; + } + return ''; +}; + +export const getSpanOrder = (order: string): string => { + if (order === 'ascend') { + return 'ascending'; + } + if (order === 'descend') { + return 'descending'; + } + return ''; +}; From 33d34af2a6f926c0326f804a1b32f2baae0c2c60 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Thu, 24 Nov 2022 18:00:02 +0530 Subject: [PATCH 15/27] feat: added exception based alerts (#1752) --- frontend/public/locales/en-GB/alerts.json | 8 +-- frontend/public/locales/en/alerts.json | 8 +-- .../CreateAlertRule/SelectAlertType/index.tsx | 10 +++- .../CreateAlertRule/SelectAlertType/styles.ts | 8 +-- .../src/container/CreateAlertRule/defaults.ts | 51 ++++++++++++++++++- .../src/container/CreateAlertRule/index.tsx | 4 ++ .../container/FormAlertRules/QuerySection.tsx | 1 + .../src/container/FormAlertRules/utils.ts | 1 - frontend/src/types/api/alerts/alertTypes.ts | 1 + 9 files changed, 74 insertions(+), 18 deletions(-) diff --git a/frontend/public/locales/en-GB/alerts.json b/frontend/public/locales/en-GB/alerts.json index fa2d9516fe..b5c769a021 100644 --- a/frontend/public/locales/en-GB/alerts.json +++ b/frontend/public/locales/en-GB/alerts.json @@ -102,9 +102,11 @@ "user_tooltip_more_help": "More details on how to create alerts", "choose_alert_type": "Choose a type for the alert:", "metric_based_alert": "Metric based Alert", - "metric_based_alert_desc": "Send a notification when a condition occurs in metric data", + "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", "log_based_alert": "Log-based Alert", - "log_based_alert_desc": "Send a notification when a condition occurs in logs data.", + "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", "traces_based_alert": "Trace-based Alert", - "traces_based_alert_desc": "Send a notification when a condition occurs in traces data." + "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", + "exceptions_based_alert": "Exceptions-based Alert", + "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data." } \ No newline at end of file diff --git a/frontend/public/locales/en/alerts.json b/frontend/public/locales/en/alerts.json index fa2d9516fe..b5c769a021 100644 --- a/frontend/public/locales/en/alerts.json +++ b/frontend/public/locales/en/alerts.json @@ -102,9 +102,11 @@ "user_tooltip_more_help": "More details on how to create alerts", "choose_alert_type": "Choose a type for the alert:", "metric_based_alert": "Metric based Alert", - "metric_based_alert_desc": "Send a notification when a condition occurs in metric data", + "metric_based_alert_desc": "Send a notification when a condition occurs in the metric data", "log_based_alert": "Log-based Alert", - "log_based_alert_desc": "Send a notification when a condition occurs in logs data.", + "log_based_alert_desc": "Send a notification when a condition occurs in the logs data.", "traces_based_alert": "Trace-based Alert", - "traces_based_alert_desc": "Send a notification when a condition occurs in traces data." + "traces_based_alert_desc": "Send a notification when a condition occurs in the traces data.", + "exceptions_based_alert": "Exceptions-based Alert", + "exceptions_based_alert_desc": "Send a notification when a condition occurs in the exceptions data." } \ No newline at end of file diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx index e892f3ac22..cc2da48727 100644 --- a/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/index.tsx @@ -1,8 +1,9 @@ +import { Row } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { AlertTypes } from 'types/api/alerts/alertTypes'; -import { AlertTypeCard, AlertTypeCards, SelectTypeContainer } from './styles'; +import { AlertTypeCard, SelectTypeContainer } from './styles'; interface OptionType { title: string; @@ -30,6 +31,11 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element { selection: AlertTypes.TRACES_BASED_ALERT, description: t('traces_based_alert_desc'), }, + { + title: t('exceptions_based_alert'), + selection: AlertTypes.EXCEPTIONS_BASED_ALERT, + description: t('exceptions_based_alert_desc'), + }, ]; return ( <> @@ -50,7 +56,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element { return (

{t('choose_alert_type')}

- {renderOptions()} + {renderOptions()}
); } diff --git a/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts b/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts index 9cbde02b5f..9c3323aea3 100644 --- a/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts +++ b/frontend/src/container/CreateAlertRule/SelectAlertType/styles.ts @@ -1,4 +1,4 @@ -import { Card, Row } from 'antd'; +import { Card } from 'antd'; import styled from 'styled-components'; export const SelectTypeContainer = styled.div` @@ -7,12 +7,6 @@ export const SelectTypeContainer = styled.div` } `; -export const AlertTypeCards = styled(Row)` - &&& { - flex-wrap: nowrap; - } -`; - export const AlertTypeCard = styled(Card)` &&& { margin: 5px; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 0172fcdff2..645d1ff454 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -117,8 +117,8 @@ export const traceAlertDefaults: AlertDef = { chQueries: { A: { name: 'A', - rawQuery: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, - query: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, + rawQuery: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\ttagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.signoz_index_v2 \nWHERE tagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, + query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\ttagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.signoz_index_v2 \nWHERE tagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, legend: '', disabled: false, }, @@ -137,3 +137,50 @@ export const traceAlertDefaults: AlertDef = { }, evalWindow: defaultEvalWindow, }; + +export const exceptionAlertDefaults: AlertDef = { + alertType: AlertTypes.EXCEPTIONS_BASED_ALERT, + condition: { + compositeMetricQuery: { + builderQueries: { + A: { + queryName: 'A', + name: 'A', + formulaOnly: false, + metricName: '', + tagFilters: { + op: 'AND', + items: [], + }, + groupBy: [], + aggregateOperator: 1, + expression: 'A', + disabled: false, + toggleDisable: false, + toggleDelete: false, + }, + }, + promQueries: {}, + chQueries: { + A: { + name: 'A', + rawQuery: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, + query: `SELECT \n\tcount() as value,\n\ttoStartOfInterval(timestamp, toIntervalMinute(1)) AS interval,\n\tserviceName\nFROM signoz_traces.signoz_error_index_v2\nWHERE exceptionType !='OSError'\nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}}\nGROUP BY serviceName, interval;\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, + legend: '', + disabled: false, + }, + }, + queryType: 2, + }, + op: defaultCompareOp, + matchType: '4', + }, + labels: { + severity: 'warning', + details: `${window.location.protocol}//${window.location.host}/exceptions`, + }, + annotations: { + description: 'A new exceptions-based alert', + }, + evalWindow: defaultEvalWindow, +}; diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index 101b0cae1b..ae3d21897e 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -5,6 +5,7 @@ import { AlertTypes } from 'types/api/alerts/alertTypes'; import { alertDefaults, + exceptionAlertDefaults, logAlertDefaults, traceAlertDefaults, } from './defaults'; @@ -27,6 +28,9 @@ function CreateRules(): JSX.Element { case AlertTypes.TRACES_BASED_ALERT: setInitValues(traceAlertDefaults); break; + case AlertTypes.EXCEPTIONS_BASED_ALERT: + setInitValues(exceptionAlertDefaults); + break; default: setInitValues(alertDefaults); } diff --git a/frontend/src/container/FormAlertRules/QuerySection.tsx b/frontend/src/container/FormAlertRules/QuerySection.tsx index c7b4053c6e..b5cf9574bd 100644 --- a/frontend/src/container/FormAlertRules/QuerySection.tsx +++ b/frontend/src/container/FormAlertRules/QuerySection.tsx @@ -292,6 +292,7 @@ function QuerySection({ switch (typ) { case AlertTypes.TRACES_BASED_ALERT: case AlertTypes.LOGS_BASED_ALERT: + case AlertTypes.EXCEPTIONS_BASED_ALERT: return ( { - console.log('c:', c[key]); chQueryList.push({ ...c[key], name: key, rawQuery: c[key].query }); }); } diff --git a/frontend/src/types/api/alerts/alertTypes.ts b/frontend/src/types/api/alerts/alertTypes.ts index ed80bb5168..b8ec9f38ef 100644 --- a/frontend/src/types/api/alerts/alertTypes.ts +++ b/frontend/src/types/api/alerts/alertTypes.ts @@ -4,4 +4,5 @@ export enum AlertTypes { METRICS_BASED_ALERT = 'METRIC_BASED_ALERT', LOGS_BASED_ALERT = 'LOGS_BASED_ALERT', TRACES_BASED_ALERT = 'TRACES_BASED_ALERT', + EXCEPTIONS_BASED_ALERT = 'EXCEPTIONS_BASED_ALERT', } From 983ca1ec6a3812eee99724e2e2f55e0978ffaa92 Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Thu, 24 Nov 2022 18:18:19 +0530 Subject: [PATCH 16/27] feat: introduce getSubTreeSpans function in clickhouse query builder & introduce smart trace detail algorithm (#1648) * perf: introduce smart trace detail algorithm * fix: remove hardcoded levels and handle null levels * feat: add support for broken trees * feat: use spanLimit env variable * fix: handle missing root span * add root spanId and root name * use permanent table * add kind, events and tagmap support * fix query formation * increase context timeout to 600s * perf improvement * handle error * return tableName as response to query * support multiple queries tableName * perf: improve memory and latency * feat: add getSubTree custom func and smart trace detail algo to ee * fix: create new functions for ee * chore: refactor codebase * chore: refactor frontend code Co-authored-by: Ankit Nayan Co-authored-by: Palash Gupta --- ee/query-service/app/api/api.go | 3 + ee/query-service/app/api/metrics.go | 236 +++++++++++ ee/query-service/app/api/traces.go | 39 ++ ee/query-service/app/db/metrics.go | 374 ++++++++++++++++++ ee/query-service/app/db/reader.go | 5 +- ee/query-service/app/db/trace.go | 222 +++++++++++ ee/query-service/app/server.go | 2 +- ee/query-service/constants/constants.go | 2 + ee/query-service/model/plans.go | 12 +- ee/query-service/model/trace.go | 22 ++ frontend/src/api/trace/getTraceItem.ts | 11 +- .../src/container/Trace/TraceTable/index.tsx | 7 +- frontend/src/container/TraceDetail/index.tsx | 17 +- frontend/src/container/TraceDetail/utils.ts | 25 ++ frontend/src/pages/TraceDetail/index.tsx | 14 +- frontend/src/types/api/trace/getTraceItem.ts | 7 + go.mod | 7 +- go.sum | 14 +- .../app/clickhouseReader/reader.go | 182 +++++---- pkg/query-service/app/http_handler.go | 17 +- pkg/query-service/app/parser.go | 24 ++ pkg/query-service/app/server.go | 2 +- pkg/query-service/interfaces/interface.go | 3 +- pkg/query-service/model/featureSet.go | 2 + pkg/query-service/model/response.go | 23 +- pkg/query-service/model/response_easyjson.go | 328 +++++++++++++++ 26 files changed, 1480 insertions(+), 120 deletions(-) create mode 100644 ee/query-service/app/api/metrics.go create mode 100644 ee/query-service/app/api/traces.go create mode 100644 ee/query-service/app/db/metrics.go create mode 100644 ee/query-service/app/db/trace.go create mode 100644 ee/query-service/model/trace.go create mode 100644 pkg/query-service/model/response_easyjson.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index a6497b615e..85bec52122 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -114,6 +114,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/invite/{token}", baseapp.OpenAccess(ah.getInvite)).Methods(http.MethodGet) router.HandleFunc("/api/v1/register", baseapp.OpenAccess(ah.registerUser)).Methods(http.MethodPost) router.HandleFunc("/api/v1/login", baseapp.OpenAccess(ah.loginUser)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/traces/{traceId}", baseapp.ViewAccess(ah.searchTraces)).Methods(http.MethodGet) + router.HandleFunc("/api/v2/metrics/query_range", baseapp.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost) + ah.APIHandler.RegisterRoutes(router) } diff --git a/ee/query-service/app/api/metrics.go b/ee/query-service/app/api/metrics.go new file mode 100644 index 0000000000..4b1a8e49dd --- /dev/null +++ b/ee/query-service/app/api/metrics.go @@ -0,0 +1,236 @@ +package api + +import ( + "bytes" + "fmt" + "net/http" + "sync" + "text/template" + "time" + + "go.signoz.io/signoz/pkg/query-service/app/metrics" + "go.signoz.io/signoz/pkg/query-service/app/parser" + "go.signoz.io/signoz/pkg/query-service/constants" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate" + "go.uber.org/zap" +) + +func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { + if !ah.CheckFeature(basemodel.CustomMetricsFunction) { + zap.S().Info("CustomMetricsFunction feature is not enabled in this plan") + ah.APIHandler.QueryRangeMetricsV2(w, r) + return + } + metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r) + + if apiErrorObj != nil { + zap.S().Errorf(apiErrorObj.Err.Error()) + RespondError(w, apiErrorObj, nil) + return + } + + // prometheus instant query needs same timestamp + if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE && + metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM { + metricsQueryRangeParams.Start = metricsQueryRangeParams.End + } + + // round up the end to nearest multiple + if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER { + end := (metricsQueryRangeParams.End) / 1000 + step := metricsQueryRangeParams.Step + metricsQueryRangeParams.End = (end / step * step) * 1000 + } + + type channelResult struct { + Series []*basemodel.Series + TableName string + Err error + Name string + Query string + } + + execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) { + var seriesList []*basemodel.Series + var tableName []string + ch := make(chan channelResult, len(queries)) + var wg sync.WaitGroup + + for name, query := range queries { + wg.Add(1) + go func(name, query string) { + defer wg.Done() + seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query) + for _, series := range seriesList { + series.QueryName = name + } + + if err != nil { + ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query} + return + } + ch <- channelResult{Series: seriesList, TableName: tableName} + }(name, query) + } + + wg.Wait() + close(ch) + + var errs []error + errQuriesByName := make(map[string]string) + // read values from the channel + for r := range ch { + if r.Err != nil { + errs = append(errs, r.Err) + errQuriesByName[r.Name] = r.Query + continue + } + seriesList = append(seriesList, r.Series...) + tableName = append(tableName, r.TableName) + } + if len(errs) != 0 { + return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName + } + return seriesList, tableName, nil, nil + } + + execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) { + var seriesList []*basemodel.Series + ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries)) + var wg sync.WaitGroup + + for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries { + if query.Disabled { + continue + } + wg.Add(1) + go func(name string, query *basemodel.PromQuery) { + var seriesList []*basemodel.Series + defer wg.Done() + tmpl := template.New("promql-query") + tmpl, tmplErr := tmpl.Parse(query.Query) + if tmplErr != nil { + ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query} + return + } + var queryBuf bytes.Buffer + tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables) + if tmplErr != nil { + ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query} + return + } + query.Query = queryBuf.String() + queryModel := basemodel.QueryRangeParams{ + Start: time.UnixMilli(metricsQueryRangeParams.Start), + End: time.UnixMilli(metricsQueryRangeParams.End), + Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)), + Query: query.Query, + } + promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel) + if err != nil { + ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query} + return + } + matrix, _ := promResult.Matrix() + for _, v := range matrix { + var s basemodel.Series + s.QueryName = name + s.Labels = v.Metric.Copy().Map() + for _, p := range v.Points { + s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.V}) + } + seriesList = append(seriesList, &s) + } + ch <- channelResult{Series: seriesList} + }(name, query) + } + + wg.Wait() + close(ch) + + var errs []error + errQuriesByName := make(map[string]string) + // read values from the channel + for r := range ch { + if r.Err != nil { + errs = append(errs, r.Err) + errQuriesByName[r.Name] = r.Query + continue + } + seriesList = append(seriesList, r.Series...) + } + if len(errs) != 0 { + return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName + } + return seriesList, nil, nil + } + + var seriesList []*basemodel.Series + var tableName []string + var err error + var errQuriesByName map[string]string + switch metricsQueryRangeParams.CompositeMetricQuery.QueryType { + case basemodel.QUERY_BUILDER: + runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME) + if runQueries.Err != nil { + RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil) + return + } + seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries) + + case basemodel.CLICKHOUSE: + queries := make(map[string]string) + + for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries { + if chQuery.Disabled { + continue + } + tmpl := template.New("clickhouse-query") + tmpl, err := tmpl.Parse(chQuery.Query) + if err != nil { + RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil) + return + } + var query bytes.Buffer + + // replace go template variables + querytemplate.AssignReservedVars(metricsQueryRangeParams) + + err = tmpl.Execute(&query, metricsQueryRangeParams.Variables) + if err != nil { + RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil) + return + } + queries[name] = query.String() + } + seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries) + case basemodel.PROM: + seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams) + default: + err = fmt.Errorf("invalid query type") + RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName) + return + } + + if err != nil { + apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err} + RespondError(w, apiErrObj, errQuriesByName) + return + } + if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE && + len(seriesList) > 1 && + (metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER || + metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) { + RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil) + return + } + + type ResponseFormat struct { + ResultType string `json:"resultType"` + Result []*basemodel.Series `json:"result"` + TableName []string `json:"tableName"` + } + resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName} + ah.Respond(w, resp) +} diff --git a/ee/query-service/app/api/traces.go b/ee/query-service/app/api/traces.go new file mode 100644 index 0000000000..22d66f7a82 --- /dev/null +++ b/ee/query-service/app/api/traces.go @@ -0,0 +1,39 @@ +package api + +import ( + "net/http" + "strconv" + + "go.signoz.io/signoz/ee/query-service/app/db" + "go.signoz.io/signoz/ee/query-service/constants" + "go.signoz.io/signoz/ee/query-service/model" + baseapp "go.signoz.io/signoz/pkg/query-service/app" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.uber.org/zap" +) + +func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { + + if !ah.CheckFeature(basemodel.SmartTraceDetail) { + zap.S().Info("SmartTraceDetail feature is not enabled in this plan") + ah.APIHandler.SearchTraces(w, r) + return + } + traceId, spanId, levelUpInt, levelDownInt, err := baseapp.ParseSearchTracesParams(r) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params") + return + } + spanLimit, err := strconv.Atoi(constants.SpanLimitStr) + if err != nil { + zap.S().Error("Error during strconv.Atoi() on SPAN_LIMIT env variable: ", err) + return + } + result, err := ah.opts.DataConnector.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, spanLimit, db.SmartTraceAlgorithm) + if ah.HandleError(w, err, http.StatusBadRequest) { + return + } + + ah.WriteJSON(w, r, result) + +} diff --git a/ee/query-service/app/db/metrics.go b/ee/query-service/app/db/metrics.go new file mode 100644 index 0000000000..77e7d50c9b --- /dev/null +++ b/ee/query-service/app/db/metrics.go @@ -0,0 +1,374 @@ +package db + +import ( + "context" + "crypto/md5" + "encoding/json" + "fmt" + "reflect" + "regexp" + "sort" + "strings" + "time" + + "go.signoz.io/signoz/ee/query-service/model" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.signoz.io/signoz/pkg/query-service/utils" + "go.uber.org/zap" +) + +// GetMetricResultEE runs the query and returns list of time series +func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*basemodel.Series, string, error) { + + defer utils.Elapsed("GetMetricResult")() + zap.S().Infof("Executing metric result query: %s", query) + + var hash string + // If getSubTreeSpans function is used in the clickhouse query + if strings.Index(query, "getSubTreeSpans(") != -1 { + var err error + query, hash, err = r.getSubTreeSpansCustomFunction(ctx, query, hash) + if err == fmt.Errorf("No spans found for the given query") { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + } + + rows, err := r.conn.Query(ctx, query) + zap.S().Debug(query) + if err != nil { + zap.S().Debug("Error in processing query: ", err) + return nil, "", fmt.Errorf("error in processing query") + } + + var ( + columnTypes = rows.ColumnTypes() + columnNames = rows.Columns() + vars = make([]interface{}, len(columnTypes)) + ) + for i := range columnTypes { + vars[i] = reflect.New(columnTypes[i].ScanType()).Interface() + } + // when group by is applied, each combination of cartesian product + // of attributes is separate series. each item in metricPointsMap + // represent a unique series. + metricPointsMap := make(map[string][]basemodel.MetricPoint) + // attribute key-value pairs for each group selection + attributesMap := make(map[string]map[string]string) + + defer rows.Close() + for rows.Next() { + if err := rows.Scan(vars...); err != nil { + return nil, "", err + } + var groupBy []string + var metricPoint basemodel.MetricPoint + groupAttributes := make(map[string]string) + // Assuming that the end result row contains a timestamp, value and option labels + // Label key and value are both strings. + for idx, v := range vars { + colName := columnNames[idx] + switch v := v.(type) { + case *string: + // special case for returning all labels + if colName == "fullLabels" { + var metric map[string]string + err := json.Unmarshal([]byte(*v), &metric) + if err != nil { + return nil, "", err + } + for key, val := range metric { + groupBy = append(groupBy, val) + groupAttributes[key] = val + } + } else { + groupBy = append(groupBy, *v) + groupAttributes[colName] = *v + } + case *time.Time: + metricPoint.Timestamp = v.UnixMilli() + case *float64: + metricPoint.Value = *v + } + } + sort.Strings(groupBy) + key := strings.Join(groupBy, "") + attributesMap[key] = groupAttributes + metricPointsMap[key] = append(metricPointsMap[key], metricPoint) + } + + var seriesList []*basemodel.Series + for key := range metricPointsMap { + points := metricPointsMap[key] + // first point in each series could be invalid since the + // aggregations are applied with point from prev series + if len(points) != 0 && len(points) > 1 { + points = points[1:] + } + attributes := attributesMap[key] + series := basemodel.Series{Labels: attributes, Points: points} + seriesList = append(seriesList, &series) + } + // err = r.conn.Exec(ctx, "DROP TEMPORARY TABLE IF EXISTS getSubTreeSpans"+hash) + // if err != nil { + // zap.S().Error("Error in dropping temporary table: ", err) + // return nil, err + // } + if hash == "" { + return seriesList, hash, nil + } else { + return seriesList, "getSubTreeSpans" + hash, nil + } +} + +func (r *ClickhouseReader) getSubTreeSpansCustomFunction(ctx context.Context, query string, hash string) (string, string, error) { + + zap.S().Debugf("Executing getSubTreeSpans function") + + // str1 := `select fromUnixTimestamp64Milli(intDiv( toUnixTimestamp64Milli ( timestamp ), 100) * 100) AS interval, toFloat64(count()) as count from (select timestamp, spanId, parentSpanId, durationNano from getSubTreeSpans(select * from signoz_traces.signoz_index_v2 where serviceName='frontend' and name='/driver.DriverService/FindNearest' and traceID='00000000000000004b0a863cb5ed7681') where name='FindDriverIDs' group by interval order by interval asc;` + + // process the query to fetch subTree query + var subtreeInput string + query, subtreeInput, hash = processQuery(query, hash) + + err := r.conn.Exec(ctx, "DROP TABLE IF EXISTS getSubTreeSpans"+hash) + if err != nil { + zap.S().Error("Error in dropping temporary table: ", err) + return query, hash, err + } + + // Create temporary table to store the getSubTreeSpans() results + zap.S().Debugf("Creating temporary table getSubTreeSpans%s", hash) + err = r.conn.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+"getSubTreeSpans"+hash+" (timestamp DateTime64(9) CODEC(DoubleDelta, LZ4), traceID FixedString(32) CODEC(ZSTD(1)), spanID String CODEC(ZSTD(1)), parentSpanID String CODEC(ZSTD(1)), rootSpanID String CODEC(ZSTD(1)), serviceName LowCardinality(String) CODEC(ZSTD(1)), name LowCardinality(String) CODEC(ZSTD(1)), rootName LowCardinality(String) CODEC(ZSTD(1)), durationNano UInt64 CODEC(T64, ZSTD(1)), kind Int8 CODEC(T64, ZSTD(1)), tagMap Map(LowCardinality(String), String) CODEC(ZSTD(1)), events Array(String) CODEC(ZSTD(2))) ENGINE = MergeTree() ORDER BY (timestamp)") + if err != nil { + zap.S().Error("Error in creating temporary table: ", err) + return query, hash, err + } + + var getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse + getSpansSubQuery := subtreeInput + // Execute the subTree query + zap.S().Debugf("Executing subTree query: %s", getSpansSubQuery) + err = r.conn.Select(ctx, &getSpansSubQueryDBResponses, getSpansSubQuery) + + // zap.S().Info(getSpansSubQuery) + + if err != nil { + zap.S().Debug("Error in processing sql query: ", err) + return query, hash, fmt.Errorf("Error in processing sql query") + } + + var searchScanResponses []basemodel.SearchSpanDBResponseItem + + // TODO : @ankit: I think the algorithm does not need to assume that subtrees are from the same TraceID. We can take this as an improvement later. + // Fetch all the spans from of same TraceID so that we can build subtree + modelQuery := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.TraceDB, r.SpansTable) + + if len(getSpansSubQueryDBResponses) == 0 { + return query, hash, fmt.Errorf("No spans found for the given query") + } + zap.S().Debugf("Executing query to fetch all the spans from the same TraceID: %s", modelQuery) + err = r.conn.Select(ctx, &searchScanResponses, modelQuery, getSpansSubQueryDBResponses[0].TraceID) + + if err != nil { + zap.S().Debug("Error in processing sql query: ", err) + return query, hash, fmt.Errorf("Error in processing sql query") + } + + // Process model to fetch the spans + zap.S().Debugf("Processing model to fetch the spans") + searchSpanResponses := []basemodel.SearchSpanResponseItem{} + for _, item := range searchScanResponses { + var jsonItem basemodel.SearchSpanResponseItem + json.Unmarshal([]byte(item.Model), &jsonItem) + jsonItem.TimeUnixNano = uint64(item.Timestamp.UnixNano()) + if jsonItem.Events == nil { + jsonItem.Events = []string{} + } + searchSpanResponses = append(searchSpanResponses, jsonItem) + } + // Build the subtree and store all the subtree spans in temporary table getSubTreeSpans+hash + // Use map to store pointer to the spans to avoid duplicates and save memory + zap.S().Debugf("Building the subtree to store all the subtree spans in temporary table getSubTreeSpans%s", hash) + + treeSearchResponse, err := getSubTreeAlgorithm(searchSpanResponses, getSpansSubQueryDBResponses) + if err != nil { + zap.S().Error("Error in getSubTreeAlgorithm function: ", err) + return query, hash, err + } + zap.S().Debugf("Preparing batch to store subtree spans in temporary table getSubTreeSpans%s", hash) + statement, err := r.conn.PrepareBatch(context.Background(), fmt.Sprintf("INSERT INTO getSubTreeSpans"+hash)) + if err != nil { + zap.S().Error("Error in preparing batch statement: ", err) + return query, hash, err + } + for _, span := range treeSearchResponse { + var parentID string + if len(span.References) > 0 && span.References[0].RefType == "CHILD_OF" { + parentID = span.References[0].SpanId + } + err = statement.Append( + time.Unix(0, int64(span.TimeUnixNano)), + span.TraceID, + span.SpanID, + parentID, + span.RootSpanID, + span.ServiceName, + span.Name, + span.RootName, + uint64(span.DurationNano), + int8(span.Kind), + span.TagMap, + span.Events, + ) + if err != nil { + zap.S().Debug("Error in processing sql query: ", err) + return query, hash, err + } + } + zap.S().Debugf("Inserting the subtree spans in temporary table getSubTreeSpans%s", hash) + err = statement.Send() + if err != nil { + zap.S().Error("Error in sending statement: ", err) + return query, hash, err + } + return query, hash, nil +} + +func processQuery(query string, hash string) (string, string, string) { + re3 := regexp.MustCompile(`getSubTreeSpans`) + + submatchall3 := re3.FindAllStringIndex(query, -1) + getSubtreeSpansMatchIndex := submatchall3[0][1] + + query2countParenthesis := query[getSubtreeSpansMatchIndex:] + + sqlCompleteIndex := 0 + countParenthesisImbalance := 0 + for i, char := range query2countParenthesis { + + if string(char) == "(" { + countParenthesisImbalance += 1 + } + if string(char) == ")" { + countParenthesisImbalance -= 1 + } + if countParenthesisImbalance == 0 { + sqlCompleteIndex = i + break + } + } + subtreeInput := query2countParenthesis[1:sqlCompleteIndex] + + // hash the subtreeInput + hmd5 := md5.Sum([]byte(subtreeInput)) + hash = fmt.Sprintf("%x", hmd5) + + // Reformat the query to use the getSubTreeSpans function + query = query[:getSubtreeSpansMatchIndex] + hash + " " + query2countParenthesis[sqlCompleteIndex+1:] + return query, subtreeInput, hash +} + +// getSubTreeAlgorithm is an algorithm to build the subtrees of the spans and return the list of spans +func getSubTreeAlgorithm(payload []basemodel.SearchSpanResponseItem, getSpansSubQueryDBResponses []model.GetSpansSubQueryDBResponse) (map[string]*basemodel.SearchSpanResponseItem, error) { + + var spans []*model.SpanForTraceDetails + for _, spanItem := range payload { + var parentID string + if len(spanItem.References) > 0 && spanItem.References[0].RefType == "CHILD_OF" { + parentID = spanItem.References[0].SpanId + } + span := &model.SpanForTraceDetails{ + TimeUnixNano: spanItem.TimeUnixNano, + SpanID: spanItem.SpanID, + TraceID: spanItem.TraceID, + ServiceName: spanItem.ServiceName, + Name: spanItem.Name, + Kind: spanItem.Kind, + DurationNano: spanItem.DurationNano, + TagMap: spanItem.TagMap, + ParentID: parentID, + Events: spanItem.Events, + HasError: spanItem.HasError, + } + spans = append(spans, span) + } + + zap.S().Debug("Building Tree") + roots, err := buildSpanTrees(&spans) + if err != nil { + return nil, err + } + searchSpansResult := make(map[string]*basemodel.SearchSpanResponseItem) + // Every span which was fetched from getSubTree Input SQL query is considered root + // For each root, get the subtree spans + for _, getSpansSubQueryDBResponse := range getSpansSubQueryDBResponses { + targetSpan := &model.SpanForTraceDetails{} + // zap.S().Debug("Building tree for span id: " + getSpansSubQueryDBResponse.SpanID + " " + strconv.Itoa(i+1) + " of " + strconv.Itoa(len(getSpansSubQueryDBResponses))) + // Search target span object in the tree + for _, root := range roots { + targetSpan, err = breadthFirstSearch(root, getSpansSubQueryDBResponse.SpanID) + if targetSpan != nil { + break + } + if err != nil { + zap.S().Error("Error during BreadthFirstSearch(): ", err) + return nil, err + } + } + if targetSpan == nil { + return nil, nil + } + // Build subtree for the target span + // Mark the target span as root by setting parent ID as empty string + targetSpan.ParentID = "" + preParents := []*model.SpanForTraceDetails{targetSpan} + children := []*model.SpanForTraceDetails{} + + // Get the subtree child spans + for i := 0; len(preParents) != 0; i++ { + parents := []*model.SpanForTraceDetails{} + for _, parent := range preParents { + children = append(children, parent.Children...) + parents = append(parents, parent.Children...) + } + preParents = parents + } + + resultSpans := children + // Add the target span to the result spans + resultSpans = append(resultSpans, targetSpan) + + for _, item := range resultSpans { + references := []basemodel.OtelSpanRef{ + { + TraceId: item.TraceID, + SpanId: item.ParentID, + RefType: "CHILD_OF", + }, + } + + if item.Events == nil { + item.Events = []string{} + } + searchSpansResult[item.SpanID] = &basemodel.SearchSpanResponseItem{ + TimeUnixNano: item.TimeUnixNano, + SpanID: item.SpanID, + TraceID: item.TraceID, + ServiceName: item.ServiceName, + Name: item.Name, + Kind: item.Kind, + References: references, + DurationNano: item.DurationNano, + TagMap: item.TagMap, + Events: item.Events, + HasError: item.HasError, + RootSpanID: getSpansSubQueryDBResponse.SpanID, + RootName: targetSpan.Name, + } + } + } + return searchSpansResult, nil +} diff --git a/ee/query-service/app/db/reader.go b/ee/query-service/app/db/reader.go index e948ee430b..fc26ec3ce2 100644 --- a/ee/query-service/app/db/reader.go +++ b/ee/query-service/app/db/reader.go @@ -6,6 +6,7 @@ import ( "github.com/jmoiron/sqlx" basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader" + "go.signoz.io/signoz/pkg/query-service/interfaces" ) type ClickhouseReader struct { @@ -14,8 +15,8 @@ type ClickhouseReader struct { *basechr.ClickHouseReader } -func NewDataConnector(localDB *sqlx.DB, promConfigPath string) *ClickhouseReader { - ch := basechr.NewReader(localDB, promConfigPath) +func NewDataConnector(localDB *sqlx.DB, promConfigPath string, lm interfaces.FeatureLookup) *ClickhouseReader { + ch := basechr.NewReader(localDB, promConfigPath, lm) return &ClickhouseReader{ conn: ch.GetConn(), appdb: localDB, diff --git a/ee/query-service/app/db/trace.go b/ee/query-service/app/db/trace.go new file mode 100644 index 0000000000..529a9a93fd --- /dev/null +++ b/ee/query-service/app/db/trace.go @@ -0,0 +1,222 @@ +package db + +import ( + "errors" + "strconv" + + "go.signoz.io/signoz/ee/query-service/model" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.uber.org/zap" +) + +// SmartTraceAlgorithm is an algorithm to find the target span and build a tree of spans around it with the given levelUp and levelDown parameters and the given spanLimit +func SmartTraceAlgorithm(payload []basemodel.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]basemodel.SearchSpansResult, error) { + var spans []*model.SpanForTraceDetails + + // Build a slice of spans from the payload + for _, spanItem := range payload { + var parentID string + if len(spanItem.References) > 0 && spanItem.References[0].RefType == "CHILD_OF" { + parentID = spanItem.References[0].SpanId + } + span := &model.SpanForTraceDetails{ + TimeUnixNano: spanItem.TimeUnixNano, + SpanID: spanItem.SpanID, + TraceID: spanItem.TraceID, + ServiceName: spanItem.ServiceName, + Name: spanItem.Name, + Kind: spanItem.Kind, + DurationNano: spanItem.DurationNano, + TagMap: spanItem.TagMap, + ParentID: parentID, + Events: spanItem.Events, + HasError: spanItem.HasError, + } + spans = append(spans, span) + } + + // Build span trees from the spans + roots, err := buildSpanTrees(&spans) + if err != nil { + return nil, err + } + targetSpan := &model.SpanForTraceDetails{} + + // Find the target span in the span trees + for _, root := range roots { + targetSpan, err = breadthFirstSearch(root, targetSpanId) + if targetSpan != nil { + break + } + if err != nil { + zap.S().Error("Error during BreadthFirstSearch(): ", err) + return nil, err + } + } + + // If the target span is not found, return span not found error + if targetSpan == nil { + return nil, errors.New("Span not found") + } + + // Build the final result + parents := []*model.SpanForTraceDetails{} + + // Get the parent spans of the target span up to the given levelUp parameter and spanLimit + preParent := targetSpan + for i := 0; i < levelUp+1; i++ { + if i == levelUp { + preParent.ParentID = "" + } + if spanLimit-len(preParent.Children) <= 0 { + parents = append(parents, preParent) + parents = append(parents, preParent.Children[:spanLimit]...) + spanLimit -= (len(preParent.Children[:spanLimit]) + 1) + preParent.ParentID = "" + break + } + parents = append(parents, preParent) + parents = append(parents, preParent.Children...) + spanLimit -= (len(preParent.Children) + 1) + preParent = preParent.ParentSpan + if preParent == nil { + break + } + } + + // Get the child spans of the target span until the given levelDown and spanLimit + preParents := []*model.SpanForTraceDetails{targetSpan} + children := []*model.SpanForTraceDetails{} + + for i := 0; i < levelDown && len(preParents) != 0 && spanLimit > 0; i++ { + parents := []*model.SpanForTraceDetails{} + for _, parent := range preParents { + if spanLimit-len(parent.Children) <= 0 { + children = append(children, parent.Children[:spanLimit]...) + spanLimit -= len(parent.Children[:spanLimit]) + break + } + children = append(children, parent.Children...) + parents = append(parents, parent.Children...) + } + preParents = parents + } + + // Store the final list of spans in the resultSpanSet map to avoid duplicates + resultSpansSet := make(map[*model.SpanForTraceDetails]struct{}) + resultSpansSet[targetSpan] = struct{}{} + for _, parent := range parents { + resultSpansSet[parent] = struct{}{} + } + for _, child := range children { + resultSpansSet[child] = struct{}{} + } + + searchSpansResult := []basemodel.SearchSpansResult{{ + Columns: []string{"__time", "SpanId", "TraceId", "ServiceName", "Name", "Kind", "DurationNano", "TagsKeys", "TagsValues", "References", "Events", "HasError"}, + Events: make([][]interface{}, len(resultSpansSet)), + }, + } + + // Convert the resultSpansSet map to searchSpansResult + i := 0 // index for spans + for item := range resultSpansSet { + references := []basemodel.OtelSpanRef{ + { + TraceId: item.TraceID, + SpanId: item.ParentID, + RefType: "CHILD_OF", + }, + } + + referencesStringArray := []string{} + for _, item := range references { + referencesStringArray = append(referencesStringArray, item.ToString()) + } + keys := make([]string, 0, len(item.TagMap)) + values := make([]string, 0, len(item.TagMap)) + + for k, v := range item.TagMap { + keys = append(keys, k) + values = append(values, v) + } + if item.Events == nil { + item.Events = []string{} + } + searchSpansResult[0].Events[i] = []interface{}{ + item.TimeUnixNano, + item.SpanID, + item.TraceID, + item.ServiceName, + item.Name, + strconv.Itoa(int(item.Kind)), + strconv.FormatInt(item.DurationNano, 10), + keys, + values, + referencesStringArray, + item.Events, + item.HasError, + } + i++ // increment index + } + return searchSpansResult, nil +} + +// buildSpanTrees builds trees of spans from a list of spans. +func buildSpanTrees(spansPtr *[]*model.SpanForTraceDetails) ([]*model.SpanForTraceDetails, error) { + + // Build a map of spanID to span for fast lookup + var roots []*model.SpanForTraceDetails + spans := *spansPtr + mapOfSpans := make(map[string]*model.SpanForTraceDetails, len(spans)) + + for _, span := range spans { + if span.ParentID == "" { + roots = append(roots, span) + } + mapOfSpans[span.SpanID] = span + } + + // Build the span tree by adding children to the parent spans + for _, span := range spans { + if span.ParentID == "" { + continue + } + parent := mapOfSpans[span.ParentID] + + // If the parent span is not found, add current span to list of roots + if parent == nil { + // zap.S().Debug("Parent Span not found parent_id: ", span.ParentID) + roots = append(roots, span) + span.ParentID = "" + continue + } + + span.ParentSpan = parent + parent.Children = append(parent.Children, span) + } + + return roots, nil +} + +// breadthFirstSearch performs a breadth-first search on the span tree to find the target span. +func breadthFirstSearch(spansPtr *model.SpanForTraceDetails, targetId string) (*model.SpanForTraceDetails, error) { + queue := []*model.SpanForTraceDetails{spansPtr} + visited := make(map[string]bool) + + for len(queue) > 0 { + current := queue[0] + visited[current.SpanID] = true + queue = queue[1:] + if current.SpanID == targetId { + return current, nil + } + + for _, child := range current.Children { + if ok, _ := visited[child.SpanID]; !ok { + queue = append(queue, child) + } + } + } + return nil, nil +} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 7002af3f41..501ad96aa9 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -98,7 +98,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { storage := os.Getenv("STORAGE") if storage == "clickhouse" { zap.S().Info("Using ClickHouse as datastore ...") - qb := db.NewDataConnector(localDB, serverOptions.PromConfigPath) + qb := db.NewDataConnector(localDB, serverOptions.PromConfigPath, lm) go qb.Start(readerReady) reader = qb } else { diff --git a/ee/query-service/constants/constants.go b/ee/query-service/constants/constants.go index ba9bb141a5..45fad74da6 100644 --- a/ee/query-service/constants/constants.go +++ b/ee/query-service/constants/constants.go @@ -10,6 +10,8 @@ const ( var LicenseSignozIo = "https://license.signoz.io/api/v1" +var SpanLimitStr = GetOrDefaultEnv("SPAN_LIMIT", "5000") + func GetOrDefaultEnv(key string, fallback string) string { v := os.Getenv(key) if len(v) == 0 { diff --git a/ee/query-service/model/plans.go b/ee/query-service/model/plans.go index e68217730a..c42712f693 100644 --- a/ee/query-service/model/plans.go +++ b/ee/query-service/model/plans.go @@ -17,11 +17,15 @@ var BasicPlan = basemodel.FeatureSet{ } var ProPlan = basemodel.FeatureSet{ - Pro: true, - SSO: true, + Pro: true, + SSO: true, + basemodel.SmartTraceDetail: true, + basemodel.CustomMetricsFunction: true, } var EnterprisePlan = basemodel.FeatureSet{ - Enterprise: true, - SSO: true, + Enterprise: true, + SSO: true, + basemodel.SmartTraceDetail: true, + basemodel.CustomMetricsFunction: true, } diff --git a/ee/query-service/model/trace.go b/ee/query-service/model/trace.go new file mode 100644 index 0000000000..708d6d1c5c --- /dev/null +++ b/ee/query-service/model/trace.go @@ -0,0 +1,22 @@ +package model + +type SpanForTraceDetails struct { + TimeUnixNano uint64 `json:"timestamp"` + SpanID string `json:"spanID"` + TraceID string `json:"traceID"` + ParentID string `json:"parentID"` + ParentSpan *SpanForTraceDetails `json:"parentSpan"` + ServiceName string `json:"serviceName"` + Name string `json:"name"` + Kind int32 `json:"kind"` + DurationNano int64 `json:"durationNano"` + TagMap map[string]string `json:"tagMap"` + Events []string `json:"event"` + HasError bool `json:"hasError"` + Children []*SpanForTraceDetails `json:"children"` +} + +type GetSpansSubQueryDBResponse struct { + SpanID string `ch:"spanID"` + TraceID string `ch:"traceID"` +} diff --git a/frontend/src/api/trace/getTraceItem.ts b/frontend/src/api/trace/getTraceItem.ts index bf93269669..054c809b33 100644 --- a/frontend/src/api/trace/getTraceItem.ts +++ b/frontend/src/api/trace/getTraceItem.ts @@ -1,15 +1,20 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; +import { formUrlParams } from 'container/TraceDetail/utils'; import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/trace/getTraceItem'; +import { GetTraceItemProps, PayloadProps } from 'types/api/trace/getTraceItem'; const getTraceItem = async ( - props: Props, + props: GetTraceItemProps, ): Promise | ErrorResponse> => { try { const response = await axios.request({ - url: `/traces/${props.id}`, + url: `/traces/${props.id}${formUrlParams({ + spanId: props.spanId, + levelUp: props.levelUp, + levelDown: props.levelDown, + })}`, method: 'get', }); diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx index aa9fc1cf6b..b7d202bdcf 100644 --- a/frontend/src/container/Trace/TraceTable/index.tsx +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -5,6 +5,7 @@ import { getSpanOrder, getSpanOrderParam, } from 'container/Trace/TraceTable/util'; +import { formUrlParams } from 'container/TraceDetail/utils'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import history from 'lib/history'; @@ -48,7 +49,11 @@ function TraceTable(): JSX.Element { type TableType = FlatArray; const getLink = (record: TableType): string => { - return `${ROUTES.TRACE}/${record.traceID}?spanId=${record.spanID}`; + return `${ROUTES.TRACE}/${record.traceID}${formUrlParams({ + spanId: record.spanID, + levelUp: 0, + levelDown: 0, + })}`; }; const getValue = (value: string): JSX.Element => { diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx index 816f59ec2f..debe73d3a1 100644 --- a/frontend/src/container/TraceDetail/index.tsx +++ b/frontend/src/container/TraceDetail/index.tsx @@ -29,6 +29,7 @@ import SelectedSpanDetails from './SelectedSpanDetails'; import * as styles from './styles'; import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles'; import { + formUrlParams, getSortedData, getTreeLevelsCount, IIntervalUnit, @@ -50,7 +51,13 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { // const [searchSpanString, setSearchSpanString] = useState(''); const [activeHoverId, setActiveHoverId] = useState(''); const [activeSelectedId, setActiveSelectedId] = useState(spanId || ''); - + const { levelDown, levelUp } = useMemo( + () => ({ + levelDown: urlQuery.get('levelDown'), + levelUp: urlQuery.get('levelUp'), + }), + [urlQuery], + ); const [treesData, setTreesData] = useState( spanToTreeUtil(response[0].events), ); @@ -77,10 +84,14 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { if (activeSelectedId) { history.replace({ pathname: history.location.pathname, - search: `?spanId=${activeSelectedId}`, + search: `${formUrlParams({ + spanId: activeSelectedId, + levelUp, + levelDown, + })}`, }); } - }, [activeSelectedId]); + }, [activeSelectedId, levelDown, levelUp]); const getSelectedNode = useMemo(() => { return getNodeById(activeSelectedId, treesData); diff --git a/frontend/src/container/TraceDetail/utils.ts b/frontend/src/container/TraceDetail/utils.ts index 2541cf9bf0..898ea7e510 100644 --- a/frontend/src/container/TraceDetail/utils.ts +++ b/frontend/src/container/TraceDetail/utils.ts @@ -98,3 +98,28 @@ export const getTreeLevelsCount = (tree: ITraceTree): number => { return levels; }; + +export const formUrlParams = (params: Record): string => { + let urlParams = ''; + Object.entries(params).forEach(([key, value], index) => { + let encodedValue: string; + try { + encodedValue = decodeURIComponent(value); + encodedValue = encodeURIComponent(encodedValue); + } catch (error) { + encodedValue = ''; + } + if (index === 0) { + if (encodedValue) { + urlParams = `?${key}=${encodedValue}`; + } else { + urlParams = `?${key}=`; + } + } else if (encodedValue) { + urlParams = `${urlParams}&${key}=${encodedValue}`; + } else { + urlParams = `${urlParams}&${key}=`; + } + }); + return urlParams; +}; diff --git a/frontend/src/pages/TraceDetail/index.tsx b/frontend/src/pages/TraceDetail/index.tsx index d8f6634888..16316aa729 100644 --- a/frontend/src/pages/TraceDetail/index.tsx +++ b/frontend/src/pages/TraceDetail/index.tsx @@ -2,16 +2,26 @@ import { Typography } from 'antd'; import getTraceItem from 'api/trace/getTraceItem'; import Spinner from 'components/Spinner'; import TraceDetailContainer from 'container/TraceDetail'; -import React from 'react'; +import useUrlQuery from 'hooks/useUrlQuery'; +import React, { useMemo } from 'react'; import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem'; function TraceDetail(): JSX.Element { const { id } = useParams(); + const urlQuery = useUrlQuery(); + const { spanId, levelUp, levelDown } = useMemo( + () => ({ + spanId: urlQuery.get('spanId'), + levelUp: urlQuery.get('levelUp'), + levelDown: urlQuery.get('levelDown'), + }), + [urlQuery], + ); const { data: traceDetailResponse, error, isLoading, isError } = useQuery( `getTraceItem/${id}`, - () => getTraceItem({ id }), + () => getTraceItem({ id, spanId, levelUp, levelDown }), { cacheTime: 3000, }, diff --git a/frontend/src/types/api/trace/getTraceItem.ts b/frontend/src/types/api/trace/getTraceItem.ts index 4b12d15b2f..9d8887bc56 100644 --- a/frontend/src/types/api/trace/getTraceItem.ts +++ b/frontend/src/types/api/trace/getTraceItem.ts @@ -2,6 +2,13 @@ export interface Props { id: string; } +export interface GetTraceItemProps { + id: string; + spanId: string | null; + levelUp: string | null; + levelDown: string | null; +} + export interface PayloadProps { [id: string]: { events: Span[]; diff --git a/go.mod b/go.mod index 8666887ad6..b25925512c 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gosimple/slug v1.10.0 github.com/jmoiron/sqlx v1.3.4 github.com/json-iterator/go v1.1.12 + github.com/mailru/easyjson v0.7.7 github.com/mattn/go-sqlite3 v1.14.8 github.com/minio/minio-go/v6 v6.0.57 github.com/mitchellh/mapstructure v1.5.0 @@ -34,7 +35,9 @@ require ( require ( github.com/beevik/etree v1.1.0 // indirect github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/cpuid v1.2.3 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/minio/md5-simd v1.1.0 // indirect @@ -68,7 +71,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.0 // indirect @@ -126,7 +129,7 @@ require ( go.uber.org/atomic v1.6.0 // indirect go.uber.org/multierr v1.5.0 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - golang.org/x/net v0.0.0-20211013171255-e13a2654a71e + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211110154304-99a53858aa08 // indirect diff --git a/go.sum b/go.sum index ba38dc1069..0782dacd7d 100644 --- a/go.sum +++ b/go.sum @@ -151,8 +151,9 @@ github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -200,8 +201,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -296,6 +298,8 @@ github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w= github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -323,6 +327,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -566,8 +572,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e h1:Xj+JO91noE97IN6F/7WZxzC5QE6yENAQPrwIYhW3bsA= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 0aa037490e..0ccb5cb73c 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -21,6 +21,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/google/uuid" + "github.com/mailru/easyjson" "github.com/oklog/oklog/pkg/group" "github.com/pkg/errors" "github.com/prometheus/common/promlog" @@ -42,6 +43,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/app/logs" "go.signoz.io/signoz/pkg/query-service/constants" am "go.signoz.io/signoz/pkg/query-service/integrations/alertManager" + "go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/utils" "go.uber.org/zap" @@ -79,13 +81,13 @@ var ( type ClickHouseReader struct { db clickhouse.Conn localDB *sqlx.DB - traceDB string + TraceDB string operationsTable string durationTable string indexTable string errorTable string usageExplorerTable string - spansTable string + SpansTable string dependencyGraphTable string topLevelOperationsTable string logsDB string @@ -99,12 +101,13 @@ type ClickHouseReader struct { promConfigFile string promConfig *config.Config alertManager am.Manager + featureFlags interfaces.FeatureLookup liveTailRefreshSeconds int } // NewTraceReader returns a TraceReader for the database -func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader { +func NewReader(localDB *sqlx.DB, configFile string, featureFlag interfaces.FeatureLookup) *ClickHouseReader { datasource := os.Getenv("ClickHouseUrl") options := NewOptions(datasource, primaryNamespace, archiveNamespace) @@ -125,14 +128,14 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader { return &ClickHouseReader{ db: db, localDB: localDB, - traceDB: options.primary.TraceDB, + TraceDB: options.primary.TraceDB, alertManager: alertManager, operationsTable: options.primary.OperationsTable, indexTable: options.primary.IndexTable, errorTable: options.primary.ErrorTable, usageExplorerTable: options.primary.UsageExplorerTable, durationTable: options.primary.DurationTable, - spansTable: options.primary.SpansTable, + SpansTable: options.primary.SpansTable, dependencyGraphTable: options.primary.DependencyGraphTable, topLevelOperationsTable: options.primary.TopLevelOperationsTable, logsDB: options.primary.LogsDB, @@ -141,6 +144,7 @@ func NewReader(localDB *sqlx.DB, configFile string) *ClickHouseReader { logsResourceKeys: options.primary.LogsResourceKeysTable, liveTailRefreshSeconds: options.primary.LiveTailRefreshSeconds, promConfigFile: configFile, + featureFlags: featureFlag, } } @@ -665,7 +669,7 @@ func (r *ClickHouseReader) GetQueryRangeResult(ctx context.Context, query *model func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, error) { services := []string{} - query := fmt.Sprintf(`SELECT DISTINCT serviceName FROM %s.%s WHERE toDate(timestamp) > now() - INTERVAL 1 DAY`, r.traceDB, r.indexTable) + query := fmt.Sprintf(`SELECT DISTINCT serviceName FROM %s.%s WHERE toDate(timestamp) > now() - INTERVAL 1 DAY`, r.TraceDB, r.indexTable) rows, err := r.db.Query(ctx, query) @@ -690,7 +694,7 @@ func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, erro func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context) (*map[string][]string, *model.ApiError) { operations := map[string][]string{} - query := fmt.Sprintf(`SELECT DISTINCT name, serviceName FROM %s.%s`, r.traceDB, r.topLevelOperationsTable) + query := fmt.Sprintf(`SELECT DISTINCT name, serviceName FROM %s.%s`, r.TraceDB, r.topLevelOperationsTable) rows, err := r.db.Query(ctx, query) @@ -745,14 +749,14 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G count(*) as numCalls FROM %s.%s WHERE serviceName = @serviceName AND name In [@names] AND timestamp>= @start AND timestamp<= @end`, - r.traceDB, r.indexTable, + r.TraceDB, r.indexTable, ) errorQuery := fmt.Sprintf( `SELECT count(*) as numErrors FROM %s.%s WHERE serviceName = @serviceName AND name In [@names] AND timestamp>= @start AND timestamp<= @end AND statusCode=2`, - r.traceDB, r.indexTable, + r.TraceDB, r.indexTable, ) args := []interface{}{} @@ -835,7 +839,7 @@ func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams * count(*) as numCalls FROM %s.%s WHERE serviceName = @serviceName AND name In [@names] AND timestamp>= @start AND timestamp<= @end`, - r.traceDB, r.indexTable, + r.TraceDB, r.indexTable, ) args := []interface{}{} args = append(args, namedArgs...) @@ -861,7 +865,7 @@ func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams * count(*) as numErrors FROM %s.%s WHERE serviceName = @serviceName AND name In [@names] AND timestamp>= @start AND timestamp<= @end AND statusCode=2`, - r.traceDB, r.indexTable, + r.TraceDB, r.indexTable, ) args = []interface{}{} args = append(args, namedArgs...) @@ -1001,7 +1005,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode case constants.TraceID: continue case constants.ServiceName: - finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT serviceName, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY serviceName" var dBResponse []model.DBResponseServiceName @@ -1018,7 +1022,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.HttpCode: - finalQuery := fmt.Sprintf("SELECT httpCode, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT httpCode, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY httpCode" var dBResponse []model.DBResponseHttpCode @@ -1035,7 +1039,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.HttpRoute: - finalQuery := fmt.Sprintf("SELECT httpRoute, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT httpRoute, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY httpRoute" var dBResponse []model.DBResponseHttpRoute @@ -1052,7 +1056,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.HttpUrl: - finalQuery := fmt.Sprintf("SELECT httpUrl, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT httpUrl, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY httpUrl" var dBResponse []model.DBResponseHttpUrl @@ -1069,7 +1073,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.HttpMethod: - finalQuery := fmt.Sprintf("SELECT httpMethod, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT httpMethod, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY httpMethod" var dBResponse []model.DBResponseHttpMethod @@ -1086,7 +1090,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.HttpHost: - finalQuery := fmt.Sprintf("SELECT httpHost, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT httpHost, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY httpHost" var dBResponse []model.DBResponseHttpHost @@ -1103,7 +1107,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.OperationRequest: - finalQuery := fmt.Sprintf("SELECT name, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT name, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY name" var dBResponse []model.DBResponseOperation @@ -1120,7 +1124,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.Component: - finalQuery := fmt.Sprintf("SELECT component, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT component, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY component" var dBResponse []model.DBResponseComponent @@ -1137,7 +1141,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } } case constants.Status: - finalQuery := fmt.Sprintf("SELECT COUNT(*) as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU AND hasError = true", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT COUNT(*) as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU AND hasError = true", r.TraceDB, r.indexTable) finalQuery += query var dBResponse []model.DBResponseTotal err := r.db.Select(ctx, &dBResponse, finalQuery, args...) @@ -1148,7 +1152,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("Error in processing sql query: %s", err)} } - finalQuery2 := fmt.Sprintf("SELECT COUNT(*) as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU AND hasError = false", r.traceDB, r.indexTable) + finalQuery2 := fmt.Sprintf("SELECT COUNT(*) as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU AND hasError = false", r.TraceDB, r.indexTable) finalQuery2 += query var dBResponse2 []model.DBResponseTotal err = r.db.Select(ctx, &dBResponse2, finalQuery2, args...) @@ -1168,7 +1172,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode traceFilterReponse.Status = map[string]uint64{"ok": 0, "error": 0} } case constants.Duration: - finalQuery := fmt.Sprintf("SELECT durationNano as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.durationTable) + finalQuery := fmt.Sprintf("SELECT durationNano as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.durationTable) finalQuery += query finalQuery += " ORDER BY durationNano LIMIT 1" var dBResponse []model.DBResponseTotal @@ -1179,7 +1183,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode zap.S().Debug("Error in processing sql query: ", err) return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("Error in processing sql query: %s", err)} } - finalQuery = fmt.Sprintf("SELECT durationNano as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.durationTable) + finalQuery = fmt.Sprintf("SELECT durationNano as numTotal FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.durationTable) finalQuery += query finalQuery += " ORDER BY durationNano DESC LIMIT 1" var dBResponse2 []model.DBResponseTotal @@ -1197,7 +1201,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode traceFilterReponse.Duration["maxDuration"] = dBResponse2[0].NumTotal } case constants.RPCMethod: - finalQuery := fmt.Sprintf("SELECT rpcMethod, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT rpcMethod, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY rpcMethod" var dBResponse []model.DBResponseRPCMethod @@ -1215,7 +1219,7 @@ func (r *ClickHouseReader) GetSpanFilters(ctx context.Context, queryParams *mode } case constants.ResponseStatusCode: - finalQuery := fmt.Sprintf("SELECT responseStatusCode, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf("SELECT responseStatusCode, count() as count FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY responseStatusCode" var dBResponse []model.DBResponseStatusCodeMethod @@ -1263,7 +1267,7 @@ func getStatusFilters(query string, statusParams []string, excludeMap map[string func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *model.GetFilteredSpansParams) (*model.GetFilterSpansResponse, *model.ApiError) { - queryTable := fmt.Sprintf("%s.%s", r.traceDB, r.indexTable) + queryTable := fmt.Sprintf("%s.%s", r.TraceDB, r.indexTable) excludeMap := make(map[string]struct{}) for _, e := range queryParams.Exclude { @@ -1333,7 +1337,7 @@ func (r *ClickHouseReader) GetFilteredSpans(ctx context.Context, queryParams *mo if len(queryParams.OrderParam) != 0 { if queryParams.OrderParam == constants.Duration { - queryTable = fmt.Sprintf("%s.%s", r.traceDB, r.durationTable) + queryTable = fmt.Sprintf("%s.%s", r.TraceDB, r.durationTable) if queryParams.Order == constants.Descending { query = query + " ORDER BY durationNano DESC" } @@ -1515,7 +1519,7 @@ func (r *ClickHouseReader) GetTagFilters(ctx context.Context, queryParams *model tagFilters := []model.TagFilters{} - finalQuery := fmt.Sprintf(`SELECT DISTINCT arrayJoin(tagMap.keys) as tagKeys FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU`, r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf(`SELECT DISTINCT arrayJoin(tagMap.keys) as tagKeys FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU`, r.TraceDB, r.indexTable) // Alternative query: SELECT groupUniqArrayArray(mapKeys(tagMap)) as tagKeys FROM signoz_index_v2 finalQuery += query err := r.db.Select(ctx, &tagFilters, finalQuery, args...) @@ -1608,7 +1612,7 @@ func (r *ClickHouseReader) GetTagValues(ctx context.Context, queryParams *model. tagValues := []model.TagValues{} - finalQuery := fmt.Sprintf(`SELECT tagMap[@key] as tagValues FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU`, r.traceDB, r.indexTable) + finalQuery := fmt.Sprintf(`SELECT tagMap[@key] as tagValues FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU`, r.TraceDB, r.indexTable) finalQuery += query finalQuery += " GROUP BY tagMap[@key]" args = append(args, clickhouse.Named("key", queryParams.TagKey)) @@ -1649,7 +1653,7 @@ func (r *ClickHouseReader) GetTopOperations(ctx context.Context, queryParams *mo name FROM %s.%s WHERE serviceName = @serviceName AND timestamp>= @start AND timestamp<= @end`, - r.traceDB, r.indexTable, + r.TraceDB, r.indexTable, ) args := []interface{}{} args = append(args, namedArgs...) @@ -1685,9 +1689,9 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU var query string if len(queryParams.ServiceName) != 0 { namedArgs = append(namedArgs, clickhouse.Named("serviceName", queryParams.ServiceName)) - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL @interval HOUR) as time, sum(count) as count FROM %s.%s WHERE service_name=@serviceName AND timestamp>=@start AND timestamp<=@end GROUP BY time ORDER BY time ASC", r.traceDB, r.usageExplorerTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL @interval HOUR) as time, sum(count) as count FROM %s.%s WHERE service_name=@serviceName AND timestamp>=@start AND timestamp<=@end GROUP BY time ORDER BY time ASC", r.TraceDB, r.usageExplorerTable) } else { - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL @interval HOUR) as time, sum(count) as count FROM %s.%s WHERE timestamp>=@start AND timestamp<=@end GROUP BY time ORDER BY time ASC", r.traceDB, r.usageExplorerTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL @interval HOUR) as time, sum(count) as count FROM %s.%s WHERE timestamp>=@start AND timestamp<=@end GROUP BY time ORDER BY time ASC", r.TraceDB, r.usageExplorerTable) } err := r.db.Select(ctx, &usageItems, query, namedArgs...) @@ -1710,13 +1714,15 @@ func (r *ClickHouseReader) GetUsage(ctx context.Context, queryParams *model.GetU return &usageItems, nil } -func (r *ClickHouseReader) SearchTraces(ctx context.Context, traceId string) (*[]model.SearchSpansResult, error) { +func (r *ClickHouseReader) SearchTraces(ctx context.Context, traceId string, spanId string, levelUp int, levelDown int, spanLimit int, smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error) { - var searchScanReponses []model.SearchSpanDBReponseItem + var searchScanResponses []model.SearchSpanDBResponseItem - query := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.traceDB, r.spansTable) + query := fmt.Sprintf("SELECT timestamp, traceID, model FROM %s.%s WHERE traceID=$1", r.TraceDB, r.SpansTable) - err := r.db.Select(ctx, &searchScanReponses, query, traceId) + start := time.Now() + + err := r.db.Select(ctx, &searchScanResponses, query, traceId) zap.S().Info(query) @@ -1724,30 +1730,43 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, traceId string) (*[ zap.S().Debug("Error in processing sql query: ", err) return nil, fmt.Errorf("Error in processing sql query") } - + end := time.Now() + zap.S().Debug("getTraceSQLQuery took: ", end.Sub(start)) searchSpansResult := []model.SearchSpansResult{{ Columns: []string{"__time", "SpanId", "TraceId", "ServiceName", "Name", "Kind", "DurationNano", "TagsKeys", "TagsValues", "References", "Events", "HasError"}, - Events: make([][]interface{}, len(searchScanReponses)), + Events: make([][]interface{}, len(searchScanResponses)), }, } - for i, item := range searchScanReponses { - var jsonItem model.SearchSpanReponseItem - json.Unmarshal([]byte(item.Model), &jsonItem) + searchSpanResponses := []model.SearchSpanResponseItem{} + start = time.Now() + for _, item := range searchScanResponses { + var jsonItem model.SearchSpanResponseItem + easyjson.Unmarshal([]byte(item.Model), &jsonItem) jsonItem.TimeUnixNano = uint64(item.Timestamp.UnixNano() / 1000000) - spanEvents := jsonItem.GetValues() - searchSpansResult[0].Events[i] = spanEvents + searchSpanResponses = append(searchSpanResponses, jsonItem) + } + end = time.Now() + zap.S().Debug("getTraceSQLQuery unmarshal took: ", end.Sub(start)) + + err = r.featureFlags.CheckFeature(model.SmartTraceDetail) + smartAlgoEnabled := err == nil + if len(searchScanResponses) > spanLimit && spanId != "" && smartAlgoEnabled { + start = time.Now() + searchSpansResult, err = smartTraceAlgorithm(searchSpanResponses, spanId, levelUp, levelDown, spanLimit) + if err != nil { + return nil, err + } + end = time.Now() + zap.S().Debug("smartTraceAlgo took: ", end.Sub(start)) + } else { + for i, item := range searchSpanResponses { + spanEvents := item.GetValues() + searchSpansResult[0].Events[i] = spanEvents + } } return &searchSpansResult, nil - -} -func interfaceArrayToStringArray(array []interface{}) []string { - var strArray []string - for _, item := range array { - strArray = append(strArray, item.(string)) - } - return strArray } func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) { @@ -1781,7 +1800,7 @@ func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams * GROUP BY src, dest`, - r.traceDB, r.dependencyGraphTable, + r.TraceDB, r.dependencyGraphTable, ) zap.S().Debug(query, args) @@ -1841,41 +1860,41 @@ func (r *ClickHouseReader) GetFilteredSpansAggregates(ctx context.Context, query if queryParams.GroupBy != "" { switch queryParams.GroupBy { case constants.ServiceName: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, serviceName as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, serviceName as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.HttpCode: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpCode as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpCode as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.HttpMethod: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpMethod as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpMethod as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.HttpUrl: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpUrl as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpUrl as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.HttpRoute: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpRoute as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpRoute as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.HttpHost: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpHost as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, httpHost as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.DBName: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbName as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbName as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.DBOperation: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbOperation as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbOperation as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.OperationRequest: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, name as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, name as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.MsgSystem: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, msgSystem as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, msgSystem as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.MsgOperation: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, msgOperation as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, msgOperation as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.DBSystem: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbSystem as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, dbSystem as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.Component: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, component as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, component as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.RPCMethod: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, rpcMethod as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, rpcMethod as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) case constants.ResponseStatusCode: - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, responseStatusCode as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, responseStatusCode as groupBy, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) default: return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("groupBy type: %s not supported", queryParams.GroupBy)} } } else { - query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.traceDB, r.indexTable) + query = fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d minute) as time, %s FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", queryParams.StepSeconds/60, aggregation_query, r.TraceDB, r.indexTable) } if len(queryParams.TraceID) > 0 { @@ -2458,7 +2477,7 @@ func (r *ClickHouseReader) ListErrors(ctx context.Context, queryParams *model.Li var getErrorResponses []model.Error - query := fmt.Sprintf("SELECT any(exceptionType) as exceptionType, any(exceptionMessage) as exceptionMessage, count() AS exceptionCount, min(timestamp) as firstSeen, max(timestamp) as lastSeen, any(serviceName) as serviceName, groupID FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU GROUP BY groupID", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT any(exceptionType) as exceptionType, any(exceptionMessage) as exceptionMessage, count() AS exceptionCount, min(timestamp) as firstSeen, max(timestamp) as lastSeen, any(serviceName) as serviceName, groupID FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU GROUP BY groupID", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} if len(queryParams.OrderParam) != 0 { if queryParams.Order == constants.Descending { @@ -2492,7 +2511,7 @@ func (r *ClickHouseReader) CountErrors(ctx context.Context, queryParams *model.C var errorCount uint64 - query := fmt.Sprintf("SELECT count(distinct(groupID)) FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT count(distinct(groupID)) FROM %s.%s WHERE timestamp >= @timestampL AND timestamp <= @timestampU", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("timestampL", strconv.FormatInt(queryParams.Start.UnixNano(), 10)), clickhouse.Named("timestampU", strconv.FormatInt(queryParams.End.UnixNano(), 10))} err := r.db.QueryRow(ctx, query, args...).Scan(&errorCount) @@ -2514,7 +2533,7 @@ func (r *ClickHouseReader) GetErrorFromErrorID(ctx context.Context, queryParams } var getErrorWithSpanReponse []model.ErrorWithSpan - query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID AND errorID = @errorID LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...) @@ -2537,7 +2556,7 @@ func (r *ClickHouseReader) GetErrorFromGroupID(ctx context.Context, queryParams var getErrorWithSpanReponse []model.ErrorWithSpan - query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT * FROM %s.%s WHERE timestamp = @timestamp AND groupID = @groupID LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getErrorWithSpanReponse, query, args...) @@ -2585,7 +2604,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode var getNextErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp >= @timestamp AND errorID != @errorID ORDER BY timestamp ASC LIMIT 2", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp >= @timestamp AND errorID != @errorID ORDER BY timestamp ASC LIMIT 2", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getNextErrorIDReponse, query, args...) @@ -2606,7 +2625,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode if getNextErrorIDReponse[0].Timestamp.UnixNano() == getNextErrorIDReponse[1].Timestamp.UnixNano() { var getNextErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp = @timestamp AND errorID > @errorID ORDER BY errorID ASC LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp = @timestamp AND errorID > @errorID ORDER BY errorID ASC LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getNextErrorIDReponse, query, args...) @@ -2620,7 +2639,7 @@ func (r *ClickHouseReader) getNextErrorID(ctx context.Context, queryParams *mode if len(getNextErrorIDReponse) == 0 { var getNextErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp > @timestamp ORDER BY timestamp ASC LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as nextErrorID, timestamp as nextTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp > @timestamp ORDER BY timestamp ASC LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getNextErrorIDReponse, query, args...) @@ -2654,7 +2673,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode var getPrevErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp <= @timestamp AND errorID != @errorID ORDER BY timestamp DESC LIMIT 2", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp <= @timestamp AND errorID != @errorID ORDER BY timestamp DESC LIMIT 2", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getPrevErrorIDReponse, query, args...) @@ -2675,7 +2694,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode if getPrevErrorIDReponse[0].Timestamp.UnixNano() == getPrevErrorIDReponse[1].Timestamp.UnixNano() { var getPrevErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp = @timestamp AND errorID < @errorID ORDER BY errorID DESC LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp = @timestamp AND errorID < @errorID ORDER BY errorID DESC LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getPrevErrorIDReponse, query, args...) @@ -2689,7 +2708,7 @@ func (r *ClickHouseReader) getPrevErrorID(ctx context.Context, queryParams *mode if len(getPrevErrorIDReponse) == 0 { var getPrevErrorIDReponse []model.NextPrevErrorIDsDBResponse - query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp < @timestamp ORDER BY timestamp DESC LIMIT 1", r.traceDB, r.errorTable) + query := fmt.Sprintf("SELECT errorID as prevErrorID, timestamp as prevTimestamp FROM %s.%s WHERE groupID = @groupID AND timestamp < @timestamp ORDER BY timestamp DESC LIMIT 1", r.TraceDB, r.errorTable) args := []interface{}{clickhouse.Named("errorID", queryParams.ErrorID), clickhouse.Named("groupID", queryParams.GroupID), clickhouse.Named("timestamp", strconv.FormatInt(queryParams.Timestamp.UnixNano(), 10))} err := r.db.Select(ctx, &getPrevErrorIDReponse, query, args...) @@ -2830,6 +2849,11 @@ func (r *ClickHouseReader) GetMetricAutocompleteMetricNames(ctx context.Context, } +func (r *ClickHouseReader) GetMetricResultEE(ctx context.Context, query string) ([]*model.Series, string, error) { + zap.S().Error("GetMetricResultEE is not implemented for opensource version") + return nil, "", fmt.Errorf("GetMetricResultEE is not implemented for opensource version") +} + // GetMetricResult runs the query and returns list of time series func (r *ClickHouseReader) GetMetricResult(ctx context.Context, query string) ([]*model.Series, error) { @@ -3001,7 +3025,7 @@ func (r *ClickHouseReader) GetLogsInfoInLastHeartBeatInterval(ctx context.Contex func (r *ClickHouseReader) GetTagsInfoInLastHeartBeatInterval(ctx context.Context) (*model.TagsInfo, error) { - queryStr := fmt.Sprintf("select tagMap['service.name'] as serviceName, tagMap['deployment.environment'] as env, tagMap['telemetry.sdk.language'] as language from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", r.traceDB, r.indexTable, 1) + queryStr := fmt.Sprintf("select tagMap['service.name'] as serviceName, tagMap['deployment.environment'] as env, tagMap['telemetry.sdk.language'] as language from %s.%s where timestamp > toUnixTimestamp(now()-toIntervalMinute(%d));", r.TraceDB, r.indexTable, 1) tagTelemetryDataList := []model.TagTelemetryData{} err := r.db.Select(ctx, &tagTelemetryDataList, queryStr) diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index fcf25b93f0..60dbbf6987 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -213,7 +213,7 @@ func writeHttpResponse(w http.ResponseWriter, data interface{}) { func (aH *APIHandler) RegisterMetricsRoutes(router *mux.Router) { subRouter := router.PathPrefix("/api/v2/metrics").Subrouter() - subRouter.HandleFunc("/query_range", ViewAccess(aH.queryRangeMetricsV2)).Methods(http.MethodPost) + subRouter.HandleFunc("/query_range", ViewAccess(aH.QueryRangeMetricsV2)).Methods(http.MethodPost) subRouter.HandleFunc("/autocomplete/list", ViewAccess(aH.metricAutocompleteMetricName)).Methods(http.MethodGet) subRouter.HandleFunc("/autocomplete/tagKey", ViewAccess(aH.metricAutocompleteTagKey)).Methods(http.MethodGet) subRouter.HandleFunc("/autocomplete/tagValue", ViewAccess(aH.metricAutocompleteTagValue)).Methods(http.MethodGet) @@ -354,7 +354,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/service/overview", ViewAccess(aH.getServiceOverview)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_operations", ViewAccess(aH.getTopOperations)).Methods(http.MethodPost) router.HandleFunc("/api/v1/service/top_level_operations", ViewAccess(aH.getServicesTopLevelOps)).Methods(http.MethodPost) - router.HandleFunc("/api/v1/traces/{traceId}", ViewAccess(aH.searchTraces)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/traces/{traceId}", ViewAccess(aH.SearchTraces)).Methods(http.MethodGet) router.HandleFunc("/api/v1/usage", ViewAccess(aH.getUsage)).Methods(http.MethodGet) router.HandleFunc("/api/v1/dependency_graph", ViewAccess(aH.dependencyGraph)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", AdminAccess(aH.setTTL)).Methods(http.MethodPost) @@ -486,7 +486,7 @@ func (aH *APIHandler) metricAutocompleteTagValue(w http.ResponseWriter, r *http. aH.Respond(w, tagValueList) } -func (aH *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { +func (aH *APIHandler) QueryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r) if apiErrorObj != nil { @@ -1360,12 +1360,15 @@ func (aH *APIHandler) getServicesList(w http.ResponseWriter, r *http.Request) { } -func (aH *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) { +func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - traceId := vars["traceId"] + traceId, spanId, levelUpInt, levelDownInt, err := ParseSearchTracesParams(r) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params") + return + } - result, err := aH.reader.SearchTraces(r.Context(), traceId) + result, err := aH.reader.SearchTraces(r.Context(), traceId, spanId, levelUpInt, levelDownInt, 0, nil) if aH.HandleError(w, err, http.StatusBadRequest) { return } diff --git a/pkg/query-service/app/parser.go b/pkg/query-service/app/parser.go index bfd4042d22..fc3154c9fc 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -225,6 +225,30 @@ func parseGetServicesRequest(r *http.Request) (*model.GetServicesParams, error) return postData, nil } +func ParseSearchTracesParams(r *http.Request) (string, string, int, int, error) { + vars := mux.Vars(r) + traceId := vars["traceId"] + spanId := r.URL.Query().Get("spanId") + levelUp := r.URL.Query().Get("levelUp") + levelDown := r.URL.Query().Get("levelDown") + if levelUp == "" || levelUp == "null" { + levelUp = "0" + } + if levelDown == "" || levelDown == "null" { + levelDown = "0" + } + + levelUpInt, err := strconv.Atoi(levelUp) + if err != nil { + return "", "", 0, 0, err + } + levelDownInt, err := strconv.Atoi(levelDown) + if err != nil { + return "", "", 0, 0, err + } + return traceId, spanId, levelUpInt, levelDownInt, nil +} + func DoesExistInSlice(item string, list []string) bool { for _, element := range list { if item == element { diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index cbb8a807fa..b79b7d011f 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -88,7 +88,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { storage := os.Getenv("STORAGE") if storage == "clickhouse" { zap.S().Info("Using ClickHouse as datastore ...") - clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath) + clickhouseReader := clickhouseReader.NewReader(localDB, serverOptions.PromConfigPath, fm) go clickhouseReader.Start(readerReady) reader = clickhouseReader } else { diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 4a83b16c80..3200b10c2f 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -46,7 +46,7 @@ type Reader interface { GetNextPrevErrorIDs(ctx context.Context, params *model.GetErrorParams) (*model.NextPrevErrorIDs, *model.ApiError) // Search Interfaces - SearchTraces(ctx context.Context, traceID string) (*[]model.SearchSpansResult, error) + SearchTraces(ctx context.Context, traceID string, spanId string, levelUp int, levelDown int, spanLimit int, smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error) // Setter Interfaces SetTTL(ctx context.Context, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) @@ -55,6 +55,7 @@ type Reader interface { GetMetricAutocompleteTagKey(ctx context.Context, params *model.MetricAutocompleteTagParams) (*[]string, *model.ApiError) GetMetricAutocompleteTagValue(ctx context.Context, params *model.MetricAutocompleteTagParams) (*[]string, *model.ApiError) GetMetricResult(ctx context.Context, query string) ([]*model.Series, error) + GetMetricResultEE(ctx context.Context, query string) ([]*model.Series, string, error) GetTotalSpans(ctx context.Context) (uint64, error) GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) diff --git a/pkg/query-service/model/featureSet.go b/pkg/query-service/model/featureSet.go index 1b59450a44..bba153e861 100644 --- a/pkg/query-service/model/featureSet.go +++ b/pkg/query-service/model/featureSet.go @@ -3,6 +3,8 @@ package model type FeatureSet map[string]bool const Basic = "BASIC_PLAN" +const SmartTraceDetail = "SMART_TRACE_DETAIL" +const CustomMetricsFunction = "CUSTOM_METRICS_FUNCTION" var BasicPlan = FeatureSet{ Basic: true, diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index 23654fb899..27b466b9ea 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -187,7 +187,7 @@ type GetFilterSpansResponse struct { TotalSpans uint64 `json:"totalSpans"` } -type SearchSpanDBReponseItem struct { +type SearchSpanDBResponseItem struct { Timestamp time.Time `ch:"timestamp"` TraceID string `ch:"traceID"` Model string `ch:"model"` @@ -200,18 +200,21 @@ type Event struct { IsError bool `json:"isError,omitempty"` } -type SearchSpanReponseItem struct { +//easyjson:json +type SearchSpanResponseItem struct { TimeUnixNano uint64 `json:"timestamp"` - SpanID string `json:"spanID"` - TraceID string `json:"traceID"` + DurationNano int64 `json:"durationNano"` + SpanID string `json:"spanId"` + RootSpanID string `json:"rootSpanId"` + TraceID string `json:"traceId"` + HasError bool `json:"hasError"` + Kind int32 `json:"kind"` ServiceName string `json:"serviceName"` Name string `json:"name"` - Kind int32 `json:"kind"` References []OtelSpanRef `json:"references,omitempty"` - DurationNano int64 `json:"durationNano"` TagMap map[string]string `json:"tagMap"` Events []string `json:"event"` - HasError bool `json:"hasError"` + RootName string `json:"rootName"` } type OtelSpanRef struct { @@ -220,14 +223,14 @@ type OtelSpanRef struct { RefType string `json:"refType,omitempty"` } -func (ref *OtelSpanRef) toString() string { +func (ref *OtelSpanRef) ToString() string { retString := fmt.Sprintf(`{TraceId=%s, SpanId=%s, RefType=%s}`, ref.TraceId, ref.SpanId, ref.RefType) return retString } -func (item *SearchSpanReponseItem) GetValues() []interface{} { +func (item *SearchSpanResponseItem) GetValues() []interface{} { references := []OtelSpanRef{} jsonbody, _ := json.Marshal(item.References) @@ -235,7 +238,7 @@ func (item *SearchSpanReponseItem) GetValues() []interface{} { referencesStringArray := []string{} for _, item := range references { - referencesStringArray = append(referencesStringArray, item.toString()) + referencesStringArray = append(referencesStringArray, item.ToString()) } if item.Events == nil { diff --git a/pkg/query-service/model/response_easyjson.go b/pkg/query-service/model/response_easyjson.go new file mode 100644 index 0000000000..8c00b2a80b --- /dev/null +++ b/pkg/query-service/model/response_easyjson.go @@ -0,0 +1,328 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package model + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson6ff3ac1dDecodeGoSignozIoSignozPkgQueryServiceModel(in *jlexer.Lexer, out *SearchSpanResponseItem) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "timestamp": + out.TimeUnixNano = uint64(in.Uint64()) + case "durationNano": + out.DurationNano = int64(in.Int64()) + case "spanId": + out.SpanID = string(in.String()) + case "rootSpanId": + out.RootSpanID = string(in.String()) + case "traceId": + out.TraceID = string(in.String()) + case "hasError": + out.HasError = bool(in.Bool()) + case "kind": + out.Kind = int32(in.Int32()) + case "serviceName": + out.ServiceName = string(in.String()) + case "name": + out.Name = string(in.String()) + case "references": + if in.IsNull() { + in.Skip() + out.References = nil + } else { + in.Delim('[') + if out.References == nil { + if !in.IsDelim(']') { + out.References = make([]OtelSpanRef, 0, 1) + } else { + out.References = []OtelSpanRef{} + } + } else { + out.References = (out.References)[:0] + } + for !in.IsDelim(']') { + var v1 OtelSpanRef + easyjson6ff3ac1dDecodeGoSignozIoSignozPkgQueryServiceModel1(in, &v1) + out.References = append(out.References, v1) + in.WantComma() + } + in.Delim(']') + } + case "tagMap": + if in.IsNull() { + in.Skip() + } else { + in.Delim('{') + out.TagMap = make(map[string]string) + for !in.IsDelim('}') { + key := string(in.String()) + in.WantColon() + var v2 string + v2 = string(in.String()) + (out.TagMap)[key] = v2 + in.WantComma() + } + in.Delim('}') + } + case "event": + if in.IsNull() { + in.Skip() + out.Events = nil + } else { + in.Delim('[') + if out.Events == nil { + if !in.IsDelim(']') { + out.Events = make([]string, 0, 4) + } else { + out.Events = []string{} + } + } else { + out.Events = (out.Events)[:0] + } + for !in.IsDelim(']') { + var v3 string + v3 = string(in.String()) + out.Events = append(out.Events, v3) + in.WantComma() + } + in.Delim(']') + } + case "rootName": + out.RootName = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6ff3ac1dEncodeGoSignozIoSignozPkgQueryServiceModel(out *jwriter.Writer, in SearchSpanResponseItem) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"timestamp\":" + out.RawString(prefix[1:]) + out.Uint64(uint64(in.TimeUnixNano)) + } + { + const prefix string = ",\"durationNano\":" + out.RawString(prefix) + out.Int64(int64(in.DurationNano)) + } + { + const prefix string = ",\"spanId\":" + out.RawString(prefix) + out.String(string(in.SpanID)) + } + { + const prefix string = ",\"rootSpanId\":" + out.RawString(prefix) + out.String(string(in.RootSpanID)) + } + { + const prefix string = ",\"traceId\":" + out.RawString(prefix) + out.String(string(in.TraceID)) + } + { + const prefix string = ",\"hasError\":" + out.RawString(prefix) + out.Bool(bool(in.HasError)) + } + { + const prefix string = ",\"kind\":" + out.RawString(prefix) + out.Int32(int32(in.Kind)) + } + { + const prefix string = ",\"serviceName\":" + out.RawString(prefix) + out.String(string(in.ServiceName)) + } + { + const prefix string = ",\"name\":" + out.RawString(prefix) + out.String(string(in.Name)) + } + if len(in.References) != 0 { + const prefix string = ",\"references\":" + out.RawString(prefix) + { + out.RawByte('[') + for v4, v5 := range in.References { + if v4 > 0 { + out.RawByte(',') + } + easyjson6ff3ac1dEncodeGoSignozIoSignozPkgQueryServiceModel1(out, v5) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tagMap\":" + out.RawString(prefix) + if in.TagMap == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 { + out.RawString(`null`) + } else { + out.RawByte('{') + v6First := true + for v6Name, v6Value := range in.TagMap { + if v6First { + v6First = false + } else { + out.RawByte(',') + } + out.String(string(v6Name)) + out.RawByte(':') + out.String(string(v6Value)) + } + out.RawByte('}') + } + } + { + const prefix string = ",\"event\":" + out.RawString(prefix) + if in.Events == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v7, v8 := range in.Events { + if v7 > 0 { + out.RawByte(',') + } + out.String(string(v8)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"rootName\":" + out.RawString(prefix) + out.String(string(in.RootName)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v SearchSpanResponseItem) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6ff3ac1dEncodeGoSignozIoSignozPkgQueryServiceModel(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v SearchSpanResponseItem) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6ff3ac1dEncodeGoSignozIoSignozPkgQueryServiceModel(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *SearchSpanResponseItem) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6ff3ac1dDecodeGoSignozIoSignozPkgQueryServiceModel(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *SearchSpanResponseItem) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6ff3ac1dDecodeGoSignozIoSignozPkgQueryServiceModel(l, v) +} +func easyjson6ff3ac1dDecodeGoSignozIoSignozPkgQueryServiceModel1(in *jlexer.Lexer, out *OtelSpanRef) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "traceId": + out.TraceId = string(in.String()) + case "spanId": + out.SpanId = string(in.String()) + case "refType": + out.RefType = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6ff3ac1dEncodeGoSignozIoSignozPkgQueryServiceModel1(out *jwriter.Writer, in OtelSpanRef) { + out.RawByte('{') + first := true + _ = first + if in.TraceId != "" { + const prefix string = ",\"traceId\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.TraceId)) + } + if in.SpanId != "" { + const prefix string = ",\"spanId\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.SpanId)) + } + if in.RefType != "" { + const prefix string = ",\"refType\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.RefType)) + } + out.RawByte('}') +} From 4b3829fd5bd0623c31045fda974a0902f664d10a Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Thu, 24 Nov 2022 18:19:07 +0530 Subject: [PATCH 17/27] fix: fixed date condition (start and end) while preparing ch query (#1751) Co-authored-by: Ankit Nayan --- pkg/query-service/rules/thresholdRule.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index 3d08f70d35..b468528335 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -343,8 +343,8 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) *qsmodel.QueryRangeParam if r.ruleCondition.QueryType() == qsmodel.CLICKHOUSE { return &qsmodel.QueryRangeParamsV2{ - Start: ts.UnixMilli(), - End: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), + Start: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), + End: ts.UnixMilli(), Step: 30, CompositeMetricQuery: r.ruleCondition.CompositeMetricQuery, Variables: make(map[string]interface{}, 0), @@ -353,8 +353,8 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) *qsmodel.QueryRangeParam // default mode return &qsmodel.QueryRangeParamsV2{ - Start: ts.UnixMilli(), - End: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), + Start: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(), + End: ts.UnixMilli(), Step: 30, CompositeMetricQuery: r.ruleCondition.CompositeMetricQuery, } From 7cec2db50386e7a6406e3a1f5d3dddf1f6d929e4 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 25 Nov 2022 12:16:47 +0530 Subject: [PATCH 18/27] fix: [alerts] solved legend not updating issue in ch query editor (#1757) * fix: [alerts] solved legend not updating issue in ch query editor * fix: [alerts]removed console.log * fix: added jsdoc description tag --- .../{ => ChQuerySection}/ChQuerySection.tsx | 27 +++++--------- .../FormAlertRules/ChQuerySection/index.ts | 3 ++ .../ChQuerySection/transform.ts | 37 +++++++++++++++++++ .../QueryBuilder/clickHouse/query.tsx | 1 + 4 files changed, 51 insertions(+), 17 deletions(-) rename frontend/src/container/FormAlertRules/{ => ChQuerySection}/ChQuerySection.tsx (70%) create mode 100644 frontend/src/container/FormAlertRules/ChQuerySection/index.ts create mode 100644 frontend/src/container/FormAlertRules/ChQuerySection/transform.ts diff --git a/frontend/src/container/FormAlertRules/ChQuerySection.tsx b/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx similarity index 70% rename from frontend/src/container/FormAlertRules/ChQuerySection.tsx rename to frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx index 49af0aed04..aa316fe558 100644 --- a/frontend/src/container/FormAlertRules/ChQuerySection.tsx +++ b/frontend/src/container/FormAlertRules/ChQuerySection/ChQuerySection.tsx @@ -3,6 +3,8 @@ import { IClickHouseQueryHandleChange } from 'container/NewWidget/LeftContainer/ import React from 'react'; import { IChQueries } from 'types/api/alerts/compositeQuery'; +import { rawQueryToIChQuery, toIClickHouseQuery } from './transform'; + function ChQuerySection({ chQueries, setChQueries, @@ -12,34 +14,25 @@ function ChQuerySection({ legend, toggleDelete, }: IClickHouseQueryHandleChange): void => { - let chQuery = chQueries.A; + const chQuery = rawQueryToIChQuery( + chQueries.A, + rawQuery, + legend, + toggleDelete, + ); - if (rawQuery) { - chQuery.rawQuery = rawQuery; - chQuery.query = rawQuery; - } - - if (legend) chQuery.legend = legend; - if (toggleDelete) { - chQuery = { - rawQuery: '', - legend: '', - name: 'A', - disabled: false, - query: '', - }; - } setChQueries({ A: { ...chQuery, }, }); }; + return ( ); diff --git a/frontend/src/container/FormAlertRules/ChQuerySection/index.ts b/frontend/src/container/FormAlertRules/ChQuerySection/index.ts new file mode 100644 index 0000000000..a28e8594db --- /dev/null +++ b/frontend/src/container/FormAlertRules/ChQuerySection/index.ts @@ -0,0 +1,3 @@ +import ChQuerySection from './ChQuerySection'; + +export default ChQuerySection; diff --git a/frontend/src/container/FormAlertRules/ChQuerySection/transform.ts b/frontend/src/container/FormAlertRules/ChQuerySection/transform.ts new file mode 100644 index 0000000000..5f5198ec81 --- /dev/null +++ b/frontend/src/container/FormAlertRules/ChQuerySection/transform.ts @@ -0,0 +1,37 @@ +import { IChQuery } from 'types/api/alerts/compositeQuery'; +import { IClickHouseQuery } from 'types/api/dashboard/getAll'; + +// @description rawQueryToIChQuery transforms raw query (from ClickHouseQueryBuilder) +// to alert specific IChQuery format +export const rawQueryToIChQuery = ( + src: IChQuery, + rawQuery: string | undefined, + legend: string | undefined, + toggleDelete: boolean | undefined, +): IChQuery => { + if (toggleDelete) { + return { + rawQuery: '', + legend: '', + name: 'A', + disabled: false, + query: '', + }; + } + + return { + rawQuery: rawQuery !== undefined ? rawQuery : src.rawQuery, + query: rawQuery !== undefined ? rawQuery : src.rawQuery, + legend: legend !== undefined ? legend : src.legend, + name: 'A', + disabled: false, + }; +}; + +// @description toIClickHouseQuery transforms IChQuery (alert specific) to +// ClickHouseQueryBuilder format. The main difference is +// use of rawQuery (in ClickHouseQueryBuilder) +// and query (in alert builder) +export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => { + return { ...src, name: 'A', rawQuery: src.query }; +}; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx index 0ace45fdc8..389b7e15c4 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/QueryBuilder/clickHouse/query.tsx @@ -54,6 +54,7 @@ function ClickHouseQueryBuilder({ } size="middle" defaultValue={queryData.legend} + value={queryData.legend} addonBefore="Legend Format" /> From 0cbba071ea19458fa4a1f8769acebdf1871571e4 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Fri, 25 Nov 2022 16:04:09 +0530 Subject: [PATCH 19/27] fix: [alerts] fixed selected interval for chart preview in ch use case (#1761) --- frontend/src/container/FormAlertRules/index.tsx | 1 + frontend/src/container/FormAlertRules/utils.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index a0791a4aa1..9f91aed088 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -468,6 +468,7 @@ function FormAlertRules({ threshold={alertDef.condition?.target} query={manualStagedQuery} userQueryKey={runQueryId} + selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); }; diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index 9e6ec66d07..f759d0e37e 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -135,7 +135,7 @@ export const toChartInterval = (evalWindow: string | undefined): Time => { case '30m0s': return '30min'; case '60m0s': - return '30min'; + return '1hr'; case '4h0m0s': return '4hr'; case '24h0m0s': From 2771d2e77429a1f78a7323e957a4076201393a11 Mon Sep 17 00:00:00 2001 From: Amol Umbark Date: Sun, 27 Nov 2022 00:59:09 -0800 Subject: [PATCH 20/27] fix: [alerts] [ch-query] added aliases in metric query result (#1760) * fix: [alerts] [ch-query] added aliases in metric query result * fix: added more column type support for target in ch query * fix: added error handling when data type is unexpected in metric result Co-authored-by: Pranay Prateek --- ee/query-service/app/db/metrics.go | 27 ++++++++++++ .../app/clickhouseReader/reader.go | 26 ++++++++++++ pkg/query-service/constants/constants.go | 5 +++ pkg/query-service/rules/thresholdRule.go | 41 +++++++++++++------ 4 files changed, 86 insertions(+), 13 deletions(-) diff --git a/ee/query-service/app/db/metrics.go b/ee/query-service/app/db/metrics.go index 77e7d50c9b..3bafc6a638 100644 --- a/ee/query-service/app/db/metrics.go +++ b/ee/query-service/app/db/metrics.go @@ -12,6 +12,7 @@ import ( "time" "go.signoz.io/signoz/ee/query-service/model" + baseconst "go.signoz.io/signoz/pkg/query-service/constants" basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/utils" "go.uber.org/zap" @@ -91,6 +92,32 @@ func (r *ClickhouseReader) GetMetricResultEE(ctx context.Context, query string) metricPoint.Timestamp = v.UnixMilli() case *float64: metricPoint.Value = *v + case **float64: + // ch seems to return this type when column is derived from + // SELECT count(*)/ SELECT count(*) + floatVal := *v + if floatVal != nil { + metricPoint.Value = *floatVal + } + case *float32: + float32Val := float32(*v) + metricPoint.Value = float64(float32Val) + case *uint8, *uint64, *uint16, *uint32: + if _, ok := baseconst.ReservedColumnTargetAliases[colName]; ok { + metricPoint.Value = float64(reflect.ValueOf(v).Elem().Uint()) + } else { + groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) + groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint()) + } + case *int8, *int16, *int32, *int64: + if _, ok := baseconst.ReservedColumnTargetAliases[colName]; ok { + metricPoint.Value = float64(reflect.ValueOf(v).Elem().Int()) + } else { + groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) + groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) + } + default: + zap.S().Errorf("invalid var found in metric builder query result", v, colName) } } sort.Strings(groupBy) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 0ccb5cb73c..49c728c5f0 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -2916,6 +2916,32 @@ func (r *ClickHouseReader) GetMetricResult(ctx context.Context, query string) ([ metricPoint.Timestamp = v.UnixMilli() case *float64: metricPoint.Value = *v + case **float64: + // ch seems to return this type when column is derived from + // SELECT count(*)/ SELECT count(*) + floatVal := *v + if floatVal != nil { + metricPoint.Value = *floatVal + } + case *float32: + float32Val := float32(*v) + metricPoint.Value = float64(float32Val) + case *uint8, *uint64, *uint16, *uint32: + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + metricPoint.Value = float64(reflect.ValueOf(v).Elem().Uint()) + } else { + groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) + groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint()) + } + case *int8, *int16, *int32, *int64: + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + metricPoint.Value = float64(reflect.ValueOf(v).Elem().Int()) + } else { + groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) + groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) + } + default: + zap.S().Errorf("invalid var found in metric builder query result", v, colName) } } sort.Strings(groupBy) diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index d376b068e9..24690e542a 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -190,3 +190,8 @@ const ( "CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as attributes_float64," + "CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " ) + +// ReservedColumnTargetAliases identifies result value from a user +// written clickhouse query. The column alias indcate which value is +// to be considered as final result (or target) +var ReservedColumnTargetAliases = map[string]bool{"result": true, "res": true, "value": true} diff --git a/pkg/query-service/rules/thresholdRule.go b/pkg/query-service/rules/thresholdRule.go index b468528335..c351d96b0f 100644 --- a/pkg/query-service/rules/thresholdRule.go +++ b/pkg/query-service/rules/thresholdRule.go @@ -422,25 +422,40 @@ func (r *ThresholdRule) runChQuery(ctx context.Context, db clickhouse.Conn, quer } case *float64: - if colName == "res" || colName == "value" { + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { sample.Point.V = *v - } else { lbls.Set(colName, fmt.Sprintf("%f", *v)) } - case *uint64: - intv := *v - if colName == "res" || colName == "value" { - sample.Point.V = float64(intv) - } else { - lbls.Set(colName, fmt.Sprintf("%d", intv)) + case **float64: + // ch seems to return this type when column is derived from + // SELECT count(*)/ SELECT count(*) + floatVal := *v + if floatVal != nil { + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + sample.Point.V = *floatVal + } else { + lbls.Set(colName, fmt.Sprintf("%f", *floatVal)) + } } - case *uint8: - intv := *v - if colName == "res" || colName == "value" { - sample.Point.V = float64(intv) + case *float32: + float32Val := float32(*v) + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + sample.Point.V = float64(float32Val) } else { - lbls.Set(colName, fmt.Sprintf("%d", intv)) + lbls.Set(colName, fmt.Sprintf("%f", float32Val)) + } + case *uint8, *uint64, *uint16, *uint32: + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + sample.Point.V = float64(reflect.ValueOf(v).Elem().Uint()) + } else { + lbls.Set(colName, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) + } + case *int8, *int16, *int32, *int64: + if _, ok := constants.ReservedColumnTargetAliases[colName]; ok { + sample.Point.V = float64(reflect.ValueOf(v).Elem().Int()) + } else { + lbls.Set(colName, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) } default: zap.S().Errorf("ruleId:", r.ID(), "\t error: invalid var found in query result", v, columnNames[i]) From d06d41af87864331571d2cc231416f2c422f40fc Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Mon, 28 Nov 2022 14:18:43 +0530 Subject: [PATCH 21/27] fix: parser updated to differentiate between params and query string (#1763) --- pkg/query-service/app/logs/parser.go | 13 +++- pkg/query-service/app/logs/parser_test.go | 85 ++++++++++++++++------- 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/pkg/query-service/app/logs/parser.go b/pkg/query-service/app/logs/parser.go index a1ab021e33..cdcd23f4e7 100644 --- a/pkg/query-service/app/logs/parser.go +++ b/pkg/query-service/app/logs/parser.go @@ -309,9 +309,16 @@ func GenerateSQLWhere(allFields *model.GetFieldsResponse, params *model.LogsFilt filterTokens = append(filterTokens, filter) } - if len(filterTokens) > 0 { - if len(tokens) > 0 { - tokens[0] = fmt.Sprintf("and %s", tokens[0]) + lenFilterTokens := len(filterTokens) + if lenFilterTokens > 0 { + // add parenthesis + filterTokens[0] = fmt.Sprintf("( %s", filterTokens[0]) + filterTokens[lenFilterTokens-1] = fmt.Sprintf("%s) ", filterTokens[lenFilterTokens-1]) + + lenTokens := len(tokens) + if lenTokens > 0 { + tokens[0] = fmt.Sprintf("and ( %s", tokens[0]) + tokens[lenTokens-1] = fmt.Sprintf("%s) ", tokens[lenTokens-1]) } filterTokens = append(filterTokens, tokens...) tokens = filterTokens diff --git a/pkg/query-service/app/logs/parser_test.go b/pkg/query-service/app/logs/parser_test.go index f4b75f41e0..e61ca76f49 100644 --- a/pkg/query-service/app/logs/parser_test.go +++ b/pkg/query-service/app/logs/parser_test.go @@ -271,32 +271,65 @@ func TestCheckIfPrevousPaginateAndModifyOrder(t *testing.T) { } } -func TestGenerateSQLQuery(t *testing.T) { - allFields := model.GetFieldsResponse{ - Selected: []model.LogField{ - { - Name: "id", - DataType: "int64", - Type: "attributes", - }, +var generateSQLQueryFields = model.GetFieldsResponse{ + Selected: []model.LogField{ + { + Name: "field1", + DataType: "int64", + Type: "attributes", }, - Interesting: []model.LogField{ - { - Name: "code", - DataType: "int64", - Type: "attributes", - }, + { + Name: "field2", + DataType: "double64", + Type: "attributes", }, - } - - query := "id lt 100 and id gt 50 and code lte 500 and code gte 400" - tsStart := uint64(1657689292000) - tsEnd := uint64(1657689294000) - idStart := "2BsKLKv8cZrLCn6rkOcRGkdjBdM" - idEnd := "2BsKG6tRpFWjYMcWsAGKfSxoQdU" - sqlWhere := "timestamp >= '1657689292000' and timestamp <= '1657689294000' and id > '2BsKLKv8cZrLCn6rkOcRGkdjBdM' and id < '2BsKG6tRpFWjYMcWsAGKfSxoQdU' and id < 100 and id > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 " - Convey("testGenerateSQL", t, func() { - res, _ := GenerateSQLWhere(&allFields, &model.LogsFilterParams{Query: query, TimestampStart: tsStart, TimestampEnd: tsEnd, IdGt: idStart, IdLT: idEnd}) - So(res, ShouldEqual, sqlWhere) - }) + { + Name: "field2", + DataType: "string", + Type: "attributes", + }, + }, + Interesting: []model.LogField{ + { + Name: "code", + DataType: "int64", + Type: "attributes", + }, + }, +} + +var generateSQLQueryTestCases = []struct { + Name string + Filter model.LogsFilterParams + SqlFilter string +}{ + { + Name: "first query with more than 1 compulsory filters", + Filter: model.LogsFilterParams{ + Query: "field1 lt 100 and field1 gt 50 and code lte 500 and code gte 400", + TimestampStart: uint64(1657689292000), + TimestampEnd: uint64(1657689294000), + IdGt: "2BsKLKv8cZrLCn6rkOcRGkdjBdM", + IdLT: "2BsKG6tRpFWjYMcWsAGKfSxoQdU", + }, + SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' and id > '2BsKLKv8cZrLCn6rkOcRGkdjBdM' and id < '2BsKG6tRpFWjYMcWsAGKfSxoQdU' ) and ( field1 < 100 and field1 > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ", + }, + { + Name: "second query with only timestamp range", + Filter: model.LogsFilterParams{ + Query: "field1 lt 100 and field1 gt 50 and code lte 500 and code gte 400", + TimestampStart: uint64(1657689292000), + TimestampEnd: uint64(1657689294000), + }, + SqlFilter: "( timestamp >= '1657689292000' and timestamp <= '1657689294000' ) and ( field1 < 100 and field1 > 50 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] <= 500 and attributes_int64_value[indexOf(attributes_int64_key, 'code')] >= 400 ) ", + }, +} + +func TestGenerateSQLQuery(t *testing.T) { + for _, test := range generateSQLQueryTestCases { + Convey("testGenerateSQL", t, func() { + res, _ := GenerateSQLWhere(&generateSQLQueryFields, &test.Filter) + So(res, ShouldEqual, test.SqlFilter) + }) + } } From 6c9036fbf4c6a17ee97c0299acae96526e44db89 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 28 Nov 2022 15:44:33 +0530 Subject: [PATCH 22/27] fix[logs][FE]: live tail is fixed (#1759) * fix: live tail is fixed * fix: graph state is updated * chore: step size is updated * chore: xaxis config is updated * chore: isDisabled state is updated for top navigation * chore: selected interval is updated in the reducer * fix: build is fixed * chore: xAxis config is updated Co-authored-by: Pranay Prateek Co-authored-by: Ankit Nayan --- frontend/src/components/Graph/xAxisConfig.ts | 7 ++- frontend/src/container/LogLiveTail/index.tsx | 15 ++++++ .../src/container/LogsAggregate/index.tsx | 15 +----- .../QueryBuilder/QueryBuilder.tsx | 54 +++++++++---------- .../LogsSearchFilter/SearchFields/styles.tsx | 5 -- .../src/container/LogsSearchFilter/index.tsx | 3 +- .../container/TopNav/AutoRefresh/index.tsx | 41 +++++++++----- frontend/src/store/reducers/global.ts | 18 +++++++ frontend/src/store/reducers/logs.ts | 1 - frontend/src/types/actions/globalTime.ts | 18 ++++++- frontend/src/types/reducer/globalTime.ts | 2 + 11 files changed, 112 insertions(+), 67 deletions(-) diff --git a/frontend/src/components/Graph/xAxisConfig.ts b/frontend/src/components/Graph/xAxisConfig.ts index d14d9bba09..ee794b6678 100644 --- a/frontend/src/components/Graph/xAxisConfig.ts +++ b/frontend/src/components/Graph/xAxisConfig.ts @@ -4,9 +4,6 @@ import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalReducer } from 'types/reducer/globalTime'; -interface ITimeUnit { - [key: string]: TimeUnit; -} interface IAxisTimeUintConfig { unitName: TimeUnit; multiplier: number; @@ -22,7 +19,7 @@ export interface ITimeRange { maxTime: number | null; } -export const TIME_UNITS: ITimeUnit = { +export const TIME_UNITS: Record = { millisecond: 'millisecond', second: 'second', minute: 'minute', @@ -31,6 +28,7 @@ export const TIME_UNITS: ITimeUnit = { week: 'week', month: 'month', year: 'year', + quarter: 'quarter', }; const TIME_UNITS_CONFIG: IAxisTimeUintConfig[] = [ @@ -93,6 +91,7 @@ export const convertTimeRange = ( } catch (error) { console.error(error); } + return { unitName: relevantTimeUnit.unitName, stepSize: Math.floor(stepSize) || 1, diff --git a/frontend/src/container/LogLiveTail/index.tsx b/frontend/src/container/LogLiveTail/index.tsx index 1c48b4d8ab..1a4a38343d 100644 --- a/frontend/src/container/LogLiveTail/index.tsx +++ b/frontend/src/container/LogLiveTail/index.tsx @@ -11,6 +11,7 @@ import { throttle } from 'lodash-es'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; +import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime'; import { FLUSH_LOGS, PUSH_LIVE_TAIL_EVENT, @@ -19,6 +20,7 @@ import { } from 'types/actions/logs'; import { TLogsLiveTailState } from 'types/api/logs/liveTail'; import AppReducer from 'types/reducer/app'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; import { TIME_PICKER_OPTIONS } from './config'; @@ -34,12 +36,20 @@ function LogLiveTail(): JSX.Element { logs, } = useSelector((state) => state.logs); const { isDarkMode } = useSelector((state) => state.app); + const { selectedAutoRefreshInterval } = useSelector( + (state) => state.globalTime, + ); + const dispatch = useDispatch(); const handleLiveTail = (toggleState: TLogsLiveTailState): void => { dispatch({ type: TOGGLE_LIVE_TAIL, payload: toggleState, }); + dispatch({ + type: UPDATE_AUTO_REFRESH_DISABLED, + payload: toggleState === 'PLAYING', + }); }; const batchedEventsRef = useRef[]>([]); @@ -131,6 +141,10 @@ function LogLiveTail(): JSX.Element { [dispatch, liveTail, liveTailStartRange], ); + const isDisabled = useMemo(() => selectedAutoRefreshInterval?.length > 0, [ + selectedAutoRefreshInterval, + ]); + return ( @@ -149,6 +163,7 @@ function LogLiveTail(): JSX.Element { type="primary" onClick={handleLiveTailStart} title="Start live tail" + disabled={isDisabled} > Go Live diff --git a/frontend/src/container/LogsAggregate/index.tsx b/frontend/src/container/LogsAggregate/index.tsx index 737164f9fd..34200049c4 100644 --- a/frontend/src/container/LogsAggregate/index.tsx +++ b/frontend/src/container/LogsAggregate/index.tsx @@ -31,6 +31,7 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { ); const reFetchIntervalRef = useRef | null>(null); + useEffect(() => { switch (liveTail) { case 'STOPPED': { @@ -38,18 +39,6 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { clearInterval(reFetchIntervalRef.current); } reFetchIntervalRef.current = null; - // getLogsAggregate({ - // timestampStart: minTime, - // timestampEnd: maxTime, - // step: getStep({ - // start: minTime, - // end: maxTime, - // inputFormat: 'ns', - // }), - // q: queryString, - // ...(idStart ? { idGt: idStart } : {}), - // ...(idEnd ? { idLt: idEnd } : {}), - // }); break; } @@ -106,7 +95,7 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element { }} type="bar" containerHeight="100%" - animate={false} + animate /> )} diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx index 6744c234da..1aab2e8e75 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/QueryBuilder/QueryBuilder.tsx @@ -21,7 +21,7 @@ import { v4 } from 'uuid'; import { SearchFieldsProps } from '..'; import FieldKey from '../FieldKey'; -import { QueryConditionContainer, QueryFieldContainer } from '../styles'; +import { QueryFieldContainer } from '../styles'; import { createParsedQueryStructure } from '../utils'; import { Container, QueryWrapper } from './styles'; import { hashCode, parseQuery } from './utils'; @@ -32,33 +32,27 @@ function QueryConditionField({ query, queryIndex, onUpdate, - style, }: QueryConditionFieldProps): JSX.Element { + const allOptions = Object.values(ConditionalOperators); return ( - - - + ); } -QueryConditionField.defaultProps = { - style: undefined, -}; - interface QueryFieldProps { query: Query; queryIndex: number; @@ -174,7 +168,6 @@ interface QueryConditionFieldProps { query: { value: string | string[]; type: string }[]; queryIndex: number; onUpdate: (arg0: unknown, arg1: number) => void; - style?: React.CSSProperties; } export type Query = { value: string | string[]; type: string }[]; @@ -233,12 +226,13 @@ function QueryBuilder({ ); return ( - +
+ +
); }); diff --git a/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx b/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx index 1f06e924ca..a826da07d4 100644 --- a/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx +++ b/frontend/src/container/LogsSearchFilter/SearchFields/styles.tsx @@ -14,8 +14,3 @@ export const QueryFieldContainer = styled.div` background: ${blue[6]}; } `; - -export const QueryConditionContainer = styled.div` - display: flex; - flex-direction: row; -`; diff --git a/frontend/src/container/LogsSearchFilter/index.tsx b/frontend/src/container/LogsSearchFilter/index.tsx index 5af48b17c1..cf71f3d3bd 100644 --- a/frontend/src/container/LogsSearchFilter/index.tsx +++ b/frontend/src/container/LogsSearchFilter/index.tsx @@ -104,7 +104,8 @@ function SearchFilter({ useEffect(() => { handleSearch(urlQueryString || ''); - }, [handleSearch, urlQueryString]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [urlQueryString, maxTime, minTime]); return ( diff --git a/frontend/src/container/TopNav/AutoRefresh/index.tsx b/frontend/src/container/TopNav/AutoRefresh/index.tsx index bc52e2cf86..aa6a74793c 100644 --- a/frontend/src/container/TopNav/AutoRefresh/index.tsx +++ b/frontend/src/container/TopNav/AutoRefresh/index.tsx @@ -22,19 +22,28 @@ import { useInterval } from 'react-use'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime'; +import { + UPDATE_AUTO_REFRESH_INTERVAL, + UPDATE_TIME_INTERVAL, +} from 'types/actions/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime'; import { options } from './config'; import { ButtonContainer, Container } from './styles'; function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { - const { minTime: initialMinTime, selectedTime } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); + const { + minTime: initialMinTime, + selectedTime, + isAutoRefreshDisabled, + } = useSelector((state) => state.globalTime); const { pathname } = useLocation(); + const isDisabled = useMemo(() => disabled || isAutoRefreshDisabled, [ + isAutoRefreshDisabled, + disabled, + ]); + const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}'); const localStorageValue = useMemo(() => localStorageData[pathname], [ @@ -46,13 +55,19 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { Boolean(localStorageValue), ); + const dispatch = useDispatch>(); + useEffect(() => { - setIsAutoRefreshfreshEnabled(Boolean(localStorageValue)); - }, [localStorageValue]); + const isAutoRefreshEnabled = Boolean(localStorageValue); + dispatch({ + type: UPDATE_AUTO_REFRESH_INTERVAL, + payload: localStorageValue, + }); + setIsAutoRefreshfreshEnabled(isAutoRefreshEnabled); + }, [localStorageValue, dispatch]); const params = useUrlQuery(); - const dispatch = useDispatch>(); const [selectedOption, setSelectedOption] = useState( localStorageValue || options[0].key, ); @@ -69,7 +84,7 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { useInterval(() => { const selectedValue = getOption?.value; - if (disabled || !isAutoRefreshEnabled) { + if (isDisabled || !isAutoRefreshEnabled) { return; } @@ -125,21 +140,23 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { Auto Refresh - Refresh Interval + + Refresh Interval + {options .filter((e) => e.label !== 'off') .map((option) => ( - + {option.label} ))} diff --git a/frontend/src/store/reducers/global.ts b/frontend/src/store/reducers/global.ts index 896c6586e3..462076e54e 100644 --- a/frontend/src/store/reducers/global.ts +++ b/frontend/src/store/reducers/global.ts @@ -2,6 +2,8 @@ import { getDefaultOption } from 'container/TopNav/DateTimeSelection/config'; import { GLOBAL_TIME_LOADING_START, GlobalTimeAction, + UPDATE_AUTO_REFRESH_DISABLED, + UPDATE_AUTO_REFRESH_INTERVAL, UPDATE_TIME_INTERVAL, } from 'types/actions/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -13,6 +15,8 @@ const intitalState: GlobalReducer = { selectedTime: getDefaultOption( typeof window !== 'undefined' ? window?.location?.pathname : '', ), + isAutoRefreshDisabled: false, + selectedAutoRefreshInterval: '', }; const globalTimeReducer = ( @@ -35,6 +39,20 @@ const globalTimeReducer = ( }; } + case UPDATE_AUTO_REFRESH_DISABLED: { + return { + ...state, + isAutoRefreshDisabled: action.payload, + }; + } + + case UPDATE_AUTO_REFRESH_INTERVAL: { + return { + ...state, + selectedAutoRefreshInterval: action.payload, + }; + } + default: return state; } diff --git a/frontend/src/store/reducers/logs.ts b/frontend/src/store/reducers/logs.ts index a170de66e3..5e1dc69977 100644 --- a/frontend/src/store/reducers/logs.ts +++ b/frontend/src/store/reducers/logs.ts @@ -106,7 +106,6 @@ export const LogsReducer = ( }${action.payload}`; const updatedParsedQuery = parseQuery(updatedQueryString); - console.log({ updatedParsedQuery, updatedQueryString, action }); return { ...state, searchFilter: { diff --git a/frontend/src/types/actions/globalTime.ts b/frontend/src/types/actions/globalTime.ts index 8e384fb569..8ce1ff3963 100644 --- a/frontend/src/types/actions/globalTime.ts +++ b/frontend/src/types/actions/globalTime.ts @@ -2,6 +2,8 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; export const UPDATE_TIME_INTERVAL = 'UPDATE_TIME_INTERVAL'; export const GLOBAL_TIME_LOADING_START = 'GLOBAL_TIME_LOADING_START'; +export const UPDATE_AUTO_REFRESH_DISABLED = 'UPDATE_AUTO_REFRESH_DISABLED'; +export const UPDATE_AUTO_REFRESH_INTERVAL = 'UPDATE_AUTO_REFRESH_INTERVAL'; export type GlobalTime = { maxTime: number; @@ -17,8 +19,22 @@ interface UpdateTimeInterval { payload: UpdateTime; } +interface UpdateAutoRefreshDisabled { + type: typeof UPDATE_AUTO_REFRESH_DISABLED; + payload: boolean; +} + interface GlobalTimeLoading { type: typeof GLOBAL_TIME_LOADING_START; } -export type GlobalTimeAction = UpdateTimeInterval | GlobalTimeLoading; +interface UpdateAutoRefreshInterval { + type: typeof UPDATE_AUTO_REFRESH_INTERVAL; + payload: string; +} + +export type GlobalTimeAction = + | UpdateTimeInterval + | GlobalTimeLoading + | UpdateAutoRefreshDisabled + | UpdateAutoRefreshInterval; diff --git a/frontend/src/types/reducer/globalTime.ts b/frontend/src/types/reducer/globalTime.ts index cb55524c75..94bb17eb73 100644 --- a/frontend/src/types/reducer/globalTime.ts +++ b/frontend/src/types/reducer/globalTime.ts @@ -6,4 +6,6 @@ export interface GlobalReducer { minTime: GlobalTime['minTime']; loading: boolean; selectedTime: Time; + isAutoRefreshDisabled: boolean; + selectedAutoRefreshInterval: string; } From 67c0c9032fc5438b4b73947dc886ee42947adaaa Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Mon, 28 Nov 2022 18:16:21 +0530 Subject: [PATCH 23/27] fix: logs aggreagte endpoint updated to differentiate between params and query string (#1768) --- pkg/query-service/app/clickhouseReader/reader.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 49c728c5f0..fc6df97b2d 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3300,16 +3300,16 @@ func (r *ClickHouseReader) AggregateLogs(ctx context.Context, params *model.Logs if params.GroupBy != "" { query = fmt.Sprintf("SELECT toInt64(toUnixTimestamp(toStartOfInterval(toDateTime(timestamp/1000000000), INTERVAL %d minute))*1000000000) as ts_start_interval, toString(%s) as groupBy, "+ "%s "+ - "FROM %s.%s WHERE timestamp >= '%d' AND timestamp <= '%d' ", + "FROM %s.%s WHERE (timestamp >= '%d' AND timestamp <= '%d' )", params.StepSeconds/60, params.GroupBy, function, r.logsDB, r.logsTable, params.TimestampStart, params.TimestampEnd) } else { query = fmt.Sprintf("SELECT toInt64(toUnixTimestamp(toStartOfInterval(toDateTime(timestamp/1000000000), INTERVAL %d minute))*1000000000) as ts_start_interval, "+ "%s "+ - "FROM %s.%s WHERE timestamp >= '%d' AND timestamp <= '%d' ", + "FROM %s.%s WHERE (timestamp >= '%d' AND timestamp <= '%d' )", params.StepSeconds/60, function, r.logsDB, r.logsTable, params.TimestampStart, params.TimestampEnd) } if filterSql != "" { - query = fmt.Sprintf("%s AND %s ", query, filterSql) + query = fmt.Sprintf("%s AND ( %s ) ", query, filterSql) } if params.GroupBy != "" { query = fmt.Sprintf("%s GROUP BY ts_start_interval, toString(%s) as groupBy ORDER BY ts_start_interval", query, params.GroupBy) From ce072bdc3fb60e0c26452abd351970a180c3ad4b Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 28 Nov 2022 18:27:09 +0530 Subject: [PATCH 24/27] fix: trace event is now not decoding the events (#1766) Co-authored-by: Pranay Prateek --- frontend/src/utils/spanToTree.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/utils/spanToTree.ts b/frontend/src/utils/spanToTree.ts index 142df3dec8..c1de73690d 100644 --- a/frontend/src/utils/spanToTree.ts +++ b/frontend/src/utils/spanToTree.ts @@ -77,12 +77,7 @@ export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => { serviceName: span[3], hasError: !!span[11], serviceColour: '', - event: span[10].map((e) => { - return ( - JSON.parse(decodeURIComponent((e as never) || ('{}' as never))) || - ({} as Record) - ); - }), + event: span[10].map((e) => JSON.parse(e || '{}') || {}), references: spanReferences, }; spanMap[span[1]] = spanObject; From 7e590f4bfb97c550cd0980bd11390ddb4a64bee7 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Mon, 28 Nov 2022 18:50:17 +0530 Subject: [PATCH 25/27] feat: meta description and image is updated (#1764) * feat: meta description is updated * chore: image is updated Co-authored-by: Pranay Prateek --- frontend/src/index.html.ejs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/index.html.ejs b/frontend/src/index.html.ejs index 34fe2fb0c0..04011f5dfa 100644 --- a/frontend/src/index.html.ejs +++ b/frontend/src/index.html.ejs @@ -20,22 +20,22 @@ Date: Tue, 29 Nov 2022 14:41:36 +0530 Subject: [PATCH 26/27] fix: logs time is fixed (#1772) * fix: logs parsing is fixed * fix: start and end time is updated --- .../container/TopNav/AutoRefresh/config.ts | 13 ++++++++ .../container/TopNav/AutoRefresh/index.tsx | 31 ++++++++++--------- frontend/src/lib/getMinMax.ts | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/frontend/src/container/TopNav/AutoRefresh/config.ts b/frontend/src/container/TopNav/AutoRefresh/config.ts index cefd0c8bf1..dfd3134e0e 100644 --- a/frontend/src/container/TopNav/AutoRefresh/config.ts +++ b/frontend/src/container/TopNav/AutoRefresh/config.ts @@ -1,3 +1,7 @@ +import GetMinMax, { GetMinMaxPayload } from 'lib/getMinMax'; + +import { Time } from '../DateTimeSelection/config'; + export const options: IOptions[] = [ { label: 'off', @@ -61,3 +65,12 @@ export interface IOptions { key: string; value: number; } + +export const getMinMax = ( + selectedTime: Time, + minTime: number, + maxTime: number, +): GetMinMaxPayload => + selectedTime !== 'custom' + ? GetMinMax(selectedTime) + : GetMinMax(selectedTime, [minTime, maxTime]); diff --git a/frontend/src/container/TopNav/AutoRefresh/index.tsx b/frontend/src/container/TopNav/AutoRefresh/index.tsx index aa6a74793c..ba0d4bb3e6 100644 --- a/frontend/src/container/TopNav/AutoRefresh/index.tsx +++ b/frontend/src/container/TopNav/AutoRefresh/index.tsx @@ -12,7 +12,6 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox'; import get from 'api/browser/localstorage/get'; import set from 'api/browser/localstorage/set'; import { DASHBOARD_TIME_IN_DURATION } from 'constants/app'; -import dayjs from 'dayjs'; import useUrlQuery from 'hooks/useUrlQuery'; import _omit from 'lodash-es/omit'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; @@ -28,21 +27,19 @@ import { } from 'types/actions/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime'; -import { options } from './config'; +import { getMinMax, options } from './config'; import { ButtonContainer, Container } from './styles'; function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { - const { - minTime: initialMinTime, - selectedTime, - isAutoRefreshDisabled, - } = useSelector((state) => state.globalTime); + const globalTime = useSelector( + (state) => state.globalTime, + ); const { pathname } = useLocation(); - const isDisabled = useMemo(() => disabled || isAutoRefreshDisabled, [ - isAutoRefreshDisabled, - disabled, - ]); + const isDisabled = useMemo( + () => disabled || globalTime.isAutoRefreshDisabled, + [globalTime.isAutoRefreshDisabled, disabled], + ); const localStorageData = JSON.parse(get(DASHBOARD_TIME_IN_DURATION) || '{}'); @@ -89,14 +86,18 @@ function AutoRefresh({ disabled = false }: AutoRefreshProps): JSX.Element { } if (selectedOption !== 'off' && selectedValue) { - const min = initialMinTime / 1000000; + const { maxTime, minTime } = getMinMax( + globalTime.selectedTime, + globalTime.minTime, + globalTime.maxTime, + ); dispatch({ type: UPDATE_TIME_INTERVAL, payload: { - maxTime: dayjs().valueOf() * 1000000, - minTime: dayjs(min).subtract(selectedValue, 'second').valueOf() * 1000000, - selectedTime, + maxTime, + minTime, + selectedTime: globalTime.selectedTime, }, }); } diff --git a/frontend/src/lib/getMinMax.ts b/frontend/src/lib/getMinMax.ts index a597314799..320d509131 100644 --- a/frontend/src/lib/getMinMax.ts +++ b/frontend/src/lib/getMinMax.ts @@ -53,7 +53,7 @@ const GetMinMax = ( }; }; -interface GetMinMaxPayload { +export interface GetMinMaxPayload { minTime: GlobalReducer['minTime']; maxTime: GlobalReducer['maxTime']; } From 8e5522820ca611d3a72a81b638f15f7b90abfccc Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Tue, 29 Nov 2022 17:12:17 +0530 Subject: [PATCH 27/27] =?UTF-8?q?chore:=20=F0=9F=93=8C=20pin=20versions:?= =?UTF-8?q?=20SigNoz=200.11.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- deploy/docker-swarm/clickhouse-setup/docker-compose.yaml | 4 ++-- deploy/docker/clickhouse-setup/docker-compose.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 88380538d7..504bb70581 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -40,7 +40,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.11.3 + image: signoz/query-service:0.11.4 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -70,7 +70,7 @@ services: - clickhouse frontend: - image: signoz/frontend:0.11.3 + image: signoz/frontend:0.11.4 deploy: restart_policy: condition: on-failure diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 1f877ffc78..7357c9df5e 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -39,7 +39,7 @@ services: # 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.11.3 + image: signoz/query-service:0.11.4 container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -69,7 +69,7 @@ services: condition: service_healthy frontend: - image: signoz/frontend:0.11.3 + image: signoz/frontend:0.11.4 container_name: frontend restart: on-failure depends_on: