From 76ba3643173f0194f2aafaea93fca61da32025dd Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:22:44 +0300 Subject: [PATCH 01/48] Feat/list infinity scroll (#2992) * feat: add custom orderBy * feat: infinity scroll list logs list * feat: add infinity table view * Fix/double query logs request (#3006) * feat: add control panel * fix: repeating query api request * fix: scroll, remove id, page size * fix: reset offset to 0 * feat: add log explorer detail (#3007) * feat: add control panel * fix: repeating query api request * feat: add log explorer detail --------- Co-authored-by: Vishal Sharma * feat: add group by in the logs chart (#3009) * feat: add control panel * fix: repeating query api request * feat: add log explorer detail * feat: add group by in the logs chart * fix: list timestamp, limit, filter order * feat: add list chart (#3037) * feat: add list chart * refactor: remove console log * feat: hide aggregate every for table view (#3046) * feat: hide aggregate every for table view * fix: text filter for inactive filters * refactor: remove log * fix: table columns * fix: timestamp type --------- Co-authored-by: Vishal Sharma --- .../LogDetail/LogDetail.interfaces.ts | 3 + frontend/src/components/LogDetail/index.tsx | 41 +++ .../src/components/Logs/ListLogView/index.tsx | 42 +-- .../src/components/Logs/RawLogView/index.tsx | 5 +- .../src/components/Logs/TableView/index.tsx | 115 +------ .../src/components/Logs/TableView/types.ts | 14 + .../Logs/TableView/useTableView.tsx | 108 ++++++ frontend/src/constants/queryBuilder.ts | 6 + .../src/constants/queryBuilderQueryNames.ts | 18 +- frontend/src/container/Controls/config.ts | 2 + .../ExplorerControlPanel.interfaces.ts | 7 + .../container/ExplorerControlPanel/index.tsx | 29 ++ .../container/ExplorerControlPanel/styles.ts | 5 + frontend/src/container/ExportPanel/index.tsx | 8 +- .../GridGraphLayout/WidgetHeader/index.tsx | 6 +- .../container/ListAlertRules/ListAlert.tsx | 10 +- frontend/src/container/LogControls/index.tsx | 15 +- .../src/container/LogDetailedView/index.tsx | 33 +- .../LogExplorerDetailedView.interfaces.ts | 6 + .../LogExplorerDetailedView/index.tsx | 16 + .../LogsExplorerChart.interfaces.ts | 6 + .../src/container/LogsExplorerChart/index.tsx | 63 ++-- .../InfinityTableView/config.ts | 6 + .../InfinityTableView/index.tsx | 98 ++++++ .../InfinityTableView/styles.ts | 40 +++ .../InfinityTableView/types.ts | 8 + .../LogsExplorerList.interfaces.ts | 12 +- .../src/container/LogsExplorerList/index.tsx | 137 +++++--- .../src/container/LogsExplorerList/styles.ts | 6 + .../src/container/LogsExplorerList/utils.ts | 11 + .../src/container/LogsExplorerTable/index.tsx | 2 +- .../src/container/LogsExplorerViews/index.tsx | 317 ++++++++++++++---- frontend/src/container/LogsTable/index.tsx | 35 +- .../NewDashboard/ComponentsSlider/index.tsx | 4 +- .../OptionsMenu/FormatField/index.tsx | 6 +- .../src/container/OptionsMenu/constants.ts | 4 +- frontend/src/container/OptionsMenu/types.ts | 3 +- .../container/OptionsMenu/useOptionsMenu.ts | 22 +- .../PageSizeSelect.interfaces.ts | 4 + .../src/container/PageSizeSelect/index.tsx | 51 +++ .../QueryBuilder/QueryBuilder.interfaces.ts | 2 + .../container/QueryBuilder/QueryBuilder.tsx | 2 + .../components/Query/Query.interfaces.ts | 3 +- .../QueryBuilder/components/Query/Query.tsx | 67 ++-- .../filters/OrderByFilter/OrderByFilter.tsx | 2 +- .../filters/OrderByFilter/utils.ts | 24 +- .../queryBuilder/useGetCompositeQueryParam.ts | 4 +- .../queryBuilder/useGetExplorerQueryRange.ts | 58 ++++ .../useGetPanelTypesQueryParam.ts | 4 +- .../hooks/queryBuilder/useQueryOperations.ts | 26 +- .../src/lib/explorer/getExplorerChartData.ts | 46 --- frontend/src/lib/getChartData.ts | 40 ++- .../newQueryBuilder/getPaginationQueryData.ts | 76 +++++ .../lib/query/createTableColumnsFromQuery.ts | 32 +- frontend/src/pages/LogsExplorer/index.tsx | 32 +- frontend/src/pages/LogsExplorer/utils.ts | 12 + frontend/src/pages/TracesExplorer/index.tsx | 17 +- frontend/src/providers/QueryBuilder.tsx | 4 +- .../store/actions/dashboard/saveDashboard.ts | 4 +- frontend/src/types/api/logs/log.ts | 2 +- .../api/queryBuilder/queryBuilderData.ts | 2 + frontend/src/types/api/widgets/getQuery.ts | 5 +- frontend/src/types/common/operations.types.ts | 4 +- 63 files changed, 1291 insertions(+), 501 deletions(-) create mode 100644 frontend/src/components/LogDetail/LogDetail.interfaces.ts create mode 100644 frontend/src/components/LogDetail/index.tsx create mode 100644 frontend/src/components/Logs/TableView/types.ts create mode 100644 frontend/src/components/Logs/TableView/useTableView.tsx create mode 100644 frontend/src/container/ExplorerControlPanel/ExplorerControlPanel.interfaces.ts create mode 100644 frontend/src/container/ExplorerControlPanel/index.tsx create mode 100644 frontend/src/container/ExplorerControlPanel/styles.ts create mode 100644 frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts create mode 100644 frontend/src/container/LogExplorerDetailedView/index.tsx create mode 100644 frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts create mode 100644 frontend/src/container/LogsExplorerList/InfinityTableView/config.ts create mode 100644 frontend/src/container/LogsExplorerList/InfinityTableView/index.tsx create mode 100644 frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts create mode 100644 frontend/src/container/LogsExplorerList/InfinityTableView/types.ts create mode 100644 frontend/src/container/LogsExplorerList/styles.ts create mode 100644 frontend/src/container/LogsExplorerList/utils.ts create mode 100644 frontend/src/container/PageSizeSelect/PageSizeSelect.interfaces.ts create mode 100644 frontend/src/container/PageSizeSelect/index.tsx create mode 100644 frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts delete mode 100644 frontend/src/lib/explorer/getExplorerChartData.ts create mode 100644 frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts create mode 100644 frontend/src/pages/LogsExplorer/utils.ts diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts new file mode 100644 index 0000000000..8e4ed857f0 --- /dev/null +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -0,0 +1,3 @@ +import { ILog } from 'types/api/logs/log'; + +export type LogDetailProps = { log: ILog | null; onClose: () => void }; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx new file mode 100644 index 0000000000..054a9c3161 --- /dev/null +++ b/frontend/src/components/LogDetail/index.tsx @@ -0,0 +1,41 @@ +import { Drawer, Tabs } from 'antd'; +import JSONView from 'container/LogDetailedView/JsonView'; +import TableView from 'container/LogDetailedView/TableView'; + +import { LogDetailProps } from './LogDetail.interfaces'; + +function LogDetail({ log, onClose }: LogDetailProps): JSX.Element { + const onDrawerClose = (): void => { + onClose(); + }; + + const items = [ + { + label: 'Table', + key: '1', + children: log && , + }, + { + label: 'JSON', + key: '2', + children: log && , + }, + ]; + + return ( + + + + ); +} + +export default LogDetail; diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index ebee0899c7..a4f1e9cc9c 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -8,13 +8,10 @@ import { useNotifications } from 'hooks/useNotifications'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; import { useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { useCopyToClipboard } from 'react-use'; // interfaces -import { AppState } from 'store/reducers'; -import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; +import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; -import { ILogsReducer } from 'types/reducer/logs'; // components import AddToQueryHOC from '../AddToQueryHOC'; @@ -79,24 +76,22 @@ function LogSelectedField({ interface ListLogViewProps { logData: ILog; + onOpenDetailedView: (log: ILog) => void; + selectedFields: IField[]; } -function ListLogView({ logData }: ListLogViewProps): JSX.Element { - const { - fields: { selected }, - } = useSelector((state) => state.logs); - - const dispatch = useDispatch(); +function ListLogView({ + logData, + selectedFields, + onOpenDetailedView, +}: ListLogViewProps): JSX.Element { const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); const [, setCopy] = useCopyToClipboard(); const { notifications } = useNotifications(); const handleDetailedView = useCallback(() => { - dispatch({ - type: SET_DETAILED_LOG_DATA, - payload: logData, - }); - }, [dispatch, logData]); + onOpenDetailedView(logData); + }, [logData, onOpenDetailedView]); const handleCopyJSON = (): void => { setCopy(JSON.stringify(logData, null, 2)); @@ -106,8 +101,16 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element { }; const updatedSelecedFields = useMemo( - () => selected.filter((e) => e.name !== 'id'), - [selected], + () => selectedFields.filter((e) => e.name !== 'id'), + [selectedFields], + ); + + const timestampValue = useMemo( + () => + typeof flattenLogData.timestamp === 'string' + ? dayjs(flattenLogData.timestamp).format() + : dayjs(flattenLogData.timestamp / 1e6).format(), + [flattenLogData.timestamp], ); return ( @@ -119,10 +122,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element { {flattenLogData.stream && ( )} - +
diff --git a/frontend/src/components/Logs/RawLogView/index.tsx b/frontend/src/components/Logs/RawLogView/index.tsx index 711b4420ad..76d12c1a22 100644 --- a/frontend/src/components/Logs/RawLogView/index.tsx +++ b/frontend/src/components/Logs/RawLogView/index.tsx @@ -30,7 +30,10 @@ function RawLogView(props: RawLogViewProps): JSX.Element { const isDarkMode = useIsDarkMode(); const text = useMemo( - () => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`, + () => + typeof data.timestamp === 'string' + ? `${dayjs(data.timestamp).format()} | ${data.body}` + : `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`, [data.timestamp, data.body], ); diff --git a/frontend/src/components/Logs/TableView/index.tsx b/frontend/src/components/Logs/TableView/index.tsx index 5ec91d87d7..014ced58a5 100644 --- a/frontend/src/components/Logs/TableView/index.tsx +++ b/frontend/src/components/Logs/TableView/index.tsx @@ -1,121 +1,18 @@ -import { ExpandAltOutlined } from '@ant-design/icons'; -import Convert from 'ansi-to-html'; -import { Table, Typography } from 'antd'; -import { ColumnsType, ColumnType } from 'antd/es/table'; -import dayjs from 'dayjs'; -import dompurify from 'dompurify'; -// utils -import { FlatLogData } from 'lib/logs/flatLogData'; -import { useMemo } from 'react'; -import { IField } from 'types/api/logs/fields'; -// interfaces -import { ILog } from 'types/api/logs/log'; +import { Table } from 'antd'; -// styles -import { ExpandIconWrapper } from '../RawLogView/styles'; // config -import { defaultCellStyle, defaultTableStyle, tableScroll } from './config'; -import { TableBodyContent } from './styles'; - -type ColumnTypeRender = ReturnType< - NonNullable['render']> ->; - -type LogsTableViewProps = { - logs: ILog[]; - fields: IField[]; - linesPerRow: number; - onClickExpand: (log: ILog) => void; -}; - -const convert = new Convert(); +import { tableScroll } from './config'; +import { LogsTableViewProps } from './types'; +import { useTableView } from './useTableView'; function LogsTableView(props: LogsTableViewProps): JSX.Element { - const { logs, fields, linesPerRow, onClickExpand } = props; - - const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [ - logs, - ]); - - const columns: ColumnsType> = useMemo(() => { - const fieldColumns: ColumnsType> = fields - .filter((e) => e.name !== 'id') - .map(({ name }) => ({ - title: name, - dataIndex: name, - key: name, - render: (field): ColumnTypeRender> => ({ - props: { - style: defaultCellStyle, - }, - children: ( - - {field} - - ), - }), - })); - - return [ - { - title: '', - dataIndex: 'id', - key: 'expand', - // https://github.com/ant-design/ant-design/discussions/36886 - render: (_, item): ColumnTypeRender> => ({ - props: { - style: defaultCellStyle, - }, - children: ( - { - onClickExpand((item as unknown) as ILog); - }} - > - - - ), - }), - }, - { - title: 'timestamp', - dataIndex: 'timestamp', - key: 'timestamp', - // https://github.com/ant-design/ant-design/discussions/36886 - render: (field): ColumnTypeRender> => { - const date = dayjs(field / 1e6).format(); - return { - children: {date}, - }; - }, - }, - ...fieldColumns, - { - title: 'body', - dataIndex: 'body', - key: 'body', - render: (field): ColumnTypeRender> => ({ - props: { - style: defaultTableStyle, - }, - children: ( - - ), - }), - }, - ]; - }, [fields, linesPerRow, onClickExpand]); + const { dataSource, columns } = useTableView(props); return ( = ReturnType< + NonNullable['render']> +>; + +export type LogsTableViewProps = { + logs: ILog[]; + fields: IField[]; + linesPerRow: number; + onClickExpand: (log: ILog) => void; +}; diff --git a/frontend/src/components/Logs/TableView/useTableView.tsx b/frontend/src/components/Logs/TableView/useTableView.tsx new file mode 100644 index 0000000000..fb66235cef --- /dev/null +++ b/frontend/src/components/Logs/TableView/useTableView.tsx @@ -0,0 +1,108 @@ +import { ExpandAltOutlined } from '@ant-design/icons'; +import Convert from 'ansi-to-html'; +import { Typography } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import dayjs from 'dayjs'; +import dompurify from 'dompurify'; +import { FlatLogData } from 'lib/logs/flatLogData'; +import { useMemo } from 'react'; +import { ILog } from 'types/api/logs/log'; + +import { ExpandIconWrapper } from '../RawLogView/styles'; +import { defaultCellStyle, defaultTableStyle } from './config'; +import { TableBodyContent } from './styles'; +import { ColumnTypeRender, LogsTableViewProps } from './types'; + +export type UseTableViewResult = { + columns: ColumnsType>; + dataSource: Record[]; +}; + +const convert = new Convert(); + +export const useTableView = (props: LogsTableViewProps): UseTableViewResult => { + const { logs, fields, linesPerRow, onClickExpand } = props; + + const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [ + logs, + ]); + + const columns: ColumnsType> = useMemo(() => { + const fieldColumns: ColumnsType> = fields + .filter((e) => e.name !== 'id') + .map(({ name }) => ({ + title: name, + dataIndex: name, + key: name, + render: (field): ColumnTypeRender> => ({ + props: { + style: defaultCellStyle, + }, + children: ( + + {field} + + ), + }), + })); + + return [ + { + title: '', + dataIndex: 'id', + key: 'expand', + // https://github.com/ant-design/ant-design/discussions/36886 + render: (_, item): ColumnTypeRender> => ({ + props: { + style: defaultCellStyle, + }, + children: ( + { + onClickExpand((item as unknown) as ILog); + }} + > + + + ), + }), + }, + { + title: 'timestamp', + dataIndex: 'timestamp', + key: 'timestamp', + // https://github.com/ant-design/ant-design/discussions/36886 + render: (field): ColumnTypeRender> => { + const date = + typeof field === 'string' + ? dayjs(field).format() + : dayjs(field / 1e6).format(); + return { + children: {date}, + }; + }, + }, + ...fieldColumns, + { + title: 'body', + dataIndex: 'body', + key: 'body', + render: (field): ColumnTypeRender> => ({ + props: { + style: defaultTableStyle, + }, + children: ( + + ), + }), + }, + ]; + }, [fields, linesPerRow, onClickExpand]); + + return { columns, dataSource: flattenLogData }; +}; diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index ef0185c399..8db0f7d297 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -14,6 +14,7 @@ import { IPromQLQuery, Query, QueryState, + TagFilter, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { @@ -113,6 +114,11 @@ export const initialAutocompleteData: BaseAutocompleteData = { type: null, }; +export const initialFilters: TagFilter = { + items: [], + op: 'AND', +}; + const initialQueryBuilderFormValues: IBuilderQuery = { dataSource: DataSource.METRICS, queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), diff --git a/frontend/src/constants/queryBuilderQueryNames.ts b/frontend/src/constants/queryBuilderQueryNames.ts index 5a9e9dbfe9..b3ee34cf89 100644 --- a/frontend/src/constants/queryBuilderQueryNames.ts +++ b/frontend/src/constants/queryBuilderQueryNames.ts @@ -1,2 +1,16 @@ -export const COMPOSITE_QUERY = 'compositeQuery'; -export const PANEL_TYPES_QUERY = 'panelTypes'; +type QueryParamNames = + | 'compositeQuery' + | 'panelTypes' + | 'pageSize' + | 'viewMode' + | 'selectedFields' + | 'linesPerRow'; + +export const queryParamNamesMap: Record = { + compositeQuery: 'compositeQuery', + panelTypes: 'panelTypes', + pageSize: 'pageSize', + viewMode: 'viewMode', + selectedFields: 'selectedFields', + linesPerRow: 'linesPerRow', +}; diff --git a/frontend/src/container/Controls/config.ts b/frontend/src/container/Controls/config.ts index cc0378c546..51d85207f8 100644 --- a/frontend/src/container/Controls/config.ts +++ b/frontend/src/container/Controls/config.ts @@ -2,6 +2,8 @@ import { CSSProperties } from 'react'; export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200]; +export const DEFAULT_PER_PAGE_VALUE = 100; + export const defaultSelectStyle: CSSProperties = { minWidth: '6rem', }; diff --git a/frontend/src/container/ExplorerControlPanel/ExplorerControlPanel.interfaces.ts b/frontend/src/container/ExplorerControlPanel/ExplorerControlPanel.interfaces.ts new file mode 100644 index 0000000000..1547001259 --- /dev/null +++ b/frontend/src/container/ExplorerControlPanel/ExplorerControlPanel.interfaces.ts @@ -0,0 +1,7 @@ +import { OptionsMenuConfig } from 'container/OptionsMenu/types'; + +export type ExplorerControlPanelProps = { + isShowPageSize: boolean; + isLoading: boolean; + optionsMenuConfig?: OptionsMenuConfig; +}; diff --git a/frontend/src/container/ExplorerControlPanel/index.tsx b/frontend/src/container/ExplorerControlPanel/index.tsx new file mode 100644 index 0000000000..4d6acb8571 --- /dev/null +++ b/frontend/src/container/ExplorerControlPanel/index.tsx @@ -0,0 +1,29 @@ +import { Col, Row } from 'antd'; +import OptionsMenu from 'container/OptionsMenu'; +import PageSizeSelect from 'container/PageSizeSelect'; + +import { ExplorerControlPanelProps } from './ExplorerControlPanel.interfaces'; +import { ContainerStyled } from './styles'; + +function ExplorerControlPanel({ + isLoading, + isShowPageSize, + optionsMenuConfig, +}: ExplorerControlPanelProps): JSX.Element { + return ( + + + {optionsMenuConfig && ( + + + + )} + + + + + + ); +} + +export default ExplorerControlPanel; diff --git a/frontend/src/container/ExplorerControlPanel/styles.ts b/frontend/src/container/ExplorerControlPanel/styles.ts new file mode 100644 index 0000000000..0c9a799fef --- /dev/null +++ b/frontend/src/container/ExplorerControlPanel/styles.ts @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +export const ContainerStyled = styled.div` + margin-bottom: 0.3rem; +`; diff --git a/frontend/src/container/ExportPanel/index.tsx b/frontend/src/container/ExportPanel/index.tsx index 087b17fe72..35ac124fae 100644 --- a/frontend/src/container/ExportPanel/index.tsx +++ b/frontend/src/container/ExportPanel/index.tsx @@ -1,5 +1,5 @@ import { Button, Dropdown, MenuProps, Modal } from 'antd'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import history from 'lib/history'; import { useCallback, useMemo, useState } from 'react'; @@ -22,9 +22,9 @@ function ExportPanel({ const onCreateAlertsHandler = useCallback(() => { history.push( - `${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent( - JSON.stringify(query), - )}`, + `${ROUTES.ALERTS_NEW}?${ + queryParamNamesMap.compositeQuery + }=${encodeURIComponent(JSON.stringify(query))}`, ); }, [query]); diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx index a68d48ef8d..0e950418b7 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -9,7 +9,7 @@ import { import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import { MenuItemType } from 'antd/es/menu/hooks/useItems'; import Spinner from 'components/Spinner'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; import { useCallback, useMemo, useState } from 'react'; @@ -64,7 +64,9 @@ function WidgetHeader({ history.push( `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${ widget.panelTypes - }&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`, + }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent( + JSON.stringify(widget.query), + )}`, ); }, [widget.id, widget.panelTypes, widget.query]); diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 76843be306..b1bfc7f725 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -5,7 +5,7 @@ import { ColumnsType } from 'antd/lib/table'; import saveAlertApi from 'api/alerts/save'; import { ResizeTable } from 'components/ResizeTable'; import TextToolTip from 'components/TextToolTip'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import useInterval from 'hooks/useInterval'; @@ -75,11 +75,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); history.push( - `${ - ROUTES.EDIT_ALERTS - }?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent( - JSON.stringify(compositeQuery), - )}`, + `${ROUTES.EDIT_ALERTS}?ruleId=${record.id.toString()}&${ + queryParamNamesMap.compositeQuery + }=${encodeURIComponent(JSON.stringify(compositeQuery))}`, ); }) .catch(handleError); diff --git a/frontend/src/container/LogControls/index.tsx b/frontend/src/container/LogControls/index.tsx index d0dfe4f27b..7c3de3fef3 100644 --- a/frontend/src/container/LogControls/index.tsx +++ b/frontend/src/container/LogControls/index.tsx @@ -83,12 +83,17 @@ function LogControls(): JSX.Element | null { const flattenLogData = useMemo( () => - logs.map((log) => - FlatLogData({ + logs.map((log) => { + const timestamp = + typeof log.timestamp === 'string' + ? dayjs(log.timestamp).format() + : dayjs(log.timestamp / 1e6).format(); + + return FlatLogData({ ...log, - timestamp: (dayjs(log.timestamp / 1e6).format() as unknown) as number, - }), - ), + timestamp, + }); + }), [logs], ); diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 47f0982448..3860d657f0 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -1,4 +1,4 @@ -import { Drawer, Tabs } from 'antd'; +import LogDetail from 'components/LogDetail'; import { useDispatch, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; @@ -6,9 +6,6 @@ import AppActions from 'types/actions'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILogsReducer } from 'types/reducer/logs'; -import JSONView from './JsonView'; -import TableView from './TableView'; - function LogDetailedView(): JSX.Element { const { detailedLog } = useSelector( (state) => state.logs, @@ -23,33 +20,7 @@ function LogDetailedView(): JSX.Element { }); }; - const items = [ - { - label: 'Table', - key: '1', - children: detailedLog && , - }, - { - label: 'JSON', - key: '2', - children: detailedLog && , - }, - ]; - - return ( - - - - ); + return ; } export default LogDetailedView; diff --git a/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts b/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts new file mode 100644 index 0000000000..ca9620657f --- /dev/null +++ b/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts @@ -0,0 +1,6 @@ +import { ILog } from 'types/api/logs/log'; + +export type LogExplorerDetailedViewProps = { + log: ILog | null; + onClose: () => void; +}; diff --git a/frontend/src/container/LogExplorerDetailedView/index.tsx b/frontend/src/container/LogExplorerDetailedView/index.tsx new file mode 100644 index 0000000000..1a573f2ec8 --- /dev/null +++ b/frontend/src/container/LogExplorerDetailedView/index.tsx @@ -0,0 +1,16 @@ +import LogDetail from 'components/LogDetail'; + +import { LogExplorerDetailedViewProps } from './LogExplorerDetailedView.interfaces'; + +function LogExplorerDetailedView({ + log, + onClose, +}: LogExplorerDetailedViewProps): JSX.Element { + const onDrawerClose = (): void => { + onClose(); + }; + + return ; +} + +export default LogExplorerDetailedView; diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts new file mode 100644 index 0000000000..6df4bacee3 --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.interfaces.ts @@ -0,0 +1,6 @@ +import { QueryData } from 'types/api/widgets/getQuery'; + +export type LogsExplorerChartProps = { + data: QueryData[]; + isLoading: boolean; +}; diff --git a/frontend/src/container/LogsExplorerChart/index.tsx b/frontend/src/container/LogsExplorerChart/index.tsx index 02cc7606d7..2e85d9c5de 100644 --- a/frontend/src/container/LogsExplorerChart/index.tsx +++ b/frontend/src/container/LogsExplorerChart/index.tsx @@ -1,48 +1,43 @@ import Graph from 'components/Graph'; import Spinner from 'components/Spinner'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; -import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; -import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { getExplorerChartData } from 'lib/explorer/getExplorerChartData'; +import getChartData, { GetChartDataProps } from 'lib/getChartData'; +import { colors } from 'lib/getRandomColor'; import { memo, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { GlobalReducer } from 'types/reducer/globalTime'; +import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces'; import { CardStyled } from './LogsExplorerChart.styled'; -function LogsExplorerChart(): JSX.Element { - const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder(); +function LogsExplorerChart({ + data, + isLoading, +}: LogsExplorerChartProps): JSX.Element { + const handleCreateDatasets: Required['createDataset'] = ( + element, + index, + allLabels, + ) => ({ + label: allLabels[index], + data: element, + backgroundColor: colors[index % colors.length] || 'red', + borderColor: colors[index % colors.length] || 'red', + }); - const { selectedTime } = useSelector( - (state) => state.globalTime, + const graphData = useMemo( + () => + getChartData({ + queryData: [ + { + queryData: data, + }, + ], + createDataset: handleCreateDatasets, + }), + [data], ); - const { data, isFetching } = useGetQueryRange( - { - query: stagedQuery || initialQueriesMap.metrics, - graphType: panelType || PANEL_TYPES.LIST, - globalSelectedInterval: selectedTime, - selectedTime: 'GLOBAL_TIME', - }, - { - queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery], - enabled: isEnabledQuery, - }, - ); - - const graphData = useMemo(() => { - if (data?.payload.data && data.payload.data.result.length > 0) { - return getExplorerChartData([data.payload.data.result[0]]); - } - - return getExplorerChartData([]); - }, [data]); - return ( - {isFetching ? ( + {isLoading ? ( ) : ( ( + {children} +); + +// eslint-disable-next-line react/function-component-definition +const CustomTableRow: TableComponents['TableRow'] = ({ + children, + context, + ...props + // eslint-disable-next-line react/jsx-props-no-spreading +}) => {children}; + +function InfinityTable({ + tableViewProps, + infitiyTableProps, +}: InfinityTableProps): JSX.Element | null { + const { onEndReached } = infitiyTableProps; + const { dataSource, columns } = useTableView(tableViewProps); + + const itemContent = useCallback( + (index: number, log: Record): JSX.Element => ( + <> + {columns.map((column) => { + if (!column.render) return ; + + const element: ColumnTypeRender> = column.render( + log[column.key as keyof Record], + log, + index, + ); + + const elementWithChildren = element as Exclude< + ColumnTypeRender>, + ReactNode + >; + + const children = elementWithChildren.children as ReactElement; + const props = elementWithChildren.props as Record; + + return ( + + {cloneElement(children, props)} + + ); + })} + + ), + [columns], + ); + + const tableHeader = useCallback( + () => ( + + {columns.map((column) => ( + + {column.title as string} + + ))} + + ), + [columns], + ); + + return ( + + ); +} + +export default InfinityTable; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts new file mode 100644 index 0000000000..a45d2d6b02 --- /dev/null +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/styles.ts @@ -0,0 +1,40 @@ +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const TableStyled = styled.table` + width: 100%; + border-top: 1px solid rgba(253, 253, 253, 0.12); + border-radius: 2px 2px 0 0; + border-collapse: separate; + border-spacing: 0; + border-inline-start: 1px solid rgba(253, 253, 253, 0.12); + border-inline-end: 1px solid rgba(253, 253, 253, 0.12); +`; + +export const TableCellStyled = styled.td` + padding: 0.5rem; + border-inline-end: 1px solid rgba(253, 253, 253, 0.12); + border-top: 1px solid rgba(253, 253, 253, 0.12); + background-color: ${themeColors.lightBlack}; +`; + +export const TableRowStyled = styled.tr` + &:hover { + ${TableCellStyled} { + background-color: #1d1d1d; + } + } +`; + +export const TableHeaderCellStyled = styled.th` + padding: 0.5rem; + border-inline-end: 1px solid rgba(253, 253, 253, 0.12); + background-color: #1d1d1d; + &:first-child { + border-start-start-radius: 2px; + } + &:last-child { + border-start-end-radius: 2px; + border-inline-end: none; + } +`; diff --git a/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts b/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts new file mode 100644 index 0000000000..abd32a8c23 --- /dev/null +++ b/frontend/src/container/LogsExplorerList/InfinityTableView/types.ts @@ -0,0 +1,8 @@ +import { LogsTableViewProps } from 'components/Logs/TableView/types'; + +export type InfinityTableProps = { + tableViewProps: LogsTableViewProps; + infitiyTableProps: { + onEndReached: (index: number) => void; + }; +}; diff --git a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts index 4d0a534ad2..b238031ac0 100644 --- a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts +++ b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts @@ -1,3 +1,11 @@ -import { QueryDataV3 } from 'types/api/widgets/getQuery'; +import { ILog } from 'types/api/logs/log'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; -export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean }; +export type LogsExplorerListProps = { + isLoading: boolean; + currentStagedQueryData: IBuilderQuery | null; + logs: ILog[]; + onEndReached: (index: number) => void; + onExpand: (log: ILog) => void; + onOpenDetailedView: (log: ILog) => void; +}; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index cacc611f90..de537036f8 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -2,38 +2,43 @@ import { Card, Typography } from 'antd'; // components import ListLogView from 'components/Logs/ListLogView'; import RawLogView from 'components/Logs/RawLogView'; -import LogsTableView from 'components/Logs/TableView'; import Spinner from 'components/Spinner'; -import { LogViewMode } from 'container/LogsTable'; -import { Container, Heading } from 'container/LogsTable/styles'; +import ExplorerControlPanel from 'container/ExplorerControlPanel'; +import { Heading } from 'container/LogsTable/styles'; +import { useOptionsMenu } from 'container/OptionsMenu'; import { contentStyle } from 'container/Trace/Search/config'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useFontFaceObserver from 'hooks/useFontObserver'; -import { memo, useCallback, useMemo, useState } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; // interfaces import { ILog } from 'types/api/logs/log'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; +import InfinityTableView from './InfinityTableView'; import { LogsExplorerListProps } from './LogsExplorerList.interfaces'; +import { InfinityWrapperStyled } from './styles'; +import { convertKeysToColumnFields } from './utils'; + +function Footer(): JSX.Element { + return ; +} function LogsExplorerList({ - data, isLoading, + currentStagedQueryData, + logs, + onOpenDetailedView, + onEndReached, + onExpand, }: LogsExplorerListProps): JSX.Element { - const [viewMode] = useState('raw'); - const [linesPerRow] = useState(20); + const { initialDataSource } = useQueryBuilder(); - const logs: ILog[] = useMemo(() => { - if (data.length > 0 && data[0].list) { - const logs: ILog[] = data[0].list.map((item) => ({ - timestamp: +item.timestamp, - ...item.data, - })); - - return logs; - } - - return []; - }, [data]); + const { options, config } = useOptionsMenu({ + dataSource: initialDataSource || DataSource.METRICS, + aggregateOperator: + currentStagedQueryData?.aggregateOperator || StringOperators.NOOP, + }); useFontFaceObserver( [ @@ -42,75 +47,107 @@ function LogsExplorerList({ weight: '300', }, ], - viewMode === 'raw', + options.format === 'raw', { timeout: 5000, }, ); - // TODO: implement here linesPerRow, mode like in useSelectedLogView + + const selectedFields = useMemo( + () => convertKeysToColumnFields(options.selectColumns), + [options], + ); const getItemContent = useCallback( - (index: number): JSX.Element => { - const log = logs[index]; - - if (viewMode === 'raw') { + (_: number, log: ILog): JSX.Element => { + if (options.format === 'raw') { return ( {}} + linesPerRow={options.maxLines} + onClickExpand={onExpand} /> ); } - return ; + return ( + + ); }, - [logs, linesPerRow, viewMode], + [ + options.format, + options.maxLines, + selectedFields, + onOpenDetailedView, + onExpand, + ], ); const renderContent = useMemo(() => { - if (viewMode === 'table') { + const components = isLoading + ? { + Footer, + } + : {}; + + if (options.format === 'table') { return ( - {}} + ); } return ( - + ); - }, [getItemContent, linesPerRow, logs, viewMode]); - - if (isLoading) { - return ; - } + }, [ + isLoading, + logs, + options.format, + options.maxLines, + onEndReached, + getItemContent, + selectedFields, + onExpand, + ]); return ( - - {viewMode !== 'table' && ( + <> + + {options.format !== 'table' && ( Event )} - {logs.length === 0 && No logs lines found} - - {renderContent} - + {renderContent} + ); } diff --git a/frontend/src/container/LogsExplorerList/styles.ts b/frontend/src/container/LogsExplorerList/styles.ts new file mode 100644 index 0000000000..1c3dde6466 --- /dev/null +++ b/frontend/src/container/LogsExplorerList/styles.ts @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const InfinityWrapperStyled = styled.div` + min-height: 40rem; + display: flex; +`; diff --git a/frontend/src/container/LogsExplorerList/utils.ts b/frontend/src/container/LogsExplorerList/utils.ts new file mode 100644 index 0000000000..6058ac4b4f --- /dev/null +++ b/frontend/src/container/LogsExplorerList/utils.ts @@ -0,0 +1,11 @@ +import { IField } from 'types/api/logs/fields'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const convertKeysToColumnFields = ( + keys: BaseAutocompleteData[], +): IField[] => + keys.map((item) => ({ + dataType: item.dataType as string, + name: item.key, + type: item.type as string, + })); diff --git a/frontend/src/container/LogsExplorerTable/index.tsx b/frontend/src/container/LogsExplorerTable/index.tsx index 40aefcf5e5..bbcb2aa99b 100644 --- a/frontend/src/container/LogsExplorerTable/index.tsx +++ b/frontend/src/container/LogsExplorerTable/index.tsx @@ -6,8 +6,8 @@ import { memo } from 'react'; import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces'; function LogsExplorerTable({ - isLoading, data, + isLoading, }: LogsExplorerTableProps): JSX.Element { const { stagedQuery } = useQueryBuilder(); diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 6229a3fe22..a9a8d70894 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,50 +1,62 @@ import { TabsProps } from 'antd'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; -import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; +import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; +import LogExplorerDetailedView from 'container/LogExplorerDetailedView'; +import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerTable from 'container/LogsExplorerTable'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; -import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { memo, useCallback, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { DataSource } from 'types/common/queryBuilder'; -import { GlobalReducer } from 'types/reducer/globalTime'; +import useUrlQueryData from 'hooks/useUrlQueryData'; +import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { ILog } from 'types/api/logs/log'; +import { + IBuilderQuery, + OrderByPayload, + Query, +} from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { TabsStyled } from './LogsExplorerViews.styled'; function LogsExplorerViews(): JSX.Element { + const { queryData: pageSize } = useUrlQueryData( + queryParamNamesMap.pageSize, + DEFAULT_PER_PAGE_VALUE, + ); + + // Context const { currentQuery, stagedQuery, panelType, - isEnabledQuery, updateAllQueriesOperators, redirectWithQueryBuilderData, } = useQueryBuilder(); - const { selectedTime } = useSelector( - (state) => state.globalTime, - ); + // State + const [activeLog, setActiveLog] = useState(null); + const [page, setPage] = useState(1); + const [logs, setLogs] = useState([]); + const [requestData, setRequestData] = useState(null); - const { data, isFetching, isError } = useGetQueryRange( - { - query: stagedQuery || initialQueriesMap.metrics, - graphType: panelType || PANEL_TYPES.LIST, - globalSelectedInterval: selectedTime, - selectedTime: 'GLOBAL_TIME', - params: { - dataSource: DataSource.LOGS, - }, - }, - { - queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery], - enabled: isEnabledQuery, - }, - ); + const currentStagedQueryData = useMemo(() => { + if (!stagedQuery || stagedQuery.builder.queryData.length !== 1) return null; + + return stagedQuery.builder.queryData[0]; + }, [stagedQuery]); + + const orderByTimestamp: OrderByPayload | null = useMemo(() => { + const timestampOrderBy = currentStagedQueryData?.orderBy.find( + (item) => item.columnName === 'timestamp', + ); + + return timestampOrderBy || null; + }, [currentStagedQueryData]); const isMultipleQueries = useMemo( () => @@ -62,35 +74,57 @@ function LogsExplorerViews(): JSX.Element { return groupByCount > 0; }, [currentQuery]); - const currentData = useMemo( - () => data?.payload.data.newResult.data.result || [], - [data], + const isLimit: boolean = useMemo(() => { + if (!currentStagedQueryData) return false; + if (!currentStagedQueryData.limit) return false; + + return logs.length >= currentStagedQueryData.limit; + }, [logs.length, currentStagedQueryData]); + + const listChartQuery = useMemo(() => { + if (!stagedQuery || !currentStagedQueryData) return null; + + const modifiedQueryData: IBuilderQuery = { + ...currentStagedQueryData, + aggregateOperator: StringOperators.COUNT, + }; + + const modifiedQuery: Query = { + ...stagedQuery, + builder: { + ...stagedQuery.builder, + queryData: stagedQuery.builder.queryData.map((item) => ({ + ...item, + ...modifiedQueryData, + })), + }, + }; + + return modifiedQuery; + }, [stagedQuery, currentStagedQueryData]); + + const listChartData = useGetExplorerQueryRange( + listChartQuery, + PANEL_TYPES.TIME_SERIES, ); - const tabsItems: TabsProps['items'] = useMemo( - () => [ - { - label: 'List View', - key: PANEL_TYPES.LIST, - disabled: isMultipleQueries || isGroupByExist, - children: , - }, - { - label: 'TimeSeries', - key: PANEL_TYPES.TIME_SERIES, - children: ( - - ), - }, - { - label: 'Table', - key: PANEL_TYPES.TABLE, - children: , - }, - ], - [isMultipleQueries, isGroupByExist, currentData, isFetching, data, isError], + const { data, isFetching, isError } = useGetExplorerQueryRange( + requestData, + panelType, + { + keepPreviousData: true, + enabled: !isLimit, + }, ); + const handleSetActiveLog = useCallback((nextActiveLog: ILog) => { + setActiveLog(nextActiveLog); + }, []); + + const handleClearActiveLog = useCallback(() => { + setActiveLog(null); + }, []); + const handleChangeView = useCallback( (newPanelType: string) => { if (newPanelType === panelType) return; @@ -101,7 +135,9 @@ function LogsExplorerViews(): JSX.Element { DataSource.LOGS, ); - redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType }); + redirectWithQueryBuilderData(query, { + [queryParamNamesMap.panelTypes]: newPanelType, + }); }, [ currentQuery, @@ -111,6 +147,75 @@ function LogsExplorerViews(): JSX.Element { ], ); + const getRequestData = useCallback( + ( + query: Query | null, + params: { page: number; log: ILog | null; pageSize: number }, + ): Query | null => { + if (!query) return null; + + const paginateData = getPaginationQueryData({ + currentStagedQueryData, + listItemId: params.log ? params.log.id : null, + orderByTimestamp, + page: params.page, + pageSize: params.pageSize, + }); + + const data: Query = { + ...query, + builder: { + ...query.builder, + queryData: query.builder.queryData.map((item) => ({ + ...item, + ...paginateData, + pageSize: params.pageSize, + })), + }, + }; + + return data; + }, + [currentStagedQueryData, orderByTimestamp], + ); + + const handleEndReached = useCallback( + (index: number) => { + if (isLimit) return; + + const lastLog = logs[index]; + + const limit = currentStagedQueryData?.limit; + + const nextLogsLenth = logs.length + pageSize; + + const nextPageSize = + limit && nextLogsLenth >= limit ? limit - logs.length : pageSize; + + if (!stagedQuery) return; + + const newRequestData = getRequestData(stagedQuery, { + page: page + 1, + log: orderByTimestamp ? lastLog : null, + pageSize: nextPageSize, + }); + + setPage((prevPage) => prevPage + 1); + + setRequestData(newRequestData); + }, + [ + isLimit, + logs, + currentStagedQueryData?.limit, + pageSize, + stagedQuery, + getRequestData, + page, + orderByTimestamp, + ], + ); + useEffect(() => { const shouldChangeView = isMultipleQueries || isGroupByExist; @@ -119,15 +224,115 @@ function LogsExplorerViews(): JSX.Element { } }, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]); + useEffect(() => { + const currentData = data?.payload.data.newResult.data.result || []; + if (currentData.length > 0 && currentData[0].list) { + const currentLogs: ILog[] = currentData[0].list.map((item) => ({ + ...item.data, + timestamp: item.timestamp, + })); + setLogs((prevLogs) => [...prevLogs, ...currentLogs]); + } + }, [data]); + + useEffect(() => { + if (requestData?.id !== stagedQuery?.id) { + const newRequestData = getRequestData(stagedQuery, { + page: 1, + log: null, + pageSize, + }); + setLogs([]); + setPage(1); + setRequestData(newRequestData); + } + }, [stagedQuery, requestData, getRequestData, pageSize]); + + const tabsItems: TabsProps['items'] = useMemo( + () => [ + { + label: 'List View', + key: PANEL_TYPES.LIST, + disabled: isMultipleQueries || isGroupByExist, + children: ( + + ), + }, + { + label: 'TimeSeries', + key: PANEL_TYPES.TIME_SERIES, + children: ( + + ), + }, + { + label: 'Table', + key: PANEL_TYPES.TABLE, + children: ( + + ), + }, + ], + [ + isMultipleQueries, + isGroupByExist, + isFetching, + currentStagedQueryData, + logs, + handleSetActiveLog, + handleEndReached, + data, + isError, + ], + ); + + const chartData = useMemo(() => { + if (!stagedQuery) return []; + + if (panelType === PANEL_TYPES.LIST) { + if ( + listChartData && + listChartData.data && + listChartData.data.payload.data.result.length > 0 + ) { + return listChartData.data.payload.data.result; + } + return []; + } + + if (!data || data.payload.data.result.length === 0) return []; + + const isGroupByExist = stagedQuery.builder.queryData.some( + (queryData) => queryData.groupBy.length > 0, + ); + + return isGroupByExist + ? data.payload.data.result + : [data.payload.data.result[0]]; + }, [stagedQuery, data, panelType, listChartData]); + return ( -
+ <> + -
+ + ); } diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 464258877e..b514aa3ee8 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -7,10 +7,11 @@ import Spinner from 'components/Spinner'; import { contentStyle } from 'container/Trace/Search/config'; import useFontFaceObserver from 'hooks/useFontObserver'; import { memo, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Virtuoso } from 'react-virtuoso'; -// interfaces import { AppState } from 'store/reducers'; +// interfaces +import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILog } from 'types/api/logs/log'; import { ILogsReducer } from 'types/reducer/logs'; @@ -28,6 +29,8 @@ type LogsTableProps = { function LogsTable(props: LogsTableProps): JSX.Element { const { viewMode, onClickExpand, linesPerRow } = props; + const dispatch = useDispatch(); + useFontFaceObserver( [ { @@ -58,6 +61,16 @@ function LogsTable(props: LogsTableProps): JSX.Element { liveTail, ]); + const handleOpenDetailedView = useCallback( + (logData: ILog) => { + dispatch({ + type: SET_DETAILED_LOG_DATA, + payload: logData, + }); + }, + [dispatch], + ); + const getItemContent = useCallback( (index: number): JSX.Element => { const log = logs[index]; @@ -73,9 +86,23 @@ function LogsTable(props: LogsTableProps): JSX.Element { ); } - return ; + return ( + + ); }, - [logs, linesPerRow, viewMode, onClickExpand], + [ + logs, + viewMode, + selected, + handleOpenDetailedView, + linesPerRow, + onClickExpand, + ], ); const renderContent = useMemo(() => { diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index f490f1f23e..8ede8520b7 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { initialQueriesMap } from 'constants/queryBuilder'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; @@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { history.push( `${history.location.pathname}/new?graphType=${name}&widgetId=${ emptyLayout.i - }&${COMPOSITE_QUERY}=${encodeURIComponent( + }&${queryParamNamesMap.compositeQuery}=${encodeURIComponent( JSON.stringify(initialQueriesMap.metrics), )}`, ); diff --git a/frontend/src/container/OptionsMenu/FormatField/index.tsx b/frontend/src/container/OptionsMenu/FormatField/index.tsx index c4d88bacbe..3b82631a5d 100644 --- a/frontend/src/container/OptionsMenu/FormatField/index.tsx +++ b/frontend/src/container/OptionsMenu/FormatField/index.tsx @@ -18,9 +18,9 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null { value={config.value} onChange={config.onChange} > - {t('options_menu.row')} - {t('options_menu.default')} - {t('options_menu.column')} + {t('options_menu.row')} + {t('options_menu.default')} + {t('options_menu.column')} ); diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 9b9d41709d..b1e5463686 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -4,6 +4,6 @@ export const URL_OPTIONS = 'options'; export const defaultOptionsQuery: OptionsQuery = { selectColumns: [], - maxLines: 0, - format: 'default', + maxLines: 2, + format: 'list', }; diff --git a/frontend/src/container/OptionsMenu/types.ts b/frontend/src/container/OptionsMenu/types.ts index f557e1dbe3..e7fdf89f97 100644 --- a/frontend/src/container/OptionsMenu/types.ts +++ b/frontend/src/container/OptionsMenu/types.ts @@ -1,10 +1,11 @@ import { InputNumberProps, RadioProps, SelectProps } from 'antd'; +import { LogViewMode } from 'container/LogsTable'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; export interface OptionsQuery { selectColumns: BaseAutocompleteData[]; maxLines: number; - format: 'default' | 'row' | 'column'; + format: LogViewMode; } export interface InitialOptions diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index f39557f9f7..578fd84e26 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -35,16 +35,17 @@ const useOptionsMenu = ({ query: optionsQuery, queryData: optionsQueryData, redirectWithQuery: redirectWithOptionsData, - } = useUrlQueryData(URL_OPTIONS); + } = useUrlQueryData(URL_OPTIONS, defaultOptionsQuery); const { data, isFetched, isLoading } = useQuery( - [QueryBuilderKeys.GET_ATTRIBUTE_KEY], + [QueryBuilderKeys.GET_ATTRIBUTE_KEY, dataSource, aggregateOperator], async () => getAggregateKeys({ searchText: '', dataSource, aggregateOperator, aggregateAttribute: '', + tagType: null, }), ); @@ -86,11 +87,16 @@ const useOptionsMenu = ({ }, [] as BaseAutocompleteData[]); redirectWithOptionsData({ - ...defaultOptionsQuery, + ...optionsQueryData, selectColumns: newSelectedColumns, }); }, - [attributeKeys, selectedColumnKeys, redirectWithOptionsData], + [ + selectedColumnKeys, + redirectWithOptionsData, + optionsQueryData, + attributeKeys, + ], ); const handleRemoveSelectedColumn = useCallback( @@ -116,21 +122,21 @@ const useOptionsMenu = ({ const handleFormatChange = useCallback( (event: RadioChangeEvent) => { redirectWithOptionsData({ - ...defaultOptionsQuery, + ...optionsQueryData, format: event.target.value, }); }, - [redirectWithOptionsData], + [optionsQueryData, redirectWithOptionsData], ); const handleMaxLinesChange = useCallback( (value: string | number | null) => { redirectWithOptionsData({ - ...defaultOptionsQuery, + ...optionsQueryData, maxLines: value as number, }); }, - [redirectWithOptionsData], + [optionsQueryData, redirectWithOptionsData], ); const optionsMenuConfig: Required = useMemo( diff --git a/frontend/src/container/PageSizeSelect/PageSizeSelect.interfaces.ts b/frontend/src/container/PageSizeSelect/PageSizeSelect.interfaces.ts new file mode 100644 index 0000000000..dc881ebacc --- /dev/null +++ b/frontend/src/container/PageSizeSelect/PageSizeSelect.interfaces.ts @@ -0,0 +1,4 @@ +export type PageSizeSelectProps = { + isLoading: boolean; + isShow: boolean; +}; diff --git a/frontend/src/container/PageSizeSelect/index.tsx b/frontend/src/container/PageSizeSelect/index.tsx new file mode 100644 index 0000000000..835549ddaa --- /dev/null +++ b/frontend/src/container/PageSizeSelect/index.tsx @@ -0,0 +1,51 @@ +import { Col, Row, Select } from 'antd'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; +import { + defaultSelectStyle, + ITEMS_PER_PAGE_OPTIONS, +} from 'container/Controls/config'; +import useUrlQueryData from 'hooks/useUrlQueryData'; +import { useCallback } from 'react'; + +import { PageSizeSelectProps } from './PageSizeSelect.interfaces'; + +function PageSizeSelect({ + isLoading, + isShow, +}: PageSizeSelectProps): JSX.Element | null { + const { redirectWithQuery, queryData: pageSize } = useUrlQueryData( + queryParamNamesMap.pageSize, + ITEMS_PER_PAGE_OPTIONS[0], + ); + + const handleChangePageSize = useCallback( + (value: number) => { + redirectWithQuery(value); + }, + [redirectWithQuery], + ); + + if (!isShow) return null; + + return ( + +
+ + + + ); +} + +export default PageSizeSelect; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index ab61c94f22..6135e0b670 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -1,5 +1,6 @@ import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { ReactNode } from 'react'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; export type QueryBuilderConfig = @@ -13,4 +14,5 @@ export type QueryBuilderProps = { config?: QueryBuilderConfig; panelType: ITEMS; actions?: ReactNode; + inactiveFilters?: Partial>; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 464c4599c1..4aefedb022 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -17,6 +17,7 @@ export const QueryBuilder = memo(function QueryBuilder({ config, panelType: newPanelType, actions, + inactiveFilters = {}, }: QueryBuilderProps): JSX.Element { const { currentQuery, @@ -74,6 +75,7 @@ export const QueryBuilder = memo(function QueryBuilder({ isAvailableToDisable={isAvailableToDisableQuery} queryVariant={config?.queryVariant || 'dropdown'} query={query} + inactiveFilters={inactiveFilters} /> ))} diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts index 414e61678b..8cee2d9dad 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts @@ -1,3 +1,4 @@ +import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; export type QueryProps = { @@ -5,4 +6,4 @@ export type QueryProps = { isAvailableToDisable: boolean; query: IBuilderQuery; queryVariant: 'static' | 'dropdown'; -}; +} & Pick; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index d0c5f126a5..dd42404c6d 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -35,6 +35,7 @@ export const Query = memo(function Query({ isAvailableToDisable, queryVariant, query, + inactiveFilters, }: QueryProps): JSX.Element { const { panelType } = useQueryBuilder(); const { @@ -47,7 +48,7 @@ export const Query = memo(function Query({ handleChangeQueryData, handleChangeOperator, handleDeleteQuery, - } = useQueryOperations({ index, query }); + } = useQueryOperations({ index, query, inactiveFilters }); const handleChangeAggregateEvery = useCallback( (value: IBuilderQuery['stepInterval']) => { @@ -109,6 +110,24 @@ export const Query = memo(function Query({ [handleChangeQueryData], ); + const renderAggregateEveryFilter = useCallback( + (): JSX.Element | null => + !inactiveFilters?.stepInterval ? ( + + + + + + + + + ) : null, + [inactiveFilters?.stepInterval, query, handleChangeAggregateEvery], + ); + const renderAdditionalFilters = useCallback((): ReactNode => { switch (panelType) { case PANEL_TYPES.TIME_SERIES: { @@ -149,19 +168,7 @@ export const Query = memo(function Query({ )} - - - - - - - - - - + {renderAggregateEveryFilter()} ); } @@ -179,19 +186,7 @@ export const Query = memo(function Query({ - - - - - - - - - - + {renderAggregateEveryFilter()} ); } @@ -230,21 +225,7 @@ export const Query = memo(function Query({ - {panelType !== PANEL_TYPES.LIST && ( - - - - - - - - - - - )} + {renderAggregateEveryFilter()} ); } @@ -253,10 +234,10 @@ export const Query = memo(function Query({ panelType, query, isMetricsDataSource, - handleChangeAggregateEvery, handleChangeHavingFilter, handleChangeLimit, handleChangeOrderByKeys, + renderAggregateEveryFilter, ]); return ( diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index f85c72dc34..3994c6c73b 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -27,7 +27,7 @@ export function OrderByFilter({ }: OrderByFilterProps): JSX.Element { const [searchText, setSearchText] = useState(''); const [selectedValue, setSelectedValue] = useState( - transformToOrderByStringValues(query.orderBy) || [], + transformToOrderByStringValues(query.orderBy), ); const { data, isFetching } = useQuery( diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts index 540674dec2..1d415ec2d5 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts @@ -8,11 +8,25 @@ export const orderByValueDelimiter = '|'; export const transformToOrderByStringValues = ( orderBy: OrderByPayload[], -): IOption[] => - orderBy.map((item) => ({ - label: `${item.columnName} ${item.order}`, - value: `${item.columnName}${orderByValueDelimiter}${item.order}`, - })); +): IOption[] => { + const prepareSelectedValue: IOption[] = orderBy.reduce( + (acc, item) => { + if (item.columnName === '#SIGNOZ_VALUE') return acc; + + const option: IOption = { + label: `${item.columnName} ${item.order}`, + value: `${item.columnName}${orderByValueDelimiter}${item.order}`, + }; + + acc.push(option); + + return acc; + }, + [], + ); + + return prepareSelectedValue; +}; export function mapLabelValuePairs( arr: BaseAutocompleteData[], diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts index 894167815b..d9060539cc 100644 --- a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -1,4 +1,4 @@ -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import useUrlQuery from 'hooks/useUrlQuery'; import { useMemo } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -7,7 +7,7 @@ export const useGetCompositeQueryParam = (): Query | null => { const urlQuery = useUrlQuery(); return useMemo(() => { - const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + const compositeQuery = urlQuery.get(queryParamNamesMap.compositeQuery); let parsedCompositeQuery: Query | null = null; try { diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts new file mode 100644 index 0000000000..15ea318556 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -0,0 +1,58 @@ +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { useMemo } from 'react'; +import { UseQueryOptions, UseQueryResult } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { useGetQueryRange } from './useGetQueryRange'; +import { useQueryBuilder } from './useQueryBuilder'; + +export const useGetExplorerQueryRange = ( + requestData: Query | null, + panelType: GRAPH_TYPES | null, + options?: UseQueryOptions, Error>, +): UseQueryResult, Error> => { + const { isEnabledQuery } = useQueryBuilder(); + const { selectedTime: globalSelectedInterval } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const key = useMemo( + () => + typeof options?.queryKey === 'string' + ? options?.queryKey + : REACT_QUERY_KEY.GET_QUERY_RANGE, + [options?.queryKey], + ); + + const isEnabled = useMemo(() => { + if (!options) return isEnabledQuery; + if (typeof options.enabled === 'boolean') { + return isEnabledQuery && options.enabled; + } + + return isEnabledQuery; + }, [options, isEnabledQuery]); + + return useGetQueryRange( + { + graphType: panelType || PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + globalSelectedInterval, + query: requestData || initialQueriesMap.metrics, + }, + { + ...options, + retry: false, + queryKey: [key, globalSelectedInterval, requestData], + enabled: isEnabled, + }, + ); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts index 06cc11829a..560790e574 100644 --- a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts @@ -1,4 +1,4 @@ -import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import useUrlQuery from 'hooks/useUrlQuery'; import { useMemo } from 'react'; @@ -9,7 +9,7 @@ export const useGetPanelTypesQueryParam = ( const urlQuery = useUrlQuery(); return useMemo(() => { - const panelTypeQuery = urlQuery.get(PANEL_TYPES_QUERY); + const panelTypeQuery = urlQuery.get(queryParamNamesMap.panelTypes); return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType; }, [urlQuery, defaultPanelType]); diff --git a/frontend/src/hooks/queryBuilder/useQueryOperations.ts b/frontend/src/hooks/queryBuilder/useQueryOperations.ts index 02af0118d0..d2cc947819 100644 --- a/frontend/src/hooks/queryBuilder/useQueryOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryOperations.ts @@ -17,7 +17,11 @@ import { import { DataSource } from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; -export const useQueryOperations: UseQueryOperations = ({ query, index }) => { +export const useQueryOperations: UseQueryOperations = ({ + query, + index, + inactiveFilters, +}) => { const { handleSetQueryData, removeQueryBuilderEntityByIndex, @@ -58,15 +62,23 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { const getNewListOfAdditionalFilters = useCallback( (dataSource: DataSource): string[] => { - const listOfFilters = mapOfFilters[dataSource].map((item) => item.text); + const result: string[] = mapOfFilters[dataSource].reduce( + (acc, item) => { + if (inactiveFilters && inactiveFilters[item.field]) { + return acc; + } - if (panelType === PANEL_TYPES.LIST) { - return listOfFilters.filter((filter) => filter !== 'Aggregation interval'); - } + acc.push(item.text); - return listOfFilters; + return acc; + }, + [], + ); + + return result; }, - [panelType], + + [inactiveFilters], ); const handleChangeAggregatorAttribute = useCallback( diff --git a/frontend/src/lib/explorer/getExplorerChartData.ts b/frontend/src/lib/explorer/getExplorerChartData.ts deleted file mode 100644 index 152b72f9ac..0000000000 --- a/frontend/src/lib/explorer/getExplorerChartData.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ChartData } from 'chart.js'; -import getLabelName from 'lib/getLabelName'; -import { QueryData } from 'types/api/widgets/getQuery'; - -import { colors } from '../getRandomColor'; - -export const getExplorerChartData = ( - queryData: QueryData[], -): ChartData<'bar'> => { - const uniqueTimeLabels = new Set(); - - const sortedData = [...queryData].sort((a, b) => { - if (a.queryName < b.queryName) return -1; - if (a.queryName > b.queryName) return 1; - return 0; - }); - - const modifiedData: { label: string }[] = sortedData.map((result) => { - const { metric, queryName, legend } = result; - result.values.forEach((value) => { - uniqueTimeLabels.add(value[0] * 1000); - }); - - return { - label: getLabelName(metric, queryName || '', legend || ''), - }; - }); - - const labels = Array.from(uniqueTimeLabels) - .sort((a, b) => a - b) - .map((value) => new Date(value)); - - const allLabels = modifiedData.map((e) => e.label); - - const data: ChartData<'bar'> = { - labels, - datasets: queryData.map((result, index) => ({ - label: allLabels[index], - data: result.values.map((item) => parseFloat(item[1])), - backgroundColor: colors[index % colors.length] || 'red', - borderColor: colors[index % colors.length] || 'red', - })), - }; - - return data; -}; diff --git a/frontend/src/lib/getChartData.ts b/frontend/src/lib/getChartData.ts index 8d32d969c2..7c57d2d721 100644 --- a/frontend/src/lib/getChartData.ts +++ b/frontend/src/lib/getChartData.ts @@ -1,11 +1,14 @@ -import { ChartData } from 'chart.js'; +import { ChartData, ChartDataset } from 'chart.js'; import getLabelName from 'lib/getLabelName'; import { QueryData } from 'types/api/widgets/getQuery'; import convertIntoEpoc from './covertIntoEpoc'; import { colors } from './getRandomColor'; -const getChartData = ({ queryData }: GetChartDataProps): ChartData => { +const getChartData = ({ + queryData, + createDataset, +}: GetChartDataProps): ChartData => { const uniqueTimeLabels = new Set(); queryData.forEach((data) => { data.queryData.forEach((query) => { @@ -60,28 +63,39 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => { .reduce((a, b) => [...a, ...b], []); return { - datasets: alldata.map((e, index) => ({ - data: e, - label: allLabels[index], - borderWidth: 1.5, - spanGaps: true, - animations: false, - borderColor: colors[index % colors.length] || 'red', - showLine: true, - pointRadius: 0, - })), + datasets: alldata.map((e, index) => { + const datasetBaseConfig = { + label: allLabels[index], + borderColor: colors[index % colors.length] || 'red', + data: e, + borderWidth: 1.5, + spanGaps: true, + animations: false, + showLine: true, + pointRadius: 0, + }; + + return createDataset + ? createDataset(e, index, allLabels) + : datasetBaseConfig; + }), labels: response .map((e) => e.map((e) => e.first)) .reduce((a, b) => [...a, ...b], [])[0], }; }; -interface GetChartDataProps { +export interface GetChartDataProps { queryData: { query?: string; legend?: string; queryData: QueryData[]; }[]; + createDataset?: ( + element: (number | null)[], + index: number, + allLabels: string[], + ) => ChartDataset; } export default getChartData; diff --git a/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts b/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts new file mode 100644 index 0000000000..d2a36392c8 --- /dev/null +++ b/frontend/src/lib/newQueryBuilder/getPaginationQueryData.ts @@ -0,0 +1,76 @@ +import { initialFilters } from 'constants/queryBuilder'; +import { FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config'; +import { + IBuilderQuery, + OrderByPayload, + TagFilter, +} from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; + +type SetupPaginationQueryDataParams = { + currentStagedQueryData: IBuilderQuery | null; + listItemId: string | null; + orderByTimestamp: OrderByPayload | null; + page: number; + pageSize: number; +}; + +type SetupPaginationQueryData = ( + params: SetupPaginationQueryDataParams, +) => Pick; + +export const getPaginationQueryData: SetupPaginationQueryData = ({ + currentStagedQueryData, + listItemId, + orderByTimestamp, + page, + pageSize, +}) => { + if (!currentStagedQueryData) { + return { limit: null, filters: initialFilters }; + } + + const filters = currentStagedQueryData.filters || initialFilters; + const offset = (page - 1) * pageSize; + + const queryProps = + (orderByTimestamp && currentStagedQueryData.orderBy.length > 1) || + !orderByTimestamp + ? { + offset, + } + : {}; + + const updatedFilters: TagFilter = { + ...filters, + items: filters.items.filter((item) => item.key?.key !== 'id'), + }; + + const tagFilters: TagFilter = { + ...filters, + items: + listItemId && orderByTimestamp + ? [ + { + id: uuid(), + key: { + key: 'id', + type: null, + dataType: 'string', + isColumn: true, + }, + op: orderByTimestamp.order === FILTERS.ASC ? '>' : '<', + value: listItemId, + }, + ...updatedFilters.items, + ] + : updatedFilters.items, + }; + + const chunkOfQueryData: Partial = { + filters: orderByTimestamp ? tagFilters : updatedFilters, + ...queryProps, + }; + + return { ...currentStagedQueryData, ...chunkOfQueryData }; +}; diff --git a/frontend/src/lib/query/createTableColumnsFromQuery.ts b/frontend/src/lib/query/createTableColumnsFromQuery.ts index 653a859ba8..0637917efd 100644 --- a/frontend/src/lib/query/createTableColumnsFromQuery.ts +++ b/frontend/src/lib/query/createTableColumnsFromQuery.ts @@ -39,6 +39,7 @@ type CreateTableDataFromQuery = ( type FillColumnData = ( queryTableData: QueryDataV3[], dynamicColumns: DynamicColumns, + query: Query, ) => { filledDynamicColumns: DynamicColumns; rowsLength: number }; type GetDynamicColumns = ( @@ -177,7 +178,8 @@ const fillEmptyRowCells = ( const fillDataFromSeria = ( seria: SeriesItem, columns: DynamicColumns, - currentQueryName: string, + queryName: string, + operator: string, ): void => { const labelEntries = Object.entries(seria.labels); @@ -193,7 +195,13 @@ const fillDataFromSeria = ( return; } - if (currentQueryName === column.key) { + if (isFormula(queryName) && queryName === column.key) { + column.data.push(parseFloat(value.value).toFixed(2)); + unusedColumnsKeys.delete(column.key); + return; + } + + if (!isFormula(queryName) && operator === column.key) { column.data.push(parseFloat(value.value).toFixed(2)); unusedColumnsKeys.delete(column.key); return; @@ -230,20 +238,25 @@ const fillDataFromList = ( }); }; -const fillColumnsData: FillColumnData = (queryTableData, cols) => { +const fillColumnsData: FillColumnData = (queryTableData, cols, query) => { const fields = cols.filter((item) => item.type === 'field'); const operators = cols.filter((item) => item.type === 'operator'); const resultColumns = [...fields, ...operators]; queryTableData.forEach((currentQuery) => { - // const currentOperator = getQueryOperator( - // query.builder.queryData, - // currentQuery.queryName, - // ); - if (currentQuery.series) { currentQuery.series.forEach((seria) => { - fillDataFromSeria(seria, resultColumns, currentQuery.queryName); + const currentOperator = getQueryOperator( + query.builder.queryData, + currentQuery.queryName, + ); + + fillDataFromSeria( + seria, + resultColumns, + currentQuery.queryName, + currentOperator, + ); }); } @@ -313,6 +326,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({ const { filledDynamicColumns, rowsLength } = fillColumnsData( queryTableData, dynamicColumns, + query, ); const dataSource = generateData(filledDynamicColumns, rowsLength); diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 519538365e..98278db076 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -1,8 +1,8 @@ import { Button, Col, Row } from 'antd'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerViews from 'container/LogsExplorerViews'; import { QueryBuilder } from 'container/QueryBuilder'; +import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; @@ -11,23 +11,33 @@ import { DataSource } from 'types/common/queryBuilder'; // ** Styles import { ButtonWrapperStyled, WrapperStyled } from './styles'; +import { prepareQueryWithDefaultTimestamp } from './utils'; function LogsExplorer(): JSX.Element { const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder(); const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); - const defaultValue = useMemo( - () => - updateAllQueriesOperators( - initialQueriesMap.logs, - PANEL_TYPES.LIST, - DataSource.LOGS, - ), - [updateAllQueriesOperators], - ); + const defaultValue = useMemo(() => { + const updatedQuery = updateAllQueriesOperators( + initialQueriesMap.logs, + PANEL_TYPES.LIST, + DataSource.LOGS, + ); + return prepareQueryWithDefaultTimestamp(updatedQuery); + }, [updateAllQueriesOperators]); useShareBuilderUrl(defaultValue); + const inactiveLogsFilters: QueryBuilderProps['inactiveFilters'] = useMemo(() => { + if (panelTypes === PANEL_TYPES.TABLE) { + const result: QueryBuilderProps['inactiveFilters'] = { stepInterval: true }; + + return result; + } + + return {}; + }, [panelTypes]); + return ( @@ -35,6 +45,7 @@ function LogsExplorer(): JSX.Element { - diff --git a/frontend/src/pages/LogsExplorer/utils.ts b/frontend/src/pages/LogsExplorer/utils.ts new file mode 100644 index 0000000000..13e8a29a4f --- /dev/null +++ b/frontend/src/pages/LogsExplorer/utils.ts @@ -0,0 +1,12 @@ +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({ + ...query, + builder: { + ...query.builder, + queryData: query.builder.queryData.map((item) => ({ + ...item, + orderBy: [{ columnName: 'timestamp', order: 'desc' }], + })), + }, +}); diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 179a9a93e0..3bcd8f7244 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -2,10 +2,7 @@ import { Tabs } from 'antd'; import axios from 'axios'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { - COMPOSITE_QUERY, - PANEL_TYPES_QUERY, -} from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import ExportPanel from 'container/ExportPanel'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; @@ -102,11 +99,9 @@ function TracesExplorer(): JSX.Element { onSuccess: (data) => { const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, { dashboardId: data?.payload?.uuid, - })}/new?${QueryParams.graphType}=graph&${ - QueryParams.widgetId - }=empty&${COMPOSITE_QUERY}=${encodeURIComponent( - JSON.stringify(exportDefaultQuery), - )}`; + })}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${ + queryParamNamesMap.compositeQuery + }=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`; history.push(dashboardEditView); }, @@ -132,7 +127,9 @@ function TracesExplorer(): JSX.Element { DataSource.TRACES, ); - redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType }); + redirectWithQueryBuilderData(query, { + [queryParamNamesMap.panelTypes]: newPanelType, + }); }, [ currentQuery, diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 7cb43a1a7e..e5578f18f8 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -13,7 +13,7 @@ import { MAX_QUERIES, PANEL_TYPES, } from 'constants/queryBuilder'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; @@ -461,7 +461,7 @@ export function QueryBuilderProvider({ }; urlQuery.set( - COMPOSITE_QUERY, + queryParamNamesMap.compositeQuery, encodeURIComponent(JSON.stringify(currentGeneratedQuery)), ); diff --git a/frontend/src/store/actions/dashboard/saveDashboard.ts b/frontend/src/store/actions/dashboard/saveDashboard.ts index 14e62fedac..827a11fc64 100644 --- a/frontend/src/store/actions/dashboard/saveDashboard.ts +++ b/frontend/src/store/actions/dashboard/saveDashboard.ts @@ -1,7 +1,7 @@ import { notification } from 'antd'; import updateDashboardApi from 'api/dashboard/update'; import { AxiosError } from 'axios'; -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; @@ -88,7 +88,7 @@ export const SaveDashboard = ({ }; const allLayout = getAllLayout(); const params = new URLSearchParams(window.location.search); - const compositeQuery = params.get(COMPOSITE_QUERY); + const compositeQuery = params.get(queryParamNamesMap.compositeQuery); const { maxTime, minTime } = store.getState().globalTime; const query = compositeQuery ? updateStepInterval( diff --git a/frontend/src/types/api/logs/log.ts b/frontend/src/types/api/logs/log.ts index eb862daa9c..a7c00885ff 100644 --- a/frontend/src/types/api/logs/log.ts +++ b/frontend/src/types/api/logs/log.ts @@ -1,6 +1,6 @@ export interface ILog { date: string; - timestamp: number; + timestamp: number | string; id: string; traceId: string; spanId: string; diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index ee9cfb6aef..b16aac3b3c 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -59,6 +59,8 @@ export type IBuilderQuery = { orderBy: OrderByPayload[]; reduceTo: ReduceOperators; legend: string; + pageSize?: number; + offset?: number; }; export interface IClickHouseQuery { diff --git a/frontend/src/types/api/widgets/getQuery.ts b/frontend/src/types/api/widgets/getQuery.ts index 0b36af1541..f60eebc0bb 100644 --- a/frontend/src/types/api/widgets/getQuery.ts +++ b/frontend/src/types/api/widgets/getQuery.ts @@ -5,7 +5,10 @@ export interface PayloadProps { result: QueryData[]; } -export type ListItem = { timestamp: string; data: Omit }; +export type ListItem = { + timestamp: string; + data: Omit; +}; export interface QueryData { metric: { diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index d5872c8bbe..2664d95ae7 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -1,11 +1,13 @@ import { QueryProps } from 'container/QueryBuilder/components/Query/Query.interfaces'; +import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { SelectOption } from './select'; -type UseQueryOperationsParams = Pick; +type UseQueryOperationsParams = Pick & + Pick; export type HandleChangeQueryData = < Key extends keyof IBuilderQuery, From c1664dde6a34d3a68b2ea6f54d2c6177a79887ae Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:00:00 +0300 Subject: [PATCH 02/48] fix: issue #3034, add columns to end of the list (#3035) * fix: issue #3034, add columns to end of the list * fix: #3033 replace columns --- .../TracesExplorer/ListView/configs.tsx | 6 ++--- .../TracesExplorer/ListView/utils.tsx | 23 ++++++++----------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/frontend/src/container/TracesExplorer/ListView/configs.tsx b/frontend/src/container/TracesExplorer/ListView/configs.tsx index 3b05ed8169..308ca1995e 100644 --- a/frontend/src/container/TracesExplorer/ListView/configs.tsx +++ b/frontend/src/container/TracesExplorer/ListView/configs.tsx @@ -1,11 +1,11 @@ import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination'; export const defaultSelectedColumns: string[] = [ - 'name', 'serviceName', - 'responseStatusCode', - 'httpMethod', + 'name', 'durationNano', + 'httpMethod', + 'responseStatusCode', ]; export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS]; diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx index eb6be9a90d..350ba98a6d 100644 --- a/frontend/src/container/TracesExplorer/ListView/utils.tsx +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -28,21 +28,18 @@ export const modifyColumns = ( columns: ColumnsType, selectedColumns: BaseAutocompleteData[], ): ColumnsType => { - const initialColumns = columns.filter(({ key }) => { - let isValidColumn = true; + const initialColumns = selectedColumns.reduce( + (acc, { key: selectedColumnKey }) => { + const column = columns.find(({ key }) => selectedColumnKey === key); - const checkIsExistColumnByKey = (attributeKey: string): boolean => - !selectedColumns.find(({ key }) => key === attributeKey) && - attributeKey === key; + if (column) { + return [...acc, column]; + } - const isSelectedSpanId = checkIsExistColumnByKey('spanID'); - const isSelectedTraceId = checkIsExistColumnByKey('traceID'); - - if (isSelectedSpanId || isSelectedTraceId || key === 'date') - isValidColumn = false; - - return isValidColumn; - }); + return acc; + }, + [] as ColumnsType, + ); const dateColumn = columns.find(({ key }) => key === 'date'); From 0750231b4bd7654979ef1ddf0ff441dea292c532 Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Thu, 6 Jul 2023 17:00:20 +0300 Subject: [PATCH 03/48] fix: remove unnecessary styles for the actions section in the query builder (#3057) --- frontend/src/container/QueryBuilder/QueryBuilder.styled.ts | 6 ------ frontend/src/container/QueryBuilder/QueryBuilder.tsx | 6 ++---- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 frontend/src/container/QueryBuilder/QueryBuilder.styled.ts diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts b/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts deleted file mode 100644 index 20dc483e01..0000000000 --- a/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Col } from 'antd'; -import styled from 'styled-components'; - -export const ActionsWrapperStyled = styled(Col)` - padding-right: 1rem; -`; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 4aefedb022..125440fb2a 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -10,8 +10,6 @@ import { memo, useEffect, useMemo } from 'react'; import { Formula, Query } from './components'; // ** Types import { QueryBuilderProps } from './QueryBuilder.interfaces'; -// ** Styles -import { ActionsWrapperStyled } from './QueryBuilder.styled'; export const QueryBuilder = memo(function QueryBuilder({ config, @@ -87,7 +85,7 @@ export const QueryBuilder = memo(function QueryBuilder({ - + + + } + /> + ); +} + +export default memo(LogExplorerQuerySection); diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index e99484af0d..51501c8b39 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -3,12 +3,13 @@ import TextToolTip from 'components/TextToolTip'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { QueryBuilder } from 'container/QueryBuilder'; +import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import useUrlQuery from 'hooks/useUrlQuery'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; @@ -101,12 +102,22 @@ function QuerySection({ handleStageQuery(currentQuery); }; + const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: false, isDisabled: true }, + }; + + return config; + }, []); + const items = [ { key: EQueryType.QUERY_BUILDER, label: 'Query Builder', tab: Query Builder, - children: , + children: ( + + ), }, { key: EQueryType.CLICKHOUSE, diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 6135e0b670..f14dfee184 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -14,5 +14,7 @@ export type QueryBuilderProps = { config?: QueryBuilderConfig; panelType: ITEMS; actions?: ReactNode; - inactiveFilters?: Partial>; + filterConfigs?: Partial< + Record + >; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index 125440fb2a..aa3cbae646 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -15,7 +15,7 @@ export const QueryBuilder = memo(function QueryBuilder({ config, panelType: newPanelType, actions, - inactiveFilters = {}, + filterConfigs = {}, }: QueryBuilderProps): JSX.Element { const { currentQuery, @@ -73,7 +73,7 @@ export const QueryBuilder = memo(function QueryBuilder({ isAvailableToDisable={isAvailableToDisableQuery} queryVariant={config?.queryVariant || 'dropdown'} query={query} - inactiveFilters={inactiveFilters} + filterConfigs={filterConfigs} /> ))} diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts index 8cee2d9dad..a2b6a55787 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts +++ b/frontend/src/container/QueryBuilder/components/Query/Query.interfaces.ts @@ -6,4 +6,4 @@ export type QueryProps = { isAvailableToDisable: boolean; query: IBuilderQuery; queryVariant: 'static' | 'dropdown'; -} & Pick; +} & Pick; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index dd42404c6d..2eba663e34 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -35,7 +35,7 @@ export const Query = memo(function Query({ isAvailableToDisable, queryVariant, query, - inactiveFilters, + filterConfigs, }: QueryProps): JSX.Element { const { panelType } = useQueryBuilder(); const { @@ -48,7 +48,7 @@ export const Query = memo(function Query({ handleChangeQueryData, handleChangeOperator, handleDeleteQuery, - } = useQueryOperations({ index, query, inactiveFilters }); + } = useQueryOperations({ index, query, filterConfigs }); const handleChangeAggregateEvery = useCallback( (value: IBuilderQuery['stepInterval']) => { @@ -112,7 +112,7 @@ export const Query = memo(function Query({ const renderAggregateEveryFilter = useCallback( (): JSX.Element | null => - !inactiveFilters?.stepInterval ? ( + !filterConfigs?.stepInterval?.isHidden ? ( @@ -120,12 +120,18 @@ export const Query = memo(function Query({ ) : null, - [inactiveFilters?.stepInterval, query, handleChangeAggregateEvery], + [ + filterConfigs?.stepInterval?.isHidden, + filterConfigs?.stepInterval?.isDisabled, + query, + handleChangeAggregateEvery, + ], ); const renderAdditionalFilters = useCallback((): ReactNode => { diff --git a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx index c68dad2cff..658987fd35 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregateEveryFilter/index.tsx @@ -8,6 +8,7 @@ import { selectStyle } from '../QueryBuilderSearch/config'; function AggregateEveryFilter({ onChange, query, + disabled, }: AggregateEveryFilterProps): JSX.Element { const isMetricsDataSource = useMemo( () => query.dataSource === DataSource.METRICS, @@ -20,7 +21,8 @@ function AggregateEveryFilter({ } }; - const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key; + const isDisabled = + (isMetricsDataSource && !query.aggregateAttribute.key) || disabled; return ( void; query: IBuilderQuery; + disabled: boolean; } export default AggregateEveryFilter; diff --git a/frontend/src/container/TracesExplorer/QuerySection/index.tsx b/frontend/src/container/TracesExplorer/QuerySection/index.tsx index dbb922c139..177311dae0 100644 --- a/frontend/src/container/TracesExplorer/QuerySection/index.tsx +++ b/frontend/src/container/TracesExplorer/QuerySection/index.tsx @@ -1,9 +1,10 @@ import { Button } from 'antd'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { QueryBuilder } from 'container/QueryBuilder'; +import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import { DataSource } from 'types/common/queryBuilder'; import { ButtonWrapper, Container } from './styles'; @@ -13,6 +14,14 @@ function QuerySection(): JSX.Element { const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const config: QueryBuilderProps['filterConfigs'] = { + stepInterval: { isHidden: false, isDisabled: true }, + }; + + return config; + }, []); + return ( - - - - } - /> + diff --git a/frontend/src/types/common/operations.types.ts b/frontend/src/types/common/operations.types.ts index 2664d95ae7..00ca569126 100644 --- a/frontend/src/types/common/operations.types.ts +++ b/frontend/src/types/common/operations.types.ts @@ -7,7 +7,7 @@ import { DataSource } from 'types/common/queryBuilder'; import { SelectOption } from './select'; type UseQueryOperationsParams = Pick & - Pick; + Pick; export type HandleChangeQueryData = < Key extends keyof IBuilderQuery, From e3d26d3f10c38ef3b6c8636472b539bae9c1427b Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Fri, 7 Jul 2023 15:49:59 +0300 Subject: [PATCH 15/48] fix: order repeating in order by filter (#3053) * fix: order repeating in order by filter * fix: filter order --------- Co-authored-by: Vishal Sharma --- .../filters/OrderByFilter/OrderByFilter.tsx | 69 +++++++++++++++---- .../filters/OrderByFilter/utils.ts | 12 ++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx index 3994c6c73b..7cacaa9877 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/OrderByFilter.tsx @@ -18,6 +18,7 @@ import { getLabelFromValue, mapLabelValuePairs, orderByValueDelimiter, + splitOrderByFromString, transformToOrderByStringValues, } from './utils'; @@ -115,7 +116,11 @@ export function OrderByFilter({ if (!match) return { label: item.label, value: item.value }; // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars const [_, order] = match.data.flat() as string[]; - if (order) return { label: item.label, value: item.value }; + if (order) + return { + label: item.label, + value: item.value, + }; return { label: `${item.value} ${FILTERS.ASC}`, @@ -131,29 +136,69 @@ export function OrderByFilter({ ); }, []); - const handleChange = (values: IOption[]): void => { - const result = getUniqValues(values); + const getValidResult = useCallback( + (result: IOption[]): IOption[] => + result.reduce((acc, item) => { + if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc; + + if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) { + const splittedOrderBy = splitOrderByFromString(item.value); + + if (splittedOrderBy) { + acc.push({ + label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`, + value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`, + }); + + return acc; + } + } + + acc.push(item); + + return acc; + }, []), + [], + ); + + const handleChange = (values: IOption[]): void => { + const validResult = getValidResult(values); + const result = getUniqValues(validResult); - setSelectedValue(result); const orderByValues: OrderByPayload[] = result.map((item) => { const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter }); - if (match) { - const [columnName, order] = match.data.flat() as string[]; + if (!match) { return { - columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key) - ? '#SIGNOZ_VALUE' - : columnName, - order: order ?? 'asc', + columnName: item.value, + order: 'asc', }; } + const [columnName, order] = match.data.flat() as string[]; + + const columnNameValue = checkIfKeyPresent( + columnName, + query.aggregateAttribute.key, + ) + ? '#SIGNOZ_VALUE' + : columnName; + + const orderValue = order ?? 'asc'; + return { - columnName: item.value, - order: 'asc', + columnName: columnNameValue, + order: orderValue, }; }); + const selectedValue: IOption[] = orderByValues.map((item) => ({ + label: `${item.columnName} ${item.order}`, + value: `${item.columnName} ${item.order}`, + })); + + setSelectedValue(selectedValue); + setSearchText(''); onChange(orderByValues); }; diff --git a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts index 1d415ec2d5..345a2eaab9 100644 --- a/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/OrderByFilter/utils.ts @@ -4,6 +4,8 @@ import * as Papa from 'papaparse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData'; +import { FILTERS } from './config'; + export const orderByValueDelimiter = '|'; export const transformToOrderByStringValues = ( @@ -66,3 +68,13 @@ export function getLabelFromValue(arr: IOption[]): string[] { export function checkIfKeyPresent(str: string, valueToCheck: string): boolean { return new RegExp(`\\(${valueToCheck}\\)`).test(str); } + +export function splitOrderByFromString(str: string): OrderByPayload | null { + const splittedStr = str.split(' '); + const order = splittedStr.pop() || FILTERS.ASC; + const columnName = splittedStr.join(' '); + + if (!columnName) return null; + + return { columnName, order }; +} From 2e85bd026419986f9d28c43ad308946f2b6090ec Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Fri, 7 Jul 2023 20:59:35 +0530 Subject: [PATCH 16/48] feat: export panel in logs explorer is added (#2993) * feat: export panel in logs explorer is added * chore: notification is updated when dashboard id is not found while updating * fix: error handling for export panel * fix: layout gap * refactor: remove log * fix: updating log page from update button * fix: redirect with correct operator * fix: redirect wioth query data * fix: refetch list --------- Co-authored-by: Yevhen Shevchenko --- frontend/src/container/ExportPanel/index.tsx | 6 +- .../LogsExplorerViews.styled.ts | 6 ++ .../src/container/LogsExplorerViews/index.tsx | 99 ++++++++++++++++++- .../queryBuilder/useGetExplorerQueryRange.ts | 4 +- frontend/src/hooks/useAxiosError.tsx | 19 ++++ frontend/src/pages/LogsExplorer/index.tsx | 2 +- frontend/src/pages/TracesExplorer/index.tsx | 31 +++++- 7 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 frontend/src/hooks/useAxiosError.tsx diff --git a/frontend/src/container/ExportPanel/index.tsx b/frontend/src/container/ExportPanel/index.tsx index 35ac124fae..c9c7d29383 100644 --- a/frontend/src/container/ExportPanel/index.tsx +++ b/frontend/src/container/ExportPanel/index.tsx @@ -84,10 +84,6 @@ function ExportPanel({ ); } -ExportPanel.defaultProps = { - isLoading: false, -}; - interface OnClickProps { key: string; } @@ -98,4 +94,6 @@ export interface ExportPanelProps { query: Query | null; } +ExportPanel.defaultProps = { isLoading: false }; + export default ExportPanel; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts index 4fd3046e3b..a73fe526fb 100644 --- a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts @@ -7,3 +7,9 @@ export const TabsStyled = styled(Tabs)` background-color: ${themeColors.lightBlack}; } `; + +export const ActionsWrapper = styled.div` + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; +`; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 560c498b3f..1875884c41 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,8 +1,12 @@ import { TabsProps } from 'antd'; +import axios from 'axios'; import TabLabel from 'components/TabLabel'; -import { PANEL_TYPES } from 'constants/queryBuilder'; +import { QueryParams } from 'constants/query'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; +import ROUTES from 'constants/routes'; import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; +import ExportPanel from 'container/ExportPanel'; import LogExplorerDetailedView from 'container/LogExplorerDetailedView'; import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerList from 'container/LogsExplorerList'; @@ -10,11 +14,16 @@ import LogsExplorerList from 'container/LogsExplorerList'; // import LogsExplorerTable from 'container/LogsExplorerTable'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { generatePath, useHistory } from 'react-router-dom'; +import { Dashboard } from 'types/api/dashboard/getAll'; import { ILog } from 'types/api/logs/log'; import { IBuilderQuery, @@ -23,9 +32,12 @@ import { } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; -import { TabsStyled } from './LogsExplorerViews.styled'; +import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled'; function LogsExplorerViews(): JSX.Element { + const { notifications } = useNotifications(); + const history = useHistory(); + const { queryData: pageSize } = useUrlQueryData( queryParamNamesMap.pageSize, DEFAULT_PER_PAGE_VALUE, @@ -105,6 +117,16 @@ function LogsExplorerViews(): JSX.Element { return modifiedQuery; }, [stagedQuery, currentStagedQueryData]); + const exportDefaultQuery = useMemo( + () => + updateAllQueriesOperators( + currentQuery || initialQueriesMap.logs, + PANEL_TYPES.TIME_SERIES, + DataSource.LOGS, + ), + [currentQuery, updateAllQueriesOperators], + ); + const listChartData = useGetExplorerQueryRange( listChartQuery, PANEL_TYPES.TIME_SERIES, @@ -218,6 +240,66 @@ function LogsExplorerViews(): JSX.Element { ], ); + const { + mutate: updateDashboard, + isLoading: isUpdateDashboardLoading, + } = useUpdateDashboard(); + + const handleExport = useCallback( + (dashboard: Dashboard | null): void => { + if (!dashboard) return; + + const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery( + dashboard, + exportDefaultQuery, + ); + + updateDashboard(updatedDashboard, { + onSuccess: (data) => { + if (data.error) { + const message = + data.error === 'feature usage exceeded' ? ( + + Panel limit exceeded for {DataSource.LOGS} in community edition. Please + checkout our paid plans{' '} + + here + + + ) : ( + data.error + ); + notifications.error({ + message, + }); + + return; + } + + const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, { + dashboardId: data?.payload?.uuid, + })}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${ + queryParamNamesMap.compositeQuery + }=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`; + + history.push(dashboardEditView); + }, + onError: (error) => { + if (axios.isAxiosError(error)) { + notifications.error({ + message: error.message, + }); + } + }, + }); + }, + [exportDefaultQuery, history, notifications, updateDashboard], + ); + useEffect(() => { const shouldChangeView = isMultipleQueries || isGroupByExist; @@ -238,7 +320,7 @@ function LogsExplorerViews(): JSX.Element { }, [data]); useEffect(() => { - if (requestData?.id !== stagedQuery?.id) { + if (requestData?.id !== stagedQuery?.id || isFetching) { const newRequestData = getRequestData(stagedQuery, { page: 1, log: null, @@ -248,7 +330,7 @@ function LogsExplorerViews(): JSX.Element { setPage(1); setRequestData(newRequestData); } - }, [stagedQuery, requestData, getRequestData, pageSize]); + }, [stagedQuery, requestData, getRequestData, pageSize, isFetching]); const tabsItems: TabsProps['items'] = useMemo( () => [ @@ -333,6 +415,15 @@ function LogsExplorerViews(): JSX.Element { return ( <> + {stagedQuery && ( + + + + )} , Error>, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); - const { selectedTime: globalSelectedInterval } = useSelector< + const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); @@ -51,7 +51,7 @@ export const useGetExplorerQueryRange = ( { ...options, retry: false, - queryKey: [key, globalSelectedInterval, requestData], + queryKey: [key, globalSelectedInterval, requestData, minTime, maxTime], enabled: isEnabled, }, ); diff --git a/frontend/src/hooks/useAxiosError.tsx b/frontend/src/hooks/useAxiosError.tsx new file mode 100644 index 0000000000..bad81754b3 --- /dev/null +++ b/frontend/src/hooks/useAxiosError.tsx @@ -0,0 +1,19 @@ +import axios from 'axios'; + +import { useNotifications } from './useNotifications'; + +const useAxiosError = (): UseAxiosError => { + const { notifications } = useNotifications(); + + return (error: unknown): void => { + if (axios.isAxiosError(error)) { + notifications.error({ + message: error.message, + }); + } + }; +}; + +type UseAxiosError = (error: unknown) => void; + +export default useAxiosError; diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index bf9bf40d9e..3e6613ed77 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -8,7 +8,7 @@ import { WrapperStyled } from './styles'; function LogsExplorer(): JSX.Element { return ( - + diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 3bcd8f7244..378e09d585 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -23,9 +23,9 @@ import { getTabsItems } from './utils'; function TracesExplorer(): JSX.Element { const { notifications } = useNotifications(); + const { currentQuery, - stagedQuery, panelType, updateAllQueriesOperators, redirectWithQueryBuilderData, @@ -77,11 +77,11 @@ function TracesExplorer(): JSX.Element { const exportDefaultQuery = useMemo( () => updateAllQueriesOperators( - stagedQuery || initialQueriesMap.traces, + currentQuery || initialQueriesMap.traces, PANEL_TYPES.TIME_SERIES, DataSource.TRACES, ), - [stagedQuery, updateAllQueriesOperators], + [currentQuery, updateAllQueriesOperators], ); const { mutate: updateDashboard, isLoading } = useUpdateDashboard(); @@ -97,6 +97,29 @@ function TracesExplorer(): JSX.Element { updateDashboard(updatedDashboard, { onSuccess: (data) => { + if (data.error) { + const message = + data.error === 'feature usage exceeded' ? ( + + Panel limit exceeded for {DataSource.TRACES} in community edition. + Please checkout our paid plans{' '} + + here + + + ) : ( + data.error + ); + notifications.error({ + message, + }); + + return; + } const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, { dashboardId: data?.payload?.uuid, })}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${ @@ -159,7 +182,7 @@ function TracesExplorer(): JSX.Element { From 2b3934b84570bca3d60cafb3341a4f82b6fc87c6 Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Fri, 7 Jul 2023 19:20:52 +0300 Subject: [PATCH 17/48] fix: add converting to ms for the durationNano attribute (#3072) * fix: add converting to ms for the durationNano attrbute * fix: add ms title to the graph --------- Co-authored-by: Vishal Sharma --- .../TimeSeriesView/TimeSeriesView.tsx | 4 +++ .../src/container/TimeSeriesView/index.tsx | 36 +++++++++++++++++-- .../src/container/TimeSeriesView/utils.ts | 26 ++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 frontend/src/container/TimeSeriesView/utils.ts diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx index ca01a0156c..6824b23995 100644 --- a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx +++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx @@ -11,6 +11,7 @@ function TimeSeriesView({ data, isLoading, isError, + yAxisUnit, }: TimeSeriesViewProps): JSX.Element { const chartData = useMemo( () => @@ -32,6 +33,7 @@ function TimeSeriesView({ @@ -42,12 +44,14 @@ function TimeSeriesView({ interface TimeSeriesViewProps { data?: SuccessResponse; + yAxisUnit?: string; isLoading: boolean; isError: boolean; } TimeSeriesView.defaultProps = { data: undefined, + yAxisUnit: 'short', }; export default TimeSeriesView; diff --git a/frontend/src/container/TimeSeriesView/index.tsx b/frontend/src/container/TimeSeriesView/index.tsx index 5212334a4d..a0618b9960 100644 --- a/frontend/src/container/TimeSeriesView/index.tsx +++ b/frontend/src/container/TimeSeriesView/index.tsx @@ -2,23 +2,43 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useMemo } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { DataSource } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; import TimeSeriesView from './TimeSeriesView'; +import { convertDataValueToMs } from './utils'; function TimeSeriesViewContainer({ dataSource = DataSource.TRACES, }: TimeSeriesViewProps): JSX.Element { - const { stagedQuery, panelType } = useQueryBuilder(); + const { stagedQuery, currentQuery, panelType } = useQueryBuilder(); const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector< AppState, GlobalReducer >((state) => state.globalTime); + const isValidToConvertToMs = useMemo(() => { + const isValid: boolean[] = []; + + currentQuery.builder.queryData.forEach( + ({ aggregateAttribute, aggregateOperator }) => { + const isExistDurationNanoAttribute = + aggregateAttribute.key === 'durationNano'; + + const isCountOperator = + aggregateOperator === 'count' || aggregateOperator === 'count_distinct'; + + isValid.push(!isCountOperator && isExistDurationNanoAttribute); + }, + ); + + return isValid.every(Boolean); + }, [currentQuery]); + const { data, isLoading, isError } = useGetQueryRange( { query: stagedQuery || initialQueriesMap[dataSource], @@ -41,7 +61,19 @@ function TimeSeriesViewContainer({ }, ); - return ; + const responseData = useMemo( + () => (isValidToConvertToMs ? convertDataValueToMs(data) : data), + [data, isValidToConvertToMs], + ); + + return ( + + ); } interface TimeSeriesViewProps { diff --git a/frontend/src/container/TimeSeriesView/utils.ts b/frontend/src/container/TimeSeriesView/utils.ts new file mode 100644 index 0000000000..b857cf9815 --- /dev/null +++ b/frontend/src/container/TimeSeriesView/utils.ts @@ -0,0 +1,26 @@ +import { SuccessResponse } from 'types/api/index'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { QueryData } from 'types/api/widgets/getQuery'; + +export const convertDataValueToMs = ( + data?: SuccessResponse, +): SuccessResponse | undefined => { + const convertedData = data; + + const convertedResult: QueryData[] = data?.payload?.data?.result + ? data.payload.data.result.map((item) => { + const values: [number, string][] = item.values.map((value) => { + const [first = 0, second = ''] = value || []; + return [first, String(Number(second) / 1000000)]; + }); + + return { ...item, values }; + }) + : []; + + if (convertedData?.payload?.data?.result && convertedResult) { + convertedData.payload.data.result = convertedResult; + } + + return convertedData; +}; From c72729f8bc9aafd5caaf5e2364e5c8e12c66a2e7 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Sat, 8 Jul 2023 07:27:34 +0300 Subject: [PATCH 18/48] fix: list scroll with time updating (#3075) --- .../src/container/LogsExplorerViews/index.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 1875884c41..082365d347 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -21,8 +21,10 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; import { generatePath, useHistory } from 'react-router-dom'; +import { AppState } from 'store/reducers'; import { Dashboard } from 'types/api/dashboard/getAll'; import { ILog } from 'types/api/logs/log'; import { @@ -31,6 +33,7 @@ import { Query, } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled'; @@ -43,6 +46,12 @@ function LogsExplorerViews(): JSX.Element { DEFAULT_PER_PAGE_VALUE, ); + const { minTime } = useSelector( + (state) => state.globalTime, + ); + + const currentMinTimeRef = useRef(minTime); + // Context const { currentQuery, @@ -320,7 +329,10 @@ function LogsExplorerViews(): JSX.Element { }, [data]); useEffect(() => { - if (requestData?.id !== stagedQuery?.id || isFetching) { + if ( + requestData?.id !== stagedQuery?.id || + currentMinTimeRef.current !== minTime + ) { const newRequestData = getRequestData(stagedQuery, { page: 1, log: null, @@ -329,8 +341,9 @@ function LogsExplorerViews(): JSX.Element { setLogs([]); setPage(1); setRequestData(newRequestData); + currentMinTimeRef.current = minTime; } - }, [stagedQuery, requestData, getRequestData, pageSize, isFetching]); + }, [stagedQuery, requestData, getRequestData, pageSize, minTime]); const tabsItems: TabsProps['items'] = useMemo( () => [ From e3e07874596ad9e296e66c4d4e09f4c4e3ae88d5 Mon Sep 17 00:00:00 2001 From: Sachin M K Date: Mon, 10 Jul 2023 10:52:28 +0530 Subject: [PATCH 19/48] fix: search not working when switching spans (#3048) --- .../SelectedSpanDetails/Tags/index.tsx | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/frontend/src/container/TraceDetail/SelectedSpanDetails/Tags/index.tsx b/frontend/src/container/TraceDetail/SelectedSpanDetails/Tags/index.tsx index 80f04062ba..529f538296 100644 --- a/frontend/src/container/TraceDetail/SelectedSpanDetails/Tags/index.tsx +++ b/frontend/src/container/TraceDetail/SelectedSpanDetails/Tags/index.tsx @@ -6,7 +6,6 @@ import { Dispatch, SetStateAction, useCallback, - useEffect, useMemo, useState, } from 'react'; @@ -24,13 +23,9 @@ function Tags({ setText, }: TagsProps): JSX.Element { const { t } = useTranslation(['traceDetails']); - const [allRenderedTags, setAllRenderedTags] = useState(tags); + const [searchText, setSearchText] = useState(''); const isSearchVisible = useMemo(() => tags.length > 5, [tags]); - useEffect(() => { - setAllRenderedTags(tags); - }, [tags]); - const getLink = useCallback( (item: Record) => `${ROUTES.TRACE}/${item.TraceId}${formUrlParams({ @@ -41,14 +36,12 @@ function Tags({ [], ); - const onChangeHandler = useCallback( - (e: ChangeEvent): void => { - const { value } = e.target; - const filteredTags = tags.filter((tag) => tag.key.includes(value)); - setAllRenderedTags(filteredTags); - }, - [tags], - ); + const onChangeHandler = (e: ChangeEvent): void => { + const { value } = e.target; + setSearchText(value); + }; + + const filteredTags = tags.filter((tag) => tag.key.includes(searchText)); if (tags.length === 0) { return No tags in selected span; @@ -61,9 +54,10 @@ function Tags({ placeholder={t('traceDetails:search_tags')} allowClear onChange={onChangeHandler} + value={searchText} /> )} - {allRenderedTags.map((tag) => ( + {filteredTags.map((tag) => ( Date: Mon, 10 Jul 2023 11:18:33 +0530 Subject: [PATCH 20/48] refactor: added check for dashboard length (#2997) Co-authored-by: Palash Gupta --- .../getDashboardVariables.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts index aa870f9bbb..1fae298675 100644 --- a/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts +++ b/frontend/src/lib/dashbaordVariables/getDashboardVariables.ts @@ -7,24 +7,27 @@ export const getDashboardVariables = (): Record => { globalTime, dashboards: { dashboards }, } = store.getState(); - const [selectedDashboard] = dashboards || []; - const { - data: { variables = {} }, - } = selectedDashboard; + if (dashboards.length > 0) { + const [selectedDashboard] = dashboards || []; + const { + data: { variables = {} }, + } = selectedDashboard; - const { start, end } = getStartEndRangeTime({ - type: 'GLOBAL_TIME', - interval: globalTime.selectedTime, - }); + const { start, end } = getStartEndRangeTime({ + type: 'GLOBAL_TIME', + interval: globalTime.selectedTime, + }); - const variablesTuple: Record = { - SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, - SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, - }; - Object.keys(variables).forEach((key) => { - variablesTuple[key] = variables[key].selectedValue; - }); - return variablesTuple; + const variablesTuple: Record = { + SIGNOZ_START_TIME: parseInt(start, 10) * 1e3, + SIGNOZ_END_TIME: parseInt(end, 10) * 1e3, + }; + Object.keys(variables).forEach((key) => { + variablesTuple[key] = variables[key].selectedValue; + }); + return variablesTuple; + } + return {}; } catch (e) { console.error(e); } From f0f93c64d21dbaf783dd3b69a3672c70ccfe0460 Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:23:00 +0300 Subject: [PATCH 21/48] feat: add supporting close the configuration popup by clicking outside (#3082) --- .../NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx index 33af3bdb3a..72a10a3f30 100644 --- a/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx +++ b/frontend/src/container/NewDashboard/DescriptionOfDashboard/SettingsDrawer.tsx @@ -26,7 +26,6 @@ function SettingsDrawer(): JSX.Element { width="70%" onClose={onClose} visible={visible} - maskClosable={false} > From ed200e50c878595633c04fe98e3908535fc09a1d Mon Sep 17 00:00:00 2001 From: dnazarenkoo <134951516+dnazarenkoo@users.noreply.github.com> Date: Mon, 10 Jul 2023 16:34:21 +0300 Subject: [PATCH 22/48] fix: disabling API call when the panel type is EMPTY (#3087) --- .../src/container/GridGraphLayout/Graph/index.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index e4f543b7d5..169582ed95 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -83,6 +83,11 @@ function GridCardGraph({ const updatedQuery = useStepInterval(widget?.query); + const isEmptyWidget = useMemo( + () => widget?.id === 'empty' || isEmpty(widget), + [widget], + ); + const queryResponse = useGetQueryRange( { selectedTime: widget?.timePreferance, @@ -101,7 +106,7 @@ function GridCardGraph({ variables, ], keepPreviousData: true, - enabled: isGraphVisible, + enabled: isGraphVisible && !isEmptyWidget, refetchOnMount: false, onError: (error) => { setErrorMessage(error.message); @@ -131,8 +136,6 @@ function GridCardGraph({ ); const onDeleteHandler = useCallback(() => { - const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget); - const widgetId = isEmptyWidget ? layout[0].i : widget?.id; featureResponse @@ -147,7 +150,8 @@ function GridCardGraph({ }); }); }, [ - widget, + isEmptyWidget, + widget?.id, layout, featureResponse, deleteWidget, From 652bd52ea7ba8408b697150d44a229f62a4efa49 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Tue, 11 Jul 2023 10:54:33 +0530 Subject: [PATCH 23/48] feat: navigation to new explorer from old page are updated (#3093) --- .../src/components/ExplorerCard/index.tsx | 27 ++++++++++ frontend/src/components/TextToolTip/styles.ts | 2 +- .../container/TopNav/NewExplorerCTA/config.ts | 3 ++ .../container/TopNav/NewExplorerCTA/index.tsx | 50 +++++++++++++++++++ frontend/src/container/TopNav/index.tsx | 13 ++++- frontend/src/pages/LogsExplorer/index.tsx | 5 +- frontend/src/pages/TracesExplorer/index.tsx | 5 +- 7 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/ExplorerCard/index.tsx create mode 100644 frontend/src/container/TopNav/NewExplorerCTA/config.ts create mode 100644 frontend/src/container/TopNav/NewExplorerCTA/index.tsx diff --git a/frontend/src/components/ExplorerCard/index.tsx b/frontend/src/components/ExplorerCard/index.tsx new file mode 100644 index 0000000000..fd8f3ceb81 --- /dev/null +++ b/frontend/src/components/ExplorerCard/index.tsx @@ -0,0 +1,27 @@ +import { Card, Space, Typography } from 'antd'; +import TextToolTip from 'components/TextToolTip'; + +function ExplorerCard({ children }: Props): JSX.Element { + return ( + + Query Builder + + + } + > + {children} + + ); +} + +interface Props { + children: React.ReactNode; +} + +export default ExplorerCard; diff --git a/frontend/src/components/TextToolTip/styles.ts b/frontend/src/components/TextToolTip/styles.ts index bb2532182d..83e318a8ef 100644 --- a/frontend/src/components/TextToolTip/styles.ts +++ b/frontend/src/components/TextToolTip/styles.ts @@ -1 +1 @@ -export const style = { fontSize: '1.3125rem' }; +export const style = { fontSize: '1rem' }; diff --git a/frontend/src/container/TopNav/NewExplorerCTA/config.ts b/frontend/src/container/TopNav/NewExplorerCTA/config.ts new file mode 100644 index 0000000000..783f576142 --- /dev/null +++ b/frontend/src/container/TopNav/NewExplorerCTA/config.ts @@ -0,0 +1,3 @@ +export const RIBBON_STYLES = { + top: '-0.75rem', +}; diff --git a/frontend/src/container/TopNav/NewExplorerCTA/index.tsx b/frontend/src/container/TopNav/NewExplorerCTA/index.tsx new file mode 100644 index 0000000000..93283b34a8 --- /dev/null +++ b/frontend/src/container/TopNav/NewExplorerCTA/index.tsx @@ -0,0 +1,50 @@ +import { CompassOutlined } from '@ant-design/icons'; +import { Badge, Button } from 'antd'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { RIBBON_STYLES } from './config'; + +function NewExplorerCTA(): JSX.Element | null { + const location = useLocation(); + + const isTraceOrLogsExplorerPage = useMemo( + () => location.pathname === ROUTES.LOGS || location.pathname === ROUTES.TRACE, + [location.pathname], + ); + + const onClickHandler = (): void => { + if (location.pathname === ROUTES.LOGS) { + history.push(ROUTES.LOGS_EXPLORER); + } else if (location.pathname === ROUTES.TRACE) { + history.push(ROUTES.TRACES_EXPLORER); + } + }; + + const buttonText = useMemo( + () => + `Try new ${ROUTES.LOGS === location.pathname ? 'Logs' : 'Traces'} Explorer`, + [location.pathname], + ); + + if (!isTraceOrLogsExplorerPage) { + return null; + } + + return ( + + + + ); +} + +export default NewExplorerCTA; diff --git a/frontend/src/container/TopNav/index.tsx b/frontend/src/container/TopNav/index.tsx index 38efc91466..a481ce77d4 100644 --- a/frontend/src/container/TopNav/index.tsx +++ b/frontend/src/container/TopNav/index.tsx @@ -1,4 +1,4 @@ -import { Col } from 'antd'; +import { Col, Row, Space } from 'antd'; import ROUTES from 'constants/routes'; import { useMemo } from 'react'; import { matchPath, useHistory } from 'react-router-dom'; @@ -6,6 +6,7 @@ import { matchPath, useHistory } from 'react-router-dom'; import ShowBreadcrumbs from './Breadcrumbs'; import DateTimeSelector from './DateTimeSelection'; import { routesToSkip } from './DateTimeSelection/config'; +import NewExplorerCTA from './NewExplorerCTA'; import { Container } from './styles'; function TopNav(): JSX.Element | null { @@ -36,7 +37,15 @@ function TopNav(): JSX.Element | null { {!isRouteToSkip && ( - + + + + +
+ +
+
+
)} diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index 3e6613ed77..229ab2f6a8 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -1,4 +1,5 @@ import { Col, Row } from 'antd'; +import ExplorerCard from 'components/ExplorerCard'; import LogExplorerQuerySection from 'container/LogExplorerQuerySection'; import LogsExplorerViews from 'container/LogsExplorerViews'; @@ -10,7 +11,9 @@ function LogsExplorer(): JSX.Element {
- + + + diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 378e09d585..f5cff0dec0 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -1,5 +1,6 @@ import { Tabs } from 'antd'; import axios from 'axios'; +import ExplorerCard from 'components/ExplorerCard'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; @@ -177,7 +178,9 @@ function TracesExplorer(): JSX.Element { return ( <> - + + + From a1a5e8bf9b538087621fe9f08d8a96f6d66d611a Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Tue, 11 Jul 2023 10:59:53 +0530 Subject: [PATCH 24/48] =?UTF-8?q?chore:=20=F0=9F=94=A7=20upgrade=20distrib?= =?UTF-8?q?uted=20locust=20to=201.2.3=20and=20updated=20related=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- deploy/README.md | 2 +- sample-apps/hotrod/hotrod-install.sh | 2 +- sample-apps/hotrod/hotrod-template.yaml | 22 +++++++++++----------- sample-apps/hotrod/hotrod.yaml | 22 +++++++++++----------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index f226c79d4c..55c3b6e8d4 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -66,7 +66,7 @@ To generate load: ```sh kubectl -n sample-application run strzal --image=djbingham/curl \ --restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \ -'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm +'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm ``` To stop load: diff --git a/sample-apps/hotrod/hotrod-install.sh b/sample-apps/hotrod/hotrod-install.sh index 197f18d76e..b7ba4f6caa 100755 --- a/sample-apps/hotrod/hotrod-install.sh +++ b/sample-apps/hotrod/hotrod-install.sh @@ -16,7 +16,7 @@ fi # Locust's docker image if [[ -z $LOCUST_IMAGE ]]; then LOCUST_REPO="${LOCUST_REPO:-signoz/locust}" - LOCUST_TAG="${LOCUST_TAG:-0.8.1-py3.6}" + LOCUST_TAG="${LOCUST_TAG:-1.2.3}" LOCUST_IMAGE="${LOCUST_REPO}:${LOCUST_TAG}" fi diff --git a/sample-apps/hotrod/hotrod-template.yaml b/sample-apps/hotrod/hotrod-template.yaml index f2d432ca4d..472cbe4d52 100644 --- a/sample-apps/hotrod/hotrod-template.yaml +++ b/sample-apps/hotrod/hotrod-template.yaml @@ -11,8 +11,10 @@ metadata: name: scripts-cm data: locustfile.py: | - from locust import HttpLocust, TaskSet, task - class UserTasks(TaskSet): + from locust import HttpUser, task, between + class UserTasks(HttpUser): + wait_time = between(5, 15) + @task def rachel(self): self.client.get("/dispatch?customer=123&nonse=0.6308392664170006") @@ -25,8 +27,6 @@ data: @task def coffee(self): self.client.get("/dispatch?customer=567&nonse=0.0022220379420636593") - class WebsiteUser(HttpLocust): - task_set = UserTasks --- apiVersion: apps/v1 kind: Deployment @@ -155,13 +155,13 @@ metadata: annotations: deployment.kubernetes.io/revision: "1" labels: - role: locust-slave - name: locust-slave + role: locust-worker + name: locust-worker spec: replicas: 1 selector: matchLabels: - role: locust-slave + role: locust-worker strategy: rollingUpdate: maxSurge: 1 @@ -170,12 +170,12 @@ spec: template: metadata: labels: - role: locust-slave + role: locust-worker spec: containers: - image: ${LOCUST_IMAGE} imagePullPolicy: IfNotPresent - name: locust-slave + name: locust-worker env: - name: ATTACKED_HOST valueFrom: @@ -183,8 +183,8 @@ spec: name: locust-cm key: ATTACKED_HOST - name: LOCUST_MODE - value: SLAVE - - name: LOCUST_MASTER + value: WORKER + - name: LOCUST_MASTER_HOST value: locust-master volumeMounts: - mountPath: /locust diff --git a/sample-apps/hotrod/hotrod.yaml b/sample-apps/hotrod/hotrod.yaml index 0793ec3d95..2dbbaec654 100644 --- a/sample-apps/hotrod/hotrod.yaml +++ b/sample-apps/hotrod/hotrod.yaml @@ -11,8 +11,10 @@ metadata: name: scripts-cm data: locustfile.py: | - from locust import HttpLocust, TaskSet, task - class UserTasks(TaskSet): + from locust import HttpUser, task, between + class UserTasks(HttpUser): + wait_time = between(5, 15) + @task def rachel(self): self.client.get("/dispatch?customer=123&nonse=0.6308392664170006") @@ -25,8 +27,6 @@ data: @task def coffee(self): self.client.get("/dispatch?customer=567&nonse=0.0022220379420636593") - class WebsiteUser(HttpLocust): - task_set = UserTasks --- apiVersion: apps/v1 kind: Deployment @@ -155,13 +155,13 @@ metadata: annotations: deployment.kubernetes.io/revision: "1" labels: - role: locust-slave - name: locust-slave + role: locust-worker + name: locust-worker spec: replicas: 1 selector: matchLabels: - role: locust-slave + role: locust-worker strategy: rollingUpdate: maxSurge: 1 @@ -170,12 +170,12 @@ spec: template: metadata: labels: - role: locust-slave + role: locust-worker spec: containers: - image: signoz/locust:1.2.3 imagePullPolicy: IfNotPresent - name: locust-slave + name: locust-worker env: - name: ATTACKED_HOST valueFrom: @@ -183,8 +183,8 @@ spec: name: locust-cm key: ATTACKED_HOST - name: LOCUST_MODE - value: SLAVE - - name: LOCUST_MASTER + value: WORKER + - name: LOCUST_MASTER_HOST value: locust-master volumeMounts: - mountPath: /locust From 44416df5dcc6e12e9a0ae1752ca6f6820e05dc53 Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Tue, 11 Jul 2023 12:30:49 +0530 Subject: [PATCH 25/48] =?UTF-8?q?chore(hotrod):=20=F0=9F=94=A7=20update=20?= =?UTF-8?q?README=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Prashant Shahi --- sample-apps/hotrod/README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/sample-apps/hotrod/README.md b/sample-apps/hotrod/README.md index 1907c23dfc..b9638281a0 100644 --- a/sample-apps/hotrod/README.md +++ b/sample-apps/hotrod/README.md @@ -5,7 +5,7 @@ Follow the steps in this section to install a sample application named HotR.O.D, ```console kubectl create ns sample-application -kubectl -n sample-application apply -f https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod.yaml +kubectl -n sample-application apply -f https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod.yaml ``` In case, you have installed SigNoz in namespace other than `platform` or selected Helm release name other than `my-release`, follow the steps below: @@ -15,7 +15,7 @@ export HELM_RELEASE=my-release-2 export SIGNOZ_NAMESPACE=platform-2 export HOTROD_NAMESPACE=sample-application-2 -curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh | bash +curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh | bash ``` To delete sample application: @@ -23,5 +23,15 @@ To delete sample application: ```console export HOTROD_NAMESPACE=sample-application-2 -curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh | bash +curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh | bash +``` + +For testing with local scripts, you can use the following commands: + +```console +# To install hotrod +cat hotrod-install.sh | bash + +# To delete hotrod +cat hotrod-delete.sh | bash ``` From cc62d2cf71291d84a19da9ad1d2461909c35eb8d Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:24:10 +0300 Subject: [PATCH 26/48] feat: add autofill autocomplete data from response (#3092) Co-authored-by: Palash Gupta --- frontend/src/constants/queryBuilder.ts | 6 + frontend/src/constants/regExp.ts | 2 + .../AggregatorFilter/AggregatorFilter.tsx | 112 ++++++++++++------ .../chooseAutocompleteFromCustomValue.ts | 19 +++ .../getAutocompleteValueAndType.ts | 17 +++ 5 files changed, 118 insertions(+), 38 deletions(-) create mode 100644 frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts create mode 100644 frontend/src/lib/newQueryBuilder/getAutocompleteValueAndType.ts diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 8db0f7d297..26cc32d3c4 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -3,6 +3,7 @@ import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { + AutocompleteType, BaseAutocompleteData, LocalDataType, } from 'types/api/queryBuilder/queryAutocompleteResponse'; @@ -50,6 +51,11 @@ export const baseAutoCompleteIdKeysOrder: (keyof Omit< 'id' >)[] = ['key', 'dataType', 'type', 'isColumn']; +export const autocompleteType: Record = { + resource: 'resource', + tag: 'tag', +}; + export const formulasNames: string[] = Array.from( Array(MAX_FORMULAS), (_, i) => `F${i + 1}`, diff --git a/frontend/src/constants/regExp.ts b/frontend/src/constants/regExp.ts index 3ea1e3179c..40de740221 100644 --- a/frontend/src/constants/regExp.ts +++ b/frontend/src/constants/regExp.ts @@ -3,3 +3,5 @@ export const FORMULA_REGEXP = /F\d+/; export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/; export const TYPE_ADDON_REGEXP = /_(.+)/; + +export const SPLIT_FIRST_UNDERSCORE = /(?([]); - const debouncedValue = useDebounce(query.aggregateAttribute.key, 300); + const [searchText, setSearchText] = useState( + query.aggregateAttribute.key, + ); + + const handleChangeAttribute = useCallback( + (data: BaseAutocompleteData[]) => { + const attribute = chooseAutocompleteFromCustomValue(data, [searchText]); + + onChange(attribute[0]); + }, + [onChange, searchText], + ); + + const debouncedSearchText = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars + const [_, value] = getAutocompleteValueAndType(searchText); + + return value; + }, [searchText]); + + const debouncedValue = useDebounce(debouncedSearchText, 300); const { isFetching } = useQuery( [ QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, @@ -64,62 +86,76 @@ export const AggregatorFilter = memo(function AggregatorFilter({ key: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), })) || []; + handleChangeAttribute(data.payload?.attributeKeys || []); + setOptionsData(options); }, }, ); - const handleChangeAttribute = useCallback( - ( - value: string, - option: ExtendedSelectOption | ExtendedSelectOption[], - ): void => { - const currentOption = option as ExtendedSelectOption; - - if (currentOption.key) { - const [key, dataType, type, isColumn] = currentOption.key.split(idDivider); - const attribute: BaseAutocompleteData = { - key, - dataType: dataType as DataType, - type: type as AutocompleteType, - isColumn: isColumn === 'true', - }; - - onChange(attribute); - } else { - const attribute = { ...initialAutocompleteData, key: value }; - - onChange(attribute); - } - }, - [onChange], - ); - - const value = useMemo( - () => - transformStringWithPrefix({ - str: query.aggregateAttribute.key, - prefix: query.aggregateAttribute.type || '', - condition: !query.aggregateAttribute.isColumn, - }), - [query], - ); + const handleSearchText = useCallback((text: string): void => { + setSearchText(text); + }, []); const placeholder: string = query.dataSource === DataSource.METRICS ? `${transformToUpperCase(query.dataSource)} name` : 'Aggregate attribute'; + const handleSelect = ( + value: string, + option: ExtendedSelectOption | ExtendedSelectOption[], + ): void => { + const currentOption = option as ExtendedSelectOption; + + if (currentOption.key) { + const [key, dataType, type, isColumn] = currentOption.key.split(idDivider); + const attribute: BaseAutocompleteData = { + key, + dataType: dataType as DataType, + type: type as AutocompleteType, + isColumn: isColumn === 'true', + }; + + const text = transformStringWithPrefix({ + str: attribute.key, + prefix: attribute.type || '', + condition: !attribute.isColumn, + }); + + setSearchText(text); + + onChange(attribute); + } else { + const customAttribute: BaseAutocompleteData = { + ...initialAutocompleteData, + key: value, + }; + + const text = transformStringWithPrefix({ + str: customAttribute.key, + prefix: customAttribute.type || '', + condition: !customAttribute.isColumn, + }); + + setSearchText(text); + + onChange(customAttribute); + } + }; + return ( : null} options={optionsData} - value={value} - onChange={handleChangeAttribute} + value={searchText} + onSelect={handleSelect} disabled={disabled} /> ); diff --git a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts new file mode 100644 index 0000000000..1834822c6d --- /dev/null +++ b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts @@ -0,0 +1,19 @@ +import { initialAutocompleteData } from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +export const chooseAutocompleteFromCustomValue = ( + sourceList: BaseAutocompleteData[], + values: string[], +): BaseAutocompleteData[] => { + console.log({ sourceList }); + return values.map((value) => { + const firstBaseAutoCompleteValue = sourceList.find( + (sourceAutoComplete) => value === sourceAutoComplete.key, + ); + + if (!firstBaseAutoCompleteValue) + return { ...initialAutocompleteData, key: value }; + + return firstBaseAutoCompleteValue; + }); +}; diff --git a/frontend/src/lib/newQueryBuilder/getAutocompleteValueAndType.ts b/frontend/src/lib/newQueryBuilder/getAutocompleteValueAndType.ts new file mode 100644 index 0000000000..bbc7c8fcfe --- /dev/null +++ b/frontend/src/lib/newQueryBuilder/getAutocompleteValueAndType.ts @@ -0,0 +1,17 @@ +import { autocompleteType } from 'constants/queryBuilder'; +import { SPLIT_FIRST_UNDERSCORE } from 'constants/regExp'; + +export const getAutocompleteValueAndType = (str: string): [string, string] => { + if (str === '') return [str, str]; + + const [firstValue, secondValue] = str.split(SPLIT_FIRST_UNDERSCORE); + + if ( + firstValue === autocompleteType.tag || + firstValue === autocompleteType.resource + ) { + return [firstValue, secondValue]; + } + + return ['', firstValue]; +}; From c51a15f1e890d844343b67a017a6ab74eca93cff Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Tue, 11 Jul 2023 15:45:47 +0530 Subject: [PATCH 27/48] fix: proper formatting for noop operator in logs (#3086) Co-authored-by: Vishal Sharma --- .../app/logs/v3/query_builder.go | 2 +- .../app/logs/v3/query_builder_test.go | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index 1e49250865..baf2a6c193 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -244,7 +244,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorNoOp: - queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s %sorder by %s" + queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s%s order by %s" query := fmt.Sprintf(queryTmpl, timeFilter, filterSubQuery, orderBy) return query, nil default: diff --git a/pkg/query-service/app/logs/v3/query_builder_test.go b/pkg/query-service/app/logs/v3/query_builder_test.go index 2712b8a874..8fab8653e2 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -617,13 +617,33 @@ var testBuildLogsQueryData = []struct { AggregateOperator: v3.AggregateOperatorNoOp, Expression: "A", Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, - OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC", IsColumn: true}}, + OrderBy: []v3.OrderBy{{ColumnName: "method", DataType: v3.AttributeKeyDataTypeString, Order: "ASC", IsColumn: true}}, }, ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," + "CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,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 " + "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by method ASC", }, + { + Name: "Test Noop with filter", + PanelType: v3.PanelTypeList, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + SelectColumns: []v3.AttributeKey{}, + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "severity_number", DataType: v3.AttributeKeyDataTypeInt64, IsColumn: true}, Operator: "!=", Value: 0}, + }}, + }, + ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as attributes_string," + + "CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,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 " + + "from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND severity_number != 0 order by timestamp", + }, { Name: "Test aggregate with having clause", PanelType: v3.PanelTypeGraph, From 54899930b0c56d7003496777336dbdd71e832238 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Tue, 11 Jul 2023 16:07:26 +0530 Subject: [PATCH 28/48] feat: search capability is added on export panel (#3083) * feat: search capablity is added on export panel * chore: instance is destroyed on close --------- Co-authored-by: Vishal Sharma --- .../src/container/ExportPanel/ExportPanel.tsx | 22 +++++++++---------- frontend/src/container/ExportPanel/index.tsx | 1 + frontend/src/container/ExportPanel/utils.ts | 8 +++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/frontend/src/container/ExportPanel/ExportPanel.tsx b/frontend/src/container/ExportPanel/ExportPanel.tsx index 80092c75dc..24e9ed210e 100644 --- a/frontend/src/container/ExportPanel/ExportPanel.tsx +++ b/frontend/src/container/ExportPanel/ExportPanel.tsx @@ -1,8 +1,7 @@ import { Button, Typography } from 'antd'; import createDashboard from 'api/dashboard/create'; -import axios from 'axios'; import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard'; -import { useNotifications } from 'hooks/useNotifications'; +import useAxiosError from 'hooks/useAxiosError'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; @@ -15,10 +14,9 @@ import { Title, Wrapper, } from './styles'; -import { getSelectOptions } from './utils'; +import { filterOptions, getSelectOptions } from './utils'; function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element { - const { notifications } = useNotifications(); const { t } = useTranslation(['dashboard']); const [selectedDashboardId, setSelectedDashboardId] = useState( @@ -31,21 +29,19 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element { refetch, } = useGetAllDashboard(); + const handleError = useAxiosError(); + const { mutate: createNewDashboard, isLoading: createDashboardLoading, } = useMutation(createDashboard, { onSuccess: (data) => { - onExport(data?.payload || null); + if (data.payload) { + onExport(data?.payload); + } refetch(); }, - onError: (error) => { - if (axios.isAxiosError(error)) { - notifications.error({ - message: error.message, - }); - } - }, + onError: handleError, }); const options = useMemo(() => getSelectOptions(data?.payload || []), [data]); @@ -90,10 +86,12 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element { - + + + + + + void; From c6568031627134432260ab18504dfae748f6f320 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:53:15 +0300 Subject: [PATCH 30/48] feat: update where clause based on log field (#3103) * feat: update where clause based on log field * chore: small changes are updated --------- Co-authored-by: Palash Gupta --- .../LogDetail/LogDetail.interfaces.ts | 6 +- frontend/src/components/LogDetail/index.tsx | 8 ++- .../src/components/Logs/AddToQueryHOC.tsx | 31 ++-------- .../src/components/Logs/ListLogView/index.tsx | 36 ++++++++++- frontend/src/constants/queryBuilder.ts | 1 - .../container/LogDetailedView/TableView.tsx | 15 ++++- .../src/container/LogDetailedView/index.tsx | 39 ++++++++++-- .../LogExplorerDetailedView/index.tsx | 61 +++++++++++++++++-- .../queryBuilder/useFetchKeysAndValues.ts | 32 +++++----- 9 files changed, 170 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 8e4ed857f0..2e1597f905 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -1,3 +1,7 @@ +import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ILog } from 'types/api/logs/log'; -export type LogDetailProps = { log: ILog | null; onClose: () => void }; +export type LogDetailProps = { + log: ILog | null; + onClose: () => void; +} & Pick; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 054a9c3161..6fb43b1218 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -4,7 +4,11 @@ import TableView from 'container/LogDetailedView/TableView'; import { LogDetailProps } from './LogDetail.interfaces'; -function LogDetail({ log, onClose }: LogDetailProps): JSX.Element { +function LogDetail({ + log, + onClose, + onAddToQuery, +}: LogDetailProps): JSX.Element { const onDrawerClose = (): void => { onClose(); }; @@ -13,7 +17,7 @@ function LogDetail({ log, onClose }: LogDetailProps): JSX.Element { { label: 'Table', key: '1', - children: log && , + children: log && , }, { label: 'JSON', diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index 874a9a0ec7..d4d1f9bd31 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,39 +1,17 @@ import { Popover } from 'antd'; -import ROUTES from 'constants/routes'; -import history from 'lib/history'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; import { memo, ReactNode, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { ILogsReducer } from 'types/reducer/logs'; import { ButtonContainer } from './styles'; function AddToQueryHOC({ fieldKey, fieldValue, + onAddToQuery, children, }: AddToQueryHOCProps): JSX.Element { - const { - searchFilter: { queryString }, - } = useSelector((store) => store.logs); - - const generatedQuery = useMemo( - () => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }), - [fieldKey, fieldValue], - ); - const handleQueryAdd = useCallback(() => { - let updatedQueryString = queryString || ''; - - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } - - history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); - }, [generatedQuery, queryString]); + onAddToQuery(fieldKey, fieldValue); + }, [fieldKey, fieldValue, onAddToQuery]); const popOverContent = useMemo(() => Add to query: {fieldKey}, [ fieldKey, @@ -48,9 +26,10 @@ function AddToQueryHOC({ ); } -interface AddToQueryHOCProps { +export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; + onAddToQuery: (fieldKey: string, fieldValue: string) => void; children: ReactNode; } diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index a4f1e9cc9c..ea4bada737 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -2,16 +2,22 @@ import { blue, grey, orange } from '@ant-design/colors'; import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons'; import Convert from 'ansi-to-html'; import { Button, Divider, Row, Typography } from 'antd'; +import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useNotifications } from 'hooks/useNotifications'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; +import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; import { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; +import { AppState } from 'store/reducers'; // interfaces import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; +import { ILogsReducer } from 'types/reducer/logs'; // components import AddToQueryHOC from '../AddToQueryHOC'; @@ -57,9 +63,37 @@ function LogSelectedField({ fieldKey = '', fieldValue = '', }: LogFieldProps): JSX.Element { + const history = useHistory(); + const { + searchFilter: { queryString }, + } = useSelector((state) => state.logs); + + const handleQueryAdd = useCallback( + (fieldKey: string, fieldValue: string) => { + const generatedQuery = generateFilterQuery({ + fieldKey, + fieldValue, + type: 'IN', + }); + + let updatedQueryString = queryString || ''; + if (updatedQueryString.length === 0) { + updatedQueryString += `${generatedQuery}`; + } else { + updatedQueryString += ` AND ${generatedQuery}`; + } + history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); + }, + [history, queryString], + ); + return ( - + {fieldKey} diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 26cc32d3c4..9ff71e1712 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -66,7 +66,6 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str)); export enum QueryBuilderKeys { GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE', GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS', - GET_ATTRIBUTE_KEY = 'GET_ATTRIBUTE_KEY', } export const mapOfOperators = { diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index 7d4db12acc..b027c0e523 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -3,7 +3,9 @@ import { LinkOutlined } from '@ant-design/icons'; import { Input, Space, Tooltip } from 'antd'; import { ColumnsType } from 'antd/es/table'; import Editor from 'components/Editor'; -import AddToQueryHOC from 'components/Logs/AddToQueryHOC'; +import AddToQueryHOC, { + AddToQueryHOCProps, +} from 'components/Logs/AddToQueryHOC'; import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC'; import { ResizeTable } from 'components/ResizeTable'; import ROUTES from 'constants/routes'; @@ -27,7 +29,10 @@ const RESTRICTED_FIELDS = ['timestamp']; interface TableViewProps { logData: ILog; } -function TableView({ logData }: TableViewProps): JSX.Element | null { + +type Props = TableViewProps & Pick; + +function TableView({ logData, onAddToQuery }: Props): JSX.Element | null { const [fieldSearchInput, setFieldSearchInput] = useState(''); const dispatch = useDispatch>(); @@ -128,7 +133,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null { if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { return ( - + {renderedField} ); diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 3860d657f0..25ccf8c50c 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -1,5 +1,9 @@ import LogDetail from 'components/LogDetail'; +import ROUTES from 'constants/routes'; +import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; +import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; @@ -7,9 +11,11 @@ import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILogsReducer } from 'types/reducer/logs'; function LogDetailedView(): JSX.Element { - const { detailedLog } = useSelector( - (state) => state.logs, - ); + const history = useHistory(); + const { + detailedLog, + searchFilter: { queryString }, + } = useSelector((state) => state.logs); const dispatch = useDispatch>(); @@ -20,7 +26,32 @@ function LogDetailedView(): JSX.Element { }); }; - return ; + const handleQueryAdd = useCallback( + (fieldKey: string, fieldValue: string) => { + const generatedQuery = generateFilterQuery({ + fieldKey, + fieldValue, + type: 'IN', + }); + + let updatedQueryString = queryString || ''; + if (updatedQueryString.length === 0) { + updatedQueryString += `${generatedQuery}`; + } else { + updatedQueryString += ` AND ${generatedQuery}`; + } + history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); + }, + [history, queryString], + ); + + return ( + + ); } export default LogDetailedView; diff --git a/frontend/src/container/LogExplorerDetailedView/index.tsx b/frontend/src/container/LogExplorerDetailedView/index.tsx index 1a573f2ec8..26bb067b88 100644 --- a/frontend/src/container/LogExplorerDetailedView/index.tsx +++ b/frontend/src/container/LogExplorerDetailedView/index.tsx @@ -1,4 +1,16 @@ import LogDetail from 'components/LogDetail'; +import { QueryBuilderKeys } from 'constants/queryBuilder'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; +import { useCallback } from 'react'; +import { useQueryClient } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { + BaseAutocompleteData, + IQueryAutocompleteResponse, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; import { LogExplorerDetailedViewProps } from './LogExplorerDetailedView.interfaces'; @@ -6,11 +18,52 @@ function LogExplorerDetailedView({ log, onClose, }: LogExplorerDetailedViewProps): JSX.Element { - const onDrawerClose = (): void => { - onClose(); - }; + const queryClient = useQueryClient(); + const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder(); - return ; + const handleAddQuery = useCallback( + (fieldKey: string, fieldValue: string): void => { + const keysAutocomplete: BaseAutocompleteData[] = + queryClient.getQueryData>( + [QueryBuilderKeys.GET_AGGREGATE_KEYS], + { exact: false }, + )?.payload.attributeKeys || []; + + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + [fieldKey], + )[0]; + + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + filters: { + ...item.filters, + items: [ + ...item.filters.items.filter( + (item) => item.key?.id !== existAutocompleteKey.id, + ), + { + id: uuid(), + key: existAutocompleteKey, + op: '=', + value: fieldValue, + }, + ], + }, + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + }, + [currentQuery, queryClient, redirectWithQueryBuilderData], + ); + + return ; } export default LogExplorerDetailedView; diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index 53651fddb9..b616398de4 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -6,8 +6,8 @@ import { getTagToken, isInNInOperator, } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; +import useDebounceValue from 'hooks/useDebounce'; import { isEqual, uniqWith } from 'lodash-es'; -import debounce from 'lodash-es/debounce'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useQuery } from 'react-query'; import { useDebounce } from 'react-use'; @@ -39,25 +39,23 @@ export const useFetchKeysAndValues = ( const [sourceKeys, setSourceKeys] = useState([]); const [results, setResults] = useState([]); - const searchParams = useMemo( - () => - debounce( - () => [ - searchKey, - query.dataSource, - query.aggregateOperator, - query.aggregateAttribute.key, - ], - 300, - ), - [ - query.aggregateAttribute.key, - query.aggregateOperator, - query.dataSource, + const memoizedSearchParams = useMemo( + () => [ searchKey, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, + ], + [ + searchKey, + query.dataSource, + query.aggregateOperator, + query.aggregateAttribute.key, ], ); + const searchParams = useDebounceValue(memoizedSearchParams, 300); + const isQueryEnabled = useMemo( () => query.dataSource === DataSource.METRICS @@ -73,7 +71,7 @@ export const useFetchKeysAndValues = ( ); const { data, isFetching, status } = useQuery( - [QueryBuilderKeys.GET_ATTRIBUTE_KEY, searchParams()], + [QueryBuilderKeys.GET_AGGREGATE_KEYS, searchParams], async () => getAggregateKeys({ searchText: searchKey, From 1d1ddbef40190d0a57df322ff0e2d73b69132a33 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Tue, 11 Jul 2023 19:38:09 +0530 Subject: [PATCH 31/48] chore: link is updated (#3105) --- frontend/src/container/LogsExplorerViews/index.tsx | 2 +- frontend/src/pages/TracesExplorer/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 082365d347..afbca2b470 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -272,7 +272,7 @@ function LogsExplorerViews(): JSX.Element { Panel limit exceeded for {DataSource.LOGS} in community edition. Please checkout our paid plans{' '} diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index f5cff0dec0..7252927670 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -105,7 +105,7 @@ function TracesExplorer(): JSX.Element { Panel limit exceeded for {DataSource.TRACES} in community edition. Please checkout our paid plans{' '} From a6e6c171c352815fbef6dd53518f391b6b8ce5cc Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Tue, 11 Jul 2023 23:02:10 +0530 Subject: [PATCH 32/48] fix: autcomplete top level keys (#3104) --- pkg/query-service/app/clickhouseReader/reader.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 7185581ab7..f30db29af6 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3958,7 +3958,10 @@ func (r *ClickHouseReader) GetLogAttributeValues(ctx context.Context, req *v3.Fi // if dataType or tagType is not present return empty response if len(req.FilterAttributeKeyDataType) == 0 || len(req.TagType) == 0 || req.FilterAttributeKey == "body" { - return &v3.FilterAttributeValueResponse{}, nil + // also check if it is not a top level key + if _, ok := constants.StaticFieldsLogsV3[req.FilterAttributeKey]; !ok { + return &v3.FilterAttributeValueResponse{}, nil + } } // if data type is bool, return true and false From 149fdebfaaf0a4682ec4db5ed7b6d28c6d6e3710 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 12 Jul 2023 01:14:52 +0300 Subject: [PATCH 33/48] feat: choose autocomplete group by (#3108) --- .../src/constants/queryBuilderFilterConfig.ts | 1 + .../LogExplorerDetailedView/index.tsx | 4 +- .../AggregatorFilter/AggregatorFilter.tsx | 126 +++++++++--------- .../filters/GroupByFilter/GroupByFilter.tsx | 41 +++--- .../queryBuilder/useFetchKeysAndValues.ts | 3 +- .../chooseAutocompleteFromCustomValue.ts | 19 ++- 6 files changed, 103 insertions(+), 91 deletions(-) create mode 100644 frontend/src/constants/queryBuilderFilterConfig.ts diff --git a/frontend/src/constants/queryBuilderFilterConfig.ts b/frontend/src/constants/queryBuilderFilterConfig.ts new file mode 100644 index 0000000000..42e092ddee --- /dev/null +++ b/frontend/src/constants/queryBuilderFilterConfig.ts @@ -0,0 +1 @@ +export const DEBOUNCE_DELAY = 200; diff --git a/frontend/src/container/LogExplorerDetailedView/index.tsx b/frontend/src/container/LogExplorerDetailedView/index.tsx index 26bb067b88..3bbd7481f8 100644 --- a/frontend/src/container/LogExplorerDetailedView/index.tsx +++ b/frontend/src/container/LogExplorerDetailedView/index.tsx @@ -31,8 +31,8 @@ function LogExplorerDetailedView({ const existAutocompleteKey = chooseAutocompleteFromCustomValue( keysAutocomplete, - [fieldKey], - )[0]; + fieldKey, + ); const nextQuery: Query = { ...currentQuery, diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index dda50c7932..8905ba9f55 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -4,22 +4,21 @@ import { AutoComplete, Spin } from 'antd'; import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute'; import { baseAutoCompleteIdKeysOrder, - idDivider, - initialAutocompleteData, QueryBuilderKeys, selectValueDivider, } from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import useDebounce from 'hooks/useDebounce'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType'; import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { memo, useCallback, useMemo, useState } from 'react'; -import { useQuery } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; +import { SuccessResponse } from 'types/api'; import { - AutocompleteType, BaseAutocompleteData, - DataType, + IQueryAutocompleteResponse, } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; import { ExtendedSelectOption } from 'types/common/select'; @@ -34,19 +33,9 @@ export const AggregatorFilter = memo(function AggregatorFilter({ disabled, onChange, }: AgregatorFilterProps): JSX.Element { + const queryClient = useQueryClient(); const [optionsData, setOptionsData] = useState([]); - const [searchText, setSearchText] = useState( - query.aggregateAttribute.key, - ); - - const handleChangeAttribute = useCallback( - (data: BaseAutocompleteData[]) => { - const attribute = chooseAutocompleteFromCustomValue(data, [searchText]); - - onChange(attribute[0]); - }, - [onChange, searchText], - ); + const [searchText, setSearchText] = useState(''); const debouncedSearchText = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars @@ -55,7 +44,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({ return value; }, [searchText]); - const debouncedValue = useDebounce(debouncedSearchText, 300); + const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY); const { isFetching } = useQuery( [ QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, @@ -86,8 +75,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({ key: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), })) || []; - handleChangeAttribute(data.payload?.attributeKeys || []); - setOptionsData(options); }, }, @@ -102,60 +89,79 @@ export const AggregatorFilter = memo(function AggregatorFilter({ ? `${transformToUpperCase(query.dataSource)} name` : 'Aggregate attribute'; - const handleSelect = ( - value: string, - option: ExtendedSelectOption | ExtendedSelectOption[], - ): void => { - const currentOption = option as ExtendedSelectOption; + const getAttributes = useCallback( + (): BaseAutocompleteData[] => + queryClient.getQueryData>( + [QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE], + { exact: false }, + )?.payload.attributeKeys || [], + [queryClient], + ); - if (currentOption.key) { - const [key, dataType, type, isColumn] = currentOption.key.split(idDivider); - const attribute: BaseAutocompleteData = { - key, - dataType: dataType as DataType, - type: type as AutocompleteType, - isColumn: isColumn === 'true', - }; + const handleChangeCustomValue = useCallback( + (value: string) => { + const aggregateAttributes = getAttributes(); - const text = transformStringWithPrefix({ - str: attribute.key, - prefix: attribute.type || '', - condition: !attribute.isColumn, - }); - - setSearchText(text); - - onChange(attribute); - } else { - const customAttribute: BaseAutocompleteData = { - ...initialAutocompleteData, - key: value, - }; - - const text = transformStringWithPrefix({ - str: customAttribute.key, - prefix: customAttribute.type || '', - condition: !customAttribute.isColumn, - }); - - setSearchText(text); + const customAttribute: BaseAutocompleteData = chooseAutocompleteFromCustomValue( + aggregateAttributes, + value, + ); onChange(customAttribute); + }, + [getAttributes, onChange], + ); + + const handleBlur = useCallback(() => { + if (searchText) { + handleChangeCustomValue(searchText); } - }; + }, [handleChangeCustomValue, searchText]); + + const handleChange = useCallback( + ( + value: string, + option: ExtendedSelectOption | ExtendedSelectOption[], + ): void => { + const currentOption = option as ExtendedSelectOption; + + const aggregateAttributes = getAttributes(); + + if (currentOption.key) { + const attribute = aggregateAttributes.find( + (item) => item.id === currentOption.key, + ); + + if (attribute) { + onChange(attribute); + } + } else { + handleChangeCustomValue(value); + } + + setSearchText(''); + }, + [getAttributes, handleChangeCustomValue, onChange], + ); + + const value = transformStringWithPrefix({ + str: query.aggregateAttribute.key, + prefix: query.aggregateAttribute.type || '', + condition: !query.aggregateAttribute.isColumn, + }); return ( : null} options={optionsData} - value={searchText} - onSelect={handleSelect} + value={value} + onBlur={handleBlur} + onChange={handleChange} disabled={disabled} /> ); diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx index 62bd165ffb..622b87bf7c 100644 --- a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx @@ -3,21 +3,22 @@ import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; // ** Constants import { idDivider, - initialAutocompleteData, QueryBuilderKeys, selectValueDivider, } from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import useDebounce from 'hooks/useDebounce'; +import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; // ** Components // ** Helpers import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { isEqual, uniqWith } from 'lodash-es'; import { memo, useCallback, useEffect, useState } from 'react'; -import { useQuery } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; +import { SuccessResponse } from 'types/api'; import { - AutocompleteType, BaseAutocompleteData, - DataType, + IQueryAutocompleteResponse, } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { SelectOption } from 'types/common/select'; @@ -29,6 +30,7 @@ export const GroupByFilter = memo(function GroupByFilter({ onChange, disabled, }: GroupByFilterProps): JSX.Element { + const queryClient = useQueryClient(); const [searchText, setSearchText] = useState(''); const [optionsData, setOptionsData] = useState[]>( [], @@ -38,7 +40,7 @@ export const GroupByFilter = memo(function GroupByFilter({ ); const [isFocused, setIsFocused] = useState(false); - const debouncedValue = useDebounce(searchText, 300); + const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY); const { isFetching } = useQuery( [QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused], @@ -81,6 +83,15 @@ export const GroupByFilter = memo(function GroupByFilter({ }, ); + const getAttributeKeys = useCallback( + (): BaseAutocompleteData[] => + queryClient.getQueryData>( + [QueryBuilderKeys.GET_AGGREGATE_KEYS], + { exact: false }, + )?.payload.attributeKeys || [], + [queryClient], + ); + const handleSearchKeys = (searchText: string): void => { setSearchText(searchText); }; @@ -97,21 +108,17 @@ export const GroupByFilter = memo(function GroupByFilter({ const handleChange = (values: SelectOption[]): void => { const groupByValues: BaseAutocompleteData[] = values.map((item) => { const [currentValue, id] = item.value.split(selectValueDivider); - if (id && id.includes(idDivider)) { - const [key, dataType, type, isColumn] = id.split(idDivider); + const keys = getAttributeKeys(); - return { - id, - key: key || currentValue, - dataType: (dataType as DataType) || initialAutocompleteData.dataType, - type: (type as AutocompleteType) || initialAutocompleteData.type, - isColumn: isColumn - ? isColumn === 'true' - : initialAutocompleteData.isColumn, - }; + if (id && id.includes(idDivider)) { + const attribute = keys.find((item) => item.id === id); + + if (attribute) { + return attribute; + } } - return { ...initialAutocompleteData, key: currentValue }; + return chooseAutocompleteFromCustomValue(keys, currentValue); }); const result = uniqWith(groupByValues, isEqual); diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index b616398de4..514bbc8617 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -1,6 +1,7 @@ import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { getAttributesValues } from 'api/queryBuilder/getAttributesValues'; import { QueryBuilderKeys } from 'constants/queryBuilder'; +import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig'; import { getRemovePrefixFromKey, getTagToken, @@ -54,7 +55,7 @@ export const useFetchKeysAndValues = ( ], ); - const searchParams = useDebounceValue(memoizedSearchParams, 300); + const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY); const isQueryEnabled = useMemo( () => diff --git a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts index 1834822c6d..ccd69df45f 100644 --- a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts +++ b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts @@ -3,17 +3,14 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe export const chooseAutocompleteFromCustomValue = ( sourceList: BaseAutocompleteData[], - values: string[], -): BaseAutocompleteData[] => { - console.log({ sourceList }); - return values.map((value) => { - const firstBaseAutoCompleteValue = sourceList.find( - (sourceAutoComplete) => value === sourceAutoComplete.key, - ); + value: string, +): BaseAutocompleteData => { + const firstBaseAutoCompleteValue = sourceList.find( + (sourceAutoComplete) => value === sourceAutoComplete.key, + ); - if (!firstBaseAutoCompleteValue) - return { ...initialAutocompleteData, key: value }; + if (!firstBaseAutoCompleteValue) + return { ...initialAutocompleteData, key: value }; - return firstBaseAutoCompleteValue; - }); + return firstBaseAutoCompleteValue; }; From 39c6410bbe7510a096d6206615dafd38e5fa7e1e Mon Sep 17 00:00:00 2001 From: Vishal Sharma Date: Wed, 12 Jul 2023 15:34:21 +0530 Subject: [PATCH 34/48] feat: add support for table view in traces (#3047) * feat: add support for table view in traces * fix: alignment issue * feat: handle table view in traces * fix: value type panel * fix: order by in table view * chore: remove obsolete code * fix: use now() as ts in query to support formula * test: update tests --- .../app/traces/v3/query_builder.go | 65 +++++++++------ .../app/traces/v3/query_builder_test.go | 79 ++++++++++++++++++- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/pkg/query-service/app/traces/v3/query_builder.go b/pkg/query-service/app/traces/v3/query_builder.go index 49572241d9..e8142eeead 100644 --- a/pkg/query-service/app/traces/v3/query_builder.go +++ b/pkg/query-service/app/traces/v3/query_builder.go @@ -102,7 +102,7 @@ func getSelectLabels(aggregatorOperator v3.AggregateOperator, groupBy []v3.Attri } else { for _, tag := range groupBy { filterName := getColumnName(tag, keys) - selectLabels += fmt.Sprintf(", %s as `%s`", filterName, tag.Key) + selectLabels += fmt.Sprintf(" %s as `%s`,", filterName, tag.Key) } } return selectLabels @@ -235,14 +235,26 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str having = " having " + having } - // Select the aggregate value for interval - queryTmpl := - "SELECT toStartOfInterval(timestamp, INTERVAL %d SECOND) AS ts" + selectLabels + - ", %s as value " + - "from " + constants.SIGNOZ_TRACE_DBNAME + "." + constants.SIGNOZ_SPAN_INDEX_TABLENAME + - " where " + spanIndexTableTimeFilter + "%s " + - "group by %s%s " + - "order by %s" + var queryTmpl string + + if panelType == v3.PanelTypeTable { + queryTmpl = + "SELECT now() as ts," + selectLabels + + " %s as value " + + "from " + constants.SIGNOZ_TRACE_DBNAME + "." + constants.SIGNOZ_SPAN_INDEX_TABLENAME + + " where " + spanIndexTableTimeFilter + "%s" + + "%s%s" + + "%s" + } else if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { + // Select the aggregate value for interval + queryTmpl = + fmt.Sprintf("SELECT toStartOfInterval(timestamp, INTERVAL %d SECOND) AS ts,", step) + selectLabels + + " %s as value " + + "from " + constants.SIGNOZ_TRACE_DBNAME + "." + constants.SIGNOZ_SPAN_INDEX_TABLENAME + + " where " + spanIndexTableTimeFilter + "%s" + + "%s%s" + + "%s" + } emptyValuesInGroupByFilter, err := handleEmptyValuesInGroupBy(keys, mq.GroupBy) if err != nil { @@ -250,10 +262,15 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str } filterSubQuery += emptyValuesInGroupByFilter - groupBy := groupByAttributeKeyTags(keys, mq.GroupBy...) + groupBy := groupByAttributeKeyTags(panelType, mq.GroupBy...) + if groupBy != "" { + groupBy = " group by " + groupBy + } enrichedOrderBy := enrichOrderBy(mq.OrderBy, keys) orderBy := orderByAttributeKeyTags(panelType, enrichedOrderBy, mq.GroupBy, keys) - + if orderBy != "" { + orderBy = " order by " + orderBy + } aggregationKey := "" if mq.AggregateAttribute.Key != "" { aggregationKey = getColumnName(mq.AggregateAttribute, keys) @@ -266,7 +283,7 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str v3.AggregateOperatorRateMin, v3.AggregateOperatorRate: op := fmt.Sprintf("%s(%s)/%d", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey, step) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorP05, @@ -279,11 +296,11 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str v3.AggregateOperatorP95, v3.AggregateOperatorP99: op := fmt.Sprintf("quantile(%v)(%s)", aggregateOperatorToPercentile[mq.AggregateOperator], aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorAvg, v3.AggregateOperatorSum, v3.AggregateOperatorMin, v3.AggregateOperatorMax: op := fmt.Sprintf("%s(%s)", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorCount: if mq.AggregateAttribute.Key != "" { @@ -299,11 +316,11 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str } } op := "toFloat64(count())" - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorCountDistinct: op := fmt.Sprintf("toFloat64(count(distinct(%s)))", aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorNoOp: var query string @@ -319,7 +336,7 @@ func buildTracesQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str return "", fmt.Errorf("select columns cannot be empty for panelType %s", panelType) } selectColumns := getSelectColumns(mq.SelectColumns, keys) - queryNoOpTmpl := fmt.Sprintf("SELECT timestamp as timestamp_datetime, spanID, traceID, "+"%s ", selectColumns) + "from " + constants.SIGNOZ_TRACE_DBNAME + "." + constants.SIGNOZ_SPAN_INDEX_TABLENAME + " where %s %s" + " order by %s" + queryNoOpTmpl := fmt.Sprintf("SELECT timestamp as timestamp_datetime, spanID, traceID, "+"%s ", selectColumns) + "from " + constants.SIGNOZ_TRACE_DBNAME + "." + constants.SIGNOZ_SPAN_INDEX_TABLENAME + " where %s %s" + "%s" query = fmt.Sprintf(queryNoOpTmpl, spanIndexTableTimeFilter, filterSubQuery, orderBy) } else { return "", fmt.Errorf("unsupported aggregate operator %s for panelType %s", mq.AggregateOperator, panelType) @@ -350,17 +367,19 @@ func enrichOrderBy(items []v3.OrderBy, keys map[string]v3.AttributeKey) []v3.Ord // groupBy returns a string of comma separated tags for group by clause // `ts` is always added to the group by clause -func groupBy(tags ...string) string { - tags = append(tags, "ts") +func groupBy(panelType v3.PanelType, tags ...string) string { + if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { + tags = append(tags, "ts") + } return strings.Join(tags, ",") } -func groupByAttributeKeyTags(keys map[string]v3.AttributeKey, tags ...v3.AttributeKey) string { +func groupByAttributeKeyTags(panelType v3.PanelType, tags ...v3.AttributeKey) string { groupTags := []string{} for _, tag := range tags { groupTags = append(groupTags, fmt.Sprintf("`%s`", tag.Key)) } - return groupBy(groupTags...) + return groupBy(panelType, groupTags...) } // orderBy returns a string of comma separated tags for order by clause @@ -403,7 +422,7 @@ func orderBy(panelType v3.PanelType, items []v3.OrderBy, tags []string, keys map if !addedToOrderBy[item.ColumnName] { attr := v3.AttributeKey{Key: item.ColumnName, DataType: item.DataType, Type: item.Type, IsColumn: item.IsColumn} name := getColumnName(attr, keys) - + if item.IsColumn { orderBy = append(orderBy, fmt.Sprintf("`%s` %s", name, item.Order)) } else { @@ -424,7 +443,7 @@ func orderByAttributeKeyTags(panelType v3.PanelType, items []v3.OrderBy, tags [] if panelType == v3.PanelTypeList && len(orderByArray) == 0 { orderByArray = append(orderByArray, constants.TIMESTAMP+" DESC") - } else if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeTable { + } else if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { orderByArray = append(orderByArray, "ts") } diff --git a/pkg/query-service/app/traces/v3/query_builder_test.go b/pkg/query-service/app/traces/v3/query_builder_test.go index 023a636d12..7ec3fa49f7 100644 --- a/pkg/query-service/app/traces/v3/query_builder_test.go +++ b/pkg/query-service/app/traces/v3/query_builder_test.go @@ -216,13 +216,13 @@ var testGetSelectLabelsData = []struct { Name: "select keys for groupBy attribute", AggregateOperator: v3.AggregateOperatorCount, GroupByTags: []v3.AttributeKey{{Key: "user.name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, - SelectLabels: ", stringTagMap['user.name'] as `user.name`", + SelectLabels: " stringTagMap['user.name'] as `user.name`,", }, { Name: "select keys for groupBy resource", AggregateOperator: v3.AggregateOperatorCount, GroupByTags: []v3.AttributeKey{{Key: "user.name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}}, - SelectLabels: ", resourceTagsMap['user.name'] as `user.name`", + SelectLabels: " resourceTagsMap['user.name'] as `user.name`,", }, { Name: "select keys for groupBy attribute and resource", @@ -231,13 +231,13 @@ var testGetSelectLabelsData = []struct { {Key: "user.name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, {Key: "host", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, }, - SelectLabels: ", resourceTagsMap['user.name'] as `user.name`, stringTagMap['host'] as `host`", + SelectLabels: " resourceTagsMap['user.name'] as `user.name`, stringTagMap['host'] as `host`,", }, { Name: "select keys for groupBy fixed columns", AggregateOperator: v3.AggregateOperatorCount, GroupByTags: []v3.AttributeKey{{Key: "host", IsColumn: true, DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, - SelectLabels: ", host as `host`", + SelectLabels: " host as `host`,", }, } @@ -956,6 +956,77 @@ var testBuildTracesQueryData = []struct { "AND stringTagMap['method'] = 'GET' group by ts having value > 10 order by ts", PanelType: v3.PanelTypeGraph, }, + { + Name: "Test count with having clause and filters", + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="}, + }, + }, + Having: []v3.Having{ + { + ColumnName: "name", + Operator: ">", + Value: 10, + }, + }, + }, + TableName: "signoz_traces.distributed_signoz_index_v2", + ExpectedQuery: "SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, toFloat64(count()) as value" + + " from signoz_traces.distributed_signoz_index_v2 where (timestamp >= '1680066360726210000' AND timestamp <= '1680066458000000000') " + + "AND stringTagMap['method'] = 'GET' AND has(stringTagMap, 'name') group by ts having value > 10 order by ts", + PanelType: v3.PanelTypeValue, + }, + { + Name: "Test aggregate PXX", + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "durationNano", IsColumn: true, DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}, + AggregateOperator: v3.AggregateOperatorP05, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, + GroupBy: []v3.AttributeKey{{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}}, + OrderBy: []v3.OrderBy{{ColumnName: "method", Order: "ASC"}}, + }, + TableName: "signoz_traces.distributed_signoz_index_v2", + ExpectedQuery: "SELECT now() as ts, stringTagMap['method'] as `method`, " + + "quantile(0.05)(durationNano) as value " + + "from signoz_traces.distributed_signoz_index_v2 " + + "where (timestamp >= '1680066360726210000' AND timestamp <= '1680066458000000000') " + + "AND has(stringTagMap, 'method') group by `method` " + + "order by `method` ASC", + PanelType: v3.PanelTypeTable, + }, + { + Name: "Test aggregate PXX", + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "durationNano", IsColumn: true, DataType: v3.AttributeKeyDataTypeFloat64, Type: v3.AttributeKeyTypeTag}, + AggregateOperator: v3.AggregateOperatorP05, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, + GroupBy: []v3.AttributeKey{}, + OrderBy: []v3.OrderBy{}, + }, + TableName: "signoz_traces.distributed_signoz_index_v2", + ExpectedQuery: "SELECT now() as ts, quantile(0.05)(durationNano) as value " + + "from signoz_traces.distributed_signoz_index_v2 " + + "where (timestamp >= '1680066360726210000' AND timestamp <= '1680066458000000000')", + PanelType: v3.PanelTypeTable, + }, { Name: "Test Noop list view", Start: 1680066360726210000, From 5bfcc1db7072be122150dd279ba3e1060b1a125f Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Wed, 12 Jul 2023 16:17:43 +0530 Subject: [PATCH 35/48] chore(hotrod): use develop branch instead of main for scripts (#3112) Signed-off-by: Prashant Shahi --- .github/workflows/e2e-k3s.yaml | 2 +- CONTRIBUTING.md | 4 ++-- deploy/README.md | 2 +- sample-apps/hotrod/hotrod-delete.sh | 2 +- sample-apps/hotrod/hotrod-install.sh | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-k3s.yaml b/.github/workflows/e2e-k3s.yaml index 8eab9d9beb..71061bfc73 100644 --- a/.github/workflows/e2e-k3s.yaml +++ b/.github/workflows/e2e-k3s.yaml @@ -37,7 +37,7 @@ jobs: kubectl create ns sample-application # apply hotrod k8s manifest file - kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml + kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml # wait for all deployments in sample-application namespace to be READY kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08a93d2e1d..6123bd747b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -338,7 +338,7 @@ to make SigNoz UI available at [localhost:3301](http://localhost:3301) **5.1.1 To install the HotROD sample app:** ```bash -curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \ +curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh \ | HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash ``` @@ -361,7 +361,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \ **5.1.4 To delete the HotROD sample app:** ```bash -curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \ +curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \ | HOTROD_NAMESPACE=sample-application bash ``` diff --git a/deploy/README.md b/deploy/README.md index 55c3b6e8d4..5e6740e6a7 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -58,7 +58,7 @@ from the HotROD application, you should see the data generated from hotrod in Si ```sh kubectl create ns sample-application -kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml +kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml ``` To generate load: diff --git a/sample-apps/hotrod/hotrod-delete.sh b/sample-apps/hotrod/hotrod-delete.sh index a7d88ebc07..9cc50b4d52 100755 --- a/sample-apps/hotrod/hotrod-delete.sh +++ b/sample-apps/hotrod/hotrod-delete.sh @@ -7,7 +7,7 @@ HOTROD_NAMESPACE=${HOTROD_NAMESPACE:-"sample-application"} if [[ "${HOTROD_NAMESPACE}" == "default" || "${HOTROD_NAMESPACE}" == "kube-system" || "${HOTROD_NAMESPACE}" == "platform" ]]; then echo "Default k8s namespace and SigNoz namespace must not be deleted" echo "Deleting components only" - kubectl delete --namespace="${HOTROD_NAMESPACE}" -f <(cat hotrod-template.yaml || curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-template.yaml) + kubectl delete --namespace="${HOTROD_NAMESPACE}" -f <(cat hotrod-template.yaml || curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-template.yaml) else echo "Delete HotROD sample app namespace ${HOTROD_NAMESPACE}" kubectl delete namespace "${HOTROD_NAMESPACE}" diff --git a/sample-apps/hotrod/hotrod-install.sh b/sample-apps/hotrod/hotrod-install.sh index b7ba4f6caa..42f29ede5f 100755 --- a/sample-apps/hotrod/hotrod-install.sh +++ b/sample-apps/hotrod/hotrod-install.sh @@ -37,7 +37,7 @@ kubectl create namespace "$HOTROD_NAMESPACE" --save-config --dry-run -o yaml 2>/ # Setup sample apps into specified namespace kubectl apply --namespace="${HOTROD_NAMESPACE}" -f <( \ - (cat hotrod-template.yaml 2>/dev/null || curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-template.yaml) | \ + (cat hotrod-template.yaml 2>/dev/null || curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-template.yaml) | \ HOTROD_NAMESPACE="${HOTROD_NAMESPACE}" \ HOTROD_IMAGE="${HOTROD_IMAGE}" \ LOCUST_IMAGE="${LOCUST_IMAGE}" \ From 7818f918a8472c828cf0c8ed08b744a2e7596f38 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Wed, 12 Jul 2023 16:40:29 +0530 Subject: [PATCH 36/48] feat: [logs] Table view (#3116) * feat: [logs] Table view * fix: support for formula in table view * fix: support for formula in table view --- .../app/logs/v3/query_builder.go | 59 +++++++++++++------ .../app/logs/v3/query_builder_test.go | 53 +++++++++++++++++ 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index baf2a6c193..0d9fb9c31a 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -183,16 +183,35 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build having = " having " + having } - queryTmpl := - "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL %d SECOND) AS ts" + selectLabels + - ", %s as value " + - "from signoz_logs.distributed_logs " + - "where " + timeFilter + "%s " + - "group by %s%s " + - "order by %s" + var queryTmpl string - groupBy := groupByAttributeKeyTags(mq.GroupBy...) + if panelType == v3.PanelTypeTable { + queryTmpl = + "SELECT now() as ts" + selectLabels + + ", %s as value " + + "from signoz_logs.distributed_logs " + + "where " + timeFilter + "%s" + + "%s%s" + + "%s" + } else if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { + // Select the aggregate value for interval + queryTmpl = + fmt.Sprintf("SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL %d SECOND) AS ts", step) + selectLabels + + ", %s as value " + + "from signoz_logs.distributed_logs " + + "where " + timeFilter + "%s" + + "%s%s" + + "%s" + } + + groupBy := groupByAttributeKeyTags(panelType, mq.GroupBy...) + if panelType != v3.PanelTypeList && groupBy != "" { + groupBy = " group by " + groupBy + } orderBy := orderByAttributeKeyTags(panelType, mq.AggregateOperator, mq.OrderBy, mq.GroupBy) + if panelType != v3.PanelTypeList && orderBy != "" { + orderBy = " order by " + orderBy + } aggregationKey := "" if mq.AggregateAttribute.Key != "" { @@ -202,7 +221,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build switch mq.AggregateOperator { case v3.AggregateOperatorRate: op := fmt.Sprintf("count(%s)/%d", aggregationKey, step) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorRateSum, @@ -210,7 +229,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build v3.AggregateOperatorRateAvg, v3.AggregateOperatorRateMin: op := fmt.Sprintf("%s(%s)/%d", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey, step) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorP05, @@ -223,11 +242,11 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build v3.AggregateOperatorP95, v3.AggregateOperatorP99: op := fmt.Sprintf("quantile(%v)(%s)", aggregateOperatorToPercentile[mq.AggregateOperator], aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorAvg, v3.AggregateOperatorSum, v3.AggregateOperatorMin, v3.AggregateOperatorMax: op := fmt.Sprintf("%s(%s)", aggregateOperatorToSQLFunc[mq.AggregateOperator], aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorCount: if mq.AggregateAttribute.Key != "" { @@ -237,11 +256,11 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build } op := "toFloat64(count(*))" - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorCountDistinct: op := fmt.Sprintf("toFloat64(count(distinct(%s)))", aggregationKey) - query := fmt.Sprintf(queryTmpl, step, op, filterSubQuery, groupBy, having, orderBy) + query := fmt.Sprintf(queryTmpl, op, filterSubQuery, groupBy, having, orderBy) return query, nil case v3.AggregateOperatorNoOp: queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s%s order by %s" @@ -254,17 +273,19 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build // groupBy returns a string of comma separated tags for group by clause // `ts` is always added to the group by clause -func groupBy(tags ...string) string { - tags = append(tags, "ts") +func groupBy(panelType v3.PanelType, tags ...string) string { + if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { + tags = append(tags, "ts") + } return strings.Join(tags, ",") } -func groupByAttributeKeyTags(tags ...v3.AttributeKey) string { +func groupByAttributeKeyTags(panelType v3.PanelType, tags ...v3.AttributeKey) string { groupTags := []string{} for _, tag := range tags { groupTags = append(groupTags, tag.Key) } - return groupBy(groupTags...) + return groupBy(panelType, groupTags...) } // orderBy returns a string of comma separated tags for order by clause @@ -325,7 +346,7 @@ func orderByAttributeKeyTags(panelType v3.PanelType, aggregatorOperator v3.Aggre if len(orderByArray) == 0 { orderByArray = append(orderByArray, constants.TIMESTAMP) } - } else { + } else if panelType == v3.PanelTypeGraph || panelType == v3.PanelTypeValue { // since in other aggregation operator we will have to add ts as it will not be present in group by orderByArray = append(orderByArray, "ts") } diff --git a/pkg/query-service/app/logs/v3/query_builder_test.go b/pkg/query-service/app/logs/v3/query_builder_test.go index 8fab8653e2..fd0526fbae 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -744,6 +744,59 @@ var testBuildLogsQueryData = []struct { TableName: "logs", ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'body')] ILIKE '%test%' group by ts having value > 10 order by ts", }, + + // Tests for table panel type + { + Name: "TABLE: Test count", + PanelType: v3.PanelTypeTable, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + }, + TableName: "logs", + ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000)", + }, + { + Name: "TABLE: Test count with groupBy", + PanelType: v3.PanelTypeTable, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + GroupBy: []v3.AttributeKey{ + {Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, + }, + }, + TableName: "logs", + ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND indexOf(attributes_string_key, 'name') > 0 group by name order by name ASC", + }, + { + Name: "TABLE: Test count with groupBy, orderBy", + PanelType: v3.PanelTypeTable, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + GroupBy: []v3.AttributeKey{ + {Key: "name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, + }, + OrderBy: []v3.OrderBy{ + {ColumnName: "name", Order: "DESC"}, + }, + }, + TableName: "logs", + ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as name, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND indexOf(attributes_string_key, 'name') > 0 group by name order by name DESC", + }, } func TestBuildLogsQuery(t *testing.T) { From 8e20ca840578c1622e83ce24831b5b6ffb20a70a Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 12 Jul 2023 16:59:33 +0300 Subject: [PATCH 37/48] fix: click on field in the list item (#3120) --- .../src/components/Logs/ListLogView/index.tsx | 48 ++++--------- .../LogExplorerDetailedView.interfaces.ts | 6 -- .../LogExplorerDetailedView/index.tsx | 69 ------------------- .../LogsExplorerList.interfaces.ts | 3 +- .../src/container/LogsExplorerList/index.tsx | 3 + .../src/container/LogsExplorerViews/index.tsx | 68 +++++++++++++++++- frontend/src/container/LogsTable/index.tsx | 29 +++++++- 7 files changed, 111 insertions(+), 115 deletions(-) delete mode 100644 frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts delete mode 100644 frontend/src/container/LogExplorerDetailedView/index.tsx diff --git a/frontend/src/components/Logs/ListLogView/index.tsx b/frontend/src/components/Logs/ListLogView/index.tsx index ea4bada737..91d0787a95 100644 --- a/frontend/src/components/Logs/ListLogView/index.tsx +++ b/frontend/src/components/Logs/ListLogView/index.tsx @@ -2,25 +2,19 @@ import { blue, grey, orange } from '@ant-design/colors'; import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons'; import Convert from 'ansi-to-html'; import { Button, Divider, Row, Typography } from 'antd'; -import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import dompurify from 'dompurify'; import { useNotifications } from 'hooks/useNotifications'; // utils import { FlatLogData } from 'lib/logs/flatLogData'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; import { useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { useCopyToClipboard } from 'react-use'; -import { AppState } from 'store/reducers'; // interfaces import { IField } from 'types/api/logs/fields'; import { ILog } from 'types/api/logs/log'; -import { ILogsReducer } from 'types/reducer/logs'; // components -import AddToQueryHOC from '../AddToQueryHOC'; +import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC'; import CopyClipboardHOC from '../CopyClipboardHOC'; // styles import { @@ -39,6 +33,10 @@ interface LogFieldProps { fieldKey: string; fieldValue: string; } + +type LogSelectedFieldProps = LogFieldProps & + Pick; + function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element { const html = useMemo( () => ({ @@ -62,37 +60,14 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element { function LogSelectedField({ fieldKey = '', fieldValue = '', -}: LogFieldProps): JSX.Element { - const history = useHistory(); - const { - searchFilter: { queryString }, - } = useSelector((state) => state.logs); - - const handleQueryAdd = useCallback( - (fieldKey: string, fieldValue: string) => { - const generatedQuery = generateFilterQuery({ - fieldKey, - fieldValue, - type: 'IN', - }); - - let updatedQueryString = queryString || ''; - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } - history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); - }, - [history, queryString], - ); - + onAddToQuery, +}: LogSelectedFieldProps): JSX.Element { return ( {fieldKey} @@ -108,15 +83,17 @@ function LogSelectedField({ ); } -interface ListLogViewProps { +type ListLogViewProps = { logData: ILog; onOpenDetailedView: (log: ILog) => void; selectedFields: IField[]; -} +} & Pick; + function ListLogView({ logData, selectedFields, onOpenDetailedView, + onAddToQuery, }: ListLogViewProps): JSX.Element { const flattenLogData = useMemo(() => FlatLogData(logData), [logData]); @@ -166,6 +143,7 @@ function ListLogView({ key={field.name} fieldKey={field.name} fieldValue={flattenLogData[field.name] as never} + onAddToQuery={onAddToQuery} /> ) : null, )} diff --git a/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts b/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts deleted file mode 100644 index ca9620657f..0000000000 --- a/frontend/src/container/LogExplorerDetailedView/LogExplorerDetailedView.interfaces.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ILog } from 'types/api/logs/log'; - -export type LogExplorerDetailedViewProps = { - log: ILog | null; - onClose: () => void; -}; diff --git a/frontend/src/container/LogExplorerDetailedView/index.tsx b/frontend/src/container/LogExplorerDetailedView/index.tsx deleted file mode 100644 index 3bbd7481f8..0000000000 --- a/frontend/src/container/LogExplorerDetailedView/index.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import LogDetail from 'components/LogDetail'; -import { QueryBuilderKeys } from 'constants/queryBuilder'; -import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; -import { useCallback } from 'react'; -import { useQueryClient } from 'react-query'; -import { SuccessResponse } from 'types/api'; -import { - BaseAutocompleteData, - IQueryAutocompleteResponse, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { Query } from 'types/api/queryBuilder/queryBuilderData'; -import { v4 as uuid } from 'uuid'; - -import { LogExplorerDetailedViewProps } from './LogExplorerDetailedView.interfaces'; - -function LogExplorerDetailedView({ - log, - onClose, -}: LogExplorerDetailedViewProps): JSX.Element { - const queryClient = useQueryClient(); - const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder(); - - const handleAddQuery = useCallback( - (fieldKey: string, fieldValue: string): void => { - const keysAutocomplete: BaseAutocompleteData[] = - queryClient.getQueryData>( - [QueryBuilderKeys.GET_AGGREGATE_KEYS], - { exact: false }, - )?.payload.attributeKeys || []; - - const existAutocompleteKey = chooseAutocompleteFromCustomValue( - keysAutocomplete, - fieldKey, - ); - - const nextQuery: Query = { - ...currentQuery, - builder: { - ...currentQuery.builder, - queryData: currentQuery.builder.queryData.map((item) => ({ - ...item, - filters: { - ...item.filters, - items: [ - ...item.filters.items.filter( - (item) => item.key?.id !== existAutocompleteKey.id, - ), - { - id: uuid(), - key: existAutocompleteKey, - op: '=', - value: fieldValue, - }, - ], - }, - })), - }, - }; - - redirectWithQueryBuilderData(nextQuery); - }, - [currentQuery, queryClient, redirectWithQueryBuilderData], - ); - - return ; -} - -export default LogExplorerDetailedView; diff --git a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts index b238031ac0..6862fe5ee9 100644 --- a/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts +++ b/frontend/src/container/LogsExplorerList/LogsExplorerList.interfaces.ts @@ -1,3 +1,4 @@ +import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; import { ILog } from 'types/api/logs/log'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; @@ -8,4 +9,4 @@ export type LogsExplorerListProps = { onEndReached: (index: number) => void; onExpand: (log: ILog) => void; onOpenDetailedView: (log: ILog) => void; -}; +} & Pick; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 7cfaaf4bc2..54ee339527 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -32,6 +32,7 @@ function LogsExplorerList({ onOpenDetailedView, onEndReached, onExpand, + onAddToQuery, }: LogsExplorerListProps): JSX.Element { const { initialDataSource } = useQueryBuilder(); @@ -79,6 +80,7 @@ function LogsExplorerList({ logData={log} selectedFields={selectedFields} onOpenDetailedView={onOpenDetailedView} + onAddToQuery={onAddToQuery} /> ); }, @@ -87,6 +89,7 @@ function LogsExplorerList({ options.maxLines, selectedFields, onOpenDetailedView, + onAddToQuery, onExpand, ], ); diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index afbca2b470..0a7d643c03 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -1,13 +1,17 @@ import { TabsProps } from 'antd'; import axios from 'axios'; +import LogDetail from 'components/LogDetail'; import TabLabel from 'components/TabLabel'; import { QueryParams } from 'constants/query'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { + initialQueriesMap, + PANEL_TYPES, + QueryBuilderKeys, +} from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config'; import ExportPanel from 'container/ExportPanel'; -import LogExplorerDetailedView from 'container/LogExplorerDetailedView'; import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerList from 'container/LogsExplorerList'; // TODO: temporary hide table view @@ -20,13 +24,20 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useNotifications } from 'hooks/useNotifications'; import useUrlQueryData from 'hooks/useUrlQueryData'; +import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue'; import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useQueryClient } from 'react-query'; import { useSelector } from 'react-redux'; import { generatePath, useHistory } from 'react-router-dom'; import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; import { Dashboard } from 'types/api/dashboard/getAll'; import { ILog } from 'types/api/logs/log'; +import { + BaseAutocompleteData, + IQueryAutocompleteResponse, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; import { IBuilderQuery, OrderByPayload, @@ -34,6 +45,7 @@ import { } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; +import { v4 as uuid } from 'uuid'; import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled'; @@ -41,6 +53,8 @@ function LogsExplorerViews(): JSX.Element { const { notifications } = useNotifications(); const history = useHistory(); + const queryClient = useQueryClient(); + const { queryData: pageSize } = useUrlQueryData( queryParamNamesMap.pageSize, DEFAULT_PER_PAGE_VALUE, @@ -212,6 +226,48 @@ function LogsExplorerViews(): JSX.Element { [currentStagedQueryData, orderByTimestamp], ); + const handleAddQuery = useCallback( + (fieldKey: string, fieldValue: string): void => { + const keysAutocomplete: BaseAutocompleteData[] = + queryClient.getQueryData>( + [QueryBuilderKeys.GET_AGGREGATE_KEYS], + { exact: false }, + )?.payload.attributeKeys || []; + + const existAutocompleteKey = chooseAutocompleteFromCustomValue( + keysAutocomplete, + fieldKey, + ); + + const nextQuery: Query = { + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + filters: { + ...item.filters, + items: [ + ...item.filters.items.filter( + (item) => item.key?.id !== existAutocompleteKey.id, + ), + { + id: uuid(), + key: existAutocompleteKey, + op: '=', + value: fieldValue, + }, + ], + }, + })), + }, + }; + + redirectWithQueryBuilderData(nextQuery); + }, + [currentQuery, queryClient, redirectWithQueryBuilderData], + ); + const handleEndReached = useCallback( (index: number) => { if (isLimit) return; @@ -365,6 +421,7 @@ function LogsExplorerViews(): JSX.Element { onOpenDetailedView={handleSetActiveLog} onEndReached={handleEndReached} onExpand={handleSetActiveLog} + onAddToQuery={handleAddQuery} /> ), }, @@ -395,6 +452,7 @@ function LogsExplorerViews(): JSX.Element { logs, handleSetActiveLog, handleEndReached, + handleAddQuery, data, isError, ], @@ -444,7 +502,11 @@ function LogsExplorerViews(): JSX.Element { onChange={handleChangeView} destroyInactiveTabPane /> - + ); } diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index b514aa3ee8..94a4ee9d6c 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -4,10 +4,13 @@ import ListLogView from 'components/Logs/ListLogView'; import RawLogView from 'components/Logs/RawLogView'; import LogsTableView from 'components/Logs/TableView'; import Spinner from 'components/Spinner'; +import ROUTES from 'constants/routes'; import { contentStyle } from 'container/Trace/Search/config'; import useFontFaceObserver from 'hooks/useFontObserver'; +import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { Virtuoso } from 'react-virtuoso'; import { AppState } from 'store/reducers'; // interfaces @@ -29,6 +32,8 @@ type LogsTableProps = { function LogsTable(props: LogsTableProps): JSX.Element { const { viewMode, onClickExpand, linesPerRow } = props; + const history = useHistory(); + const dispatch = useDispatch(); useFontFaceObserver( @@ -47,6 +52,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { const { logs, fields: { selected }, + searchFilter: { queryString }, isLoading, liveTail, } = useSelector((state) => state.logs); @@ -71,6 +77,25 @@ function LogsTable(props: LogsTableProps): JSX.Element { [dispatch], ); + const handleQueryAdd = useCallback( + (fieldKey: string, fieldValue: string) => { + const generatedQuery = generateFilterQuery({ + fieldKey, + fieldValue, + type: 'IN', + }); + + let updatedQueryString = queryString || ''; + if (updatedQueryString.length === 0) { + updatedQueryString += `${generatedQuery}`; + } else { + updatedQueryString += ` AND ${generatedQuery}`; + } + history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); + }, + [history, queryString], + ); + const getItemContent = useCallback( (index: number): JSX.Element => { const log = logs[index]; @@ -92,6 +117,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logData={log} selectedFields={selected} onOpenDetailedView={handleOpenDetailedView} + onAddToQuery={handleQueryAdd} /> ); }, @@ -99,9 +125,10 @@ function LogsTable(props: LogsTableProps): JSX.Element { logs, viewMode, selected, - handleOpenDetailedView, linesPerRow, onClickExpand, + handleOpenDetailedView, + handleQueryAdd, ], ); From 10ffbf7d813f450d00dad5861a8b7a61c9dfa268 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 12 Jul 2023 17:15:40 +0300 Subject: [PATCH 38/48] fix: filter change value (#3117) * fix: filter change value Fix for getting value from current autocomplete response instead of all options. * fix: change group by with correct values --------- Co-authored-by: Vishal Sharma Co-authored-by: Palash Gupta --- .../AggregatorFilter/AggregatorFilter.tsx | 54 +++++++++----- .../filters/GroupByFilter/GroupByFilter.tsx | 72 ++++++++++++------- 2 files changed, 82 insertions(+), 44 deletions(-) diff --git a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx index 8905ba9f55..62c07ae91d 100644 --- a/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/AggregatorFilter/AggregatorFilter.tsx @@ -89,34 +89,54 @@ export const AggregatorFilter = memo(function AggregatorFilter({ ? `${transformToUpperCase(query.dataSource)} name` : 'Aggregate attribute'; - const getAttributes = useCallback( + const getAttributesData = useCallback( (): BaseAutocompleteData[] => - queryClient.getQueryData>( - [QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE], - { exact: false }, - )?.payload.attributeKeys || [], - [queryClient], + queryClient.getQueryData>([ + QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, + debouncedValue, + query.aggregateOperator, + query.dataSource, + ])?.payload.attributeKeys || [], + [debouncedValue, query.aggregateOperator, query.dataSource, queryClient], ); - const handleChangeCustomValue = useCallback( - (value: string) => { - const aggregateAttributes = getAttributes(); + const getResponseAttributes = useCallback(async () => { + const response = await queryClient.fetchQuery( + [ + QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE, + searchText, + query.aggregateOperator, + query.dataSource, + ], + async () => + getAggregateAttribute({ + searchText, + aggregateOperator: query.aggregateOperator, + dataSource: query.dataSource, + }), + ); + return response.payload?.attributeKeys || []; + }, [query.aggregateOperator, query.dataSource, queryClient, searchText]); + + const handleChangeCustomValue = useCallback( + async (value: string, attributes: BaseAutocompleteData[]) => { const customAttribute: BaseAutocompleteData = chooseAutocompleteFromCustomValue( - aggregateAttributes, + attributes, value, ); onChange(customAttribute); }, - [getAttributes, onChange], + [onChange], ); - const handleBlur = useCallback(() => { + const handleBlur = useCallback(async () => { if (searchText) { - handleChangeCustomValue(searchText); + const aggregateAttributes = await getResponseAttributes(); + handleChangeCustomValue(searchText, aggregateAttributes); } - }, [handleChangeCustomValue, searchText]); + }, [getResponseAttributes, handleChangeCustomValue, searchText]); const handleChange = useCallback( ( @@ -125,7 +145,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({ ): void => { const currentOption = option as ExtendedSelectOption; - const aggregateAttributes = getAttributes(); + const aggregateAttributes = getAttributesData(); if (currentOption.key) { const attribute = aggregateAttributes.find( @@ -136,12 +156,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({ onChange(attribute); } } else { - handleChangeCustomValue(value); + handleChangeCustomValue(value, aggregateAttributes); } setSearchText(''); }, - [getAttributes, handleChangeCustomValue, onChange], + [getAttributesData, handleChangeCustomValue, onChange], ); const value = transformStringWithPrefix({ diff --git a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx index 622b87bf7c..a41d5005d5 100644 --- a/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/GroupByFilter/GroupByFilter.tsx @@ -15,11 +15,7 @@ import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { isEqual, uniqWith } from 'lodash-es'; import { memo, useCallback, useEffect, useState } from 'react'; import { useQuery, useQueryClient } from 'react-query'; -import { SuccessResponse } from 'types/api'; -import { - BaseAutocompleteData, - IQueryAutocompleteResponse, -} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { SelectOption } from 'types/common/select'; import { selectStyle } from '../QueryBuilderSearch/config'; @@ -83,14 +79,27 @@ export const GroupByFilter = memo(function GroupByFilter({ }, ); - const getAttributeKeys = useCallback( - (): BaseAutocompleteData[] => - queryClient.getQueryData>( - [QueryBuilderKeys.GET_AGGREGATE_KEYS], - { exact: false }, - )?.payload.attributeKeys || [], - [queryClient], - ); + const getAttributeKeys = useCallback(async () => { + const response = await queryClient.fetchQuery( + [QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText, isFocused], + async () => + getAggregateKeys({ + aggregateAttribute: query.aggregateAttribute.key, + dataSource: query.dataSource, + aggregateOperator: query.aggregateOperator, + searchText, + }), + ); + + return response.payload?.attributeKeys || []; + }, [ + isFocused, + query.aggregateAttribute.key, + query.aggregateOperator, + query.dataSource, + queryClient, + searchText, + ]); const handleSearchKeys = (searchText: string): void => { setSearchText(searchText); @@ -105,26 +114,35 @@ export const GroupByFilter = memo(function GroupByFilter({ setIsFocused(true); }; - const handleChange = (values: SelectOption[]): void => { - const groupByValues: BaseAutocompleteData[] = values.map((item) => { - const [currentValue, id] = item.value.split(selectValueDivider); - const keys = getAttributeKeys(); + const handleChange = useCallback( + async (values: SelectOption[]): Promise => { + const keys = await getAttributeKeys(); - if (id && id.includes(idDivider)) { - const attribute = keys.find((item) => item.id === id); + const groupByValues: BaseAutocompleteData[] = values.map((item) => { + const [currentValue, id] = item.value.split(selectValueDivider); - if (attribute) { - return attribute; + if (id && id.includes(idDivider)) { + const attribute = keys.find((item) => item.id === id); + const existAttribute = query.groupBy.find((item) => item.id === id); + + if (attribute) { + return attribute; + } + + if (existAttribute) { + return existAttribute; + } } - } - return chooseAutocompleteFromCustomValue(keys, currentValue); - }); + return chooseAutocompleteFromCustomValue(keys, currentValue); + }); - const result = uniqWith(groupByValues, isEqual); + const result = uniqWith(groupByValues, isEqual); - onChange(result); - }; + onChange(result); + }, + [getAttributeKeys, onChange, query.groupBy], + ); const clearSearch = useCallback(() => { setSearchText(''); From 538261aa997ac69bb95a557ef07fc4c13ebd7bee Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Wed, 12 Jul 2023 18:20:17 +0300 Subject: [PATCH 39/48] fix: list pagination (#3121) --- frontend/src/container/LogsExplorerViews/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 0a7d643c03..580b80e200 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -271,6 +271,7 @@ function LogsExplorerViews(): JSX.Element { const handleEndReached = useCallback( (index: number) => { if (isLimit) return; + if (logs.length < pageSize) return; const lastLog = logs[index]; @@ -368,7 +369,7 @@ function LogsExplorerViews(): JSX.Element { useEffect(() => { const shouldChangeView = isMultipleQueries || isGroupByExist; - if (panelType === 'list' && shouldChangeView) { + if (panelType === PANEL_TYPES.LIST && shouldChangeView) { handleChangeView(PANEL_TYPES.TIME_SERIES); } }, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]); From 08d496e31445ad61625e10bada5ca5e3e77ac5a7 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 13 Jul 2023 14:22:30 +0530 Subject: [PATCH 40/48] feat: allow limit on metrics time series result (#2979) --- .../app/clickhouseReader/reader.go | 17 +- pkg/query-service/app/http_handler.go | 33 ++ pkg/query-service/app/http_handler_test.go | 346 ++++++++++++++++++ .../app/metrics/v3/query_builder.go | 31 +- .../app/metrics/v3/query_builder_test.go | 2 +- pkg/query-service/model/v3/v3.go | 5 +- 6 files changed, 423 insertions(+), 11 deletions(-) diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index f30db29af6..0a32730f97 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -4162,7 +4162,22 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam var seriesList []*v3.Series for key := range seriesToPoints { - series := v3.Series{Labels: seriesToAttrs[key], Points: seriesToPoints[key]} + points := seriesToPoints[key] + + // find the grouping sets point for the series + // this is the point with the zero timestamp + // if there is no such point, then the series is not grouped + // and we can skip this step + var groupingSetsPoint *v3.Point + for idx, point := range points { + if point.Timestamp <= 0 { + groupingSetsPoint = &point + // remove the grouping sets point from the list of points + points = append(points[:idx], points[idx+1:]...) + break + } + } + series := v3.Series{Labels: seriesToAttrs[key], Points: points, GroupingSetsPoint: groupingSetsPoint} seriesList = append(seriesList, &series) } return seriesList, nil diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 604b1f046a..e4803b4c2e 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net/http" + "sort" "strconv" "strings" "sync" @@ -2758,6 +2759,8 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que return } + applyMetricLimit(result, queryRangeParams) + resp := v3.QueryRangeResponse{ Result: result, } @@ -2775,3 +2778,33 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) { aH.queryRangeV3(r.Context(), queryRangeParams, w, r) } + +func applyMetricLimit(results []*v3.Result, queryRangeParams *v3.QueryRangeParamsV3) { + // apply limit if any for metrics + // use the grouping set points to apply the limit + + for _, result := range results { + builderQueries := queryRangeParams.CompositeQuery.BuilderQueries + if builderQueries != nil && builderQueries[result.QueryName].DataSource == v3.DataSourceMetrics { + limit := builderQueries[result.QueryName].Limit + var orderAsc bool + for _, item := range builderQueries[result.QueryName].OrderBy { + if item.ColumnName == constants.SigNozOrderByValue { + orderAsc = strings.ToLower(item.Order) == "asc" + break + } + } + if limit != 0 { + sort.Slice(result.Series, func(i, j int) bool { + if orderAsc { + return result.Series[i].Points[0].Value < result.Series[j].Points[0].Value + } + return result.Series[i].Points[0].Value > result.Series[j].Points[0].Value + }) + if len(result.Series) > int(limit) { + result.Series = result.Series[:limit] + } + } + } + } +} diff --git a/pkg/query-service/app/http_handler_test.go b/pkg/query-service/app/http_handler_test.go index 84782f7cae..958bcdeee2 100644 --- a/pkg/query-service/app/http_handler_test.go +++ b/pkg/query-service/app/http_handler_test.go @@ -8,7 +8,9 @@ import ( "strings" "testing" + "go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/model" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" ) func TestPrepareQuery(t *testing.T) { @@ -130,3 +132,347 @@ func TestPrepareQuery(t *testing.T) { }) } } + +func TestApplyLimitOnMetricResult(t *testing.T) { + cases := []struct { + name string + inputResult []*v3.Result + params *v3.QueryRangeParamsV3 + expectedResult []*v3.Result + }{ + { + name: "test limit 1 without order", // top most (latency/error) as default + inputResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "frontend", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 19.2, + }, + { + Timestamp: 1689220096000, + Value: 19.5, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 19.3, + }, + }, + { + Labels: map[string]string{ + "service_name": "route", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 8.83, + }, + { + Timestamp: 1689220096000, + Value: 8.83, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 8.83, + }, + }, + }, + }, + }, + params: &v3.QueryRangeParamsV3{ + Start: 1689220036000, + End: 1689220096000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "signo_calls_total"}, + DataSource: v3.DataSourceMetrics, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + GroupBy: []v3.AttributeKey{{Key: "service_name"}}, + Limit: 1, + }, + }, + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + }, + }, + expectedResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "frontend", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 19.2, + }, + { + Timestamp: 1689220096000, + Value: 19.5, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 19.3, + }, + }, + }, + }, + }, + }, + { + name: "test limit with order asc", + inputResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "frontend", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 19.2, + }, + { + Timestamp: 1689220096000, + Value: 19.5, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 19.3, + }, + }, + { + Labels: map[string]string{ + "service_name": "route", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 8.83, + }, + { + Timestamp: 1689220096000, + Value: 8.83, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 8.83, + }, + }, + }, + }, + }, + params: &v3.QueryRangeParamsV3{ + Start: 1689220036000, + End: 1689220096000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "signo_calls_total"}, + DataSource: v3.DataSourceMetrics, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + GroupBy: []v3.AttributeKey{{Key: "service_name"}}, + Limit: 1, + OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}, + }, + }, + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + }, + }, + expectedResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "route", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 8.83, + }, + { + Timestamp: 1689220096000, + Value: 8.83, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 8.83, + }, + }, + }, + }, + }, + }, + { + name: "test data source not metrics", + inputResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "frontend", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 69, + }, + { + Timestamp: 1689220096000, + Value: 240, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 154.5, + }, + }, + { + Labels: map[string]string{ + "service_name": "redis", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 420, + }, + { + Timestamp: 1689220096000, + Value: 260, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 340, + }, + }, + }, + }, + }, + params: &v3.QueryRangeParamsV3{ + Start: 1689220036000, + End: 1689220096000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "service_name"}, + DataSource: v3.DataSourceTraces, + AggregateOperator: v3.AggregateOperatorSum, + Expression: "A", + GroupBy: []v3.AttributeKey{{Key: "service_name"}}, + Limit: 1, + OrderBy: []v3.OrderBy{{ColumnName: constants.SigNozOrderByValue, Order: "asc"}}, + }, + }, + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + }, + }, + expectedResult: []*v3.Result{ + { + QueryName: "A", + Series: []*v3.Series{ + { + Labels: map[string]string{ + "service_name": "frontend", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 69, + }, + { + Timestamp: 1689220096000, + Value: 240, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 154.5, + }, + }, + { + Labels: map[string]string{ + "service_name": "redis", + }, + Points: []v3.Point{ + { + Timestamp: 1689220036000, + Value: 420, + }, + { + Timestamp: 1689220096000, + Value: 260, + }, + }, + GroupingSetsPoint: &v3.Point{ + Timestamp: 0, + Value: 340, + }, + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + result := c.inputResult + applyMetricLimit(result, c.params) + if len(result) != len(c.expectedResult) { + t.Errorf("expected result length: %d, but got: %d", len(c.expectedResult), len(result)) + } + for i, r := range result { + if r.QueryName != c.expectedResult[i].QueryName { + t.Errorf("expected query name: %s, but got: %s", c.expectedResult[i].QueryName, r.QueryName) + } + if len(r.Series) != len(c.expectedResult[i].Series) { + t.Errorf("expected series length: %d, but got: %d", len(c.expectedResult[i].Series), len(r.Series)) + } + for j, s := range r.Series { + if len(s.Points) != len(c.expectedResult[i].Series[j].Points) { + t.Errorf("expected points length: %d, but got: %d", len(c.expectedResult[i].Series[j].Points), len(s.Points)) + } + for k, p := range s.Points { + if p.Timestamp != c.expectedResult[i].Series[j].Points[k].Timestamp { + t.Errorf("expected point timestamp: %d, but got: %d", c.expectedResult[i].Series[j].Points[k].Timestamp, p.Timestamp) + } + if p.Value != c.expectedResult[i].Series[j].Points[k].Value { + t.Errorf("expected point value: %f, but got: %f", c.expectedResult[i].Series[j].Points[k].Value, p.Value) + } + } + } + } + }) + } +} diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go index f7e3956cca..1bff2ae2ba 100644 --- a/pkg/query-service/app/metrics/v3/query_builder.go +++ b/pkg/query-service/app/metrics/v3/query_builder.go @@ -193,6 +193,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str groupBy := groupByAttributeKeyTags(metricQueryGroupBy...) groupTags := groupSelectAttributeKeyTags(metricQueryGroupBy...) + groupSets := groupingSetsByAttributeKeyTags(metricQueryGroupBy...) orderBy := orderByAttributeKeyTags(mq.OrderBy, metricQueryGroupBy) if len(orderBy) != 0 { @@ -226,7 +227,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str ) // labels will be same so any should be fine query := `SELECT %s ts, ` + rateWithoutNegative + `as value FROM(%s) WHERE isNaN(value) = 0` query = fmt.Sprintf(query, groupTags, subQuery) - query = fmt.Sprintf(`SELECT %s ts, %s(value) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTags, aggregateOperatorToSQLFunc[mq.AggregateOperator], query, groupBy, orderBy) + query = fmt.Sprintf(`SELECT %s ts, %s(value) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTags, aggregateOperatorToSQLFunc[mq.AggregateOperator], query, groupSets, orderBy) return query, nil case v3.AggregateOperatorRateSum, @@ -234,7 +235,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str v3.AggregateOperatorRateAvg, v3.AggregateOperatorRateMin: op := fmt.Sprintf("%s(value)", aggregateOperatorToSQLFunc[mq.AggregateOperator]) - subQuery := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + subQuery := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupSets, orderBy) query := `SELECT %s ts, ` + rateWithoutNegative + `as value FROM(%s) WHERE isNaN(value) = 0` query = fmt.Sprintf(query, groupTags, subQuery) return query, nil @@ -249,7 +250,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str v3.AggregateOperatorP95, v3.AggregateOperatorP99: op := fmt.Sprintf("quantile(%v)(value)", aggregateOperatorToPercentile[mq.AggregateOperator]) - query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupSets, orderBy) return query, nil case v3.AggregateOperatorHistQuant50, v3.AggregateOperatorHistQuant75, v3.AggregateOperatorHistQuant90, v3.AggregateOperatorHistQuant95, v3.AggregateOperatorHistQuant99: rateGroupBy := "fingerprint, " + groupBy @@ -261,22 +262,22 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str ) // labels will be same so any should be fine query := `SELECT %s ts, ` + rateWithoutNegative + ` as value FROM(%s) WHERE isNaN(value) = 0` query = fmt.Sprintf(query, groupTags, subQuery) - query = fmt.Sprintf(`SELECT %s ts, sum(value) as value FROM (%s) GROUP BY %s HAVING isNaN(value) = 0 ORDER BY %s ts`, groupTags, query, groupBy, orderBy) + query = fmt.Sprintf(`SELECT %s ts, sum(value) as value FROM (%s) GROUP BY %s HAVING isNaN(value) = 0 ORDER BY %s ts`, groupTags, query, groupSets, orderBy) value := aggregateOperatorToPercentile[mq.AggregateOperator] query = fmt.Sprintf(`SELECT %s ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), %.3f) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTagsWithoutLe, value, query, groupByWithoutLe, orderWithoutLe) return query, nil case v3.AggregateOperatorAvg, v3.AggregateOperatorSum, v3.AggregateOperatorMin, v3.AggregateOperatorMax: op := fmt.Sprintf("%s(value)", aggregateOperatorToSQLFunc[mq.AggregateOperator]) - query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupSets, orderBy) return query, nil case v3.AggregateOperatorCount: op := "toFloat64(count(*))" - query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupSets, orderBy) return query, nil case v3.AggregateOperatorCountDistinct: op := "toFloat64(count(distinct(value)))" - query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupSets, orderBy) return query, nil case v3.AggregateOperatorNoOp: queryTmpl := @@ -297,6 +298,13 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str } } +// groupingSets returns a string of comma separated tags for group by clause +// `ts` is always added to the group by clause +func groupingSets(tags ...string) string { + withTs := append(tags, "ts") + return fmt.Sprintf(`GROUPING SETS ( (%s), (%s) )`, strings.Join(withTs, ", "), strings.Join(tags, ", ")) +} + // groupBy returns a string of comma separated tags for group by clause // `ts` is always added to the group by clause func groupBy(tags ...string) string { @@ -313,6 +321,14 @@ func groupSelect(tags ...string) string { return groupTags } +func groupingSetsByAttributeKeyTags(tags ...v3.AttributeKey) string { + groupTags := []string{} + for _, tag := range tags { + groupTags = append(groupTags, tag.Key) + } + return groupingSets(groupTags...) +} + func groupByAttributeKeyTags(tags ...v3.AttributeKey) string { groupTags := []string{} for _, tag := range tags { @@ -346,6 +362,7 @@ func orderBy(items []v3.OrderBy, tags []string) string { orderBy = append(orderBy, fmt.Sprintf("%s ASC", tag)) } } + return strings.Join(orderBy, ",") } diff --git a/pkg/query-service/app/metrics/v3/query_builder_test.go b/pkg/query-service/app/metrics/v3/query_builder_test.go index 7319236254..f3f91129a8 100644 --- a/pkg/query-service/app/metrics/v3/query_builder_test.go +++ b/pkg/query-service/app/metrics/v3/query_builder_test.go @@ -238,7 +238,7 @@ func TestBuildQueryOperators(t *testing.T) { func TestBuildQueryXRate(t *testing.T) { t.Run("TestBuildQueryXRate", func(t *testing.T) { - tmpl := `SELECT ts, %s(value) as value FROM (SELECT ts, if (runningDifference(value) < 0 OR runningDifference(ts) <= 0, nan, runningDifference(value)/runningDifference(ts))as value FROM(SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 0 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'name') as filtered_time_series USING fingerprint WHERE metric_name = 'name' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY ts ORDER BY ts` + tmpl := `SELECT ts, %s(value) as value FROM (SELECT ts, if (runningDifference(value) < 0 OR runningDifference(ts) <= 0, nan, runningDifference(value)/runningDifference(ts))as value FROM(SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 0 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'name') as filtered_time_series USING fingerprint WHERE metric_name = 'name' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY GROUPING SETS ( (ts), () ) ORDER BY ts` cases := []struct { aggregateOperator v3.AggregateOperator diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 57f290f133..e320cc2877 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -576,8 +576,9 @@ type Result struct { } type Series struct { - Labels map[string]string `json:"labels"` - Points []Point `json:"values"` + Labels map[string]string `json:"labels"` + Points []Point `json:"values"` + GroupingSetsPoint *Point `json:"-"` } func (s *Series) SortPoints() { From 98745fc30799fb3264aee3e0f98968d8950323a2 Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Thu, 13 Jul 2023 15:17:59 +0530 Subject: [PATCH 41/48] Refactor the Metric Application Layer (#2984) * refactor: seperate the dependency of top overview query * refactor: added error handle for api call using usequery * refactor: update api layar and condition in component * fix: onDragSelect re-render all graph data * refactor: removed console * refactor: corrected names and updated implemented required condition * fix: the api call issue * refactor: removed useeffect * refactor: reverted the unnecessary changes * refactor: removed the login from service level * refactor: removed the unwanted code * refactor: reverted the unwanted changes in getDashboardVariable * refactor: instead of useQuery used useQueries * refactor: changed the dependencies of useQuery key * refactor: linter fixes * refactor: delete the unrequired files * fix: generecity of the type * fix: moved the type to component * fix: move the logic from container layer to pages layer * refactor: optimised some part of the code * refactor: review changes * refactor: optimised the checks * refactor: checking if the dependency data loaded in full view * refactor: resolve the error of props in overview.ts * refactor: small changes * refactor: enforced the typecasting of constant variable * refactor: refactoring in some of the changes are updated * refactor: refactoring in some of the changes are updated * refactor: removed the extra parameter from useGetQueryRange * refactor: revert the changes back for tab * refactor: metrics application is updated * chore: loading condition is updated for full view component * chore: moved the serviceDataProps type to api layer * chore: message name is updated --------- Co-authored-by: Srikanth Chekuri Co-authored-by: Palash Gupta --- frontend/src/AppRoutes/pageComponents.ts | 2 +- .../src/api/metrics/getServiceOverview.ts | 32 +-- .../src/api/metrics/getTopLevelOperations.ts | 24 +- frontend/src/api/metrics/getTopOperations.ts | 30 +-- frontend/src/constants/api.ts | 4 +- .../Graph/FullView/index.metricsBuilder.tsx | 8 +- .../MetricsApplication/Tabs/Overview.tsx | 220 +++++++++++++----- .../container/MetricsApplication/index.tsx | 90 ------- .../src/pages/MetricApplication/index.tsx | 76 ------ .../src/pages/MetricsApplication/config.ts | 8 + .../src/pages/MetricsApplication/index.tsx | 54 +++++ .../src/pages/MetricsApplication/types.ts | 11 + .../useMetricsApplicationTabKey.tsx | 14 ++ .../src/pages/MetricsApplication/utils.ts | 17 ++ .../store/actions/metrics/getInitialData.ts | 137 ----------- frontend/src/store/reducers/global.ts | 4 +- 16 files changed, 305 insertions(+), 426 deletions(-) delete mode 100644 frontend/src/container/MetricsApplication/index.tsx delete mode 100644 frontend/src/pages/MetricApplication/index.tsx create mode 100644 frontend/src/pages/MetricsApplication/config.ts create mode 100644 frontend/src/pages/MetricsApplication/index.tsx create mode 100644 frontend/src/pages/MetricsApplication/types.ts create mode 100644 frontend/src/pages/MetricsApplication/useMetricsApplicationTabKey.tsx create mode 100644 frontend/src/pages/MetricsApplication/utils.ts delete mode 100644 frontend/src/store/actions/metrics/getInitialData.ts diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index a4d6b89c16..f7bc5e1704 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -7,7 +7,7 @@ export const ServicesTablePage = Loadable( export const ServiceMetricsPage = Loadable( () => import( - /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricApplication' + /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricsApplication' ), ); diff --git a/frontend/src/api/metrics/getServiceOverview.ts b/frontend/src/api/metrics/getServiceOverview.ts index ea0ddb4062..47febaa3d8 100644 --- a/frontend/src/api/metrics/getServiceOverview.ts +++ b/frontend/src/api/metrics/getServiceOverview.ts @@ -1,30 +1,16 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/metrics/getServiceOverview'; -const getServiceOverview = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/service/overview`, { - start: `${props.start}`, - end: `${props.end}`, - service: props.service, - step: props.step, - tags: props.selectedTags, - }); +const getServiceOverview = async (props: Props): Promise => { + const response = await axios.post(`/service/overview`, { + start: `${props.start}`, + end: `${props.end}`, + service: props.service, + step: props.step, + tags: props.selectedTags, + }); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } + return response.data; }; export default getServiceOverview; diff --git a/frontend/src/api/metrics/getTopLevelOperations.ts b/frontend/src/api/metrics/getTopLevelOperations.ts index 5ecfd2a67a..2f5a2026b2 100644 --- a/frontend/src/api/metrics/getTopLevelOperations.ts +++ b/frontend/src/api/metrics/getTopLevelOperations.ts @@ -1,24 +1,12 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/metrics/getTopLevelOperations'; -const getTopLevelOperations = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/service/top_level_operations`); +const getTopLevelOperations = async (): Promise => { + const response = await axios.post(`/service/top_level_operations`); + return response.data; +}; - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data[props.service], - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } +export type ServiceDataProps = { + [serviceName: string]: string[]; }; export default getTopLevelOperations; diff --git a/frontend/src/api/metrics/getTopOperations.ts b/frontend/src/api/metrics/getTopOperations.ts index cf07f0ee5d..9c85602e40 100644 --- a/frontend/src/api/metrics/getTopOperations.ts +++ b/frontend/src/api/metrics/getTopOperations.ts @@ -1,29 +1,15 @@ import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/metrics/getTopOperations'; -const getTopOperations = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/service/top_operations`, { - start: `${props.start}`, - end: `${props.end}`, - service: props.service, - tags: props.selectedTags, - }); +const getTopOperations = async (props: Props): Promise => { + const response = await axios.post(`/service/top_operations`, { + start: `${props.start}`, + end: `${props.end}`, + service: props.service, + tags: props.selectedTags, + }); - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } + return response.data; }; export default getTopOperations; diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 8ebfe3b73c..abb7d42ad6 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -1,3 +1,5 @@ +const SOMETHING_WENT_WRONG = 'Something went wrong'; + const getVersion = 'version'; -export { getVersion }; +export { getVersion, SOMETHING_WENT_WRONG }; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx index f500a0a098..3208517b86 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.metricsBuilder.tsx @@ -26,6 +26,7 @@ function FullView({ name, yAxisUnit, onDragSelect, + isDependedDataLoaded = false, }: FullViewProps): JSX.Element { const { selectedTime: globalSelectedTime } = useSelector< AppState, @@ -61,6 +62,7 @@ function FullView({ }, { queryKey, + enabled: !isDependedDataLoaded, }, ); @@ -76,9 +78,7 @@ function FullView({ [response], ); - const isLoading = response.isLoading === true; - - if (isLoading) { + if (response.status === 'idle' || response.status === 'loading') { return ; } @@ -123,6 +123,7 @@ interface FullViewProps { name: string; yAxisUnit?: string; onDragSelect?: (start: number, end: number) => void; + isDependedDataLoaded?: boolean; } FullView.defaultProps = { @@ -130,6 +131,7 @@ FullView.defaultProps = { onClickHandler: undefined, yAxisUnit: undefined, onDragSelect: undefined, + isDependedDataLoaded: undefined, }; export default FullView; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 701e6ff219..5cd571a548 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -1,5 +1,13 @@ +import { Typography } from 'antd'; +import getServiceOverview from 'api/metrics/getServiceOverview'; +import getTopLevelOperations, { + ServiceDataProps, +} from 'api/metrics/getTopLevelOperations'; +import getTopOperations from 'api/metrics/getTopOperations'; +import axios from 'axios'; import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; import Graph from 'components/Graph'; +import Spinner from 'components/Spinner'; import { QueryParams } from 'constants/query'; import ROUTES from 'constants/routes'; import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder'; @@ -12,16 +20,22 @@ import { } from 'hooks/useResourceAttribute/utils'; import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond'; import { colors } from 'lib/getRandomColor'; +import getStep from 'lib/getStep'; import history from 'lib/history'; import { useCallback, useMemo, useState } from 'react'; +import { useQueries, UseQueryResult } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation, useParams } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; +import { PayloadProps } from 'types/api/metrics/getServiceOverview'; +import { PayloadProps as PayloadPropsTopOpertions } from 'types/api/metrics/getTopOperations'; import { EQueryType } from 'types/common/dashboard'; -import MetricReducer from 'types/reducer/metrics'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { Tags } from 'types/reducer/trace'; import { v4 as uuid } from 'uuid'; +import { SOMETHING_WENT_WRONG } from '../../../constants/api'; import { getWidgetQueryBuilder } from '../MetricsApplication.factory'; import { errorPercentage, @@ -37,9 +51,17 @@ import { } from './util'; function Application(): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); const { servicename } = useParams<{ servicename?: string }>(); const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); const { search } = useLocation(); + const { queries } = useResourceAttribute(); + const selectedTags = useMemo( + () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [], + [queries], + ); const handleSetTimeStamp = useCallback((selectTime: number) => { setSelectedTimeStamp(selectTime); @@ -64,12 +86,53 @@ function Application(): JSX.Element { [handleSetTimeStamp], ); - const { topOperations, serviceOverview, topLevelOperations } = useSelector< - AppState, - MetricReducer - >((state) => state.metrics); + const queryResult = useQueries< + [ + UseQueryResult, + UseQueryResult, + UseQueryResult, + ] + >([ + { + queryKey: [servicename, selectedTags, minTime, maxTime], + queryFn: (): Promise => + getServiceOverview({ + service: servicename || '', + start: minTime, + end: maxTime, + step: getStep({ + start: minTime, + end: maxTime, + inputFormat: 'ns', + }), + selectedTags, + }), + }, + { + queryKey: [minTime, maxTime, servicename, selectedTags], + queryFn: (): Promise => + getTopOperations({ + service: servicename || '', + start: minTime, + end: maxTime, + selectedTags, + }), + }, + { + queryKey: [servicename, minTime, maxTime, selectedTags], + queryFn: (): Promise => getTopLevelOperations(), + }, + ]); - const { queries } = useResourceAttribute(); + const serviceOverview = queryResult[0].data; + const serviceOverviewError = queryResult[0].error; + const serviceOverviewIsError = queryResult[0].isError; + const serviceOverviewIsLoading = queryResult[0].isLoading; + const topOperations = queryResult[1].data; + const topLevelOperations = queryResult[2].data; + const topLevelOperationsError = queryResult[2].error; + const topLevelOperationsIsError = queryResult[2].isError; + const topLevelOperationsIsLoading = queryResult[2].isLoading; const selectedTraceTags: string = JSON.stringify( convertRawQueriesToTraceSelectedTags(queries) || [], @@ -89,7 +152,9 @@ function Application(): JSX.Element { builder: operationPerSec({ servicename, tagFilterItems, - topLevelOperations, + topLevelOperations: topLevelOperations + ? topLevelOperations[servicename || ''] + : [], }), clickhouse_sql: [], id: uuid(), @@ -105,7 +170,9 @@ function Application(): JSX.Element { builder: errorPercentage({ servicename, tagFilterItems, - topLevelOperations, + topLevelOperations: topLevelOperations + ? topLevelOperations[servicename || ''] + : [], }), clickhouse_sql: [], id: uuid(), @@ -158,8 +225,12 @@ function Application(): JSX.Element { [], ); - const dataSets = useMemo( - () => [ + const dataSets = useMemo(() => { + if (!serviceOverview) { + return []; + } + + return [ { data: serviceOverview.map((e) => parseFloat(convertToNanoSecondsToSecond(e.p99)), @@ -178,19 +249,25 @@ function Application(): JSX.Element { ), ...generalChartDataProperties('p50 Latency', 2), }, - ], - [generalChartDataProperties, serviceOverview], - ); + ]; + }, [generalChartDataProperties, serviceOverview]); - const data = useMemo( - () => ({ + const data = useMemo(() => { + if (!serviceOverview) { + return { + datasets: [], + labels: [], + }; + } + + return { datasets: dataSets, labels: serviceOverview.map( (e) => new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))), ), - }), - [serviceOverview, dataSets], - ); + }; + }, [serviceOverview, dataSets]); + return ( <> @@ -208,18 +285,33 @@ function Application(): JSX.Element { View Traces - Latency - - - + {serviceOverviewIsError ? ( + + {axios.isAxiosError(serviceOverviewError) + ? serviceOverviewError.response?.data + : SOMETHING_WENT_WRONG} + + ) : ( + <> + Latency + {serviceOverviewIsLoading && ( + + )} + {!serviceOverviewIsLoading && ( + + + + )} + + )} @@ -237,17 +329,28 @@ function Application(): JSX.Element { View Traces - Rate (ops/s) - - - + {topLevelOperationsIsError ? ( + + {axios.isAxiosError(topLevelOperationsError) + ? topLevelOperationsError.response?.data + : SOMETHING_WENT_WRONG} + + ) : ( + <> + Rate (ops/s) + + + + + )} @@ -265,23 +368,34 @@ function Application(): JSX.Element { - Error Percentage - - - + {topLevelOperationsIsError ? ( + + {axios.isAxiosError(topLevelOperationsError) + ? topLevelOperationsError.response?.data + : SOMETHING_WENT_WRONG} + + ) : ( + <> + Error Percentage + + + + + )} - + diff --git a/frontend/src/container/MetricsApplication/index.tsx b/frontend/src/container/MetricsApplication/index.tsx deleted file mode 100644 index 5709742b33..0000000000 --- a/frontend/src/container/MetricsApplication/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import RouteTab from 'components/RouteTab'; -import ROUTES from 'constants/routes'; -import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; -import history from 'lib/history'; -import { memo, useMemo } from 'react'; -import { generatePath, useParams } from 'react-router-dom'; -import { useLocation } from 'react-use'; - -import DBCall from './Tabs/DBCall'; -import External from './Tabs/External'; -import Overview from './Tabs/Overview'; - -function OverViewTab(): JSX.Element { - return ; -} - -function DbCallTab(): JSX.Element { - return ; -} - -function ExternalTab(): JSX.Element { - return ; -} - -function ServiceMetrics(): JSX.Element { - const { search } = useLocation(); - const { servicename } = useParams<{ servicename: string }>(); - - const searchParams = new URLSearchParams(search); - const tab = searchParams.get('tab'); - - const overMetrics = 'Overview Metrics'; - const dbCallMetrics = 'Database Calls'; - const externalMetrics = 'External Calls'; - - const getActiveKey = (): string => { - switch (tab) { - case null: { - return overMetrics; - } - case dbCallMetrics: { - return dbCallMetrics; - } - case externalMetrics: { - return externalMetrics; - } - default: { - return overMetrics; - } - } - }; - - const activeKey = getActiveKey(); - - const routes = useMemo( - () => [ - { - Component: OverViewTab, - name: overMetrics, - route: `${generatePath(ROUTES.SERVICE_METRICS, { - servicename, - })}?tab=${overMetrics}`, - }, - { - Component: DbCallTab, - name: dbCallMetrics, - route: `${generatePath(ROUTES.SERVICE_METRICS, { - servicename, - })}?tab=${dbCallMetrics}`, - }, - { - Component: ExternalTab, - name: externalMetrics, - route: `${generatePath(ROUTES.SERVICE_METRICS, { - servicename, - })}?tab=${externalMetrics}`, - }, - ], - [servicename], - ); - - return ( - <> - - - - ); -} - -export default memo(ServiceMetrics); diff --git a/frontend/src/pages/MetricApplication/index.tsx b/frontend/src/pages/MetricApplication/index.tsx deleted file mode 100644 index 9ed0fd3ab2..0000000000 --- a/frontend/src/pages/MetricApplication/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Typography } from 'antd'; -import Spinner from 'components/Spinner'; -import MetricsApplicationContainer from 'container/MetricsApplication'; -import useResourceAttribute from 'hooks/useResourceAttribute'; -import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - GetInitialData, - GetInitialDataProps, -} from 'store/actions/metrics/getInitialData'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import MetricReducer from 'types/reducer/metrics'; -import { Tags } from 'types/reducer/trace'; - -function MetricsApplication({ getInitialData }: MetricsProps): JSX.Element { - const { minTime, maxTime } = useSelector( - (state) => state.globalTime, - ); - const { error, errorMessage, metricsApplicationLoading } = useSelector< - AppState, - MetricReducer - >((state) => state.metrics); - - const { servicename } = useParams(); - const { queries } = useResourceAttribute(); - - const selectedTags = useMemo( - () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [], - [queries], - ); - - useEffect(() => { - if (servicename !== undefined) { - getInitialData({ - serviceName: servicename, - maxTime, - minTime, - selectedTags, - }); - } - }, [servicename, getInitialData, maxTime, minTime, selectedTags]); - - if (metricsApplicationLoading) { - return ; - } - - if (error) { - return {errorMessage}; - } - - return ; -} - -interface DispatchProps { - getInitialData: (props: GetInitialDataProps) => void; -} - -interface ServiceProps { - servicename?: string; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getInitialData: bindActionCreators(GetInitialData, dispatch), -}); - -type MetricsProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(MetricsApplication); diff --git a/frontend/src/pages/MetricsApplication/config.ts b/frontend/src/pages/MetricsApplication/config.ts new file mode 100644 index 0000000000..1e29113b86 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/config.ts @@ -0,0 +1,8 @@ +import { MetricsApplicationTab } from './types'; + +export const TAB_KEYS_VS_METRICS_APPLICATION_KEY = { + [MetricsApplicationTab.DB_CALL_METRICS]: MetricsApplicationTab.DB_CALL_METRICS, + [MetricsApplicationTab.EXTERNAL_METRICS]: + MetricsApplicationTab.EXTERNAL_METRICS, + [MetricsApplicationTab.OVER_METRICS]: MetricsApplicationTab.OVER_METRICS, +}; diff --git a/frontend/src/pages/MetricsApplication/index.tsx b/frontend/src/pages/MetricsApplication/index.tsx new file mode 100644 index 0000000000..230fbe9c8f --- /dev/null +++ b/frontend/src/pages/MetricsApplication/index.tsx @@ -0,0 +1,54 @@ +import RouteTab from 'components/RouteTab'; +import ROUTES from 'constants/routes'; +import DBCall from 'container/MetricsApplication/Tabs/DBCall'; +import External from 'container/MetricsApplication/Tabs/External'; +import Overview from 'container/MetricsApplication/Tabs/Overview'; +import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; +import history from 'lib/history'; +import { useMemo } from 'react'; +import { generatePath, useParams } from 'react-router-dom'; + +import { MetricsApplicationTab, TAB_KEY_VS_LABEL } from './types'; +import useMetricsApplicationTabKey from './useMetricsApplicationTabKey'; + +function MetricsApplication(): JSX.Element { + const { servicename } = useParams<{ servicename: string }>(); + + const activeKey = useMetricsApplicationTabKey(); + + const routes = useMemo( + () => [ + { + Component: Overview, + name: TAB_KEY_VS_LABEL[MetricsApplicationTab.OVER_METRICS], + route: `${generatePath(ROUTES.SERVICE_METRICS, { + servicename, + })}?tab=${MetricsApplicationTab.OVER_METRICS}`, + }, + { + Component: DBCall, + name: TAB_KEY_VS_LABEL[MetricsApplicationTab.DB_CALL_METRICS], + route: `${generatePath(ROUTES.SERVICE_METRICS, { + servicename, + })}?tab=${MetricsApplicationTab.DB_CALL_METRICS}`, + }, + { + Component: External, + name: TAB_KEY_VS_LABEL[MetricsApplicationTab.EXTERNAL_METRICS], + route: `${generatePath(ROUTES.SERVICE_METRICS, { + servicename, + })}?tab=${MetricsApplicationTab.EXTERNAL_METRICS}`, + }, + ], + [servicename], + ); + + return ( + <> + + + + ); +} + +export default MetricsApplication; diff --git a/frontend/src/pages/MetricsApplication/types.ts b/frontend/src/pages/MetricsApplication/types.ts new file mode 100644 index 0000000000..0bd7166eaa --- /dev/null +++ b/frontend/src/pages/MetricsApplication/types.ts @@ -0,0 +1,11 @@ +export enum MetricsApplicationTab { + OVER_METRICS = 'OVER_METRICS', + DB_CALL_METRICS = 'DB_CALL_METRICS', + EXTERNAL_METRICS = 'EXTERNAL_METRICS', +} + +export const TAB_KEY_VS_LABEL = { + [MetricsApplicationTab.OVER_METRICS]: 'Overview', + [MetricsApplicationTab.DB_CALL_METRICS]: 'DB Call Metrics', + [MetricsApplicationTab.EXTERNAL_METRICS]: 'External Metrics', +}; diff --git a/frontend/src/pages/MetricsApplication/useMetricsApplicationTabKey.tsx b/frontend/src/pages/MetricsApplication/useMetricsApplicationTabKey.tsx new file mode 100644 index 0000000000..ff2131f727 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/useMetricsApplicationTabKey.tsx @@ -0,0 +1,14 @@ +import useUrlQuery from 'hooks/useUrlQuery'; + +import { TAB_KEY_VS_LABEL } from './types'; +import { getMetricsApplicationKey } from './utils'; + +const useMetricsApplicationTabKey = (): string => { + const urlParams = useUrlQuery(); + + const tab = urlParams.get('tab'); + + return TAB_KEY_VS_LABEL[getMetricsApplicationKey(tab)]; +}; + +export default useMetricsApplicationTabKey; diff --git a/frontend/src/pages/MetricsApplication/utils.ts b/frontend/src/pages/MetricsApplication/utils.ts new file mode 100644 index 0000000000..ec47b3ca62 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/utils.ts @@ -0,0 +1,17 @@ +import { TAB_KEYS_VS_METRICS_APPLICATION_KEY } from './config'; +import { MetricsApplicationTab } from './types'; + +export const isMetricsApplicationTab = ( + tab: string, +): tab is MetricsApplicationTab => + Object.values(MetricsApplicationTab).includes(tab as MetricsApplicationTab); + +export const getMetricsApplicationKey = ( + tab: string | null, +): MetricsApplicationTab => { + if (tab && isMetricsApplicationTab(tab)) { + return TAB_KEYS_VS_METRICS_APPLICATION_KEY[tab]; + } + + return MetricsApplicationTab.OVER_METRICS; +}; diff --git a/frontend/src/store/actions/metrics/getInitialData.ts b/frontend/src/store/actions/metrics/getInitialData.ts deleted file mode 100644 index 205aed6ed4..0000000000 --- a/frontend/src/store/actions/metrics/getInitialData.ts +++ /dev/null @@ -1,137 +0,0 @@ -// import getDBOverView from 'api/metrics/getDBOverView'; -// import getExternalAverageDuration from 'api/metrics/getExternalAverageDuration'; -// import getExternalError from 'api/metrics/getExternalError'; -// import getExternalService from 'api/metrics/getExternalService'; -import getServiceOverview from 'api/metrics/getServiceOverview'; -import getTopLevelOperations from 'api/metrics/getTopLevelOperations'; -import getTopOperations from 'api/metrics/getTopOperations'; -import { AxiosError } from 'axios'; -import GetMinMax from 'lib/getMinMax'; -import getStep from 'lib/getStep'; -import { Dispatch } from 'redux'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { Props } from 'types/api/metrics/getDBOverview'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { Tags } from 'types/reducer/trace'; - -export const GetInitialData = ( - props: GetInitialDataProps, -): (( - dispatch: Dispatch, - getState: () => AppState, -) => void) => async (dispatch, getState): Promise => { - try { - const { globalTime } = getState(); - - /** - * @description This is because we keeping the store as source of truth - */ - if ( - props.maxTime !== globalTime.maxTime && - props.minTime !== globalTime.minTime - ) { - return; - } - - dispatch({ - type: 'GET_INITIAL_APPLICATION_LOADING', - }); - - const { maxTime, minTime } = GetMinMax(globalTime.selectedTime, [ - globalTime.minTime / 1000000, - globalTime.maxTime / 1000000, - ]); - - const [ - // getDBOverViewResponse, - // getExternalAverageDurationResponse, - // getExternalErrorResponse, - // getExternalServiceResponse, - getServiceOverviewResponse, - getTopOperationsResponse, - getTopLevelOperationsResponse, - ] = await Promise.all([ - // getDBOverView({ - // ...props, - // }), - // getExternalAverageDuration({ - // ...props, - // }), - // getExternalError({ - // ...props, - // }), - // getExternalService({ - // ...props, - // }), - getServiceOverview({ - end: maxTime, - service: props.serviceName, - start: minTime, - step: getStep({ start: minTime, end: maxTime, inputFormat: 'ns' }), - selectedTags: props.selectedTags, - }), - getTopOperations({ - end: maxTime, - service: props.serviceName, - start: minTime, - selectedTags: props.selectedTags, - }), - getTopLevelOperations({ - service: props.serviceName, - }), - ]); - - if ( - // getDBOverViewResponse.statusCode === 200 && - // getExternalAverageDurationResponse.statusCode === 200 && - // getExternalErrorResponse.statusCode === 200 && - // getExternalServiceResponse.statusCode === 200 && - getServiceOverviewResponse.statusCode === 200 && - getTopOperationsResponse.statusCode === 200 && - getTopLevelOperationsResponse.statusCode === 200 - ) { - dispatch({ - type: 'GET_INTIAL_APPLICATION_DATA', - payload: { - // dbOverView: getDBOverViewResponse.payload, - // externalAverageDuration: getExternalAverageDurationResponse.payload, - // externalError: getExternalErrorResponse.payload, - // externalService: getExternalServiceResponse.payload, - serviceOverview: getServiceOverviewResponse.payload, - topOperations: getTopOperationsResponse.payload, - topLevelOperations: getTopLevelOperationsResponse.payload, - }, - }); - } else { - dispatch({ - type: 'GET_INITIAL_APPLICATION_ERROR', - payload: { - errorMessage: - getTopOperationsResponse.error || - getServiceOverviewResponse.error || - getTopLevelOperationsResponse.error || - // getExternalServiceResponse.error || - // getExternalErrorResponse.error || - // getExternalAverageDurationResponse.error || - // getDBOverViewResponse.error || - 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'GET_INITIAL_APPLICATION_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } -}; - -export interface GetInitialDataProps { - serviceName: Props['service']; - maxTime: GlobalReducer['maxTime']; - minTime: GlobalReducer['minTime']; - selectedTags: Tags[]; -} diff --git a/frontend/src/store/reducers/global.ts b/frontend/src/store/reducers/global.ts index 6c430eefe0..121f27218b 100644 --- a/frontend/src/store/reducers/global.ts +++ b/frontend/src/store/reducers/global.ts @@ -12,7 +12,7 @@ import { } from 'types/actions/logs'; import { GlobalReducer } from 'types/reducer/globalTime'; -const intitalState: GlobalReducer = { +const initialState: GlobalReducer = { maxTime: Date.now() * 1000000, minTime: (Date.now() - 15 * 60 * 1000) * 1000000, loading: true, @@ -24,7 +24,7 @@ const intitalState: GlobalReducer = { }; const globalTimeReducer = ( - state = intitalState, + state = initialState, action: GlobalTimeAction, ): GlobalReducer => { switch (action.type) { From 7f162e53816846882cd8bed0222019d32c2f25eb Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 13 Jul 2023 16:23:51 +0530 Subject: [PATCH 42/48] chore: limit filter is disabled for now (#3124) Co-authored-by: Vishal Sharma --- .../filters/LimitFilter/LimitFilter.tsx | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx index a96107fc84..ceda8a6a61 100644 --- a/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx +++ b/frontend/src/container/QueryBuilder/filters/LimitFilter/LimitFilter.tsx @@ -1,8 +1,8 @@ -import { InputNumber } from 'antd'; -import { useMemo } from 'react'; +import { InputNumber, Tooltip } from 'antd'; +// import { useMemo } from 'react'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource } from 'types/common/queryBuilder'; +// import { DataSource } from 'types/common/queryBuilder'; import { selectStyle } from '../QueryBuilderSearch/config'; function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { @@ -21,21 +21,25 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element { } }; - const isMetricsDataSource = useMemo( - () => query.dataSource === DataSource.METRICS, - [query.dataSource], - ); + // const isMetricsDataSource = useMemo( + // () => query.dataSource === DataSource.METRICS, + // [query.dataSource], + // ); + + // const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key; return ( - + + + ); } From 60c0836d3ed99610d1f2dafbbb7bbc7b3c489977 Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 13 Jul 2023 17:48:52 +0530 Subject: [PATCH 43/48] chore: placeholder text is updated (#3127) --- .../QueryBuilder/filters/QueryBuilderSearch/constant.ts | 2 ++ .../QueryBuilder/filters/QueryBuilderSearch/index.tsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/constant.ts diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/constant.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/constant.ts new file mode 100644 index 0000000000..f042244a69 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/constant.ts @@ -0,0 +1,2 @@ +export const PLACEHOLDER = + 'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options'; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index 093e71df7a..8b3e8e54ea 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -18,6 +18,7 @@ import { DataSource } from 'types/common/queryBuilder'; import { v4 as uuid } from 'uuid'; import { selectStyle } from './config'; +import { PLACEHOLDER } from './constant'; import { StyledCheckOutlined, TypographyText } from './style'; import { getOperatorValue, @@ -155,7 +156,7 @@ function QueryBuilderSearch({ filterOption={false} autoClearSearchValue={false} mode="multiple" - placeholder="Search Filter" + placeholder={PLACEHOLDER} value={queryTags} searchValue={searchValue} disabled={isMetricsDataSource && !query.aggregateAttribute.key} From d26022efb1b47ee12e149ef940fd418995ddd313 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Thu, 13 Jul 2023 15:55:43 +0300 Subject: [PATCH 44/48] fix: exporer log details action buttons (#3126) * fix: exporer log details action buttons * chore: magic strings is removed --------- Co-authored-by: Palash Gupta --- .../LogDetail/LogDetail.interfaces.ts | 4 +- frontend/src/components/LogDetail/index.tsx | 9 +- .../src/components/Logs/AddToQueryHOC.tsx | 5 +- .../container/LogDetailedView/ActionItem.tsx | 169 ++++-------------- .../container/LogDetailedView/TableView.tsx | 20 ++- .../src/container/LogDetailedView/index.tsx | 143 +++++++++++++-- .../src/container/LogsExplorerViews/index.tsx | 17 +- frontend/src/container/LogsTable/index.tsx | 23 +-- frontend/src/hooks/queryBuilder/useTag.ts | 8 +- .../src/lib/getGeneratedFilterQueryString.ts | 24 +++ frontend/src/lib/removeJSONStringifyQuotes.ts | 10 ++ 11 files changed, 244 insertions(+), 188 deletions(-) create mode 100644 frontend/src/lib/getGeneratedFilterQueryString.ts create mode 100644 frontend/src/lib/removeJSONStringifyQuotes.ts diff --git a/frontend/src/components/LogDetail/LogDetail.interfaces.ts b/frontend/src/components/LogDetail/LogDetail.interfaces.ts index 2e1597f905..198e2abdcd 100644 --- a/frontend/src/components/LogDetail/LogDetail.interfaces.ts +++ b/frontend/src/components/LogDetail/LogDetail.interfaces.ts @@ -1,7 +1,9 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC'; +import { ActionItemProps } from 'container/LogDetailedView/ActionItem'; import { ILog } from 'types/api/logs/log'; export type LogDetailProps = { log: ILog | null; onClose: () => void; -} & Pick; +} & Pick & + Pick; diff --git a/frontend/src/components/LogDetail/index.tsx b/frontend/src/components/LogDetail/index.tsx index 6fb43b1218..f8a320e8e9 100644 --- a/frontend/src/components/LogDetail/index.tsx +++ b/frontend/src/components/LogDetail/index.tsx @@ -8,6 +8,7 @@ function LogDetail({ log, onClose, onAddToQuery, + onClickActionItem, }: LogDetailProps): JSX.Element { const onDrawerClose = (): void => { onClose(); @@ -17,7 +18,13 @@ function LogDetail({ { label: 'Table', key: '1', - children: log && , + children: log && ( + + ), }, { label: 'JSON', diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index d4d1f9bd31..35fed9c92a 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -1,4 +1,5 @@ import { Popover } from 'antd'; +import { OPERATORS } from 'constants/queryBuilder'; import { memo, ReactNode, useCallback, useMemo } from 'react'; import { ButtonContainer } from './styles'; @@ -10,7 +11,7 @@ function AddToQueryHOC({ children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = useCallback(() => { - onAddToQuery(fieldKey, fieldValue); + onAddToQuery(fieldKey, fieldValue, OPERATORS.IN); }, [fieldKey, fieldValue, onAddToQuery]); const popOverContent = useMemo(() => Add to query: {fieldKey}, [ @@ -29,7 +30,7 @@ function AddToQueryHOC({ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; - onAddToQuery: (fieldKey: string, fieldValue: string) => void; + onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; children: ReactNode; } diff --git a/frontend/src/container/LogDetailedView/ActionItem.tsx b/frontend/src/container/LogDetailedView/ActionItem.tsx index 2d2350a324..8c476c2c99 100644 --- a/frontend/src/container/LogDetailedView/ActionItem.tsx +++ b/frontend/src/container/LogDetailedView/ActionItem.tsx @@ -1,148 +1,43 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; import { Button, Col, Popover } from 'antd'; -import getStep from 'lib/getStep'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import { getIdConditions } from 'pages/Logs/utils'; -import { memo, useMemo } from 'react'; -import { connect, useDispatch, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { getLogs } from 'store/actions/logs/getLogs'; -import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { ILogsReducer } from 'types/reducer/logs'; +import { OPERATORS } from 'constants/queryBuilder'; +import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; +import { memo, useCallback, useMemo } from 'react'; -const removeJSONStringifyQuotes = (s: string): string => { - if (!s || !s.length) { - return s; - } - - if (s[0] === '"' && s[s.length - 1] === '"') { - return s.slice(1, s.length - 1); - } - return s; -}; - -interface ActionItemProps { - fieldKey: string; - fieldValue: string; - getLogs: (props: Parameters[0]) => ReturnType; - getLogsAggregate: ( - props: Parameters[0], - ) => ReturnType; -} function ActionItem({ fieldKey, fieldValue, - getLogs, - getLogsAggregate, -}: ActionItemProps): JSX.Element | unknown { - const { - searchFilter: { queryString }, - logLinesPerPage, - idStart, - liveTail, - idEnd, - order, - } = useSelector((store) => store.logs); - const dispatch = useDispatch>(); + onClickActionItem, +}: ActionItemProps): JSX.Element { + const handleClick = useCallback( + (operator: string) => { + const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); - const { maxTime, minTime } = useSelector( - (state) => state.globalTime, + onClickActionItem(fieldKey, validatedFieldValue, operator); + }, + [onClickActionItem, fieldKey, fieldValue], ); - const handleQueryAdd = (newQueryString: string): void => { - let updatedQueryString = queryString || ''; + const onClickHandler = useCallback( + (operator: string) => (): void => { + handleClick(operator); + }, + [handleClick], + ); - if (updatedQueryString.length === 0) { - updatedQueryString += `${newQueryString}`; - } else { - updatedQueryString += ` AND ${newQueryString}`; - } - dispatch({ - type: SET_SEARCH_QUERY_STRING, - payload: { - searchQueryString: updatedQueryString, - }, - }); - - if (liveTail === 'STOPPED') { - getLogs({ - q: updatedQueryString, - limit: logLinesPerPage, - orderBy: 'timestamp', - order, - timestampStart: minTime, - timestampEnd: maxTime, - ...getIdConditions(idStart, idEnd, order), - }); - getLogsAggregate({ - timestampStart: minTime, - timestampEnd: maxTime, - step: getStep({ - start: minTime, - end: maxTime, - inputFormat: 'ns', - }), - q: updatedQueryString, - }); - } else if (liveTail === 'PLAYING') { - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: 'PAUSED', - }); - setTimeout( - () => - dispatch({ - type: TOGGLE_LIVE_TAIL, - payload: liveTail, - }), - 0, - ); - } - }; - const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); const PopOverMenuContent = useMemo( () => ( -
- ), - // eslint-disable-next-line react-hooks/exhaustive-deps - [fieldKey, validatedFieldValue], + [onClickHandler], ); return ( @@ -152,19 +47,15 @@ function ActionItem({ ); } -interface DispatchProps { - getLogs: (props: Parameters[0]) => (dispatch: never) => void; - getLogsAggregate: ( - props: Parameters[0], - ) => (dispatch: never) => void; + +export interface ActionItemProps { + fieldKey: string; + fieldValue: string; + onClickActionItem: ( + fieldKey: string, + fieldValue: string, + operator: string, + ) => void; } -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getLogs: bindActionCreators(getLogs, dispatch), - getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default connect(null, mapDispatchToProps)(memo(ActionItem as any)); +export default memo(ActionItem); diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index b027c0e523..0d861bcebe 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -20,7 +20,7 @@ import AppActions from 'types/actions'; import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; import { ILog } from 'types/api/logs/log'; -import ActionItem from './ActionItem'; +import ActionItem, { ActionItemProps } from './ActionItem'; import { flattenObject, recursiveParseJSON } from './utils'; // Fields which should be restricted from adding it to query @@ -30,9 +30,15 @@ interface TableViewProps { logData: ILog; } -type Props = TableViewProps & Pick; +type Props = TableViewProps & + Pick & + Pick; -function TableView({ logData, onAddToQuery }: Props): JSX.Element | null { +function TableView({ + logData, + onAddToQuery, + onClickActionItem, +}: Props): JSX.Element | null { const [fieldSearchInput, setFieldSearchInput] = useState(''); const dispatch = useDispatch>(); @@ -89,7 +95,13 @@ function TableView({ logData, onAddToQuery }: Props): JSX.Element | null { render: (fieldData: Record): JSX.Element | null => { const fieldKey = fieldData.field.split('.').slice(-1); if (!RESTRICTED_FIELDS.includes(fieldKey[0])) { - return ; + return ( + + ); } return null; }, diff --git a/frontend/src/container/LogDetailedView/index.tsx b/frontend/src/container/LogDetailedView/index.tsx index 25ccf8c50c..fe5b2cd3af 100644 --- a/frontend/src/container/LogDetailedView/index.tsx +++ b/frontend/src/container/LogDetailedView/index.tsx @@ -1,21 +1,49 @@ import LogDetail from 'components/LogDetail'; import ROUTES from 'constants/routes'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; -import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; +import getStep from 'lib/getStep'; +import { getIdConditions } from 'pages/Logs/utils'; +import { memo, useCallback } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Dispatch } from 'redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { getLogs } from 'store/actions/logs/getLogs'; +import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { SET_DETAILED_LOG_DATA } from 'types/actions/logs'; +import { + SET_DETAILED_LOG_DATA, + SET_SEARCH_QUERY_STRING, + TOGGLE_LIVE_TAIL, +} from 'types/actions/logs'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { ILogsReducer } from 'types/reducer/logs'; -function LogDetailedView(): JSX.Element { +type LogDetailedViewProps = { + getLogs: (props: Parameters[0]) => ReturnType; + getLogsAggregate: ( + props: Parameters[0], + ) => ReturnType; +}; + +function LogDetailedView({ + getLogs, + getLogsAggregate, +}: LogDetailedViewProps): JSX.Element { const history = useHistory(); const { detailedLog, searchFilter: { queryString }, + logLinesPerPage, + idStart, + liveTail, + idEnd, + order, } = useSelector((state) => state.logs); + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); const dispatch = useDispatch>(); @@ -26,32 +54,109 @@ function LogDetailedView(): JSX.Element { }); }; - const handleQueryAdd = useCallback( - (fieldKey: string, fieldValue: string) => { - const generatedQuery = generateFilterQuery({ + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string) => { + const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - type: 'IN', - }); + operator, + queryString, + ); - let updatedQueryString = queryString || ''; - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); }, [history, queryString], ); + const handleClickActionItem = useCallback( + (fieldKey: string, fieldValue: string, operator: string): void => { + const updatedQueryString = getGeneratedFilterQueryString( + fieldKey, + fieldValue, + operator, + queryString, + ); + + dispatch({ + type: SET_SEARCH_QUERY_STRING, + payload: { + searchQueryString: updatedQueryString, + }, + }); + + if (liveTail === 'STOPPED') { + getLogs({ + q: updatedQueryString, + limit: logLinesPerPage, + orderBy: 'timestamp', + order, + timestampStart: minTime, + timestampEnd: maxTime, + ...getIdConditions(idStart, idEnd, order), + }); + getLogsAggregate({ + timestampStart: minTime, + timestampEnd: maxTime, + step: getStep({ + start: minTime, + end: maxTime, + inputFormat: 'ns', + }), + q: updatedQueryString, + }); + } else if (liveTail === 'PLAYING') { + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: 'PAUSED', + }); + setTimeout( + () => + dispatch({ + type: TOGGLE_LIVE_TAIL, + payload: liveTail, + }), + 0, + ); + } + }, + [ + dispatch, + getLogs, + getLogsAggregate, + idEnd, + idStart, + liveTail, + logLinesPerPage, + maxTime, + minTime, + order, + queryString, + ], + ); + return ( ); } -export default LogDetailedView; +interface DispatchProps { + getLogs: (props: Parameters[0]) => (dispatch: never) => void; + getLogsAggregate: ( + props: Parameters[0], + ) => (dispatch: never) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getLogs: bindActionCreators(getLogs, dispatch), + getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch), +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any)); diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 580b80e200..e626f71d8c 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -5,6 +5,7 @@ import TabLabel from 'components/TabLabel'; import { QueryParams } from 'constants/query'; import { initialQueriesMap, + OPERATORS, PANEL_TYPES, QueryBuilderKeys, } from 'constants/queryBuilder'; @@ -226,8 +227,8 @@ function LogsExplorerViews(): JSX.Element { [currentStagedQueryData, orderByTimestamp], ); - const handleAddQuery = useCallback( - (fieldKey: string, fieldValue: string): void => { + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string): void => { const keysAutocomplete: BaseAutocompleteData[] = queryClient.getQueryData>( [QueryBuilderKeys.GET_AGGREGATE_KEYS], @@ -239,6 +240,9 @@ function LogsExplorerViews(): JSX.Element { fieldKey, ); + const currentOperator = + Object.keys(OPERATORS).find((op) => op === operator) || ''; + const nextQuery: Query = { ...currentQuery, builder: { @@ -254,7 +258,7 @@ function LogsExplorerViews(): JSX.Element { { id: uuid(), key: existAutocompleteKey, - op: '=', + op: currentOperator, value: fieldValue, }, ], @@ -422,7 +426,7 @@ function LogsExplorerViews(): JSX.Element { onOpenDetailedView={handleSetActiveLog} onEndReached={handleEndReached} onExpand={handleSetActiveLog} - onAddToQuery={handleAddQuery} + onAddToQuery={handleAddToQuery} /> ), }, @@ -453,7 +457,7 @@ function LogsExplorerViews(): JSX.Element { logs, handleSetActiveLog, handleEndReached, - handleAddQuery, + handleAddToQuery, data, isError, ], @@ -506,7 +510,8 @@ function LogsExplorerViews(): JSX.Element { ); diff --git a/frontend/src/container/LogsTable/index.tsx b/frontend/src/container/LogsTable/index.tsx index 94a4ee9d6c..73fdaa8b4b 100644 --- a/frontend/src/container/LogsTable/index.tsx +++ b/frontend/src/container/LogsTable/index.tsx @@ -7,7 +7,7 @@ import Spinner from 'components/Spinner'; import ROUTES from 'constants/routes'; import { contentStyle } from 'container/Trace/Search/config'; import useFontFaceObserver from 'hooks/useFontObserver'; -import { generateFilterQuery } from 'lib/logs/generateFilterQuery'; +import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString'; import { memo, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -77,20 +77,15 @@ function LogsTable(props: LogsTableProps): JSX.Element { [dispatch], ); - const handleQueryAdd = useCallback( - (fieldKey: string, fieldValue: string) => { - const generatedQuery = generateFilterQuery({ + const handleAddToQuery = useCallback( + (fieldKey: string, fieldValue: string, operator: string) => { + const updatedQueryString = getGeneratedFilterQueryString( fieldKey, fieldValue, - type: 'IN', - }); + operator, + queryString, + ); - let updatedQueryString = queryString || ''; - if (updatedQueryString.length === 0) { - updatedQueryString += `${generatedQuery}`; - } else { - updatedQueryString += ` AND ${generatedQuery}`; - } history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`); }, [history, queryString], @@ -117,7 +112,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { logData={log} selectedFields={selected} onOpenDetailedView={handleOpenDetailedView} - onAddToQuery={handleQueryAdd} + onAddToQuery={handleAddToQuery} /> ); }, @@ -128,7 +123,7 @@ function LogsTable(props: LogsTableProps): JSX.Element { linesPerRow, onClickExpand, handleOpenDetailedView, - handleQueryAdd, + handleAddToQuery, ], ); diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts index 0a00c81a4c..3bc272fc86 100644 --- a/frontend/src/hooks/queryBuilder/useTag.ts +++ b/frontend/src/hooks/queryBuilder/useTag.ts @@ -34,8 +34,12 @@ export const useTag = ( () => (query?.filters?.items || []).map((ele) => { if (isInNInOperator(getOperatorFromValue(ele.op))) { - const csvString = Papa.unparse([ele.value]); - return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`; + try { + const csvString = Papa.unparse([ele.value]); + return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${csvString}`; + } catch { + return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`; + } } return `${ele.key?.key} ${getOperatorFromValue(ele.op)} ${ele.value}`; }), diff --git a/frontend/src/lib/getGeneratedFilterQueryString.ts b/frontend/src/lib/getGeneratedFilterQueryString.ts new file mode 100644 index 0000000000..ea737c0b14 --- /dev/null +++ b/frontend/src/lib/getGeneratedFilterQueryString.ts @@ -0,0 +1,24 @@ +import { generateFilterQuery } from './logs/generateFilterQuery'; + +export const getGeneratedFilterQueryString = ( + fieldKey: string, + fieldValue: string, + operator: string, + queryString: string, +): string => { + let updatedQueryString = queryString || ''; + + const generatedString = generateFilterQuery({ + fieldKey, + fieldValue, + type: operator, + }); + + if (updatedQueryString.length === 0) { + updatedQueryString += `${generatedString}`; + } else { + updatedQueryString += ` AND ${generatedString}`; + } + + return updatedQueryString; +}; diff --git a/frontend/src/lib/removeJSONStringifyQuotes.ts b/frontend/src/lib/removeJSONStringifyQuotes.ts new file mode 100644 index 0000000000..5dd8c7af50 --- /dev/null +++ b/frontend/src/lib/removeJSONStringifyQuotes.ts @@ -0,0 +1,10 @@ +export const removeJSONStringifyQuotes = (s: string): string => { + if (!s || !s.length) { + return s; + } + + if (s[0] === '"' && s[s.length - 1] === '"') { + return s.slice(1, s.length - 1); + } + return s; +}; From 6b77165d09f10668c7cc500693b380fa0c0190cb Mon Sep 17 00:00:00 2001 From: Palash Gupta Date: Thu, 13 Jul 2023 18:33:47 +0530 Subject: [PATCH 45/48] fix: on delete current query length should be more than 1 (#3125) Co-authored-by: Vishal Sharma --- frontend/src/hooks/queryBuilder/useQueryOperations.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/hooks/queryBuilder/useQueryOperations.ts b/frontend/src/hooks/queryBuilder/useQueryOperations.ts index 38ed75fb0a..04c2002d30 100644 --- a/frontend/src/hooks/queryBuilder/useQueryOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryOperations.ts @@ -27,6 +27,7 @@ export const useQueryOperations: UseQueryOperations = ({ removeQueryBuilderEntityByIndex, panelType, initialDataSource, + currentQuery, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState< @@ -121,8 +122,10 @@ export const useQueryOperations: UseQueryOperations = ({ ); const handleDeleteQuery = useCallback(() => { - removeQueryBuilderEntityByIndex('queryData', index); - }, [removeQueryBuilderEntityByIndex, index]); + if (currentQuery.builder.queryData.length > 1) { + removeQueryBuilderEntityByIndex('queryData', index); + } + }, [removeQueryBuilderEntityByIndex, index, currentQuery]); const handleChangeQueryData: HandleChangeQueryData = useCallback( (key, value) => { From 216499051dd737b7587c4697dab74fc30357dc77 Mon Sep 17 00:00:00 2001 From: Srikanth Chekuri Date: Thu, 13 Jul 2023 18:50:19 +0530 Subject: [PATCH 46/48] Add support for Delta temporality (#2505) * Revert "upgraded some deprecated packages (#2423)" This reverts commit c4b052c51ec3a22e25923aa92fb66665e6ca589b. * chore: delta working with QB * chore: use enum * Revert "Revert "upgraded some deprecated packages (#2423)"" This reverts commit d88f3594a62582a16fa04528a56ff075f628bf4e. * chore: add test * chore: add delta for query range v3 * chore: explicit check for temporality * chore: fix tests * chore: conditionally fetch temporality when --prefer-delta is set --- ee/query-service/app/api/api.go | 2 + ee/query-service/app/server.go | 2 + ee/query-service/main.go | 3 + .../app/clickhouseReader/reader.go | 26 +++ pkg/query-service/app/http_handler.go | 55 ++++++ pkg/query-service/app/metrics/v3/delta.go | 174 ++++++++++++++++++ .../app/metrics/v3/query_builder.go | 22 ++- .../app/metrics/v3/query_builder_test.go | 13 +- .../app/queryBuilder/query_builder_test.go | 165 ++++++++++++++++- pkg/query-service/app/server.go | 2 + pkg/query-service/interfaces/interface.go | 1 + pkg/query-service/main.go | 4 + pkg/query-service/model/v3/v3.go | 9 + 13 files changed, 467 insertions(+), 11 deletions(-) create mode 100644 pkg/query-service/app/metrics/v3/delta.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index df1220d80c..2eddf1d83c 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -17,6 +17,7 @@ import ( type APIHandlerOptions struct { DataConnector interfaces.DataConnector SkipConfig *basemodel.SkipConfig + PreferDelta bool AppDao dao.ModelDao RulesManager *rules.Manager FeatureFlags baseint.FeatureLookup @@ -34,6 +35,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) { baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{ Reader: opts.DataConnector, SkipConfig: opts.SkipConfig, + PerferDelta: opts.PreferDelta, AppDao: opts.AppDao, RuleManager: opts.RulesManager, FeatureFlags: opts.FeatureFlags}) diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index ec2895acd8..a2e86023e3 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -56,6 +56,7 @@ type ServerOptions struct { // alert specific params DisableRules bool RuleRepoURL string + PreferDelta bool } // Server runs HTTP api service @@ -170,6 +171,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { apiOpts := api.APIHandlerOptions{ DataConnector: reader, SkipConfig: skipConfig, + PreferDelta: serverOptions.PreferDelta, AppDao: modelDao, RulesManager: rm, FeatureFlags: lm, diff --git a/ee/query-service/main.go b/ee/query-service/main.go index 67cbde2151..52ce63ba20 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -83,10 +83,12 @@ func main() { var ruleRepoURL string var enableQueryServiceLogOTLPExport bool + var preferDelta bool flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") + flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over raw metrics)") flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.Parse() @@ -102,6 +104,7 @@ func main() { HTTPHostPort: baseconst.HTTPHostPort, PromConfigPath: promConfigPath, SkipTopLvlOpsPath: skipTopLvlOpsPath, + PreferDelta: preferDelta, PrivateHostPort: baseconst.PrivateHostPort, DisableRules: disableRules, RuleRepoURL: ruleRepoURL, diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 0a32730f97..c537cac8ef 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -3249,6 +3249,32 @@ func (r *ClickHouseReader) GetSpansInLastHeartBeatInterval(ctx context.Context) return spansInLastHeartBeatInterval, nil } +func (r *ClickHouseReader) FetchTemporality(ctx context.Context, metricNames []string) (map[string]map[v3.Temporality]bool, error) { + + metricNameToTemporality := make(map[string]map[v3.Temporality]bool) + + query := fmt.Sprintf(`SELECT DISTINCT metric_name, temporality FROM %s.%s WHERE metric_name IN [$1]`, signozMetricDBName, signozTSTableName) + + rows, err := r.db.Query(ctx, query, metricNames) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var metricName, temporality string + err := rows.Scan(&metricName, &temporality) + if err != nil { + return nil, err + } + if _, ok := metricNameToTemporality[metricName]; !ok { + metricNameToTemporality[metricName] = make(map[v3.Temporality]bool) + } + metricNameToTemporality[metricName][v3.Temporality(temporality)] = true + } + return metricNameToTemporality, nil +} + // func sum(array []tsByMetricName) uint64 { // var result uint64 // result = 0 diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index e4803b4c2e..47e115343f 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -71,6 +71,7 @@ type APIHandler struct { featureFlags interfaces.FeatureLookup ready func(http.HandlerFunc) http.HandlerFunc queryBuilder *queryBuilder.QueryBuilder + preferDelta bool // SetupCompleted indicates if SigNoz is ready for general use. // at the moment, we mark the app ready when the first user @@ -84,6 +85,8 @@ type APIHandlerOpts struct { Reader interfaces.Reader SkipConfig *model.SkipConfig + + PerferDelta bool // dao layer to perform crud on app objects like dashboard, alerts etc AppDao dao.ModelDao @@ -106,6 +109,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { reader: opts.Reader, appDao: opts.AppDao, skipConfig: opts.SkipConfig, + preferDelta: opts.PerferDelta, alertManager: alertManager, ruleManager: opts.RuleManager, featureFlags: opts.FeatureFlags, @@ -452,6 +456,48 @@ func (aH *APIHandler) metricAutocompleteTagValue(w http.ResponseWriter, r *http. aH.Respond(w, tagValueList) } +func (aH *APIHandler) addTemporality(ctx context.Context, qp *v3.QueryRangeParamsV3) error { + + metricNames := make([]string, 0) + metricNameToTemporality := make(map[string]map[v3.Temporality]bool) + if qp.CompositeQuery != nil && len(qp.CompositeQuery.BuilderQueries) > 0 { + for _, query := range qp.CompositeQuery.BuilderQueries { + if query.DataSource == v3.DataSourceMetrics { + metricNames = append(metricNames, query.AggregateAttribute.Key) + if _, ok := metricNameToTemporality[query.AggregateAttribute.Key]; !ok { + metricNameToTemporality[query.AggregateAttribute.Key] = make(map[v3.Temporality]bool) + } + } + } + } + + var err error + + if aH.preferDelta { + zap.S().Debug("fetching metric temporality") + metricNameToTemporality, err = aH.reader.FetchTemporality(ctx, metricNames) + if err != nil { + return err + } + } + + if qp.CompositeQuery != nil && len(qp.CompositeQuery.BuilderQueries) > 0 { + for name := range qp.CompositeQuery.BuilderQueries { + query := qp.CompositeQuery.BuilderQueries[name] + if query.DataSource == v3.DataSourceMetrics { + if aH.preferDelta && metricNameToTemporality[query.AggregateAttribute.Key][v3.Delta] { + query.Temporality = v3.Delta + } else if metricNameToTemporality[query.AggregateAttribute.Key][v3.Cumulative] { + query.Temporality = v3.Cumulative + } else { + query.Temporality = v3.Unspecified + } + } + } + } + return nil +} + func (aH *APIHandler) QueryRangeMetricsV2(w http.ResponseWriter, r *http.Request) { metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r) @@ -2776,6 +2822,15 @@ func (aH *APIHandler) QueryRangeV3(w http.ResponseWriter, r *http.Request) { return } + // add temporality for each metric + + temporalityErr := aH.addTemporality(r.Context(), queryRangeParams) + if temporalityErr != nil { + zap.S().Errorf("Error while adding temporality for metrics: %v", temporalityErr) + RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: temporalityErr}, nil) + return + } + aH.queryRangeV3(r.Context(), queryRangeParams, w, r) } diff --git a/pkg/query-service/app/metrics/v3/delta.go b/pkg/query-service/app/metrics/v3/delta.go new file mode 100644 index 0000000000..ba3d2783ce --- /dev/null +++ b/pkg/query-service/app/metrics/v3/delta.go @@ -0,0 +1,174 @@ +package v3 + +import ( + "fmt" + + "go.signoz.io/signoz/pkg/query-service/constants" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" + "go.signoz.io/signoz/pkg/query-service/utils" +) + +func buildDeltaMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName string) (string, error) { + + metricQueryGroupBy := mq.GroupBy + + // if the aggregate operator is a histogram quantile, and user has not forgotten + // the le tag in the group by then add the le tag to the group by + if mq.AggregateOperator == v3.AggregateOperatorHistQuant50 || + mq.AggregateOperator == v3.AggregateOperatorHistQuant75 || + mq.AggregateOperator == v3.AggregateOperatorHistQuant90 || + mq.AggregateOperator == v3.AggregateOperatorHistQuant95 || + mq.AggregateOperator == v3.AggregateOperatorHistQuant99 { + found := false + for _, tag := range mq.GroupBy { + if tag.Key == "le" { + found = true + break + } + } + if !found { + metricQueryGroupBy = append( + metricQueryGroupBy, + v3.AttributeKey{ + Key: "le", + DataType: v3.AttributeKeyDataTypeString, + Type: v3.AttributeKeyTypeTag, + IsColumn: false, + }, + ) + } + } + + if mq.Filters != nil { + mq.Filters.Items = append(mq.Filters.Items, v3.FilterItem{ + Key: v3.AttributeKey{Key: "__temporality__"}, + Operator: v3.FilterOperatorEqual, + Value: "Delta", + }) + } + + filterSubQuery, err := buildMetricsTimeSeriesFilterQuery(mq.Filters, metricQueryGroupBy, mq) + if err != nil { + return "", err + } + + samplesTableTimeFilter := fmt.Sprintf("metric_name = %s AND timestamp_ms >= %d AND timestamp_ms <= %d", utils.ClickHouseFormattedValue(mq.AggregateAttribute.Key), start, end) + + // Select the aggregate value for interval + queryTmpl := + "SELECT %s" + + " toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL %d SECOND) as ts," + + " %s as value" + + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_TABLENAME + + " GLOBAL INNER JOIN" + + " (%s) as filtered_time_series" + + " USING fingerprint" + + " WHERE " + samplesTableTimeFilter + + " GROUP BY %s" + + " ORDER BY %s ts" + + // tagsWithoutLe is used to group by all tags except le + // This is done because we want to group by le only when we are calculating quantile + // Otherwise, we want to group by all tags except le + tagsWithoutLe := []string{} + for _, tag := range mq.GroupBy { + if tag.Key != "le" { + tagsWithoutLe = append(tagsWithoutLe, tag.Key) + } + } + + groupByWithoutLe := groupBy(tagsWithoutLe...) + groupTagsWithoutLe := groupSelect(tagsWithoutLe...) + orderWithoutLe := orderBy(mq.OrderBy, tagsWithoutLe) + + groupBy := groupByAttributeKeyTags(metricQueryGroupBy...) + groupTags := groupSelectAttributeKeyTags(metricQueryGroupBy...) + orderBy := orderByAttributeKeyTags(mq.OrderBy, metricQueryGroupBy) + + if len(orderBy) != 0 { + orderBy += "," + } + if len(orderWithoutLe) != 0 { + orderWithoutLe += "," + } + + switch mq.AggregateOperator { + case v3.AggregateOperatorRate: + // Calculate rate of change of metric for each unique time series + groupBy = "fingerprint, ts" + orderBy = "fingerprint, " + groupTags = "fingerprint," + op := fmt.Sprintf("sum(value)/%d", step) + query := fmt.Sprintf( + queryTmpl, "any(labels) as fullLabels, "+groupTags, step, op, filterSubQuery, groupBy, orderBy, + ) // labels will be same so any should be fine + + return query, nil + case v3.AggregateOperatorSumRate, v3.AggregateOperatorAvgRate, v3.AggregateOperatorMaxRate, v3.AggregateOperatorMinRate: + op := fmt.Sprintf("%s(value)/%d", aggregateOperatorToSQLFunc[mq.AggregateOperator], step) + query := fmt.Sprintf( + queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy, + ) + return query, nil + case + v3.AggregateOperatorRateSum, + v3.AggregateOperatorRateMax, + v3.AggregateOperatorRateAvg, + v3.AggregateOperatorRateMin: + op := fmt.Sprintf("%s(value)/%d", aggregateOperatorToSQLFunc[mq.AggregateOperator], step) + query := fmt.Sprintf( + queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy, + ) + return query, nil + case + v3.AggregateOperatorP05, + v3.AggregateOperatorP10, + v3.AggregateOperatorP20, + v3.AggregateOperatorP25, + v3.AggregateOperatorP50, + v3.AggregateOperatorP75, + v3.AggregateOperatorP90, + v3.AggregateOperatorP95, + v3.AggregateOperatorP99: + op := fmt.Sprintf("quantile(%v)(value)", aggregateOperatorToPercentile[mq.AggregateOperator]) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + return query, nil + case v3.AggregateOperatorHistQuant50, v3.AggregateOperatorHistQuant75, v3.AggregateOperatorHistQuant90, v3.AggregateOperatorHistQuant95, v3.AggregateOperatorHistQuant99: + op := fmt.Sprintf("sum(value)/%d", step) + query := fmt.Sprintf( + queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy, + ) // labels will be same so any should be fine + value := aggregateOperatorToPercentile[mq.AggregateOperator] + + query = fmt.Sprintf(`SELECT %s ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), %.3f) as value FROM (%s) GROUP BY %s ORDER BY %s ts`, groupTagsWithoutLe, value, query, groupByWithoutLe, orderWithoutLe) + return query, nil + case v3.AggregateOperatorAvg, v3.AggregateOperatorSum, v3.AggregateOperatorMin, v3.AggregateOperatorMax: + op := fmt.Sprintf("%s(value)", aggregateOperatorToSQLFunc[mq.AggregateOperator]) + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + return query, nil + case v3.AggregateOperatorCount: + op := "toFloat64(count(*))" + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + return query, nil + case v3.AggregateOperatorCountDistinct: + op := "toFloat64(count(distinct(value)))" + query := fmt.Sprintf(queryTmpl, groupTags, step, op, filterSubQuery, groupBy, orderBy) + return query, nil + case v3.AggregateOperatorNoOp: + queryTmpl := + "SELECT fingerprint, labels as fullLabels," + + " toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL %d SECOND) as ts," + + " any(value) as value" + + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_TABLENAME + + " GLOBAL INNER JOIN" + + " (%s) as filtered_time_series" + + " USING fingerprint" + + " WHERE " + samplesTableTimeFilter + + " GROUP BY fingerprint, labels, ts" + + " ORDER BY fingerprint, labels, ts" + query := fmt.Sprintf(queryTmpl, step, filterSubQuery) + return query, nil + default: + return "", fmt.Errorf("unsupported aggregate operator") + } +} diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go index 1bff2ae2ba..c416d746f8 100644 --- a/pkg/query-service/app/metrics/v3/query_builder.go +++ b/pkg/query-service/app/metrics/v3/query_builder.go @@ -44,13 +44,19 @@ var aggregateOperatorToSQLFunc = map[v3.AggregateOperator]string{ } // See https://github.com/SigNoz/signoz/issues/2151#issuecomment-1467249056 -var rateWithoutNegative = `if (runningDifference(value) < 0 OR runningDifference(ts) <= 0, nan, runningDifference(value)/runningDifference(ts))` +var rateWithoutNegative = `if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) ` // buildMetricsTimeSeriesFilterQuery builds the sub-query to be used for filtering // timeseries based on search criteria -func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.AttributeKey, metricName string, aggregateOperator v3.AggregateOperator) (string, error) { +func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.AttributeKey, mq *v3.BuilderQuery) (string, error) { + metricName := mq.AggregateAttribute.Key + aggregateOperator := mq.AggregateOperator var conditions []string - conditions = append(conditions, fmt.Sprintf("metric_name = %s", utils.ClickHouseFormattedValue(metricName))) + if mq.Temporality == v3.Delta { + conditions = append(conditions, fmt.Sprintf("metric_name = %s AND temporality = '%s' ", utils.ClickHouseFormattedValue(metricName), v3.Delta)) + } else { + conditions = append(conditions, fmt.Sprintf("metric_name = %s AND temporality IN ['%s', '%s']", utils.ClickHouseFormattedValue(metricName), v3.Cumulative, v3.Unspecified)) + } if fs != nil && len(fs.Items) != 0 { for _, item := range fs.Items { @@ -157,7 +163,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str } } - filterSubQuery, err := buildMetricsTimeSeriesFilterQuery(mq.Filters, metricQueryGroupBy, mq.AggregateAttribute.Key, mq.AggregateOperator) + filterSubQuery, err := buildMetricsTimeSeriesFilterQuery(mq.Filters, metricQueryGroupBy, mq) if err != nil { return "", err } @@ -413,7 +419,13 @@ func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v } func PrepareMetricQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery) (string, error) { - query, err := buildMetricQuery(start, end, mq.StepInterval, mq, constants.SIGNOZ_TIMESERIES_TABLENAME) + var query string + var err error + if mq.Temporality == v3.Delta { + query, err = buildDeltaMetricQuery(start, end, mq.StepInterval, mq, constants.SIGNOZ_TIMESERIES_TABLENAME) + } else { + query, err = buildMetricQuery(start, end, mq.StepInterval, mq, constants.SIGNOZ_TIMESERIES_TABLENAME) + } if err != nil { return "", err } diff --git a/pkg/query-service/app/metrics/v3/query_builder_test.go b/pkg/query-service/app/metrics/v3/query_builder_test.go index f3f91129a8..595a8c4557 100644 --- a/pkg/query-service/app/metrics/v3/query_builder_test.go +++ b/pkg/query-service/app/metrics/v3/query_builder_test.go @@ -57,7 +57,7 @@ func TestBuildQueryWithFilters(t *testing.T) { query, err := PrepareMetricQuery(q.Start, q.End, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"]) require.NoError(t, err) - require.Contains(t, query, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'a') != 'b'") + require.Contains(t, query, "WHERE metric_name = 'name' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'a') != 'b'") require.Contains(t, query, rateWithoutNegative) require.Contains(t, query, "not match(JSONExtractString(labels, 'code'), 'ERROR_*')") }) @@ -93,7 +93,7 @@ func TestBuildQueryWithMultipleQueries(t *testing.T) { query, err := PrepareMetricQuery(q.Start, q.End, q.CompositeQuery.QueryType, q.CompositeQuery.PanelType, q.CompositeQuery.BuilderQueries["A"]) require.NoError(t, err) - require.Contains(t, query, "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']") + require.Contains(t, query, "WHERE metric_name = 'name' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'in') IN ['a','b','c']") require.Contains(t, query, rateWithoutNegative) }) } @@ -228,7 +228,12 @@ func TestBuildQueryOperators(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) { - whereClause, err := buildMetricsTimeSeriesFilterQuery(&tc.filterSet, []v3.AttributeKey{}, "signoz_calls_total", "sum") + mq := v3.BuilderQuery{ + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "signoz_calls_total"}, + AggregateOperator: v3.AggregateOperatorSum, + } + whereClause, err := buildMetricsTimeSeriesFilterQuery(&tc.filterSet, []v3.AttributeKey{}, &mq) require.NoError(t, err) require.Contains(t, whereClause, tc.expectedWhereClause) }) @@ -238,7 +243,7 @@ func TestBuildQueryOperators(t *testing.T) { func TestBuildQueryXRate(t *testing.T) { t.Run("TestBuildQueryXRate", func(t *testing.T) { - tmpl := `SELECT ts, %s(value) as value FROM (SELECT ts, if (runningDifference(value) < 0 OR runningDifference(ts) <= 0, nan, runningDifference(value)/runningDifference(ts))as value FROM(SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 0 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'name') as filtered_time_series USING fingerprint WHERE metric_name = 'name' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY GROUPING SETS ( (ts), () ) ORDER BY ts` + tmpl := `SELECT ts, %s(value) as value FROM (SELECT ts, if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) as value FROM(SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 0 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'name' AND temporality IN ['Cumulative', 'Unspecified']) as filtered_time_series USING fingerprint WHERE metric_name = 'name' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY GROUPING SETS ( (ts), () ) ORDER BY ts` cases := []struct { aggregateOperator v3.AggregateOperator diff --git a/pkg/query-service/app/queryBuilder/query_builder_test.go b/pkg/query-service/app/queryBuilder/query_builder_test.go index aacb895e75..f2679f0aff 100644 --- a/pkg/query-service/app/queryBuilder/query_builder_test.go +++ b/pkg/query-service/app/queryBuilder/query_builder_test.go @@ -51,8 +51,8 @@ func TestBuildQueryWithMultipleQueriesAndFormula(t *testing.T) { require.NoError(t, err) require.Contains(t, queries["C"], "SELECT A.ts as ts, A.value / B.value") - require.Contains(t, queries["C"], "WHERE metric_name = 'name' AND JSONExtractString(labels, 'in') IN ['a','b','c']") - require.Contains(t, queries["C"], "runningDifference(value)/runningDifference(ts)") + require.Contains(t, queries["C"], "WHERE metric_name = 'name' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'in') IN ['a','b','c']") + require.Contains(t, queries["C"], "runningDifference(value) / runningDifference(ts)") }) } @@ -205,3 +205,164 @@ func TestBuildQueryWithThreeOrMoreQueriesRefAndFormula(t *testing.T) { // So(strings.Count(queries["F5"], " ON "), ShouldEqual, 1) }) } + +func TestDeltaQueryBuilder(t *testing.T) { + cases := []struct { + name string + query *v3.QueryRangeParamsV3 + queryToTest string + expected string + }{ + { + name: "TestQueryWithName - Request rate", + query: &v3.QueryRangeParamsV3{ + Start: 1650991982000, + End: 1651078382000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + DataSource: v3.DataSourceMetrics, + QueryName: "A", + AggregateAttribute: v3.AttributeKey{Key: "signoz_latency_count"}, + StepInterval: 60, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + Temporality: v3.Delta, + Filters: &v3.FilterSet{ + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{Key: "service_name"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"frontend"}, + }, + { + Key: v3.AttributeKey{Key: "operation"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"HTTP GET /dispatch"}, + }, + }, + }, + }, + }, + }, + }, + queryToTest: "A", + expected: "SELECT toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_count' AND temporality = 'Delta' AND JSONExtractString(labels, 'service_name') IN ['frontend'] AND JSONExtractString(labels, 'operation') IN ['HTTP GET /dispatch'] AND JSONExtractString(labels, '__temporality__') = 'Delta') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY ts ORDER BY ts", + }, + { + name: "TestQueryWithExpression - Error rate", + query: &v3.QueryRangeParamsV3{ + Start: 1650991982000, + End: 1651078382000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{Key: "signoz_latency_count"}, + StepInterval: 60, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "A", + Temporality: v3.Delta, + Filters: &v3.FilterSet{ + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{Key: "service_name"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"frontend"}, + }, + { + Key: v3.AttributeKey{Key: "operation"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"HTTP GET /dispatch"}, + }, + { + Key: v3.AttributeKey{Key: "status_code"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"STATUS_CODE_ERROR"}, + }, + }, + }, + }, + "B": { + QueryName: "B", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{Key: "signoz_latency_count"}, + StepInterval: 60, + AggregateOperator: v3.AggregateOperatorSumRate, + Expression: "B", + Temporality: v3.Delta, + Filters: &v3.FilterSet{ + Items: []v3.FilterItem{ + { + Key: v3.AttributeKey{Key: "service_name"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"frontend"}, + }, + { + Key: v3.AttributeKey{Key: "operation"}, + Operator: v3.FilterOperatorIn, + Value: []interface{}{"HTTP GET /dispatch"}, + }, + }, + }, + }, + "C": { + QueryName: "C", + Expression: "A*100/B", + }, + }, + }, + }, + queryToTest: "C", + expected: "SELECT A.ts as ts, A.value * 100 / B.value as value FROM (SELECT toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_count' AND temporality = 'Delta' AND JSONExtractString(labels, 'service_name') IN ['frontend'] AND JSONExtractString(labels, 'operation') IN ['HTTP GET /dispatch'] AND JSONExtractString(labels, 'status_code') IN ['STATUS_CODE_ERROR'] AND JSONExtractString(labels, '__temporality__') = 'Delta') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY ts ORDER BY ts) as A GLOBAL INNER JOIN(SELECT toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_count' AND temporality = 'Delta' AND JSONExtractString(labels, 'service_name') IN ['frontend'] AND JSONExtractString(labels, 'operation') IN ['HTTP GET /dispatch'] AND JSONExtractString(labels, '__temporality__') = 'Delta') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY ts ORDER BY ts) as B ON A.ts = B.ts", + }, + { + name: "TestQuery - Quantile", + query: &v3.QueryRangeParamsV3{ + Start: 1650991982000, + End: 1651078382000, + Step: 60, + CompositeQuery: &v3.CompositeQuery{ + QueryType: v3.QueryTypeBuilder, + PanelType: v3.PanelTypeGraph, + BuilderQueries: map[string]*v3.BuilderQuery{ + "A": { + QueryName: "A", + DataSource: v3.DataSourceMetrics, + AggregateAttribute: v3.AttributeKey{Key: "signoz_latency_bucket"}, + StepInterval: 60, + AggregateOperator: v3.AggregateOperatorHistQuant95, + Expression: "A", + Temporality: v3.Delta, + GroupBy: []v3.AttributeKey{ + {Key: "service_name"}, + }, + }, + }, + }, + }, + queryToTest: "A", + expected: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.950) as value FROM (SELECT service_name,le, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' ) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1650991982000 AND timestamp_ms <= 1651078382000 GROUP BY service_name,le,ts ORDER BY service_name ASC,le ASC, ts) GROUP BY service_name,ts ORDER BY service_name ASC, ts", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + qbOptions := QueryBuilderOptions{ + BuildMetricQuery: metricsv3.PrepareMetricQuery, + } + qb := NewQueryBuilder(qbOptions) + queries, err := qb.PrepareQueries(c.query) + + require.NoError(t, err) + require.Equal(t, c.expected, queries[c.queryToTest]) + }) + } +} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 293d3f8753..7e3a913fac 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -48,6 +48,7 @@ type ServerOptions struct { // alert specific params DisableRules bool RuleRepoURL string + PreferDelta bool } // Server runs HTTP, Mux and a grpc server @@ -125,6 +126,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { apiHandler, err := NewAPIHandler(APIHandlerOpts{ Reader: reader, SkipConfig: skipConfig, + PerferDelta: serverOptions.PreferDelta, AppDao: dao.DB(), RuleManager: rm, FeatureFlags: fm, diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index b6a8015fc0..638de6e4d8 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -56,6 +56,7 @@ type Reader interface { // Setter Interfaces SetTTL(ctx context.Context, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) + FetchTemporality(ctx context.Context, metricNames []string) (map[string]map[v3.Temporality]bool, error) GetMetricAutocompleteMetricNames(ctx context.Context, matchText string, limit int) (*[]string, *model.ApiError) GetMetricAutocompleteTagKey(ctx context.Context, params *model.MetricAutocompleteTagParams) (*[]string, *model.ApiError) GetMetricAutocompleteTagValue(ctx context.Context, params *model.MetricAutocompleteTagParams) (*[]string, *model.ApiError) diff --git a/pkg/query-service/main.go b/pkg/query-service/main.go index 9d769a0940..7e8b8f4bf6 100644 --- a/pkg/query-service/main.go +++ b/pkg/query-service/main.go @@ -34,9 +34,12 @@ func main() { // the url used to build link in the alert messages in slack and other systems var ruleRepoURL string + var preferDelta bool + flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") + flag.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over gauge)") flag.StringVar(&ruleRepoURL, "rules.repo-url", constants.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.Parse() @@ -51,6 +54,7 @@ func main() { HTTPHostPort: constants.HTTPHostPort, PromConfigPath: promConfigPath, SkipTopLvlOpsPath: skipTopLvlOpsPath, + PreferDelta: preferDelta, PrivateHostPort: constants.PrivateHostPort, DisableRules: disableRules, RuleRepoURL: ruleRepoURL, diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index e320cc2877..7647e5a75d 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -417,12 +417,21 @@ func (c *CompositeQuery) Validate() error { return nil } +type Temporality string + +const ( + Unspecified Temporality = "Unspecified" + Delta Temporality = "Delta" + Cumulative Temporality = "Cumulative" +) + type BuilderQuery struct { QueryName string `json:"queryName"` StepInterval int64 `json:"stepInterval"` DataSource DataSource `json:"dataSource"` AggregateOperator AggregateOperator `json:"aggregateOperator"` AggregateAttribute AttributeKey `json:"aggregateAttribute,omitempty"` + Temporality Temporality `json:"temporality,omitempty"` Filters *FilterSet `json:"filters,omitempty"` GroupBy []AttributeKey `json:"groupBy,omitempty"` Expression string `json:"expression"` From 783a54a8ee0865f1264880c977d4f659e21365ef Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Thu, 13 Jul 2023 19:58:19 +0530 Subject: [PATCH 47/48] =?UTF-8?q?chore(release):=20=F0=9F=93=8C=20pin=20ve?= =?UTF-8?q?rsions:=20SigNoz=200.23.0?= 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 34c639ae43..1add70ef15 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -137,7 +137,7 @@ services: condition: on-failure query-service: - image: signoz/query-service:0.22.0 + image: signoz/query-service:0.23.0 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -166,7 +166,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.22.0 + image: signoz/frontend:0.23.0 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 5d05f15528..37463a8ebf 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -153,7 +153,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:${DOCKER_TAG:-0.22.0} + image: signoz/query-service:${DOCKER_TAG:-0.23.0} container_name: query-service command: ["-config=/root/config/prometheus.yml"] # ports: @@ -181,7 +181,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:${DOCKER_TAG:-0.22.0} + image: signoz/frontend:${DOCKER_TAG:-0.23.0} container_name: frontend restart: on-failure depends_on: From 225b2248c89566316a282e378844a785cd73ecc4 Mon Sep 17 00:00:00 2001 From: Prashant Shahi Date: Thu, 13 Jul 2023 20:40:19 +0530 Subject: [PATCH 48/48] =?UTF-8?q?chore(release):=20=F0=9F=93=8C=20pin=20ve?= =?UTF-8?q?rsions:=20SigNoz=20OtelCollector=200.79.3?= 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-core.yaml | 4 ++-- deploy/docker/clickhouse-setup/docker-compose.yaml | 4 ++-- pkg/query-service/tests/test-deploy/docker-compose.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index 1add70ef15..826d17094e 100644 --- a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml @@ -179,7 +179,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -208,7 +208,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose-core.yaml b/deploy/docker/clickhouse-setup/docker-compose-core.yaml index 79ae0c765e..4e59682c4b 100644 --- a/deploy/docker/clickhouse-setup/docker-compose-core.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose-core.yaml @@ -41,7 +41,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` otel-collector: container_name: otel-collector - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] # user: root # required for reading docker container logs volumes: @@ -67,7 +67,7 @@ services: otel-collector-metrics: container_name: otel-collector-metrics - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/deploy/docker/clickhouse-setup/docker-compose.yaml b/deploy/docker/clickhouse-setup/docker-compose.yaml index 37463a8ebf..2ff2fcb77e 100644 --- a/deploy/docker/clickhouse-setup/docker-compose.yaml +++ b/deploy/docker/clickhouse-setup/docker-compose.yaml @@ -193,7 +193,7 @@ services: - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf otel-collector: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3} command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -219,7 +219,7 @@ services: <<: *clickhouse-depend otel-collector-metrics: - image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2} + image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3} command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml diff --git a/pkg/query-service/tests/test-deploy/docker-compose.yaml b/pkg/query-service/tests/test-deploy/docker-compose.yaml index a511230df6..6681409e57 100644 --- a/pkg/query-service/tests/test-deploy/docker-compose.yaml +++ b/pkg/query-service/tests/test-deploy/docker-compose.yaml @@ -169,7 +169,7 @@ services: <<: *clickhouse-depends otel-collector: - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] user: root # required for reading docker container logs volumes: @@ -195,7 +195,7 @@ services: <<: *clickhouse-depends otel-collector-metrics: - image: signoz/signoz-otel-collector:0.79.2 + image: signoz/signoz-otel-collector:0.79.3 command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] volumes: - ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
Empty