diff --git a/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml b/deploy/docker-swarm/clickhouse-setup/docker-compose.yaml index aa5cbb5ff3..34c4073389 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.24.0 + image: signoz/query-service:0.25.0 command: ["-config=/root/config/prometheus.yml"] # ports: # - "6060:6060" # pprof port @@ -166,7 +166,7 @@ services: <<: *clickhouse-depend frontend: - image: signoz/frontend:0.24.0 + image: signoz/frontend:0.25.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 5f16d26489..6998e59236 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.24.0} + image: signoz/query-service:${DOCKER_TAG:-0.25.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.24.0} + image: signoz/frontend:${DOCKER_TAG:-0.25.0} container_name: frontend restart: on-failure depends_on: diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index f7bc5e1704..f3d40ccbcd 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -1,7 +1,7 @@ import Loadable from 'components/Loadable'; export const ServicesTablePage = Loadable( - () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Metrics'), + () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Services'), ); export const ServiceMetricsPage = Loadable( diff --git a/frontend/src/api/metrics/getService.ts b/frontend/src/api/metrics/getService.ts index d3bb27c741..731da11c81 100644 --- a/frontend/src/api/metrics/getService.ts +++ b/frontend/src/api/metrics/getService.ts @@ -1,28 +1,13 @@ 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/getService'; -const getService = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post(`/services`, { - start: `${props.start}`, - end: `${props.end}`, - tags: props.selectedTags, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } +const getService = async (props: Props): Promise => { + const response = await axios.post(`/services`, { + start: `${props.start}`, + end: `${props.end}`, + tags: props.selectedTags, + }); + return response.data; }; export default getService; diff --git a/frontend/src/assets/Dashboard/Table.tsx b/frontend/src/assets/Dashboard/Table.tsx new file mode 100644 index 0000000000..e1ade19994 --- /dev/null +++ b/frontend/src/assets/Dashboard/Table.tsx @@ -0,0 +1,18 @@ +function Table(): JSX.Element { + return ( + + + + ); +} + +export default Table; diff --git a/frontend/src/assets/Dashboard/TimeSeries.tsx b/frontend/src/assets/Dashboard/TimeSeries.tsx index 54d8100a63..a9ddbc5717 100644 --- a/frontend/src/assets/Dashboard/TimeSeries.tsx +++ b/frontend/src/assets/Dashboard/TimeSeries.tsx @@ -1,33 +1,16 @@ function TimeSeries(): JSX.Element { return ( - - - - - - - + ); } diff --git a/frontend/src/assets/Dashboard/Value.tsx b/frontend/src/assets/Dashboard/Value.tsx index c43b63ac13..708e16d6b9 100644 --- a/frontend/src/assets/Dashboard/Value.tsx +++ b/frontend/src/assets/Dashboard/Value.tsx @@ -1,26 +1,22 @@ -import { CSSProperties } from 'react'; - -function Value(props: ValueProps): JSX.Element { - const { fillColor } = props; - +function Value(): JSX.Element { return ( + ); } -interface ValueProps { - fillColor: CSSProperties['color']; -} - export default Value; diff --git a/frontend/src/components/Graph/Plugin/Legend.ts b/frontend/src/components/Graph/Plugin/Legend.ts index 787933f442..8880657855 100644 --- a/frontend/src/components/Graph/Plugin/Legend.ts +++ b/frontend/src/components/Graph/Plugin/Legend.ts @@ -19,6 +19,7 @@ const getOrCreateLegendList = ( listContainer.style.overflowY = 'scroll'; listContainer.style.justifyContent = isLonger ? 'start' : 'center'; listContainer.style.alignItems = isLonger ? 'start' : 'center'; + listContainer.style.minHeight = '2rem'; listContainer.style.height = '100%'; listContainer.style.flexWrap = 'wrap'; listContainer.style.justifyContent = 'center'; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index cd988286cd..621f57ebf7 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -343,7 +343,7 @@ type CustomChartOptions = ChartOptions & { }; }; -interface GraphProps { +export interface GraphProps { animate?: boolean; type: ChartType; data: Chart['data']; diff --git a/frontend/src/constants/panelTypes.ts b/frontend/src/constants/panelTypes.ts new file mode 100644 index 0000000000..811e06f0d0 --- /dev/null +++ b/frontend/src/constants/panelTypes.ts @@ -0,0 +1,14 @@ +import Graph from 'components/Graph'; +import GridTableComponent from 'container/GridTableComponent'; +import GridValueComponent from 'container/GridValueComponent'; + +import { PANEL_TYPES } from './queryBuilder'; + +export const PANEL_TYPES_COMPONENT_MAP = { + [PANEL_TYPES.TIME_SERIES]: Graph, + [PANEL_TYPES.VALUE]: GridValueComponent, + [PANEL_TYPES.TABLE]: GridTableComponent, + [PANEL_TYPES.TRACE]: null, + [PANEL_TYPES.LIST]: null, + [PANEL_TYPES.EMPTY_WIDGET]: null, +} as const; diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 9ff71e1712..515be4d080 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -1,5 +1,4 @@ // ** Helpers -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { @@ -24,7 +23,6 @@ import { LogsAggregatorOperator, MetricAggregateOperator, NumberOperators, - PanelTypeKeys, QueryAdditionalFilter, QueryBuilderData, ReduceOperators, @@ -124,10 +122,10 @@ export const initialFilters: TagFilter = { op: 'AND', }; -const initialQueryBuilderFormValues: IBuilderQuery = { +export const initialQueryBuilderFormValues: IBuilderQuery = { dataSource: DataSource.METRICS, queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), - aggregateOperator: MetricAggregateOperator.NOOP, + aggregateOperator: MetricAggregateOperator.COUNT, aggregateAttribute: initialAutocompleteData, filters: { items: [], op: 'AND' }, expression: createNewBuilderItemName({ @@ -238,14 +236,15 @@ export const operatorsByTypes: Record = { bool: Object.values(BoolOperators), }; -export const PANEL_TYPES: Record = { - TIME_SERIES: 'graph', - VALUE: 'value', - TABLE: 'table', - LIST: 'list', - TRACE: 'trace', - EMPTY_WIDGET: 'EMPTY_WIDGET', -}; +// eslint-disable-next-line @typescript-eslint/naming-convention +export enum PANEL_TYPES { + TIME_SERIES = 'graph', + VALUE = 'value', + TABLE = 'table', + LIST = 'list', + TRACE = 'trace', + EMPTY_WIDGET = 'EMPTY_WIDGET', +} export type IQueryBuilderState = 'search'; diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index f08d22df96..b8d66c4334 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -2,8 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { StaticLineProps } from 'components/Graph'; import Spinner from 'components/Spinner'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import GridGraphComponent from 'container/GridGraphComponent'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import GridPanelSwitch from 'container/GridPanelSwitch'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; @@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles'; export interface ChartPreviewProps { name: string; query: Query | null; - graphType?: GRAPH_TYPES; + graphType?: PANEL_TYPES; selectedTime?: timePreferenceType; selectedInterval?: Time; headline?: JSX.Element; @@ -113,13 +112,15 @@ function ChartPreview({ )} {chartDataSet && !queryResponse.isError && ( - )} diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 59c8333f06..109e3ea9d5 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -3,6 +3,7 @@ import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd'; import saveAlertApi from 'api/alerts/save'; import testAlertApi from 'api/alerts/testAlert'; import { FeatureKeys } from 'constants/features'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag'; import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag'; @@ -58,6 +59,7 @@ function FormAlertRules({ const { currentQuery, + panelType, stagedQuery, handleRunQuery, redirectWithQueryBuilderData, @@ -351,7 +353,12 @@ function FormAlertRules({ const renderQBChartPreview = (): JSX.Element => ( } + headline={ + + } name="" threshold={alertDef.condition?.target} query={stagedQuery} @@ -361,7 +368,12 @@ function FormAlertRules({ const renderPromChartPreview = (): JSX.Element => ( } + headline={ + + } name="Chart Preview" threshold={alertDef.condition?.target} query={stagedQuery} @@ -370,7 +382,12 @@ function FormAlertRules({ const renderChQueryChartPreview = (): JSX.Element => ( } + headline={ + + } name="Chart Preview" threshold={alertDef.condition?.target} query={stagedQuery} diff --git a/frontend/src/container/GridGraphComponent/index.tsx b/frontend/src/container/GridGraphComponent/index.tsx deleted file mode 100644 index 22fa25f98d..0000000000 --- a/frontend/src/container/GridGraphComponent/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Typography } from 'antd'; -import { ChartData } from 'chart.js'; -import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph'; -import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; -import ValueGraph from 'components/ValueGraph'; -import { PANEL_TYPES } from 'constants/queryBuilder'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; -import history from 'lib/history'; - -import { TitleContainer, ValueContainer } from './styles'; - -function GridGraphComponent({ - GRAPH_TYPES, - data, - title, - opacity, - isStacked, - onClickHandler, - name, - yAxisUnit, - staticLine, - onDragSelect, -}: GridGraphComponentProps): JSX.Element | null { - const location = history.location.pathname; - - const isDashboardPage = location.split('/').length === 3; - - if (GRAPH_TYPES === PANEL_TYPES.TIME_SERIES) { - return ( - - ); - } - - if (GRAPH_TYPES === PANEL_TYPES.VALUE) { - const value = (((data.datasets[0] || []).data || [])[0] || 0) as number; - - if (data.datasets.length === 0) { - return ( - - No Data - - ); - } - - return ( - <> - - {title} - - - - - - ); - } - - return null; -} - -export interface GridGraphComponentProps { - GRAPH_TYPES: GRAPH_TYPES; - data: ChartData; - title?: string; - opacity?: string; - isStacked?: boolean; - onClickHandler?: GraphOnClickHandler; - name: string; - yAxisUnit?: string; - staticLine?: StaticLineProps; - onDragSelect?: (start: number, end: number) => void; -} - -GridGraphComponent.defaultProps = { - title: undefined, - opacity: undefined, - isStacked: undefined, - onClickHandler: undefined, - yAxisUnit: undefined, - staticLine: undefined, - onDragSelect: undefined, -}; - -export default GridGraphComponent; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx index 3208517b86..b177de9722 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -2,7 +2,7 @@ import { Button } from 'antd'; import { GraphOnClickHandler } from 'components/Graph'; import Spinner from 'components/Spinner'; import TimePreference from 'components/TimePreferenceDropDown'; -import GridGraphComponent from 'container/GridGraphComponent'; +import GridPanelSwitch from 'container/GridPanelSwitch'; import { timeItems, timePreferance, @@ -85,7 +85,7 @@ function FullView({ return ( <> {fullViewOptions && ( - + )} - ); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts index 4fbc1a18c0..377e891a35 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts @@ -1,4 +1,9 @@ -import styled from 'styled-components'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import styled, { css, FlattenSimpleInterpolation } from 'styled-components'; + +interface Props { + $panelType: PANEL_TYPES; +} export const NotFoundContainer = styled.div` display: flex; @@ -7,7 +12,13 @@ export const NotFoundContainer = styled.div` min-height: 55vh; `; -export const TimeContainer = styled.div` +export const TimeContainer = styled.div` display: flex; justify-content: flex-end; + ${({ $panelType }): FlattenSimpleInterpolation => + $panelType === PANEL_TYPES.TABLE + ? css` + margin-bottom: 1rem; + ` + : css``} `; diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index c0f6e9a5a9..1ee8cedeb8 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -2,8 +2,8 @@ import { Typography } from 'antd'; import { ChartData } from 'chart.js'; import { GraphOnClickHandler } from 'components/Graph'; import Spinner from 'components/Spinner'; -import GridGraphComponent from 'container/GridGraphComponent'; import { UpdateDashboard } from 'container/GridGraphLayout/utils'; +import GridPanelSwitch from 'container/GridPanelSwitch'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useStepInterval } from 'hooks/queryBuilder/useStepInterval'; import { useNotifications } from 'hooks/useNotifications'; @@ -60,6 +60,7 @@ function GridCardGraph({ allowDelete, allowClone, allowEdit, + isQueryEnabled, }: GridCardGraphProps): JSX.Element { const { ref: graphRef, inView: isGraphVisible } = useInView({ threshold: 0, @@ -115,7 +116,7 @@ function GridCardGraph({ variables, ], keepPreviousData: true, - enabled: isGraphVisible && !isEmptyWidget, + enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled, refetchOnMount: false, onError: (error) => { setErrorMessage(error.message); @@ -271,8 +272,8 @@ function GridCardGraph({ allowEdit={allowEdit} /> - )} @@ -287,7 +290,7 @@ function GridCardGraph({ ); } - if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) { + if (queryResponse.status === 'loading' || queryResponse.status === 'idle') { return ( {!isEmpty(widget) && prevChartDataSetRef?.labels ? ( @@ -307,8 +310,8 @@ function GridCardGraph({ allowEdit={allowEdit} /> - ) : ( @@ -362,8 +367,8 @@ function GridCardGraph({ {!isEmptyLayout && getModals()} {!isEmpty(widget) && !!queryResponse.data?.payload && ( - )} @@ -399,6 +406,7 @@ interface GridCardGraphProps extends DispatchProps { allowDelete?: boolean; allowClone?: boolean; allowEdit?: boolean; + isQueryEnabled?: boolean; } GridCardGraph.defaultProps = { @@ -407,6 +415,7 @@ GridCardGraph.defaultProps = { allowDelete: true, allowClone: true, allowEdit: true, + isQueryEnabled: true, }; const mapDispatchToProps = ( diff --git a/frontend/src/container/GridGraphLayout/GraphLayout.tsx b/frontend/src/container/GridGraphLayout/GraphLayout.tsx index 02cbb7ed6c..316b6877cc 100644 --- a/frontend/src/container/GridGraphLayout/GraphLayout.tsx +++ b/frontend/src/container/GridGraphLayout/GraphLayout.tsx @@ -1,4 +1,5 @@ import { PlusOutlined, SaveFilled } from '@ant-design/icons'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import useComponentPermission from 'hooks/useComponentPermission'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { Dispatch, SetStateAction } from 'react'; @@ -83,7 +84,7 @@ function GraphLayout({ key={currentWidget?.id || 'empty'} // don't change this key data-grid={rest} > - + diff --git a/frontend/src/container/GridGraphLayout/index.tsx b/frontend/src/container/GridGraphLayout/index.tsx index 6b6ec7d883..f728781a28 100644 --- a/frontend/src/container/GridGraphLayout/index.tsx +++ b/frontend/src/container/GridGraphLayout/index.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/no-unstable-nested-components */ import updateDashboardApi from 'api/dashboard/update'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import useComponentPermission from 'hooks/useComponentPermission'; import { useNotifications } from 'hooks/useNotifications'; import { @@ -260,7 +261,7 @@ function GridGraph(props: Props): JSX.Element { { data, generateWidgetId: id, - graphType: 'EMPTY_WIDGET', + graphType: PANEL_TYPES.EMPTY_WIDGET, selectedDashboard, layout, isRedirected: false, diff --git a/frontend/src/container/GridGraphLayout/styles.ts b/frontend/src/container/GridGraphLayout/styles.ts index 9bb4b219bb..a08ffdc361 100644 --- a/frontend/src/container/GridGraphLayout/styles.ts +++ b/frontend/src/container/GridGraphLayout/styles.ts @@ -1,11 +1,16 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { StyledCSS } from 'container/GantChart/Trace/styles'; import RGL, { WidthProvider } from 'react-grid-layout'; -import styled, { css } from 'styled-components'; +import styled, { css, FlattenSimpleInterpolation } from 'styled-components'; const ReactGridLayoutComponent = WidthProvider(RGL); -export const Card = styled(CardComponent)` +interface CardProps { + $panelType: PANEL_TYPES; +} + +export const Card = styled(CardComponent)` &&& { height: 100%; } @@ -13,6 +18,12 @@ export const Card = styled(CardComponent)` .ant-card-body { height: 95%; padding: 0; + ${({ $panelType }): FlattenSimpleInterpolation => + $panelType === PANEL_TYPES.TABLE + ? css` + padding-top: 1.8rem; + ` + : css``} } `; @@ -21,6 +32,8 @@ interface Props { } export const CardContainer = styled.div` + overflow: auto; + :hover { .react-resizable-handle { position: absolute; @@ -44,6 +57,7 @@ export const CardContainer = styled.div` background-image: ${(): string => `url("${uri}")`}; `; }} + } } `; diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 6f6910b3b5..2caa48b705 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -1,7 +1,6 @@ import { NotificationInstance } from 'antd/es/notification/interface'; import updateDashboardApi from 'api/dashboard/update'; -import { initialQueriesMap } from 'constants/queryBuilder'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { Layout } from 'react-grid-layout'; import store from 'store'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; @@ -69,7 +68,7 @@ export const UpdateDashboard = async ( interface UpdateDashboardProps { data: Dashboard['data']; - graphType: GRAPH_TYPES; + graphType: PANEL_TYPES; generateWidgetId: string; layout: Layout[]; selectedDashboard: Dashboard; diff --git a/frontend/src/container/GridPanelSwitch/index.tsx b/frontend/src/container/GridPanelSwitch/index.tsx new file mode 100644 index 0000000000..790ed404da --- /dev/null +++ b/frontend/src/container/GridPanelSwitch/index.tsx @@ -0,0 +1,73 @@ +import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config'; +import { FC, memo, useMemo } from 'react'; + +import { GridPanelSwitchProps, PropsTypePropsMap } from './types'; + +function GridPanelSwitch({ + panelType, + data, + title, + isStacked, + onClickHandler, + name, + yAxisUnit, + staticLine, + onDragSelect, + panelData, + query, +}: GridPanelSwitchProps): JSX.Element | null { + const currentProps: PropsTypePropsMap = useMemo(() => { + const result: PropsTypePropsMap = { + [PANEL_TYPES.TIME_SERIES]: { + type: 'line', + data, + title, + isStacked, + onClickHandler, + name, + yAxisUnit, + staticLine, + onDragSelect, + }, + [PANEL_TYPES.VALUE]: { + title, + data, + yAxisUnit, + }, + [PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query }, + [PANEL_TYPES.LIST]: null, + [PANEL_TYPES.TRACE]: null, + [PANEL_TYPES.EMPTY_WIDGET]: null, + }; + + return result; + }, [ + data, + isStacked, + name, + onClickHandler, + onDragSelect, + staticLine, + title, + yAxisUnit, + panelData, + query, + ]); + + const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC< + PropsTypePropsMap[typeof panelType] + >; + const componentProps = useMemo(() => currentProps[panelType], [ + panelType, + currentProps, + ]); + + if (!Component || !componentProps) return null; + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; +} + +export default memo(GridPanelSwitch); diff --git a/frontend/src/container/GridPanelSwitch/types.ts b/frontend/src/container/GridPanelSwitch/types.ts new file mode 100644 index 0000000000..5f16fc5b2d --- /dev/null +++ b/frontend/src/container/GridPanelSwitch/types.ts @@ -0,0 +1,36 @@ +import { ChartData } from 'chart.js'; +import { + GraphOnClickHandler, + GraphProps, + StaticLineProps, +} from 'components/Graph'; +import { GridTableComponentProps } from 'container/GridTableComponent/types'; +import { GridValueComponentProps } from 'container/GridValueComponent/types'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryDataV3 } from 'types/api/widgets/getQuery'; + +import { PANEL_TYPES } from '../../constants/queryBuilder'; + +export type GridPanelSwitchProps = { + panelType: PANEL_TYPES; + data: ChartData; + title?: string; + opacity?: string; + isStacked?: boolean; + onClickHandler?: GraphOnClickHandler; + name: string; + yAxisUnit?: string; + staticLine?: StaticLineProps; + onDragSelect?: (start: number, end: number) => void; + panelData: QueryDataV3[]; + query: Query; +}; + +export type PropsTypePropsMap = { + [PANEL_TYPES.TIME_SERIES]: GraphProps; + [PANEL_TYPES.VALUE]: GridValueComponentProps; + [PANEL_TYPES.TABLE]: GridTableComponentProps; + [PANEL_TYPES.TRACE]: null; + [PANEL_TYPES.LIST]: null; + [PANEL_TYPES.EMPTY_WIDGET]: null; +}; diff --git a/frontend/src/container/GridTableComponent/config.ts b/frontend/src/container/GridTableComponent/config.ts new file mode 100644 index 0000000000..fe9b8b8eb3 --- /dev/null +++ b/frontend/src/container/GridTableComponent/config.ts @@ -0,0 +1,9 @@ +import { TableProps } from 'antd'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; + +export const GRID_TABLE_CONFIG: Omit< + TableProps, + 'columns' | 'dataSource' +> = { + size: 'small', +}; diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx new file mode 100644 index 0000000000..e3659c1c95 --- /dev/null +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -0,0 +1,25 @@ +import { QueryTable } from 'container/QueryTable'; +import { memo } from 'react'; + +import { WrapperStyled } from './styles'; +import { GridTableComponentProps } from './types'; + +function GridTableComponent({ + data, + query, + ...props +}: GridTableComponentProps): JSX.Element { + return ( + + + + ); +} + +export default memo(GridTableComponent); diff --git a/frontend/src/container/GridTableComponent/styles.ts b/frontend/src/container/GridTableComponent/styles.ts new file mode 100644 index 0000000000..29e4d7ad83 --- /dev/null +++ b/frontend/src/container/GridTableComponent/styles.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const WrapperStyled = styled.div` + height: 100%; + overflow: hidden; + + & .ant-table-wrapper { + height: 100%; + } + & .ant-spin-nested-loading { + height: 100%; + } + + & .ant-spin-container { + height: 100%; + display: flex; + flex-direction: column; + } + & .ant-table { + flex: 1; + overflow: auto; + } +`; diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts new file mode 100644 index 0000000000..cd8e446822 --- /dev/null +++ b/frontend/src/container/GridTableComponent/types.ts @@ -0,0 +1,10 @@ +import { TableProps } from 'antd'; +import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces'; +import { RowData } from 'lib/query/createTableColumnsFromQuery'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +export type GridTableComponentProps = { query: Query } & Pick< + LogsExplorerTableProps, + 'data' +> & + Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/GridValueComponent/config.ts b/frontend/src/container/GridValueComponent/config.ts new file mode 100644 index 0000000000..fa3386c522 --- /dev/null +++ b/frontend/src/container/GridValueComponent/config.ts @@ -0,0 +1,3 @@ +import { GridValueComponentProps } from './types'; + +export const GridValueConfig: Pick = {}; diff --git a/frontend/src/container/GridValueComponent/index.tsx b/frontend/src/container/GridValueComponent/index.tsx new file mode 100644 index 0000000000..73275bee90 --- /dev/null +++ b/frontend/src/container/GridValueComponent/index.tsx @@ -0,0 +1,47 @@ +import { Typography } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import ValueGraph from 'components/ValueGraph'; +import { memo } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { TitleContainer, ValueContainer } from './styles'; +import { GridValueComponentProps } from './types'; + +function GridValueComponent({ + data, + title, + yAxisUnit, +}: GridValueComponentProps): JSX.Element { + const value = (((data.datasets[0] || []).data || [])[0] || 0) as number; + + const location = useLocation(); + + const isDashboardPage = location.pathname.split('/').length === 3; + + if (data.datasets.length === 0) { + return ( + + No Data + + ); + } + + return ( + <> + + {title} + + + + + + ); +} + +export default memo(GridValueComponent); diff --git a/frontend/src/container/GridGraphComponent/styles.ts b/frontend/src/container/GridValueComponent/styles.ts similarity index 100% rename from frontend/src/container/GridGraphComponent/styles.ts rename to frontend/src/container/GridValueComponent/styles.ts diff --git a/frontend/src/container/GridValueComponent/types.ts b/frontend/src/container/GridValueComponent/types.ts new file mode 100644 index 0000000000..cef4a69e30 --- /dev/null +++ b/frontend/src/container/GridValueComponent/types.ts @@ -0,0 +1,7 @@ +import { ChartData } from 'chart.js'; + +export type GridValueComponentProps = { + data: ChartData; + title?: string; + yAxisUnit?: string; +}; diff --git a/frontend/src/container/ListOfDashboard/index.tsx b/frontend/src/container/ListOfDashboard/index.tsx index 60da406603..bcd5d81fca 100644 --- a/frontend/src/container/ListOfDashboard/index.tsx +++ b/frontend/src/container/ListOfDashboard/index.tsx @@ -224,6 +224,7 @@ function ListOfAllDashboard(): JSX.Element { key: t('import_grafana_json').toString(), label: t('import_grafana_json'), onClick: (): void => onModalHandler(true), + disabled: true, }); return menuItems; diff --git a/frontend/src/container/LogExplorerQuerySection/index.tsx b/frontend/src/container/LogExplorerQuerySection/index.tsx index 3cd972c2fa..165bc8c20f 100644 --- a/frontend/src/container/LogExplorerQuerySection/index.tsx +++ b/frontend/src/container/LogExplorerQuerySection/index.tsx @@ -1,5 +1,9 @@ import { Button } from 'antd'; -import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { + initialQueriesMap, + OPERATORS, + PANEL_TYPES, +} from 'constants/queryBuilder'; import ExplorerOrderBy from 'container/ExplorerOrderBy'; import { QueryBuilder } from 'container/QueryBuilder'; import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces'; @@ -28,8 +32,14 @@ function LogExplorerQuerySection(): JSX.Element { const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { const isTable = panelTypes === PANEL_TYPES.TABLE; + const isList = panelTypes === PANEL_TYPES.LIST; const config: QueryBuilderProps['filterConfigs'] = { stepInterval: { isHidden: isTable, isDisabled: true }, + having: { isHidden: isList, isDisabled: true }, + filters: { + customKey: 'body', + customOp: OPERATORS.CONTAINS, + }, }; return config; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index a2fadefc53..0523b4d89e 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -17,7 +17,6 @@ import GoToTop from 'container/GoToTop'; import LogsExplorerChart from 'container/LogsExplorerChart'; import LogsExplorerList from 'container/LogsExplorerList'; import LogsExplorerTable from 'container/LogsExplorerTable'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; @@ -179,7 +178,7 @@ function LogsExplorerViews(): JSX.Element { }, []); const getUpdateQuery = useCallback( - (newPanelType: GRAPH_TYPES): Query => { + (newPanelType: PANEL_TYPES): Query => { let query = updateAllQueriesOperators( currentQuery, newPanelType, @@ -201,7 +200,7 @@ function LogsExplorerViews(): JSX.Element { const handleChangeView = useCallback( (type: string) => { - const newPanelType = type as GRAPH_TYPES; + const newPanelType = type as PANEL_TYPES; if (newPanelType === panelType) return; diff --git a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts index 880d9edba9..26400a7541 100644 --- a/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts +++ b/frontend/src/container/MetricsApplication/MetricsApplication.factory.ts @@ -1,17 +1,19 @@ -import { PANEL_TYPES } from 'constants/queryBuilder'; import { Widgets } from 'types/api/dashboard/getAll'; import { v4 } from 'uuid'; -export const getWidgetQueryBuilder = ( - query: Widgets['query'], +import { GetWidgetQueryBuilderProps } from './types'; + +export const getWidgetQueryBuilder = ({ + query, title = '', -): Widgets => ({ + panelTypes, +}: GetWidgetQueryBuilderProps): Widgets => ({ description: '', id: v4(), isStacked: false, nullZeroValues: '', opacity: '0', - panelTypes: PANEL_TYPES.TIME_SERIES, + panelTypes, query, timePreferance: 'GLOBAL_TIME', title, diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts index c1896b6884..72b9703cdd 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/DBCallQueries.ts @@ -1,7 +1,11 @@ import { OPERATORS } from 'constants/queryBuilder'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource, QueryBuilderData } from 'types/common/queryBuilder'; +import { + DataSource, + MetricAggregateOperator, + QueryBuilderData, +} from 'types/common/queryBuilder'; import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant'; import { IServiceName } from '../Tabs/types'; @@ -44,13 +48,14 @@ export const databaseCallsRPS = ({ ]; const legends = [legend]; + const dataSource = DataSource.METRICS; return getQueryBuilderQueries({ autocompleteData, groupBy, legends, filterItems, - dataSource: DataSource.METRICS, + dataSource, }); }; @@ -85,17 +90,36 @@ export const databaseCallsAvgDuration = ({ }, ...tagFilterItems, ]; - const additionalItemsB = additionalItemsA; - return getQueryBuilderQuerieswithFormula({ + const autocompleteData: BaseAutocompleteData[] = [ autocompleteDataA, autocompleteDataB, + ]; + + const additionalItems: TagFilterItem[][] = [ additionalItemsA, - additionalItemsB, - legend: '', - disabled: true, - expression: FORMULA.DATABASE_CALLS_AVG_DURATION, - legendFormula: 'Average Duration', + additionalItemsA, + ]; + + const legends = ['', '']; + const disabled = [true, true]; + const legendFormula = 'Average Duration'; + const expression = FORMULA.DATABASE_CALLS_AVG_DURATION; + const aggregateOperators = [ + MetricAggregateOperator.SUM, + MetricAggregateOperator.SUM, + ]; + const dataSource = DataSource.METRICS; + + return getQueryBuilderQuerieswithFormula({ + autocompleteData, + additionalItems, + legends, + disabled, + expression, + legendFormula, + aggregateOperators, + dataSource, }); }; diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts index 1d375f2e53..b140882c46 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/ExternalQueries.ts @@ -1,10 +1,17 @@ import { OPERATORS } from 'constants/queryBuilder'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource, QueryBuilderData } from 'types/common/queryBuilder'; +import { + DataSource, + MetricAggregateOperator, + QueryBuilderData, +} from 'types/common/queryBuilder'; import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant'; -import { IServiceName } from '../Tabs/types'; +import { + ExternalCallDurationByAddressProps, + ExternalCallProps, +} from '../Tabs/types'; import { getQueryBuilderQueries, getQueryBuilderQuerieswithFormula, @@ -36,6 +43,7 @@ export const externalCallErrorPercent = ({ isColumn: true, type: null, }; + const additionalItemsA: TagFilterItem[] = [ { id: '', @@ -71,23 +79,38 @@ export const externalCallErrorPercent = ({ type: MetricsType.Resource, }, op: OPERATORS.IN, - value: [`${servicename}`], + value: [servicename], }, ...tagFilterItems, ]; + const legendFormula = legend; const expression = FORMULA.ERROR_PERCENTAGE; - const disabled = true; - return getQueryBuilderQuerieswithFormula({ + const autocompleteData: BaseAutocompleteData[] = [ autocompleteDataA, autocompleteDataB, + ]; + + const additionalItems: TagFilterItem[][] = [ additionalItemsA, additionalItemsB, - legend, + ]; + + const legends = Array(2).fill(legend); + const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM); + const disabled = Array(2).fill(true); + const dataSource = DataSource.METRICS; + + return getQueryBuilderQuerieswithFormula({ + autocompleteData, + additionalItems, + legends, groupBy, disabled, expression, legendFormula, + aggregateOperators, + dataSource, }); }; @@ -107,10 +130,11 @@ export const externalCallDuration = ({ key: WidgetKeys.SignozExternalCallLatencyCount, type: null, }; + const expression = FORMULA.DATABASE_CALLS_AVG_DURATION; const legendFormula = 'Average Duration'; const legend = ''; - const disabled = true; + const disabled = Array(2).fill(true); const additionalItemsA: TagFilterItem[] = [ { id: '', @@ -125,17 +149,29 @@ export const externalCallDuration = ({ }, ...tagFilterItems, ]; - const additionalItemsB = additionalItemsA; - return getQueryBuilderQuerieswithFormula({ + const autocompleteData: BaseAutocompleteData[] = [ autocompleteDataA, autocompleteDataB, + ]; + + const additionalItems: TagFilterItem[][] = [ additionalItemsA, - additionalItemsB, - legend, + additionalItemsA, + ]; + + const legends = Array(2).fill(legend); + const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM); + + return getQueryBuilderQuerieswithFormula({ + autocompleteData, + additionalItems, + legends, disabled, expression, legendFormula, + aggregateOperators, + dataSource: DataSource.METRICS, }); }; @@ -169,13 +205,15 @@ export const externalCallRpsByAddress = ({ ], ]; - const legends: string[] = [legend]; + const legends = [legend]; + const dataSource = DataSource.METRICS; + return getQueryBuilderQueries({ autocompleteData, groupBy, legends, filterItems, - dataSource: DataSource.METRICS, + dataSource, }); }; @@ -198,7 +236,7 @@ export const externalCallDurationByAddress = ({ }; const expression = FORMULA.DATABASE_CALLS_AVG_DURATION; const legendFormula = legend; - const disabled = true; + const disabled = [true, true]; const additionalItemsA: TagFilterItem[] = [ { id: '', @@ -213,26 +251,30 @@ export const externalCallDurationByAddress = ({ }, ...tagFilterItems, ]; - const additionalItemsB = additionalItemsA; - return getQueryBuilderQuerieswithFormula({ + const autocompleteData: BaseAutocompleteData[] = [ autocompleteDataA, autocompleteDataB, + ]; + + const additionalItems: TagFilterItem[][] = [ additionalItemsA, - additionalItemsB, - legend, + additionalItemsA, + ]; + + const legends = Array(2).fill(legend); + const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM_RATE); + const dataSource = DataSource.METRICS; + + return getQueryBuilderQuerieswithFormula({ + autocompleteData, + additionalItems, + legends, groupBy, disabled, expression, legendFormula, + aggregateOperators, + dataSource, }); }; - -interface ExternalCallDurationByAddressProps extends ExternalCallProps { - legend: string; -} - -export interface ExternalCallProps { - servicename: IServiceName['servicename']; - tagFilterItems: TagFilterItem[]; -} diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts index 58f535cd04..2412dfce47 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts @@ -1,20 +1,21 @@ import { + alphabet, initialFormulaBuilderFormValues, initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import getStep from 'lib/getStep'; import store from 'store'; -import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { - IBuilderQuery, - TagFilterItem, -} from 'types/api/queryBuilder/queryBuilderData'; -import { - DataSource, MetricAggregateOperator, QueryBuilderData, } from 'types/common/queryBuilder'; +import { + BuilderQueriesProps, + BuilderQuerieswithFormulaProps, +} from '../Tabs/types'; + export const getQueryBuilderQueries = ({ autocompleteData, groupBy = [], @@ -61,15 +62,15 @@ export const getQueryBuilderQueries = ({ }); export const getQueryBuilderQuerieswithFormula = ({ - autocompleteDataA, - autocompleteDataB, - additionalItemsA, - additionalItemsB, - legend, + autocompleteData, + additionalItems, + legends, groupBy = [], disabled, expression, legendFormula, + aggregateOperators, + dataSource, }: BuilderQuerieswithFormulaProps): QueryBuilderData => ({ queryFormulas: [ { @@ -78,66 +79,25 @@ export const getQueryBuilderQuerieswithFormula = ({ legend: legendFormula, }, ], - queryData: [ - { - ...initialQueryBuilderFormValuesMap.metrics, - aggregateOperator: MetricAggregateOperator.SUM_RATE, - disabled, - groupBy, - legend, - aggregateAttribute: autocompleteDataA, - reduceTo: 'sum', - filters: { - items: additionalItemsA, - op: 'AND', - }, - stepInterval: getStep({ - end: store.getState().globalTime.maxTime, - inputFormat: 'ns', - start: store.getState().globalTime.minTime, - }), + queryData: autocompleteData.map((_, index) => ({ + ...initialQueryBuilderFormValuesMap.metrics, + aggregateOperator: aggregateOperators[index], + disabled: disabled[index], + groupBy, + legend: legends[index], + aggregateAttribute: autocompleteData[index], + queryName: alphabet[index], + expression: alphabet[index], + reduceTo: 'sum', + filters: { + items: additionalItems[index], + op: 'AND', }, - { - ...initialQueryBuilderFormValuesMap.metrics, - aggregateOperator: MetricAggregateOperator.SUM_RATE, - disabled, - groupBy, - legend, - aggregateAttribute: autocompleteDataB, - queryName: 'B', - expression: 'B', - reduceTo: 'sum', - filters: { - items: additionalItemsB, - op: 'AND', - }, - stepInterval: getStep({ - end: store.getState().globalTime.maxTime, - inputFormat: 'ns', - start: store.getState().globalTime.minTime, - }), - }, - ], + stepInterval: getStep({ + end: store.getState().globalTime.maxTime, + inputFormat: 'ns', + start: store.getState().globalTime.minTime, + }), + dataSource, + })), }); - -interface BuilderQueriesProps { - autocompleteData: BaseAutocompleteData[]; - groupBy?: BaseAutocompleteData[]; - legends: string[]; - filterItems: TagFilterItem[][]; - aggregateOperator?: string[]; - dataSource: DataSource; - queryNameAndExpression?: string[]; -} - -interface BuilderQuerieswithFormulaProps { - autocompleteDataA: BaseAutocompleteData; - autocompleteDataB: BaseAutocompleteData; - legend: string; - disabled: boolean; - groupBy?: BaseAutocompleteData[]; - expression: string; - legendFormula: string; - additionalItemsA: TagFilterItem[]; - additionalItemsB: TagFilterItem[]; -} diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts index ec2d7b9272..e1137f4cfc 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/OverviewQueries.ts @@ -1,7 +1,11 @@ import { OPERATORS } from 'constants/queryBuilder'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource, QueryBuilderData } from 'types/common/queryBuilder'; +import { + DataSource, + MetricAggregateOperator, + QueryBuilderData, +} from 'types/common/queryBuilder'; import { DataType, @@ -14,7 +18,7 @@ import { QUERYNAME_AND_EXPRESSION, WidgetKeys, } from '../constant'; -import { IServiceName } from '../Tabs/types'; +import { LatencyProps, OperationPerSecProps } from '../Tabs/types'; import { getQueryBuilderQueries, getQueryBuilderQuerieswithFormula, @@ -35,9 +39,7 @@ export const latency = ({ type: isSpanMetricEnable ? null : MetricsType.Tag, }; - const autocompleteData: BaseAutocompleteData[] = Array(3).fill( - newAutoCompleteData, - ); + const autocompleteData = Array(3).fill(newAutoCompleteData); const filterItem: TagFilterItem[] = [ { @@ -65,17 +67,21 @@ export const latency = ({ ...tagFilterItems, ]; - const filterItems: TagFilterItem[][] = Array(3).fill([...filterItem]); + const filterItems = Array(3).fill([...filterItem]); + const legends = LATENCY_AGGREGATEOPERATOR; + const aggregateOperator = isSpanMetricEnable + ? LATENCY_AGGREGATEOPERATOR_SPAN_METRICS + : LATENCY_AGGREGATEOPERATOR; + const dataSource = isSpanMetricEnable ? DataSource.METRICS : DataSource.TRACES; + const queryNameAndExpression = QUERYNAME_AND_EXPRESSION; return getQueryBuilderQueries({ autocompleteData, - legends: LATENCY_AGGREGATEOPERATOR, + legends, filterItems, - aggregateOperator: isSpanMetricEnable - ? LATENCY_AGGREGATEOPERATOR_SPAN_METRICS - : LATENCY_AGGREGATEOPERATOR, - dataSource: isSpanMetricEnable ? DataSource.METRICS : DataSource.TRACES, - queryNameAndExpression: QUERYNAME_AND_EXPRESSION, + aggregateOperator, + dataSource, + queryNameAndExpression, }); }; @@ -121,11 +127,14 @@ export const operationPerSec = ({ ], ]; + const legends = OPERATION_LEGENDS; + const dataSource = DataSource.METRICS; + return getQueryBuilderQueries({ autocompleteData, - legends: OPERATION_LEGENDS, + legends, filterItems, - dataSource: DataSource.METRICS, + dataSource, }); }; @@ -146,6 +155,9 @@ export const errorPercentage = ({ isColumn: true, type: null, }; + + const autocompleteData = [autocompleteDataA, autocompleteDataB]; + const additionalItemsA: TagFilterItem[] = [ { id: '', @@ -209,27 +221,25 @@ export const errorPercentage = ({ ...tagFilterItems, ]; + const additionalItems = [additionalItemsA, additionalItemsB]; + const legends = [GraphTitle.ERROR_PERCENTAGE]; + const disabled = [true, true]; + const expression = FORMULA.ERROR_PERCENTAGE; + const legendFormula = GraphTitle.ERROR_PERCENTAGE; + const aggregateOperators = [ + MetricAggregateOperator.SUM_RATE, + MetricAggregateOperator.SUM_RATE, + ]; + const dataSource = DataSource.METRICS; + return getQueryBuilderQuerieswithFormula({ - autocompleteDataA, - autocompleteDataB, - additionalItemsA, - additionalItemsB, - legend: GraphTitle.ERROR_PERCENTAGE, - disabled: true, - expression: FORMULA.ERROR_PERCENTAGE, - legendFormula: GraphTitle.ERROR_PERCENTAGE, + autocompleteData, + additionalItems, + legends, + disabled, + expression, + legendFormula, + aggregateOperators, + dataSource, }); }; - -export interface OperationPerSecProps { - servicename: IServiceName['servicename']; - tagFilterItems: TagFilterItem[]; - topLevelOperations: string[]; -} - -export interface LatencyProps { - servicename: IServiceName['servicename']; - tagFilterItems: TagFilterItem[]; - isSpanMetricEnable?: boolean; - topLevelOperationsRoute: string[]; -} diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts new file mode 100644 index 0000000000..6f75d9666d --- /dev/null +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/TopOperationQueries.ts @@ -0,0 +1,142 @@ +import { OPERATORS } from 'constants/queryBuilder'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { + DataSource, + MetricAggregateOperator, + QueryBuilderData, +} from 'types/common/queryBuilder'; + +import { + DataType, + GraphTitle, + KeyOperationTableHeader, + MetricsType, + WidgetKeys, +} from '../constant'; +import { TopOperationQueryFactoryProps } from '../Tabs/types'; +import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory'; + +export const topOperationQueries = ({ + servicename, +}: TopOperationQueryFactoryProps): QueryBuilderData => { + const latencyAutoCompleteData: BaseAutocompleteData = { + key: WidgetKeys.Signoz_latency_bucket, + dataType: DataType.FLOAT64, + isColumn: true, + type: null, + }; + + const errorRateAutoCompleteData: BaseAutocompleteData = { + key: WidgetKeys.SignozCallsTotal, + dataType: DataType.FLOAT64, + isColumn: true, + type: null, + }; + + const numOfCallAutoCompleteData: BaseAutocompleteData = { + key: WidgetKeys.SignozLatencyCount, + dataType: DataType.FLOAT64, + isColumn: true, + type: null, + }; + + const latencyAndNumberOfCallAdditionalItems: TagFilterItem[] = [ + { + id: '', + key: { + key: WidgetKeys.Service_name, + dataType: DataType.STRING, + isColumn: false, + type: MetricsType.Resource, + }, + value: [servicename], + op: OPERATORS.IN, + }, + ]; + + const errorRateAdditionalItemsA: TagFilterItem[] = [ + { + id: '', + key: { + dataType: DataType.STRING, + isColumn: false, + key: WidgetKeys.Service_name, + type: MetricsType.Resource, + }, + op: OPERATORS.IN, + value: [servicename], + }, + { + id: '', + key: { + dataType: DataType.INT64, + isColumn: false, + key: WidgetKeys.StatusCode, + type: MetricsType.Tag, + }, + op: OPERATORS.IN, + value: ['STATUS_CODE_ERROR'], + }, + ]; + + const errorRateAdditionalItemsB = latencyAndNumberOfCallAdditionalItems; + + const groupBy: BaseAutocompleteData[] = [ + { + dataType: DataType.STRING, + isColumn: false, + key: WidgetKeys.Operation, + type: MetricsType.Tag, + }, + ]; + + const autocompleteData = [ + latencyAutoCompleteData, + latencyAutoCompleteData, + latencyAutoCompleteData, + errorRateAutoCompleteData, + errorRateAutoCompleteData, + numOfCallAutoCompleteData, + ]; + const additionalItems = [ + latencyAndNumberOfCallAdditionalItems, + latencyAndNumberOfCallAdditionalItems, + latencyAndNumberOfCallAdditionalItems, + errorRateAdditionalItemsA, + errorRateAdditionalItemsB, + latencyAndNumberOfCallAdditionalItems, + ]; + const disabled = [false, false, false, true, true, false]; + const legends = [ + KeyOperationTableHeader.P50, + KeyOperationTableHeader.P90, + KeyOperationTableHeader.P99, + KeyOperationTableHeader.ERROR_RATE, + KeyOperationTableHeader.ERROR_RATE, + KeyOperationTableHeader.NUM_OF_CALLS, + ]; + const aggregateOperators = [ + MetricAggregateOperator.HIST_QUANTILE_50, + MetricAggregateOperator.HIST_QUANTILE_90, + MetricAggregateOperator.HIST_QUANTILE_99, + MetricAggregateOperator.SUM_RATE, + MetricAggregateOperator.SUM_RATE, + MetricAggregateOperator.SUM_RATE, + ]; + const expression = 'D*100/E'; + const legendFormula = GraphTitle.ERROR_PERCENTAGE; + const dataSource = DataSource.METRICS; + + return getQueryBuilderQuerieswithFormula({ + autocompleteData, + additionalItems, + disabled, + legends, + aggregateOperators, + expression, + legendFormula, + dataSource, + groupBy, + }); +}; diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index e2fd6d240f..678d271d11 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -1,4 +1,5 @@ import { Col } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridGraphLayout/Graph/'; import { databaseCallsAvgDuration, @@ -50,8 +51,8 @@ function DBCall(): JSX.Element { const databaseCallsRPSWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: databaseCallsRPS({ @@ -62,14 +63,15 @@ function DBCall(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.DATABASE_CALLS_RPS, - ), + title: GraphTitle.DATABASE_CALLS_RPS, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); const databaseCallsAverageDurationWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: databaseCallsAvgDuration({ @@ -79,8 +81,9 @@ function DBCall(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.DATABASE_CALLS_AVG_DURATION, - ), + title: GraphTitle.DATABASE_CALLS_AVG_DURATION, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index ab1e99f430..6595a11808 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -1,4 +1,5 @@ import { Col } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridGraphLayout/Graph/'; import { externalCallDuration, @@ -41,8 +42,8 @@ function External(): JSX.Element { const externalCallErrorWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: externalCallErrorPercent({ @@ -53,8 +54,9 @@ function External(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, - ), + title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); @@ -65,8 +67,8 @@ function External(): JSX.Element { const externalCallDurationWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: externalCallDuration({ @@ -76,15 +78,16 @@ function External(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.EXTERNAL_CALL_DURATION, - ), + title: GraphTitle.EXTERNAL_CALL_DURATION, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); const externalCallRPSWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: externalCallRpsByAddress({ @@ -95,15 +98,16 @@ function External(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, - ), + title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); const externalCallDurationAddressWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: externalCallDurationByAddress({ @@ -114,8 +118,9 @@ function External(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, - ), + title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 9d30b624f2..bb34eb78d7 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -2,10 +2,13 @@ import getTopLevelOperations, { ServiceDataProps, } from 'api/metrics/getTopLevelOperations'; import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; +import { FeatureKeys } from 'constants/features'; import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; import { routeConfig } from 'container/SideNav/config'; import { getQueryString } from 'container/SideNav/helper'; +import useFeatureFlag from 'hooks/useFeatureFlag'; import useResourceAttribute from 'hooks/useResourceAttribute'; import { convertRawQueriesToTraceSelectedTags, @@ -29,10 +32,11 @@ import { errorPercentage, operationPerSec, } from '../MetricsPageQueries/OverviewQueries'; -import { Col, Row } from '../styles'; +import { Card, Col, Row } from '../styles'; import ServiceOverview from './Overview/ServiceOverview'; import TopLevelOperation from './Overview/TopLevelOperations'; import TopOperation from './Overview/TopOperation'; +import TopOperationMetrics from './Overview/TopOperationMetrics'; import { Button } from './styles'; import { IServiceName } from './types'; import { @@ -53,6 +57,8 @@ function Application(): JSX.Element { () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [], [queries], ); + const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS) + ?.active; const handleSetTimeStamp = useCallback((selectTime: number) => { setSelectedTimeStamp(selectTime); @@ -104,8 +110,8 @@ function Application(): JSX.Element { const operationPerSecWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: operationPerSec({ @@ -116,15 +122,16 @@ function Application(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.RATE_PER_OPS, - ), + title: GraphTitle.RATE_PER_OPS, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems, topLevelOperationsRoute], ); const errorPercentageWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: errorPercentage({ @@ -135,8 +142,9 @@ function Application(): JSX.Element { clickhouse_sql: [], id: uuid(), }, - GraphTitle.ERROR_PERCENTAGE, - ), + title: GraphTitle.ERROR_PERCENTAGE, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems, topLevelOperationsRoute], ); @@ -239,7 +247,9 @@ function Application(): JSX.Element { - + + {isSpanMetricEnabled ? : } + diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index 3b1ad2b7d9..28895d2909 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -1,4 +1,5 @@ import { FeatureKeys } from 'constants/features'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import Graph from 'container/GridGraphLayout/Graph/'; import { GraphTitle } from 'container/MetricsApplication/constant'; import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; @@ -31,8 +32,8 @@ function ServiceOverview({ const latencyWidget = useMemo( () => - getWidgetQueryBuilder( - { + getWidgetQueryBuilder({ + query: { queryType: EQueryType.QUERY_BUILDER, promql: [], builder: latency({ @@ -44,11 +45,14 @@ function ServiceOverview({ clickhouse_sql: [], id: uuid(), }, - GraphTitle.LATENCY, - ), + title: GraphTitle.LATENCY, + panelTypes: PANEL_TYPES.TIME_SERIES, + }), [servicename, tagFilterItems, isSpanMetricEnable, topLevelOperationsRoute], ); + const isQueryEnabled = topLevelOperationsRoute.length > 0; + return ( <> - - - ), - [], - ); - - type DataIndex = keyof ServicesList; - - const getColumnSearchProps = useCallback( - (dataIndex: DataIndex): ColumnType => ({ - filterDropdown, - filterIcon: FilterIcon, - onFilter: (value: string | number | boolean, record: DataProps): boolean => - record[dataIndex] - .toString() - .toLowerCase() - .includes(value.toString().toLowerCase()), - render: (metrics: string): JSX.Element => { - const urlParams = new URLSearchParams(search); - const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; - const queryString = getQueryString(avialableParams, urlParams); - - return ( - - {metrics} - - ); - }, - }), - [filterDropdown, FilterIcon, search], - ); - - const columns: ColumnsType = useMemo( - () => [ - { - title: 'Application', - dataIndex: 'serviceName', - width: 200, - key: 'serviceName', - ...getColumnSearchProps('serviceName'), - }, - { - title: 'P99 latency (in ms)', - dataIndex: 'p99', - key: 'p99', - width: 150, - defaultSortOrder: 'descend', - sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, - render: (value: number): string => (value / 1000000).toFixed(2), - }, - { - title: 'Error Rate (% of total)', - dataIndex: 'errorRate', - key: 'errorRate', - width: 150, - sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate, - render: (value: number): string => value.toFixed(2), - }, - { - title: 'Operations Per Second', - dataIndex: 'callRate', - key: 'callRate', - width: 150, - sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate, - render: (value: number): string => value.toFixed(2), - }, - ], - [getColumnSearchProps], - ); - - if ( - services.length === 0 && - loading === false && - !skipOnboarding && - error === true - ) { - return ; - } - - return ( - - - - ); -} - -type DataProps = ServicesList; - -export default Metrics; diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index 8ede8520b7..a5c48ed307 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - -import { initialQueriesMap } from 'constants/queryBuilder'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -17,7 +15,7 @@ import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import DashboardReducer from 'types/reducer/dashboards'; -import menuItems, { ITEMS } from './menuItems'; +import menuItems from './menuItems'; import { Card, Container, Text } from './styles'; function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { @@ -31,7 +29,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { const { data } = selectedDashboard; const onClickHandler = useCallback( - async (name: ITEMS) => { + (name: PANEL_TYPES) => (): void => { try { const emptyLayout = data.layout?.find((e) => e.i === 'empty'); @@ -65,14 +63,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { return ( {menuItems.map(({ name, Icon, display }) => ( - { - event.preventDefault(); - onClickHandler(name); - }} - id={name} - key={name} - > + {display} @@ -81,8 +72,6 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { ); } -export type GRAPH_TYPES = ITEMS; - interface DispatchProps { toggleAddWidget: ( props: ToggleAddWidgetProps, diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts index c17893dbc9..560da8deef 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/menuItems.ts @@ -1,4 +1,5 @@ -import TimeSeries from 'assets/Dashboard/TimeSeries'; +import TableIcon from 'assets/Dashboard/Table'; +import TimeSeriesIcon from 'assets/Dashboard/TimeSeries'; import ValueIcon from 'assets/Dashboard/Value'; import { PANEL_TYPES } from 'constants/queryBuilder'; import { CSSProperties } from 'react'; @@ -6,7 +7,7 @@ import { CSSProperties } from 'react'; const Items: ItemsProps[] = [ { name: PANEL_TYPES.TIME_SERIES, - Icon: TimeSeries, + Icon: TimeSeriesIcon, display: 'Time Series', }, { @@ -14,18 +15,11 @@ const Items: ItemsProps[] = [ Icon: ValueIcon, display: 'Value', }, + { name: PANEL_TYPES.TABLE, Icon: TableIcon, display: 'Table' }, ]; -export type ITEMS = - | 'graph' - | 'value' - | 'list' - | 'table' - | 'EMPTY_WIDGET' - | 'trace'; - interface ItemsProps { - name: ITEMS; + name: PANEL_TYPES; Icon: (props: IconProps) => JSX.Element; display: string; } diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts index ad59f774a8..d7d3c6a7f2 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts +++ b/frontend/src/container/NewDashboard/ComponentsSlider/styles.ts @@ -16,6 +16,7 @@ export const Card = styled(CardComponent)` display: flex; flex-direction: column; justify-content: space-between; + align-items: center; } `; diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index 51501c8b39..52f4bbc92c 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -1,6 +1,6 @@ import { Button, Tabs, Typography } from 'antd'; import TextToolTip from 'components/TextToolTip'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { QueryBuilder } from 'container/QueryBuilder'; import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces'; @@ -170,7 +170,7 @@ const mapDispatchToProps = ( }); interface QueryProps extends DispatchProps { - selectedGraph: GRAPH_TYPES; + selectedGraph: PANEL_TYPES; selectedTime: WidgetGraphProps['selectedTime']; } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx index 6fc58de892..219d4e011b 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/PlotTag.tsx @@ -1,20 +1,23 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { EQueryType } from 'types/common/dashboard'; import QueryTypeTag from '../QueryTypeTag'; +import { PlotTagWrapperStyled } from './styles'; interface IPlotTagProps { queryType: EQueryType; + panelType: PANEL_TYPES; } -function PlotTag({ queryType }: IPlotTagProps): JSX.Element | null { +function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null { if (queryType === undefined) { return null; } return ( -
+ Plotted using -
+ ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx index 3728daec4c..6f4430ec38 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraph.tsx @@ -1,8 +1,9 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; -import GridGraphComponent from 'container/GridGraphComponent'; +import GridPanelSwitch from 'container/GridPanelSwitch'; import { WidgetGraphProps } from 'container/NewWidget/types'; import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import getChartData from 'lib/getChartData'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; @@ -16,6 +17,7 @@ function WidgetGraph({ yAxisUnit, selectedTime, }: WidgetGraphProps): JSX.Element { + const { stagedQuery } = useQueryBuilder(); const { dashboards } = useSelector( (state) => state.dashboards, ); @@ -39,7 +41,7 @@ function WidgetGraph({ return Invalid widget; } - const { title, opacity, isStacked } = selectedWidget; + const { title, opacity, isStacked, query } = selectedWidget; if (getWidgetQueryRange.error) { return ( @@ -66,14 +68,18 @@ function WidgetGraph({ }); return ( - ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index a30b7e9fe5..62cab5f861 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -41,12 +41,12 @@ function WidgetGraph({ }); if (selectedWidget === undefined) { - return Invalid widget; + return Invalid widget; } return ( - - + + {getWidgetQueryRange.error && ( diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts index 32a8c15f08..58239ad942 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/styles.ts @@ -1,15 +1,23 @@ import { Card, Tooltip } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import styled from 'styled-components'; -export const Container = styled(Card)` +interface Props { + $panelType: PANEL_TYPES; +} + +export const Container = styled(Card)` &&& { position: relative; } .ant-card-body { - padding: 1.5rem 0; + padding: ${({ $panelType }): string => + $panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'}; height: 57vh; - /* padding-bottom: 2rem; */ + overflow: auto; + display: flex; + flex-direction: column; } `; @@ -23,5 +31,14 @@ export const NotFoundContainer = styled.div` display: flex; justify-content: center; align-items: center; - min-height: 55vh; + min-height: 47vh; +`; + +export const PlotTagWrapperStyled = styled.div` + margin-left: 2rem; + margin-top: ${({ $panelType }): string => + $panelType === PANEL_TYPES.TABLE ? '1rem' : '0'}; + + margin-bottom: ${({ $panelType }): string => + $panelType === PANEL_TYPES.TABLE ? '1rem' : '0'}; `; diff --git a/frontend/src/container/NewWidget/RightContainer/index.tsx b/frontend/src/container/NewWidget/RightContainer/index.tsx index 0f9ab1c597..b2fff041f9 100644 --- a/frontend/src/container/NewWidget/RightContainer/index.tsx +++ b/frontend/src/container/NewWidget/RightContainer/index.tsx @@ -1,10 +1,8 @@ import { Input, Select } from 'antd'; import InputComponent from 'components/Input'; import TimePreference from 'components/TimePreferenceDropDown'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; -import GraphTypes, { - ITEMS, -} from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems'; import { Dispatch, SetStateAction, useCallback } from 'react'; import { Container, Title } from './styles'; @@ -144,12 +142,12 @@ interface RightContainerProps { setOpacity: Dispatch>; selectedNullZeroValue: string; setSelectedNullZeroValue: Dispatch>; - selectedGraph: GRAPH_TYPES; + selectedGraph: PANEL_TYPES; setSelectedTime: Dispatch>; selectedTime: timePreferance; yAxisUnit: string; setYAxisUnit: Dispatch>; - setGraphHandler: (type: ITEMS) => void; + setGraphHandler: (type: PANEL_TYPES) => void; } export default RightContainer; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 17cb4ea884..eb49ec8bd0 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -1,8 +1,8 @@ import { LockFilled } from '@ant-design/icons'; import { Button, Modal, Tooltip, Typography } from 'antd'; import { FeatureKeys } from 'constants/features'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; @@ -148,7 +148,7 @@ function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element { history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); }, [dashboardId, dispatch]); - const setGraphHandler = (type: ITEMS): void => { + const setGraphHandler = (type: PANEL_TYPES): void => { const params = new URLSearchParams(search); params.set('graphType', type); history.push({ search: params.toString() }); diff --git a/frontend/src/container/NewWidget/types.ts b/frontend/src/container/NewWidget/types.ts index ca6a121382..e4fce1ba89 100644 --- a/frontend/src/container/NewWidget/types.ts +++ b/frontend/src/container/NewWidget/types.ts @@ -1,10 +1,10 @@ -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { Widgets } from 'types/api/dashboard/getAll'; import { timePreferance } from './RightContainer/timeItems'; export interface NewWidgetProps { - selectedGraph: GRAPH_TYPES; + selectedGraph: PANEL_TYPES; yAxisUnit: Widgets['yAxisUnit']; } diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index fec93c7106..0923395296 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -1,10 +1,18 @@ -import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete'; import { ReactNode } from 'react'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { DataSource } from 'types/common/queryBuilder'; import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces'; +type FilterConfigs = { + [Key in keyof Omit]: { + isHidden: boolean; + isDisabled: boolean; + }; +} & { filters: WhereClauseConfig }; + export type QueryBuilderConfig = | { queryVariant: 'static'; @@ -14,10 +22,8 @@ export type QueryBuilderConfig = export type QueryBuilderProps = { config?: QueryBuilderConfig; - panelType: ITEMS; + panelType: PANEL_TYPES; actions?: ReactNode; - filterConfigs?: Partial< - Record - >; + filterConfigs?: Partial; queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode }; }; diff --git a/frontend/src/container/QueryBuilder/components/Query/Query.tsx b/frontend/src/container/QueryBuilder/components/Query/Query.tsx index e60c562200..7f8b4c1cdc 100644 --- a/frontend/src/container/QueryBuilder/components/Query/Query.tsx +++ b/frontend/src/container/QueryBuilder/components/Query/Query.tsx @@ -220,16 +220,19 @@ export const Query = memo(function Query({ - - - - - - - - - - + {!filterConfigs?.having?.isHidden && ( + + + + + + + + + + + )} + @@ -248,6 +251,7 @@ export const Query = memo(function Query({ panelType, isMetricsDataSource, query, + filterConfigs?.having?.isHidden, handleChangeLimit, handleChangeHavingFilter, renderOrderByFilter, @@ -305,7 +309,11 @@ export const Query = memo(function Query({ )} - + diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index 8b3e8e54ea..7111c1b0e8 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -1,5 +1,8 @@ import { Select, Spin, Tag, Tooltip } from 'antd'; -import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete'; +import { + useAutoComplete, + WhereClauseConfig, +} from 'hooks/queryBuilder/useAutoComplete'; import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues'; import { KeyboardEvent, @@ -31,6 +34,7 @@ import { function QueryBuilderSearch({ query, onChange, + whereClauseConfig, }: QueryBuilderSearchProps): JSX.Element { const { updateTag, @@ -45,7 +49,7 @@ function QueryBuilderSearch({ isFetching, setSearchKey, searchKey, - } = useAutoComplete(query); + } = useAutoComplete(query, whereClauseConfig); const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues( searchValue, @@ -169,7 +173,7 @@ function QueryBuilderSearch({ notFoundContent={isFetching ? : null} > {options.map((option) => ( - + {option.label} {option.selected && } @@ -181,8 +185,13 @@ function QueryBuilderSearch({ interface QueryBuilderSearchProps { query: IBuilderQuery; onChange: (value: TagFilter) => void; + whereClauseConfig?: WhereClauseConfig; } +QueryBuilderSearch.defaultProps = { + whereClauseConfig: undefined, +}; + export interface CustomTagProps { label: ReactNode; value: string; diff --git a/frontend/src/container/QueryTable/QueryTable.intefaces.ts b/frontend/src/container/QueryTable/QueryTable.intefaces.ts index f76aba08b3..de3885701a 100644 --- a/frontend/src/container/QueryTable/QueryTable.intefaces.ts +++ b/frontend/src/container/QueryTable/QueryTable.intefaces.ts @@ -13,4 +13,5 @@ export type QueryTableProps = Omit< query: Query; renderActionCell?: (record: RowData) => ReactNode; modifyColumns?: (columns: ColumnsType) => ColumnsType; + renderColumnCell?: Record ReactNode>; }; diff --git a/frontend/src/container/QueryTable/QueryTable.tsx b/frontend/src/container/QueryTable/QueryTable.tsx index 309b0fd338..6220540b9a 100644 --- a/frontend/src/container/QueryTable/QueryTable.tsx +++ b/frontend/src/container/QueryTable/QueryTable.tsx @@ -9,6 +9,7 @@ export function QueryTable({ query, renderActionCell, modifyColumns, + renderColumnCell, ...props }: QueryTableProps): JSX.Element { const { columns, dataSource } = useMemo( @@ -17,15 +18,12 @@ export function QueryTable({ query, queryTableData, renderActionCell, + renderColumnCell, }), - [query, queryTableData, renderActionCell], + [query, queryTableData, renderColumnCell, renderActionCell], ); - const filteredColumns = columns.filter((item) => item.key !== 'timestamp'); - - const tableColumns = modifyColumns - ? modifyColumns(filteredColumns) - : filteredColumns; + const tableColumns = modifyColumns ? modifyColumns(columns) : columns; return ( => ({ + filterDropdown, + filterIcon: , + onFilter: (value: string | number | boolean, record: ServicesList): boolean => + record[dataIndex] + .toString() + .toLowerCase() + .includes(value.toString().toLowerCase()), + render: (metrics: string): JSX.Element => { + const urlParams = new URLSearchParams(search); + const avialableParams = routeConfig[ROUTES.SERVICE_METRICS]; + const queryString = getQueryString(avialableParams, urlParams); + + return ( + + {metrics} + + ); + }, +}); diff --git a/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts b/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts new file mode 100644 index 0000000000..f613a9dab4 --- /dev/null +++ b/frontend/src/container/ServiceTable/Columns/ServiceColumn.ts @@ -0,0 +1,46 @@ +import type { ColumnsType } from 'antd/es/table'; +import { ServicesList } from 'types/api/metrics/getService'; + +import { + ColumnKey, + ColumnTitle, + ColumnWidth, + SORTING_ORDER, +} from './ColumnContants'; +import { getColumnSearchProps } from './GetColumnSearchProps'; + +export const getColumns = (search: string): ColumnsType => [ + { + title: ColumnTitle[ColumnKey.Application], + dataIndex: ColumnKey.Application, + width: ColumnWidth.Application, + key: ColumnKey.Application, + ...getColumnSearchProps('serviceName', search), + }, + { + title: ColumnTitle[ColumnKey.P99], + dataIndex: ColumnKey.P99, + key: ColumnKey.P99, + width: ColumnWidth.P99, + defaultSortOrder: SORTING_ORDER, + sorter: (a: ServicesList, b: ServicesList): number => a.p99 - b.p99, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: ColumnTitle[ColumnKey.ErrorRate], + dataIndex: ColumnKey.ErrorRate, + key: ColumnKey.ErrorRate, + width: 150, + sorter: (a: ServicesList, b: ServicesList): number => + a.errorRate - b.errorRate, + render: (value: number): string => value.toFixed(2), + }, + { + title: ColumnTitle[ColumnKey.Operations], + dataIndex: ColumnKey.Operations, + key: ColumnKey.Operations, + width: ColumnWidth.Operations, + sorter: (a: ServicesList, b: ServicesList): number => a.callRate - b.callRate, + render: (value: number): string => value.toFixed(2), + }, +]; diff --git a/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx b/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx new file mode 100644 index 0000000000..1dc4a12d89 --- /dev/null +++ b/frontend/src/container/ServiceTable/Filter/FilterDropdown.tsx @@ -0,0 +1,41 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { Button, Card, Input, Space } from 'antd'; +import type { FilterDropdownProps } from 'antd/es/table/interface'; + +import { SEARCH_PLACEHOLDER } from '../Columns/ColumnContants'; + +export const filterDropdown = ({ + setSelectedKeys, + selectedKeys, + confirm, +}: FilterDropdownProps): JSX.Element => { + const handleSearch = (): void => { + confirm(); + }; + + const selectedKeysHandler = (e: React.ChangeEvent): void => { + setSelectedKeys(e.target.value ? [e.target.value] : []); + }; + + return ( + + + + + + + ); +}; diff --git a/frontend/src/container/ServiceTable/Service.test.tsx b/frontend/src/container/ServiceTable/Service.test.tsx new file mode 100644 index 0000000000..4fc9231a78 --- /dev/null +++ b/frontend/src/container/ServiceTable/Service.test.tsx @@ -0,0 +1,50 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import ROUTES from 'constants/routes'; +import { BrowserRouter } from 'react-router-dom'; + +import { Services } from './__mock__/servicesListMock'; +import Metrics from './index'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: (): { pathname: string } => ({ + pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.APPLICATION}/`, + }), +})); + +describe('Metrics Component', () => { + it('renders without errors', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText(/application/i)).toBeInTheDocument(); + expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument(); + expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument(); + expect(screen.getByText(/operations per second/i)).toBeInTheDocument(); + }); + }); + + it('renders if the data is loaded in the table', async () => { + render( + + + , + ); + + expect(screen.getByText('frontend')).toBeInTheDocument(); + }); + + it('renders no data when required conditions are met', async () => { + render( + + + , + ); + + expect(screen.getByText('No data')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx b/frontend/src/container/ServiceTable/SkipOnBoardModal/index.tsx similarity index 100% rename from frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx rename to frontend/src/container/ServiceTable/SkipOnBoardModal/index.tsx diff --git a/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts b/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts new file mode 100644 index 0000000000..283ba7718b --- /dev/null +++ b/frontend/src/container/ServiceTable/__mock__/servicesListMock.ts @@ -0,0 +1,22 @@ +import { ServicesList } from 'types/api/metrics/getService'; + +export const Services: ServicesList[] = [ + { + serviceName: 'frontend', + p99: 1261498140, + avgDuration: 768497850.9803921, + numCalls: 255, + callRate: 0.9444444444444444, + numErrors: 0, + errorRate: 0, + }, + { + serviceName: 'customer', + p99: 890150740.0000001, + avgDuration: 369612035.2941176, + numCalls: 255, + callRate: 0.9444444444444444, + numErrors: 0, + errorRate: 0, + }, +]; diff --git a/frontend/src/container/ServiceTable/index.tsx b/frontend/src/container/ServiceTable/index.tsx new file mode 100644 index 0000000000..6c9fa1aede --- /dev/null +++ b/frontend/src/container/ServiceTable/index.tsx @@ -0,0 +1,26 @@ +import { ResizeTable } from 'components/ResizeTable'; +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; + +import { getColumns } from './Columns/ServiceColumn'; +import { Container } from './styles'; +import ServiceTableProp from './types'; + +function Services({ services, isLoading }: ServiceTableProp): JSX.Element { + const { search } = useLocation(); + + const tableColumns = useMemo(() => getColumns(search), [search]); + + return ( + + + + ); +} + +export default Services; diff --git a/frontend/src/container/MetricsTable/styles.ts b/frontend/src/container/ServiceTable/styles.ts similarity index 100% rename from frontend/src/container/MetricsTable/styles.ts rename to frontend/src/container/ServiceTable/styles.ts diff --git a/frontend/src/container/ServiceTable/types.ts b/frontend/src/container/ServiceTable/types.ts new file mode 100644 index 0000000000..7118bfa2fe --- /dev/null +++ b/frontend/src/container/ServiceTable/types.ts @@ -0,0 +1,6 @@ +import { ServicesList } from 'types/api/metrics/getService'; + +export default interface ServiceTableProp { + services: ServicesList[]; + isLoading: boolean; +} diff --git a/frontend/src/container/TracesExplorer/QuerySection/index.tsx b/frontend/src/container/TracesExplorer/QuerySection/index.tsx index 81514b496d..80f2925b37 100644 --- a/frontend/src/container/TracesExplorer/QuerySection/index.tsx +++ b/frontend/src/container/TracesExplorer/QuerySection/index.tsx @@ -17,12 +17,14 @@ function QuerySection(): JSX.Element { const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => { + const isList = panelTypes === PANEL_TYPES.LIST; const config: QueryBuilderProps['filterConfigs'] = { stepInterval: { isHidden: false, isDisabled: true }, + having: { isHidden: isList, isDisabled: true }, }; return config; - }, []); + }, [panelTypes]); const renderOrderBy = useCallback( ({ query, onChange }: OrderByFilterProps) => ( diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts index c7519761ec..e326a0e33f 100644 --- a/frontend/src/hooks/queryBuilder/useAutoComplete.ts +++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts @@ -1,7 +1,6 @@ import { getRemovePrefixFromKey, getTagToken, - isExistsNotExistsOperator, replaceStringWithMaxLength, tagRegexp, } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; @@ -16,7 +15,15 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator'; import { useTag } from './useTag'; import { useTagValidation } from './useTagValidation'; -export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => { +export type WhereClauseConfig = { + customKey: string; + customOp: string; +}; + +export const useAutoComplete = ( + query: IBuilderQuery, + whereClauseConfig?: WhereClauseConfig, +): IAutoComplete => { const [searchValue, setSearchValue] = useState(''); const [searchKey, setSearchKey] = useState(''); @@ -40,11 +47,11 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => { ); const { handleAddTag, handleClearTag, tags, updateTag } = useTag( - key, isValidTag, handleSearch, query, setSearchKey, + whereClauseConfig, ); const handleSelect = useCallback( @@ -59,11 +66,10 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => { }); } if (!isMulti) { - if (isExistsNotExistsOperator(value)) handleAddTag(value); - if (isValidTag && !isExistsNotExistsOperator(value)) handleAddTag(value); + handleAddTag(value); } }, - [handleAddTag, isMulti, isValidTag], + [handleAddTag, isMulti], ); const handleKeyDown = useCallback( @@ -102,6 +108,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => { isExist, results, result, + whereClauseConfig, ); return { diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index 92789b960e..408e598727 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -1,6 +1,5 @@ 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'; @@ -15,7 +14,7 @@ import { useQueryBuilder } from './useQueryBuilder'; export const useGetExplorerQueryRange = ( requestData: Query | null, - panelType: GRAPH_TYPES | null, + panelType: PANEL_TYPES | null, options?: UseQueryOptions, Error>, ): UseQueryResult, Error> => { const { isEnabledQuery } = useQueryBuilder(); diff --git a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts index 560790e574..8b5aed7a44 100644 --- a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts +++ b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts @@ -1,11 +1,11 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import useUrlQuery from 'hooks/useUrlQuery'; import { useMemo } from 'react'; -export const useGetPanelTypesQueryParam = ( +export const useGetPanelTypesQueryParam = ( defaultPanelType?: T, -): T extends undefined ? GRAPH_TYPES | null : GRAPH_TYPES => { +): T extends undefined ? PANEL_TYPES | null : PANEL_TYPES => { const urlQuery = useUrlQuery(); return useMemo(() => { diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts index 07a326852f..82dc2c1e24 100644 --- a/frontend/src/hooks/queryBuilder/useOptions.ts +++ b/frontend/src/hooks/queryBuilder/useOptions.ts @@ -7,8 +7,11 @@ import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { WhereClauseConfig } from './useAutoComplete'; import { useOperators } from './useOperators'; +export const WHERE_CLAUSE_CUSTOM_SUFFIX = '-custom'; + export const useOptions = ( key: string, keys: BaseAutocompleteData[], @@ -19,6 +22,7 @@ export const useOptions = ( isExist: boolean, results: string[], result: string[], + whereClauseConfig?: WhereClauseConfig, ): Option[] => { const [options, setOptions] = useState([]); const operators = useOperators(key, keys); @@ -51,21 +55,64 @@ export const useOptions = ( [key, operator], ); + const getOptionsWithValidOperator = useCallback( + (key: string, results: string[], searchValue: string) => { + const hasAllResults = results.every((value) => result.includes(value)); + const values = getKeyOpValue(results); + + return hasAllResults + ? [ + { + label: searchValue, + value: searchValue, + }, + ] + : [ + { + label: searchValue, + value: searchValue, + }, + ...values, + ]; + }, + [getKeyOpValue, result], + ); + + const getKeyOperatorOptions = useCallback( + (key: string) => { + const operatorsOptions = operators?.map((operator) => ({ + value: `${key} ${operator} `, + label: `${key} ${operator} `, + })); + if (whereClauseConfig) { + return [ + { + label: `${searchValue} `, + value: `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`, + }, + ...operatorsOptions, + ]; + } + return operatorsOptions; + }, + [operators, searchValue, whereClauseConfig], + ); + useEffect(() => { let newOptions: Option[] = []; if (!key) { newOptions = searchValue ? [ - { label: `${searchValue} `, value: `${searchValue} ` }, + { + label: `${searchValue} `, + value: `${searchValue} `, + }, ...getOptionsFromKeys(keys), ] : getOptionsFromKeys(keys); } else if (key && !operator) { - newOptions = operators?.map((operator) => ({ - value: `${key} ${operator} `, - label: `${key} ${operator} `, - })); + newOptions = getKeyOperatorOptions(key); } else if (key && operator) { if (isMulti) { newOptions = results.map((item) => ({ @@ -75,17 +122,14 @@ export const useOptions = ( } else if (isExist) { newOptions = []; } else if (isValidOperator) { - const hasAllResults = results.every((value) => result.includes(value)); - const values = getKeyOpValue(results); - newOptions = hasAllResults - ? [{ label: searchValue, value: searchValue }] - : [{ label: searchValue, value: searchValue }, ...values]; + newOptions = getOptionsWithValidOperator(key, results, searchValue); } } if (newOptions.length > 0) { setOptions(newOptions); } }, [ + whereClauseConfig, getKeyOpValue, getOptionsFromKeys, isExist, @@ -98,6 +142,8 @@ export const useOptions = ( result, results, searchValue, + getKeyOperatorOptions, + getOptionsWithValidOperator, ]); return useMemo( diff --git a/frontend/src/hooks/queryBuilder/useQueryOperations.ts b/frontend/src/hooks/queryBuilder/useQueryOperations.ts index 04c2002d30..8e0251d0b5 100644 --- a/frontend/src/hooks/queryBuilder/useQueryOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryOperations.ts @@ -63,9 +63,18 @@ export const useQueryOperations: UseQueryOperations = ({ const getNewListOfAdditionalFilters = useCallback( (dataSource: DataSource): string[] => { + const additionalFiltersKeys: (keyof Pick< + IBuilderQuery, + 'orderBy' | 'limit' | 'having' | 'stepInterval' + >)[] = ['having', 'limit', 'orderBy', 'stepInterval']; + const result: string[] = mapOfFilters[dataSource].reduce( (acc, item) => { - if (filterConfigs && filterConfigs[item.field]?.isHidden) { + if ( + filterConfigs && + filterConfigs[item.field as typeof additionalFiltersKeys[number]] + ?.isHidden + ) { return acc; } diff --git a/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts index c848373b6c..2da205b349 100644 --- a/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts +++ b/frontend/src/hooks/queryBuilder/useSetCurrentKeyAndOperator.ts @@ -15,17 +15,14 @@ export const useSetCurrentKeyAndOperator = ( let key = ''; let operator = ''; let result: string[] = []; - - if (value) { - const { tagKey, tagOperator, tagValue } = getTagToken(value); - const isSuggestKey = keys?.some( - (el) => el?.key === getRemovePrefixFromKey(tagKey), - ); - if (isSuggestKey || keys.length === 0) { - key = tagKey || ''; - operator = tagOperator || ''; - result = tagValue || []; - } + const { tagKey, tagOperator, tagValue } = getTagToken(value); + const isSuggestKey = keys?.some( + (el) => el?.key === getRemovePrefixFromKey(tagKey), + ); + if (isSuggestKey || keys.length === 0) { + key = tagKey || ''; + operator = tagOperator || ''; + result = tagValue || []; } return [key, operator, result]; diff --git a/frontend/src/hooks/queryBuilder/useTag.ts b/frontend/src/hooks/queryBuilder/useTag.ts index 3bc272fc86..704d8796c1 100644 --- a/frontend/src/hooks/queryBuilder/useTag.ts +++ b/frontend/src/hooks/queryBuilder/useTag.ts @@ -1,5 +1,6 @@ import { getOperatorFromValue, + getTagToken, isExistsNotExistsOperator, isInNInOperator, } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; @@ -8,6 +9,8 @@ import * as Papa from 'papaparse'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { WhereClauseConfig } from './useAutoComplete'; + type IUseTag = { handleAddTag: (value: string) => void; handleClearTag: (value: string) => void; @@ -24,11 +27,11 @@ type IUseTag = { */ export const useTag = ( - key: string, isValidTag: boolean, handleSearch: (value: string) => void, query: IBuilderQuery, setSearchKey: (value: string) => void, + whereClauseConfig?: WhereClauseConfig, ): IUseTag => { const initTagsData = useMemo( () => @@ -57,15 +60,31 @@ export const useTag = ( * Adds a new tag to the tag list. * @param {string} value - The tag value to be added. */ + const handleAddTag = useCallback( (value: string): void => { + const { tagKey } = getTagToken(value); + const [key, id] = tagKey.split('-'); + + if (id === 'custom') { + const customValue = whereClauseConfig + ? `${whereClauseConfig.customKey} ${whereClauseConfig.customOp} ${key}` + : ''; + setTags((prevTags) => + prevTags.includes(customValue) ? prevTags : [...prevTags, customValue], + ); + handleSearch(''); + setSearchKey(''); + return; + } + if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) { setTags((prevTags) => [...prevTags, value]); handleSearch(''); setSearchKey(''); } }, - [key, isValidTag, handleSearch, setSearchKey], + [whereClauseConfig, isValidTag, handleSearch, setSearchKey], ); /** diff --git a/frontend/src/hooks/useErrorNotification.ts b/frontend/src/hooks/useErrorNotification.ts new file mode 100644 index 0000000000..b2961f66cc --- /dev/null +++ b/frontend/src/hooks/useErrorNotification.ts @@ -0,0 +1,17 @@ +import { AxiosError } from 'axios'; +import { useEffect } from 'react'; + +import { useNotifications } from './useNotifications'; + +const useErrorNotification = (error: AxiosError | null): void => { + const { notifications } = useNotifications(); + useEffect(() => { + if (error) { + notifications.error({ + message: error.message, + }); + } + }, [error, notifications]); +}; + +export default useErrorNotification; diff --git a/frontend/src/hooks/useQueryService.ts b/frontend/src/hooks/useQueryService.ts new file mode 100644 index 0000000000..0307460ccc --- /dev/null +++ b/frontend/src/hooks/useQueryService.ts @@ -0,0 +1,29 @@ +import getService from 'api/metrics/getService'; +import { AxiosError } from 'axios'; +import { Time } from 'container/TopNav/DateTimeSelection/config'; +import { useQuery, UseQueryResult } from 'react-query'; +import { PayloadProps } from 'types/api/metrics/getService'; +import { Tags } from 'types/reducer/trace'; + +export const useQueryService = ({ + minTime, + maxTime, + selectedTime, + selectedTags, +}: UseQueryServiceProps): UseQueryResult => { + const queryKey = [minTime, maxTime, selectedTime, selectedTags]; + return useQuery(queryKey, () => + getService({ + end: maxTime, + start: minTime, + selectedTags, + }), + ); +}; + +interface UseQueryServiceProps { + minTime: number; + maxTime: number; + selectedTime: Time; + selectedTags: Tags[]; +} diff --git a/frontend/src/lib/getStartEndRangeTime.ts b/frontend/src/lib/getStartEndRangeTime.ts index 486b6d0784..13a2d40031 100644 --- a/frontend/src/lib/getStartEndRangeTime.ts +++ b/frontend/src/lib/getStartEndRangeTime.ts @@ -1,4 +1,4 @@ -import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Time } from 'container/TopNav/DateTimeSelection/config'; import store from 'store'; @@ -36,7 +36,7 @@ const getStartEndRangeTime = ({ interface GetStartEndRangeTimesProps { type?: timePreferenceType; - graphType?: ITEMS | null; + graphType?: PANEL_TYPES | null; interval?: Time; } diff --git a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts index dd6869847b..e189275802 100644 --- a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts +++ b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts @@ -1,11 +1,14 @@ import { mapOfOperators, PANEL_TYPES } from 'constants/queryBuilder'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; -import { DataSource, StringOperators } from 'types/common/queryBuilder'; +import { + DataSource, + MetricAggregateOperator, + StringOperators, +} from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; type GetQueryOperatorsParams = { dataSource: DataSource; - panelType: GRAPH_TYPES; + panelType: PANEL_TYPES; }; // Modify this function if need special conditions for filtering of the operators @@ -20,6 +23,13 @@ export const getOperatorsBySourceAndPanelType = ({ (operator) => operator.value === StringOperators.NOOP, ); } + if (panelType === PANEL_TYPES.TABLE && dataSource === DataSource.METRICS) { + operatorsByDataSource = operatorsByDataSource.filter( + (operator) => + operator.value !== MetricAggregateOperator.NOOP && + operator.value !== MetricAggregateOperator.RATE, + ); + } if ( dataSource !== DataSource.METRICS && panelType !== PANEL_TYPES.LIST && diff --git a/frontend/src/lib/query/createTableColumnsFromQuery.ts b/frontend/src/lib/query/createTableColumnsFromQuery.ts index 7fbbfbe3c7..4e830ce641 100644 --- a/frontend/src/lib/query/createTableColumnsFromQuery.ts +++ b/frontend/src/lib/query/createTableColumnsFromQuery.ts @@ -1,8 +1,12 @@ import { ColumnsType } from 'antd/es/table'; import { ColumnType } from 'antd/lib/table'; +import { + initialFormulaBuilderFormValues, + initialQueryBuilderFormValues, +} from 'constants/queryBuilder'; import { FORMULA_REGEXP } from 'constants/regExp'; +import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config'; import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces'; -import { toCapitalize } from 'lib/toCapitalize'; import { ReactNode } from 'react'; import { IBuilderFormula, @@ -15,7 +19,7 @@ import { v4 as uuid } from 'uuid'; type CreateTableDataFromQueryParams = Pick< QueryTableProps, - 'queryTableData' | 'query' | 'renderActionCell' + 'queryTableData' | 'query' | 'renderActionCell' | 'renderColumnCell' >; export type RowData = { @@ -25,9 +29,10 @@ export type RowData = { }; type DynamicColumn = { - key: keyof RowData; + query: IBuilderQuery | IBuilderFormula; + field: string; + dataIndex: string; title: string; - sourceLabel: string; data: (string | number)[]; type: 'field' | 'operator' | 'formula'; // sortable: boolean; @@ -55,7 +60,6 @@ type GetDynamicColumns = ( type ListItemData = ListItem['data']; type ListItemKey = keyof ListItemData; -type SeriesItemLabels = SeriesItem['labels']; const isFormula = (queryName: string): boolean => FORMULA_REGEXP.test(queryName); @@ -74,32 +78,31 @@ const getQueryByName = ( builder: QueryBuilderData, currentQueryName: string, type: T, -): (T extends 'queryData' ? IBuilderQuery : IBuilderFormula) | null => { +): T extends 'queryData' ? IBuilderQuery : IBuilderFormula => { const queryArray = builder[type]; + const defaultValue = + type === 'queryData' + ? initialQueryBuilderFormValues + : initialFormulaBuilderFormValues; const currentQuery = - queryArray.find((q) => q.queryName === currentQueryName) || null; - - if (!currentQuery) return null; + queryArray.find((q) => q.queryName === currentQueryName) || defaultValue; return currentQuery as T extends 'queryData' ? IBuilderQuery : IBuilderFormula; }; -const createLabels = ( - // labels: T, - label: keyof T, +const addListLabels = ( + query: IBuilderQuery | IBuilderFormula, + label: ListItemKey, dynamicColumns: DynamicColumns, ): void => { - if (isValueExist('key', label as string, dynamicColumns)) return; - - // const labelValue = labels[label]; - - // const isNumber = !Number.isNaN(parseFloat(String(labelValue))); + if (isValueExist('dataIndex', label, dynamicColumns)) return; const fieldObj: DynamicColumn = { - key: label as string, + query, + field: 'label', + dataIndex: label as string, title: label as string, - sourceLabel: label as string, data: [], type: 'field', // sortable: isNumber, @@ -108,42 +111,59 @@ const createLabels = ( dynamicColumns.push(fieldObj); }; -const appendOperatorFormulaColumns = ( - builder: QueryBuilderData, - currentQueryName: string, +const addSeriaLabels = ( + label: string, dynamicColumns: DynamicColumns, + query: IBuilderQuery | IBuilderFormula, ): void => { - const currentFormula = getQueryByName( - builder, - currentQueryName, - 'queryFormulas', - ); - if (currentFormula) { - let formulaLabel = `${currentFormula.queryName}(${currentFormula.expression})`; + if (isValueExist('dataIndex', label, dynamicColumns)) return; - if (currentFormula.legend) { - formulaLabel += ` - ${currentFormula.legend}`; + // const labelValue = labels[label]; + + // const isNumber = !Number.isNaN(parseFloat(String(labelValue))); + + const fieldObj: DynamicColumn = { + query, + field: label as string, + dataIndex: label, + title: label, + data: [], + type: 'field', + // sortable: isNumber, + }; + + dynamicColumns.push(fieldObj); +}; + +const addOperatorFormulaColumns = ( + query: IBuilderFormula | IBuilderQuery, + dynamicColumns: DynamicColumns, + customLabel?: string, +): void => { + if (isFormula(query.queryName)) { + const formulaQuery = query as IBuilderFormula; + let formulaLabel = `${formulaQuery.queryName}(${formulaQuery.expression})`; + + if (formulaQuery.legend) { + formulaLabel = formulaQuery.legend; } const formulaColumn: DynamicColumn = { - key: currentQueryName, - title: formulaLabel, - sourceLabel: formulaLabel, + query, + field: formulaQuery.queryName, + dataIndex: formulaQuery.queryName, + title: customLabel || formulaLabel, data: [], type: 'formula', // sortable: isNumber, }; dynamicColumns.push(formulaColumn); + + return; } - const currentQueryData = getQueryByName( - builder, - currentQueryName, - 'queryData', - ); - - if (!currentQueryData) return; + const currentQueryData = query as IBuilderQuery; let operatorLabel = `${currentQueryData.aggregateOperator}`; if (currentQueryData.aggregateAttribute.key) { @@ -151,17 +171,14 @@ const appendOperatorFormulaColumns = ( } if (currentQueryData.legend) { - operatorLabel += ` - ${currentQueryData.legend}`; - } else { - operatorLabel += ` - ${currentQueryData.queryName}`; + operatorLabel = currentQueryData.legend; } - const resultValue = `${toCapitalize(operatorLabel)}`; - const operatorColumn: DynamicColumn = { - key: currentQueryName, - title: resultValue, - sourceLabel: resultValue, + query, + field: currentQueryData.queryName, + dataIndex: currentQueryData.queryName, + title: customLabel || operatorLabel, data: [], type: 'operator', // sortable: isNumber, @@ -170,59 +187,71 @@ const appendOperatorFormulaColumns = ( dynamicColumns.push(operatorColumn); }; -const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => { - const dynamicColumns: DynamicColumns = []; - - queryTableData.forEach((currentQuery) => { - if (currentQuery.list) { - currentQuery.list.forEach((listItem) => { - Object.keys(listItem.data).forEach((label) => { - createLabels(label as ListItemKey, dynamicColumns); - }); - }); - } - - if (currentQuery.series) { - if (!isValueExist('key', 'timestamp', dynamicColumns)) { - dynamicColumns.push({ - key: 'timestamp', - title: 'Timestamp', - sourceLabel: 'Timestamp', - data: [], - type: 'field', - // sortable: true, - }); - } - - appendOperatorFormulaColumns( - query.builder, - currentQuery.queryName, - dynamicColumns, - ); - - currentQuery.series.forEach((seria) => { - Object.keys(seria.labels).forEach((label) => { - createLabels(label, dynamicColumns); - }); - }); - } - }); - - return dynamicColumns.map((item) => { - if (isFormula(item.key as string)) { +const transformColumnTitles = ( + dynamicColumns: DynamicColumns, +): DynamicColumns => + dynamicColumns.map((item) => { + if (isFormula(item.field as string)) { return item; } const sameValues = dynamicColumns.filter( - (column) => column.sourceLabel === item.sourceLabel, + (column) => column.title === item.title, ); if (sameValues.length > 1) { - return { ...item, title: `${item.title} - ${item.key}` }; + return { + ...item, + dataIndex: `${item.title} - ${item.query.queryName}`, + title: `${item.title} - ${item.query.queryName}`, + }; } return item; }); + +const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => { + const dynamicColumns: DynamicColumns = []; + + queryTableData.forEach((currentQuery) => { + const { series, queryName, list } = currentQuery; + + const currentStagedQuery = getQueryByName( + query.builder, + queryName, + isFormula(queryName) ? 'queryFormulas' : 'queryData', + ); + if (list) { + list.forEach((listItem) => { + Object.keys(listItem.data).forEach((label) => { + addListLabels(currentStagedQuery, label as ListItemKey, dynamicColumns); + }); + }); + } + + if (series) { + const isValuesColumnExist = series.some((item) => item.values.length > 0); + const isEveryValuesExist = series.every((item) => item.values.length > 0); + + if (isValuesColumnExist) { + addOperatorFormulaColumns( + currentStagedQuery, + dynamicColumns, + isEveryValuesExist ? undefined : currentStagedQuery.queryName, + ); + } + + series.forEach((seria) => { + Object.keys(seria.labels).forEach((label) => { + if (label === currentQuery?.queryName) return; + + addSeriaLabels(label as string, dynamicColumns, currentStagedQuery); + }); + }); + } + }); + + return transformColumnTitles(dynamicColumns); }; const fillEmptyRowCells = ( @@ -231,8 +260,8 @@ const fillEmptyRowCells = ( currentColumn: DynamicColumn, ): void => { unusedColumnsKeys.forEach((key) => { - if (key === currentColumn.key) { - const unusedCol = sourceColumns.find((item) => item.key === key); + if (key === currentColumn.field) { + const unusedCol = sourceColumns.find((item) => item.field === key); if (unusedCol) { unusedCol.data.push('N/A'); @@ -242,33 +271,98 @@ const fillEmptyRowCells = ( }); }; -const fillDataFromSeria = ( - seria: SeriesItem, - columns: DynamicColumns, - queryName: string, -): void => { - const labelEntries = Object.entries(seria.labels); +const findSeriaValueFromAnotherQuery = ( + currentLabels: Record, + nextQuery: QueryDataV3 | null, +): SeriesItem | null => { + if (!nextQuery || !nextQuery.series) return null; + + let value = null; + + const labelEntries = Object.entries(currentLabels); + + nextQuery.series.forEach((seria) => { + const localLabelEntries = Object.entries(seria.labels); + if (localLabelEntries.length !== labelEntries.length) return; + + const isExistLabels = localLabelEntries.find(([key, value]) => + labelEntries.find( + ([currentKey, currentValue]) => + currentKey === key && currentValue === value, + ), + ); + + if (isExistLabels) { + value = seria; + } + }); + + return value; +}; + +const isEqualQueriesByLabel = ( + equalQueries: string[], + queryName: string, +): boolean => equalQueries.includes(queryName); + +const fillDataFromSeries = ( + currentQuery: QueryDataV3, + queryTableData: QueryDataV3[], + columns: DynamicColumns, + equalQueriesByLabels: string[], + // TODO: fix it + // eslint-disable-next-line sonarjs/cognitive-complexity +): void => { + const { series, queryName } = currentQuery; + const isEqualQuery = isEqualQueriesByLabel(equalQueriesByLabels, queryName); + + if (!series) return; + + series.forEach((seria) => { + const labelEntries = Object.entries(seria.labels); - seria.values.forEach((value) => { const unusedColumnsKeys = new Set( - columns.map((item) => item.key), + columns.map((item) => item.field), ); columns.forEach((column) => { - if (column.key === 'timestamp') { - column.data.push(value.timestamp); - unusedColumnsKeys.delete('timestamp'); + if (queryName === column.field) { + if (seria.values.length === 0) return; + + column.data.push(parseFloat(seria.values[0].value).toFixed(2)); + unusedColumnsKeys.delete(column.field); return; } - if (queryName === column.key) { - column.data.push(parseFloat(value.value).toFixed(2)); - unusedColumnsKeys.delete(column.key); + if (column.type !== 'field' && column.field !== queryName) { + const nextQueryData = + queryTableData.find((q) => q.queryName === column.field) || null; + + const targetSeria = findSeriaValueFromAnotherQuery( + seria.labels, + nextQueryData, + ); + + if (targetSeria) { + const isEqual = isEqualQueriesByLabel(equalQueriesByLabels, column.field); + if (!isEqual) { + equalQueriesByLabels.push(column.field); + } + + column.data.push(parseFloat(targetSeria.values[0].value).toFixed(2)); + } else { + column.data.push('N/A'); + } + + unusedColumnsKeys.delete(column.field); + return; } + if (isEqualQuery) return; + labelEntries.forEach(([key, currentValue]) => { - if (column.key === key) { + if (column.field === key) { column.data.push(currentValue); unusedColumnsKeys.delete(key); } @@ -284,10 +378,10 @@ const fillDataFromList = ( columns: DynamicColumns, ): void => { columns.forEach((column) => { - if (isFormula(column.key as string)) return; + if (isFormula(column.field)) return; Object.keys(listItem.data).forEach((label) => { - if (column.key === label) { + if (column.dataIndex === label) { if (listItem.data[label as ListItemKey] !== '') { column.data.push(listItem.data[label as ListItemKey].toString()); } else { @@ -304,15 +398,20 @@ const fillColumnsData: FillColumnData = (queryTableData, cols) => { const formulas = cols.filter((item) => item.type === 'formula'); const resultColumns = [...fields, ...operators, ...formulas]; - queryTableData.forEach((currentQuery) => { - if (currentQuery.series) { - currentQuery.series.forEach((seria) => { - fillDataFromSeria(seria, resultColumns, currentQuery.queryName); - }); - } + const equalQueriesByLabels: string[] = []; - if (currentQuery.list) { - currentQuery.list.forEach((listItem) => { + queryTableData.forEach((currentQuery) => { + const { list } = currentQuery; + + fillDataFromSeries( + currentQuery, + queryTableData, + resultColumns, + equalQueriesByLabels, + ); + + if (list) { + list.forEach((listItem) => { fillDataFromList(listItem, resultColumns); }); } @@ -331,9 +430,9 @@ const generateData = ( for (let i = 0; i < rowsLength; i += 1) { const rowData: RowData = dynamicColumns.reduce((acc, item) => { - const { key } = item; + const { dataIndex } = item; - acc[key] = item.data[i]; + acc[dataIndex] = item.data[i]; acc.key = uuid(); return acc; @@ -347,14 +446,16 @@ const generateData = ( const generateTableColumns = ( dynamicColumns: DynamicColumns, + renderColumnCell?: QueryTableProps['renderColumnCell'], ): ColumnsType => { const columns: ColumnsType = dynamicColumns.reduce< ColumnsType >((acc, item) => { const column: ColumnType = { - dataIndex: item.key, - key: item.key, + dataIndex: item.dataIndex, title: item.title, + width: QUERY_TABLE_CONFIG.width, + render: renderColumnCell && renderColumnCell[item.dataIndex], // sorter: item.sortable // ? (a: RowData, b: RowData): number => // (a[item.key] as number) - (b[item.key] as number) @@ -371,6 +472,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({ query, queryTableData, renderActionCell, + renderColumnCell, }) => { const dynamicColumns = getDynamicColumns(queryTableData, query); @@ -381,7 +483,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({ const dataSource = generateData(filledDynamicColumns, rowsLength); - const columns = generateTableColumns(filledDynamicColumns); + const columns = generateTableColumns(filledDynamicColumns, renderColumnCell); const actionsCell: ColumnType | null = renderActionCell ? { diff --git a/frontend/src/pages/DashboardWidget/index.tsx b/frontend/src/pages/DashboardWidget/index.tsx index a4ed88e264..fc8a712b63 100644 --- a/frontend/src/pages/DashboardWidget/index.tsx +++ b/frontend/src/pages/DashboardWidget/index.tsx @@ -1,7 +1,7 @@ import { Card, Typography } from 'antd'; import Spinner from 'components/Spinner'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import ROUTES from 'constants/routes'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import NewWidget from 'container/NewWidget'; import history from 'lib/history'; import { useEffect, useRef, useState } from 'react'; @@ -18,7 +18,7 @@ function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element { const { search } = useLocation(); const { dashboardId } = useParams(); - const [selectedGraph, setSelectedGraph] = useState(); + const [selectedGraph, setSelectedGraph] = useState(); const { loading, dashboards, error, errorMessage } = useSelector< AppState, DashboardReducer @@ -34,7 +34,7 @@ function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element { useEffect(() => { const params = new URLSearchParams(search); - const graphType = params.get('graphType') as GRAPH_TYPES | null; + const graphType = params.get('graphType') as PANEL_TYPES | null; if (graphType === null) { history.push(generatePath(ROUTES.DASHBOARD, { dashboardId })); diff --git a/frontend/src/pages/Metrics/index.tsx b/frontend/src/pages/Metrics/index.tsx deleted file mode 100644 index 1d83d28b33..0000000000 --- a/frontend/src/pages/Metrics/index.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Space } from 'antd'; -import getLocalStorageKey from 'api/browser/localstorage/get'; -import ReleaseNote from 'components/ReleaseNote'; -import Spinner from 'components/Spinner'; -import { SKIP_ONBOARDING } from 'constants/onboarding'; -import MetricTable from 'container/MetricsTable'; -import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; -import { useNotifications } from 'hooks/useNotifications'; -import useResourceAttribute from 'hooks/useResourceAttribute'; -import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; -import { useEffect, useMemo } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GetService, GetServiceProps } from 'store/actions/metrics'; -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 Metrics({ getService }: MetricsProps): JSX.Element { - const { minTime, maxTime, loading, selectedTime } = useSelector< - AppState, - GlobalReducer - >((state) => state.globalTime); - const location = useLocation(); - const { services, error, errorMessage } = useSelector( - (state) => state.metrics, - ); - const { notifications } = useNotifications(); - - useEffect(() => { - if (error) { - notifications.error({ - message: errorMessage, - }); - } - }, [error, errorMessage, notifications]); - - const { queries } = useResourceAttribute(); - - const selectedTags = useMemo( - () => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [], - [queries], - ); - - const isSkipped = getLocalStorageKey(SKIP_ONBOARDING) === 'true'; - - useEffect(() => { - if (loading === false) { - getService({ - maxTime, - minTime, - selectedTags, - }); - } - }, [getService, loading, maxTime, minTime, selectedTags]); - - useEffect(() => { - let timeInterval: NodeJS.Timeout; - - if (loading === false && !isSkipped && services.length === 0) { - timeInterval = setInterval(() => { - getService({ - maxTime, - minTime, - selectedTags, - }); - }, 50000); - } - - return (): void => { - clearInterval(timeInterval); - }; - }, [ - getService, - isSkipped, - loading, - maxTime, - minTime, - services, - selectedTime, - selectedTags, - ]); - - if (loading) { - return ; - } - - return ( - - - - - - - ); -} - -interface DispatchProps { - getService: ( - props: GetServiceProps, - ) => (dispatch: Dispatch, getState: () => AppState) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getService: bindActionCreators(GetService, dispatch), -}); - -type MetricsProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(Metrics); diff --git a/frontend/src/pages/Services/index.tsx b/frontend/src/pages/Services/index.tsx new file mode 100644 index 0000000000..87c0720efb --- /dev/null +++ b/frontend/src/pages/Services/index.tsx @@ -0,0 +1,70 @@ +import { Space } from 'antd'; +import localStorageGet from 'api/browser/localstorage/get'; +import localStorageSet from 'api/browser/localstorage/set'; +import ReleaseNote from 'components/ReleaseNote'; +import { SKIP_ONBOARDING } from 'constants/onboarding'; +import ResourceAttributesFilter from 'container/ResourceAttributesFilter'; +import ServicesTable from 'container/ServiceTable'; +import SkipOnBoardingModal from 'container/ServiceTable/SkipOnBoardModal'; +import useErrorNotification from 'hooks/useErrorNotification'; +import { useQueryService } from 'hooks/useQueryService'; +import useResourceAttribute from 'hooks/useResourceAttribute'; +import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; +import { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { Tags } from 'types/reducer/trace'; + +function Metrics(): JSX.Element { + const { minTime, maxTime, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); + + const location = useLocation(); + const { queries } = useResourceAttribute(); + const [skipOnboarding, setSkipOnboarding] = useState( + localStorageGet(SKIP_ONBOARDING) === 'true', + ); + + const onContinueClick = (): void => { + localStorageSet(SKIP_ONBOARDING, 'true'); + setSkipOnboarding(true); + }; + + const selectedTags = useMemo( + () => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [], + [queries], + ); + + const { data, error, isLoading, isError } = useQueryService({ + minTime, + maxTime, + selectedTime, + selectedTags, + }); + + useErrorNotification(error); + + if ( + data?.length === 0 && + isLoading === false && + !skipOnboarding && + isError === true + ) { + return ; + } + + return ( + + + + + + + ); +} + +export default Metrics; diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index 855f82ba40..6cb81093f3 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -10,7 +10,6 @@ import { import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import ExportPanel from 'container/ExportPanel'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants'; import QuerySection from 'container/TracesExplorer/QuerySection'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; @@ -149,7 +148,7 @@ function TracesExplorer(): JSX.Element { ); const getUpdateQuery = useCallback( - (newPanelType: GRAPH_TYPES): Query => { + (newPanelType: PANEL_TYPES): Query => { let query = updateAllQueriesOperators( currentQuery, newPanelType, @@ -174,7 +173,7 @@ function TracesExplorer(): JSX.Element { const handleTabChange = useCallback( (type: string): void => { - const newPanelType = type as GRAPH_TYPES; + const newPanelType = type as PANEL_TYPES; if (panelType === newPanelType) return; const query = getUpdateQuery(newPanelType); diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 04d74f2f19..4de8de7d4a 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -14,7 +14,6 @@ import { PANEL_TYPES, } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import useUrlQuery from 'hooks/useUrlQuery'; @@ -92,7 +91,7 @@ export function QueryBuilderProvider({ null, ); - const [panelType, setPanelType] = useState(null); + const [panelType, setPanelType] = useState(null); const [currentQuery, setCurrentQuery] = useState( queryState || initialQueryState, @@ -105,7 +104,7 @@ export function QueryBuilderProvider({ ( queryData: IBuilderQuery, dataSource: DataSource, - currentPanelType: GRAPH_TYPES, + currentPanelType: PANEL_TYPES, ): IBuilderQuery => { const initialOperators = getOperatorsBySourceAndPanelType({ dataSource, @@ -212,7 +211,7 @@ export function QueryBuilderProvider({ ); const updateAllQueriesOperators = useCallback( - (query: Query, panelType: GRAPH_TYPES, dataSource: DataSource): Query => { + (query: Query, panelType: PANEL_TYPES, dataSource: DataSource): Query => { const queryData = query.builder.queryData.map((item) => getElementWithActualOperator(item, dataSource, panelType), ); @@ -496,7 +495,7 @@ export function QueryBuilderProvider({ ); const handleSetConfig = useCallback( - (newPanelType: GRAPH_TYPES, dataSource: DataSource | null) => { + (newPanelType: PANEL_TYPES, dataSource: DataSource | null) => { setPanelType(newPanelType); setInitialDataSource(dataSource); }, diff --git a/frontend/src/store/actions/dashboard/getDashboard.ts b/frontend/src/store/actions/dashboard/getDashboard.ts index f84461c655..76305bae3b 100644 --- a/frontend/src/store/actions/dashboard/getDashboard.ts +++ b/frontend/src/store/actions/dashboard/getDashboard.ts @@ -1,6 +1,5 @@ import getDashboard from 'api/dashboard/get'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; import { Props } from 'types/api/dashboard/get'; @@ -65,5 +64,5 @@ export const GetDashboard = ({ export interface GetDashboardProps { uuid: Props['uuid']; widgetId?: string; - graphType?: GRAPH_TYPES; + graphType?: PANEL_TYPES; } diff --git a/frontend/src/store/actions/dashboard/saveDashboard.ts b/frontend/src/store/actions/dashboard/saveDashboard.ts index 827a11fc64..1339a3a7b8 100644 --- a/frontend/src/store/actions/dashboard/saveDashboard.ts +++ b/frontend/src/store/actions/dashboard/saveDashboard.ts @@ -1,9 +1,9 @@ import { notification } from 'antd'; import updateDashboardApi from 'api/dashboard/update'; import { AxiosError } from 'axios'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { queryParamNamesMap } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; -import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval'; import history from 'lib/history'; import { Layout } from 'react-grid-layout'; @@ -170,5 +170,5 @@ export interface SaveDashboardProps { widgetId: Widgets['id']; dashboardId: string; yAxisUnit: Widgets['yAxisUnit']; - graphType: ITEMS; + graphType: PANEL_TYPES; } diff --git a/frontend/src/store/actions/metrics/getService.ts b/frontend/src/store/actions/metrics/getService.ts index 90d65c0c43..8de8f3c134 100644 --- a/frontend/src/store/actions/metrics/getService.ts +++ b/frontend/src/store/actions/metrics/getService.ts @@ -1,5 +1,6 @@ import getService from 'api/metrics/getService'; import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; import { AppState } from 'store/reducers'; @@ -38,16 +39,16 @@ export const GetService = ( selectedTags: props.selectedTags, }); - if (response.statusCode === 200) { + if (response.length > 0) { dispatch({ type: 'GET_SERVICE_LIST_SUCCESS', - payload: response.payload, + payload: response, }); } else { dispatch({ type: 'GET_SERVICE_LIST_ERROR', payload: { - errorMessage: response.error || 'Something went wrong', + errorMessage: SOMETHING_WENT_WRONG, }, }); } @@ -55,7 +56,7 @@ export const GetService = ( dispatch({ type: 'GET_SERVICE_LIST_ERROR', payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', + errorMessage: (error as AxiosError).toString() || SOMETHING_WENT_WRONG, }, }); } diff --git a/frontend/src/types/api/alerts/compositeQuery.ts b/frontend/src/types/api/alerts/compositeQuery.ts index 2bb0288400..1f25718367 100644 --- a/frontend/src/types/api/alerts/compositeQuery.ts +++ b/frontend/src/types/api/alerts/compositeQuery.ts @@ -1,4 +1,4 @@ -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { BuilderClickHouseResource, BuilderPromQLResource, @@ -11,5 +11,5 @@ export interface ICompositeMetricQuery { promQueries: BuilderPromQLResource; chQueries: BuilderClickHouseResource; queryType: EQueryType; - panelType: GRAPH_TYPES; + panelType: PANEL_TYPES; } diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 20e2a07702..69a3dc6401 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -1,4 +1,4 @@ -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; import { Layout } from 'react-grid-layout'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; @@ -57,7 +57,7 @@ export interface DashboardData { export interface IBaseWidget { isStacked: boolean; id: string; - panelTypes: GRAPH_TYPES; + panelTypes: PANEL_TYPES; title: string; description: string; opacity: string; diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index f607cac9f7..5e7cf57b02 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -1,4 +1,4 @@ -import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { IBuilderFormula, IBuilderQuery, @@ -157,7 +157,7 @@ export type QueryBuilderContextType = { currentQuery: Query; stagedQuery: Query | null; initialDataSource: DataSource | null; - panelType: GRAPH_TYPES | null; + panelType: PANEL_TYPES | null; isEnabledQuery: boolean; handleSetQueryData: (index: number, queryData: IBuilderQuery) => void; handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void; @@ -167,7 +167,7 @@ export type QueryBuilderContextType = { newQueryData: IPromQLQuery | IClickHouseQuery, ) => void; handleSetConfig: ( - newPanelType: GRAPH_TYPES, + newPanelType: PANEL_TYPES, dataSource: DataSource | null, ) => void; removeQueryBuilderEntityByIndex: ( @@ -189,7 +189,7 @@ export type QueryBuilderContextType = { resetStagedQuery: () => void; updateAllQueriesOperators: ( queryData: Query, - panelType: GRAPH_TYPES, + panelType: PANEL_TYPES, dataSource: DataSource, ) => Query; updateQueriesData: ( diff --git a/pkg/query-service/app/apdex.go b/pkg/query-service/app/apdex.go new file mode 100644 index 0000000000..6854a91367 --- /dev/null +++ b/pkg/query-service/app/apdex.go @@ -0,0 +1,46 @@ +package app + +import ( + "context" + "net/http" + "strings" + + "go.signoz.io/signoz/pkg/query-service/dao" + "go.signoz.io/signoz/pkg/query-service/model" +) + +func (aH *APIHandler) setApdexSettings(w http.ResponseWriter, r *http.Request) { + req, err := parseSetApdexScoreRequest(r) + if aH.HandleError(w, err, http.StatusBadRequest) { + return + } + + if err := dao.DB().SetApdexSettings(context.Background(), req); err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, map[string]string{"data": "apdex score updated successfully"}) +} + +func (aH *APIHandler) getApdexSettings(w http.ResponseWriter, r *http.Request) { + services := r.URL.Query().Get("services") + apdexSet, err := dao.DB().GetApdexSettings(context.Background(), strings.Split(strings.TrimSpace(services), ",")) + if err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, apdexSet) +} + +func (aH *APIHandler) getLatencyMetricMetadata(w http.ResponseWriter, r *http.Request) { + metricName := r.URL.Query().Get("metricName") + metricMetadata, err := aH.reader.GetLatencyMetricMetadata(r.Context(), metricName, aH.preferDelta) + if err != nil { + RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil) + return + } + + aH.WriteJSON(w, r, metricMetadata) +} diff --git a/pkg/query-service/app/clickhouseReader/options.go b/pkg/query-service/app/clickhouseReader/options.go index 35e5e1732c..1f45e47115 100644 --- a/pkg/query-service/app/clickhouseReader/options.go +++ b/pkg/query-service/app/clickhouseReader/options.go @@ -36,7 +36,7 @@ const ( defaultLogAttributeKeysTable string = "distributed_logs_attribute_keys" defaultLogResourceKeysTable string = "distributed_logs_resource_keys" defaultLogTagAttributeTable string = "distributed_tag_attributes" - defaultLiveTailRefreshSeconds int = 10 + defaultLiveTailRefreshSeconds int = 5 defaultWriteBatchDelay time.Duration = 5 * time.Second defaultWriteBatchSize int = 10000 defaultEncoding Encoding = EncodingJSON diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 2bf15b90cd..bb3072df12 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "encoding/json" + "math" "fmt" "io/ioutil" @@ -3829,6 +3830,52 @@ func (r *ClickHouseReader) GetMetricAttributeValues(ctx context.Context, req *v3 return &attributeValues, nil } +func (r *ClickHouseReader) GetLatencyMetricMetadata(ctx context.Context, metricName string, preferDelta bool) (*v3.LatencyMetricMetadataResponse, error) { + query := fmt.Sprintf("SELECT DISTINCT(temporality) from %s.%s WHERE metric_name='%s'", signozMetricDBName, signozTSTableName, metricName) + rows, err := r.db.Query(ctx, query, metricName) + if err != nil { + zap.S().Error(err) + return nil, fmt.Errorf("error while executing query: %s", err.Error()) + } + defer rows.Close() + + var deltaExists bool + for rows.Next() { + var temporality string + if err := rows.Scan(&temporality); err != nil { + return nil, fmt.Errorf("error while scanning rows: %s", err.Error()) + } + if temporality == string(v3.Delta) { + deltaExists = true + } + } + + query = fmt.Sprintf("SELECT DISTINCT(toFloat64(JSONExtractString(labels, 'le'))) as le from %s.%s WHERE metric_name='%s' ORDER BY le", signozMetricDBName, signozTSTableName, metricName) + rows, err = r.db.Query(ctx, query, metricName) + if err != nil { + zap.S().Error(err) + return nil, fmt.Errorf("error while executing query: %s", err.Error()) + } + defer rows.Close() + + var leFloat64 []float64 + for rows.Next() { + var le float64 + if err := rows.Scan(&le); err != nil { + return nil, fmt.Errorf("error while scanning rows: %s", err.Error()) + } + if math.IsInf(le, 0) { + continue + } + leFloat64 = append(leFloat64, le) + } + + return &v3.LatencyMetricMetadataResponse{ + Delta: deltaExists && preferDelta, + Le: leFloat64, + }, nil +} + func isColumn(tableStatement, field string) bool { return strings.Contains(tableStatement, fmt.Sprintf("`%s` ", field)) } @@ -3983,13 +4030,18 @@ func (r *ClickHouseReader) GetLogAttributeValues(ctx context.Context, req *v3.Fi var attributeValues v3.FilterAttributeValueResponse // if dataType or tagType is not present return empty response - if len(req.FilterAttributeKeyDataType) == 0 || len(req.TagType) == 0 || req.FilterAttributeKey == "body" { + if len(req.FilterAttributeKeyDataType) == 0 || len(req.TagType) == 0 { // also check if it is not a top level key if _, ok := constants.StaticFieldsLogsV3[req.FilterAttributeKey]; !ok { return &v3.FilterAttributeValueResponse{}, nil } } + // ignore autocomplete request for body + if req.FilterAttributeKey == "body" { + return &v3.FilterAttributeValueResponse{}, nil + } + // if data type is bool, return true and false if req.FilterAttributeKeyDataType == v3.AttributeKeyDataTypeBool { return &v3.FilterAttributeValueResponse{ @@ -4076,7 +4128,7 @@ func (r *ClickHouseReader) GetLogAttributeValues(ctx context.Context, req *v3.Fi } -func readRow(vars []interface{}, columnNames []string) ([]string, map[string]string, v3.Point) { +func readRow(vars []interface{}, columnNames []string) ([]string, map[string]string, []map[string]string, v3.Point) { // Each row will have a value and a timestamp, and an optional list of label values // example: {Timestamp: ..., Value: ...} // The timestamp may also not present in some cases where the time series is reduced to single value @@ -4086,6 +4138,7 @@ func readRow(vars []interface{}, columnNames []string) ([]string, map[string]str // example: ["frontend", "/fetch"] var groupBy []string + var groupAttributesArray []map[string]string // groupAttributes is a container to hold the key-value pairs for the current // metric point. // example: {"serviceName": "frontend", "operation": "/fetch"} @@ -4104,10 +4157,16 @@ func readRow(vars []interface{}, columnNames []string) ([]string, map[string]str } for key, val := range metric { groupBy = append(groupBy, val) + if _, ok := groupAttributes[key]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{key: val}) + } groupAttributes[key] = val } } else { groupBy = append(groupBy, *v) + if _, ok := groupAttributes[colName]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{colName: *v}) + } groupAttributes[colName] = *v } case *time.Time: @@ -4117,6 +4176,9 @@ func readRow(vars []interface{}, columnNames []string) ([]string, map[string]str point.Value = float64(reflect.ValueOf(v).Elem().Float()) } else { groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Float())) + if _, ok := groupAttributes[colName]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{colName: fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Float())}) + } groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Float()) } case *uint8, *uint64, *uint16, *uint32: @@ -4124,6 +4186,9 @@ func readRow(vars []interface{}, columnNames []string) ([]string, map[string]str point.Value = float64(reflect.ValueOf(v).Elem().Uint()) } else { groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())) + if _, ok := groupAttributes[colName]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{colName: fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint())}) + } groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Uint()) } case *int8, *int16, *int32, *int64: @@ -4131,17 +4196,23 @@ func readRow(vars []interface{}, columnNames []string) ([]string, map[string]str point.Value = float64(reflect.ValueOf(v).Elem().Int()) } else { groupBy = append(groupBy, fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())) + if _, ok := groupAttributes[colName]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{colName: fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int())}) + } groupAttributes[colName] = fmt.Sprintf("%v", reflect.ValueOf(v).Elem().Int()) } case *bool: groupBy = append(groupBy, fmt.Sprintf("%v", *v)) + if _, ok := groupAttributes[colName]; !ok { + groupAttributesArray = append(groupAttributesArray, map[string]string{colName: fmt.Sprintf("%v", *v)}) + } groupAttributes[colName] = fmt.Sprintf("%v", *v) default: zap.S().Errorf("unsupported var type %v found in query builder query result for column %s", v, colName) } } - return groupBy, groupAttributes, point + return groupBy, groupAttributes, groupAttributesArray, point } func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNames []string) ([]*v3.Series, error) { @@ -4174,25 +4245,25 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam // "order,/order": {"serviceName": "order", "operation": "/order"}, // } seriesToAttrs := make(map[string]map[string]string) - + labelsArray := make(map[string][]map[string]string) for rows.Next() { if err := rows.Scan(vars...); err != nil { return nil, err } - groupBy, groupAttributes, metricPoint := readRow(vars, columnNames) + groupBy, groupAttributes, groupAttributesArray, metricPoint := readRow(vars, columnNames) sort.Strings(groupBy) key := strings.Join(groupBy, "") if _, exists := seriesToAttrs[key]; !exists { keys = append(keys, key) } seriesToAttrs[key] = groupAttributes + labelsArray[key] = groupAttributesArray seriesToPoints[key] = append(seriesToPoints[key], metricPoint) } var seriesList []*v3.Series for _, key := range keys { 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 @@ -4206,7 +4277,7 @@ func readRowsForTimeSeriesResult(rows driver.Rows, vars []interface{}, columnNam break } } - series := v3.Series{Labels: seriesToAttrs[key], Points: points, GroupingSetsPoint: groupingSetsPoint} + series := v3.Series{Labels: seriesToAttrs[key], Points: points, GroupingSetsPoint: groupingSetsPoint, LabelsArray: labelsArray[key]} seriesList = append(seriesList, &series) } return seriesList, nil @@ -4504,3 +4575,46 @@ func (r *ClickHouseReader) GetSpanAttributeKeys(ctx context.Context) (map[string } return response, nil } + +func (r *ClickHouseReader) LiveTailLogsV3(ctx context.Context, query string, timestampStart uint64, idStart string, client *v3.LogsLiveTailClient) { + if timestampStart == 0 { + timestampStart = uint64(time.Now().UnixNano()) + } + + ticker := time.NewTicker(time.Duration(r.liveTailRefreshSeconds) * time.Second) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + done := true + client.Done <- &done + zap.S().Debug("closing go routine : " + client.Name) + return + case <-ticker.C: + // get the new 100 logs as anything more older won't make sense + tmpQuery := fmt.Sprintf("timestamp >='%d'", timestampStart) + if idStart != "" { + tmpQuery = fmt.Sprintf("%s AND id > '%s'", tmpQuery, idStart) + } + tmpQuery = fmt.Sprintf(query, tmpQuery) + // the reason we are doing desc is that we need the latest logs first + tmpQuery = fmt.Sprintf("%s order by timestamp desc, id desc limit 100", tmpQuery) + + // using the old structure since we can directly read it to the struct as use it. + response := []model.GetLogsResponse{} + err := r.db.Select(ctx, &response, tmpQuery) + if err != nil { + zap.S().Error(err) + client.Error <- err + return + } + for i := len(response) - 1; i >= 0; i-- { + client.Logs <- &response[i] + if i == 0 { + timestampStart = response[i].Timestamp + idStart = response[i].ID + } + } + } + } +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index c32ae0d426..5008d6edae 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -272,6 +272,9 @@ func (aH *APIHandler) RegisterQueryRangeV3Routes(router *mux.Router, am *AuthMid subRouter.HandleFunc("/autocomplete/attribute_values", am.ViewAccess( withCacheControl(AutoCompleteCacheControlAge, aH.autoCompleteAttributeValues))).Methods(http.MethodGet) subRouter.HandleFunc("/query_range", am.ViewAccess(aH.QueryRangeV3)).Methods(http.MethodPost) + + // live logs + subRouter.HandleFunc("/logs/livetail", am.ViewAccess(aH.liveTailLogs)).Methods(http.MethodPost) } func (aH *APIHandler) Respond(w http.ResponseWriter, data interface{}) { @@ -329,6 +332,10 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v1/dependency_graph", am.ViewAccess(aH.dependencyGraph)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", am.AdminAccess(aH.setTTL)).Methods(http.MethodPost) router.HandleFunc("/api/v1/settings/ttl", am.ViewAccess(aH.getTTL)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/settings/apdex", am.AdminAccess(aH.setApdexSettings)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/settings/apdex", am.ViewAccess(aH.getApdexSettings)).Methods(http.MethodGet) + + router.HandleFunc("/api/v1/metric_meta", am.ViewAccess(aH.getLatencyMetricMetadata)).Methods(http.MethodGet) router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet) router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) @@ -2903,3 +2910,79 @@ func applyMetricLimit(results []*v3.Result, queryRangeParams *v3.QueryRangeParam } } } + +func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) { + + queryRangeParams, apiErrorObj := ParseQueryRangeParams(r) + if apiErrorObj != nil { + zap.S().Errorf(apiErrorObj.Err.Error()) + RespondError(w, apiErrorObj, nil) + return + } + + var err error + var queryString string + switch queryRangeParams.CompositeQuery.QueryType { + case v3.QueryTypeBuilder: + // check if any enrichment is required for logs if yes then enrich them + if logsv3.EnrichmentRequired(queryRangeParams) { + // get the fields if any logs query is present + var fields map[string]v3.AttributeKey + fields, err = aH.getLogFieldsV3(r.Context(), queryRangeParams) + if err != nil { + apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} + RespondError(w, apiErrObj, nil) + return + } + logsv3.Enrich(queryRangeParams, fields) + } + + queryString, err = aH.queryBuilder.PrepareLiveTailQuery(queryRangeParams) + if err != nil { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + + default: + err = fmt.Errorf("invalid query type") + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil) + return + } + + // create the client + client := &v3.LogsLiveTailClient{Name: r.RemoteAddr, Logs: make(chan *model.GetLogsResponse, 1000), Done: make(chan *bool), Error: make(chan error)} + go aH.reader.LiveTailLogsV3(r.Context(), queryString, uint64(queryRangeParams.Start), "", client) + + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.WriteHeader(200) + + flusher, ok := w.(http.Flusher) + if !ok { + err := model.ApiError{Typ: model.ErrorStreamingNotSupported, Err: nil} + RespondError(w, &err, "streaming is not supported") + return + } + // flush the headers + flusher.Flush() + for { + select { + case log := <-client.Logs: + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.Encode(log) + fmt.Fprintf(w, "event: log\ndata: %v\n\n", buf.String()) + flusher.Flush() + case <-client.Done: + zap.S().Debug("done!") + return + case err := <-client.Error: + zap.S().Error("error occured!", err) + fmt.Fprintf(w, "event: error\ndata: %v\n\n", err.Error()) + flusher.Flush() + return + } + } +} diff --git a/pkg/query-service/app/logs/v3/query_builder.go b/pkg/query-service/app/logs/v3/query_builder.go index 79c92e3810..4c6ee73bad 100644 --- a/pkg/query-service/app/logs/v3/query_builder.go +++ b/pkg/query-service/app/logs/v3/query_builder.go @@ -228,7 +228,7 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build } if graphLimitQtype == constants.SecondQueryGraphLimit { - filterSubQuery = filterSubQuery + " AND " + fmt.Sprintf("(%s) IN (", getSelectKeys(mq.AggregateOperator, mq.GroupBy)) + "%s)" + filterSubQuery = filterSubQuery + " AND " + fmt.Sprintf("(%s) GLOBAL IN (", getSelectKeys(mq.AggregateOperator, mq.GroupBy)) + "%s)" } aggregationKey := "" @@ -289,6 +289,27 @@ func buildLogsQuery(panelType v3.PanelType, start, end, step int64, mq *v3.Build } } +func buildLogsLiveTailQuery(mq *v3.BuilderQuery) (string, error) { + filterSubQuery, err := buildLogsTimeSeriesFilterQuery(mq.Filters, mq.GroupBy) + if err != nil { + return "", err + } + + switch mq.AggregateOperator { + case v3.AggregateOperatorNoOp: + queryTmpl := constants.LogsSQLSelect + "from signoz_logs.distributed_logs where %s" + if len(filterSubQuery) == 0 { + filterSubQuery = "%s" + } else { + filterSubQuery = "%s " + filterSubQuery + } + query := fmt.Sprintf(queryTmpl, filterSubQuery) + return query, nil + default: + return "", fmt.Errorf("unsupported aggregate operator in live tail") + } +} + // groupBy returns a string of comma separated tags for group by clause // `ts` is always added to the group by clause func groupBy(panelType v3.PanelType, graphLimitQtype string, tags ...string) string { @@ -377,6 +398,9 @@ func reduceQuery(query string, reduceTo v3.ReduceToOperator, aggregateOperator v } func addLimitToQuery(query string, limit uint64) string { + if limit == 0 { + return query + } return fmt.Sprintf("%s LIMIT %d", query, limit) } @@ -384,26 +408,36 @@ func addOffsetToQuery(query string, offset uint64) string { return fmt.Sprintf("%s OFFSET %d", query, offset) } -func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, graphLimitQtype string) (string, error) { +type Options struct { + GraphLimitQtype string + IsLivetailQuery bool +} - if graphLimitQtype == constants.FirstQueryGraphLimit { +func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.PanelType, mq *v3.BuilderQuery, options Options) (string, error) { + if options.IsLivetailQuery { + query, err := buildLogsLiveTailQuery(mq) + if err != nil { + return "", err + } + return query, nil + } else if options.GraphLimitQtype == constants.FirstQueryGraphLimit { // give me just the groupby names - query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, graphLimitQtype) + query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype) if err != nil { return "", err } query = addLimitToQuery(query, mq.Limit) return query, nil - } else if graphLimitQtype == constants.SecondQueryGraphLimit { - query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, graphLimitQtype) + } else if options.GraphLimitQtype == constants.SecondQueryGraphLimit { + query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype) if err != nil { return "", err } return query, nil } - query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, graphLimitQtype) + query, err := buildLogsQuery(panelType, start, end, mq.StepInterval, mq, options.GraphLimitQtype) if err != nil { return "", err } @@ -411,7 +445,7 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan query, err = reduceQuery(query, mq.ReduceTo, mq.AggregateOperator) } - if panelType == v3.PanelTypeList || panelType == v3.PanelTypeTable { + if panelType == v3.PanelTypeList { if mq.PageSize > 0 { if mq.Limit > 0 && mq.Offset > mq.Limit { return "", fmt.Errorf("max limit exceeded") @@ -421,8 +455,9 @@ func PrepareLogsQuery(start, end int64, queryType v3.QueryType, panelType v3.Pan } else { query = addLimitToQuery(query, mq.Limit) } + } else if panelType == v3.PanelTypeTable { + query = addLimitToQuery(query, mq.Limit) } return query, err - } 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 0253a0b21f..361c1cefa7 100644 --- a/pkg/query-service/app/logs/v3/query_builder_test.go +++ b/pkg/query-service/app/logs/v3/query_builder_test.go @@ -989,7 +989,7 @@ var testPrepLogsQueryData = []struct { TableName string AggregateOperator v3.AggregateOperator ExpectedQuery string - Type string + Options Options }{ { Name: "Test TS with limit- first", @@ -1011,7 +1011,7 @@ var testPrepLogsQueryData = []struct { }, TableName: "logs", ExpectedQuery: "SELECT method from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 group by method order by value DESC) LIMIT 10", - Type: constants.FirstQueryGraphLimit, + Options: Options{GraphLimitQtype: constants.FirstQueryGraphLimit}, }, { Name: "Test TS with limit- first - with order by value", @@ -1034,7 +1034,7 @@ var testPrepLogsQueryData = []struct { }, TableName: "logs", ExpectedQuery: "SELECT method from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 group by method order by value ASC) LIMIT 10", - Type: constants.FirstQueryGraphLimit, + Options: Options{GraphLimitQtype: constants.FirstQueryGraphLimit}, }, { Name: "Test TS with limit- first - with order by attribute", @@ -1057,7 +1057,7 @@ var testPrepLogsQueryData = []struct { }, TableName: "logs", ExpectedQuery: "SELECT method from (SELECT attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 group by method order by method ASC) LIMIT 10", - Type: constants.FirstQueryGraphLimit, + Options: Options{GraphLimitQtype: constants.FirstQueryGraphLimit}, }, { Name: "Test TS with limit- second", @@ -1078,8 +1078,8 @@ var testPrepLogsQueryData = []struct { Limit: 2, }, TableName: "logs", - ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 0 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND (method) IN (%s) group by method,ts order by value DESC", - Type: constants.SecondQueryGraphLimit, + ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 0 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND (method) GLOBAL IN (%s) group by method,ts order by value DESC", + Options: Options{GraphLimitQtype: constants.SecondQueryGraphLimit}, }, { Name: "Test TS with limit- second - with order by", @@ -1101,15 +1101,84 @@ var testPrepLogsQueryData = []struct { Limit: 2, }, TableName: "logs", - ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 0 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND (method) IN (%s) group by method,ts order by method ASC", - Type: constants.SecondQueryGraphLimit, + ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 0 SECOND) AS ts, attributes_string_value[indexOf(attributes_string_key, 'method')] as method, 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, 'method')] = 'GET' AND indexOf(attributes_string_key, 'method') > 0 AND (method) GLOBAL IN (%s) group by method,ts order by method ASC", + Options: Options{GraphLimitQtype: constants.SecondQueryGraphLimit}, + }, + // Live tail + { + Name: "Live Tail Query", + PanelType: v3.PanelTypeList, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ + {Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="}, + }, + }, + }, + TableName: "logs", + 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 %s AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET'", + Options: Options{IsLivetailQuery: true}, + }, + { + Name: "Live Tail Query W/O filter", + PanelType: v3.PanelTypeList, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorNoOp, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, + }, + TableName: "logs", + 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 %s", + Options: Options{IsLivetailQuery: true}, + }, + { + Name: "Table query w/o limit", + PanelType: v3.PanelTypeTable, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, + }, + TableName: "logs", + ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by value DESC", + Options: Options{}, + }, + { + Name: "Table query with limit", + PanelType: v3.PanelTypeTable, + Start: 1680066360726210000, + End: 1680066458000000000, + Step: 60, + BuilderQuery: &v3.BuilderQuery{ + QueryName: "A", + AggregateOperator: v3.AggregateOperatorCount, + Expression: "A", + Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{}}, + Limit: 10, + }, + TableName: "logs", + ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10", + Options: Options{}, }, } func TestPrepareLogsQuery(t *testing.T) { for _, tt := range testPrepLogsQueryData { Convey("TestBuildLogsQuery", t, func() { - query, err := PrepareLogsQuery(tt.Start, tt.End, "", tt.PanelType, tt.BuilderQuery, tt.Type) + query, err := PrepareLogsQuery(tt.Start, tt.End, "", tt.PanelType, tt.BuilderQuery, tt.Options) So(err, ShouldBeNil) So(query, ShouldEqual, tt.ExpectedQuery) diff --git a/pkg/query-service/app/metrics/query_builder.go b/pkg/query-service/app/metrics/query_builder.go index 435e011dbd..00f9fb3788 100644 --- a/pkg/query-service/app/metrics/query_builder.go +++ b/pkg/query-service/app/metrics/query_builder.go @@ -150,7 +150,7 @@ func BuildMetricsTimeSeriesFilterQuery(fs *model.FilterSet, groupTags []string, } } - filterSubQuery := fmt.Sprintf("SELECT %s fingerprint FROM %s.%s WHERE %s", selectLabels, constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_TABLENAME, queryString) + filterSubQuery := fmt.Sprintf("SELECT %s fingerprint FROM %s.%s WHERE %s", selectLabels, constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_LOCAL_TABLENAME, queryString) return filterSubQuery, nil } @@ -174,7 +174,7 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + @@ -261,7 +261,7 @@ func BuildMetricQuery(qp *model.QueryRangeParamsV2, mq *model.MetricQuery, table " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + @@ -435,7 +435,7 @@ func expressionToQuery(qp *model.QueryRangeParamsV2, varToQuery map[string]strin formulaSubQuery = strings.TrimSuffix(formulaSubQuery, " AND ") } if idx < len(vars)-1 { - formulaSubQuery += " GLOBAL INNER JOIN" + formulaSubQuery += " INNER JOIN" } prevVar = var_ } diff --git a/pkg/query-service/app/metrics/v3/cumulative_table.go b/pkg/query-service/app/metrics/v3/cumulative_table.go index fbd5c27447..8722532eb5 100644 --- a/pkg/query-service/app/metrics/v3/cumulative_table.go +++ b/pkg/query-service/app/metrics/v3/cumulative_table.go @@ -75,7 +75,7 @@ func buildMetricQueryForTable(start, end, _ int64, mq *v3.BuilderQuery, tableNam " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + @@ -88,7 +88,7 @@ func buildMetricQueryForTable(start, end, _ int64, mq *v3.BuilderQuery, tableNam " toStartOfHour(now()) as ts," + // now() has no menaing & used as a placeholder for ts " %s as value" + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_TABLENAME + - " GLOBAL INNER JOIN" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + diff --git a/pkg/query-service/app/metrics/v3/cumulative_table_test.go b/pkg/query-service/app/metrics/v3/cumulative_table_test.go index 6c79c70bde..d1faa12874 100644 --- a/pkg/query-service/app/metrics/v3/cumulative_table_test.go +++ b/pkg/query-service/app/metrics/v3/cumulative_table_test.go @@ -38,7 +38,7 @@ func TestPanelTableForCumulative(t *testing.T) { }, Expression: "A", }, - expected: "SELECT toStartOfHour(now()) as ts, sum(value)/29 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 60 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 = 'signoz_latency_count' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'service_name') IN ['frontend'] AND JSONExtractString(labels, 'operation') IN ['HTTP GET /dispatch']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY ts ORDER BY ts", + expected: "SELECT toStartOfHour(now()) as ts, sum(value)/29 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 60 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT fingerprint FROM signoz_metrics.time_series_v2 WHERE metric_name = 'signoz_latency_count' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'service_name') IN ['frontend'] AND JSONExtractString(labels, 'operation') IN ['HTTP GET /dispatch']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, ts ORDER BY fingerprint, ts) WHERE isNaN(value) = 0) GROUP BY ts ORDER BY ts", }, { name: "latency p50", @@ -61,7 +61,7 @@ func TestPanelTableForCumulative(t *testing.T) { }, Expression: "A", }, - expected: "SELECT toStartOfHour(now()) as ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.500) as value FROM (SELECT le, toStartOfHour(now()) as ts, sum(value)/29 as value FROM (SELECT le, ts, if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) as value FROM(SELECT fingerprint, le, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'service_name') = 'frontend') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, le,ts ORDER BY fingerprint, le ASC, ts) WHERE isNaN(value) = 0) GROUP BY le,ts HAVING isNaN(value) = 0 ORDER BY le ASC, ts) GROUP BY ts ORDER BY ts", + expected: "SELECT toStartOfHour(now()) as ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.500) as value FROM (SELECT le, toStartOfHour(now()) as ts, sum(value)/29 as value FROM (SELECT le, ts, if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) as value FROM(SELECT fingerprint, le, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality IN ['Cumulative', 'Unspecified'] AND JSONExtractString(labels, 'service_name') = 'frontend') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, le,ts ORDER BY fingerprint, le ASC, ts) WHERE isNaN(value) = 0) GROUP BY le,ts HAVING isNaN(value) = 0 ORDER BY le ASC, ts) GROUP BY ts ORDER BY ts", }, { name: "latency p99 with group by", @@ -80,7 +80,7 @@ func TestPanelTableForCumulative(t *testing.T) { }, Expression: "A", }, - expected: "SELECT service_name, toStartOfHour(now()) as ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name,le, toStartOfHour(now()) as ts, sum(value)/29 as value FROM (SELECT service_name,le, ts, if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) as value FROM(SELECT fingerprint, service_name,le, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, max(value) 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 IN ['Cumulative', 'Unspecified']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, service_name,le,ts ORDER BY fingerprint, service_name ASC,le ASC, ts) WHERE isNaN(value) = 0) GROUP BY service_name,le,ts HAVING isNaN(value) = 0 ORDER BY service_name ASC,le ASC, ts) GROUP BY service_name,ts ORDER BY service_name ASC, ts", + expected: "SELECT service_name, toStartOfHour(now()) as ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name,le, toStartOfHour(now()) as ts, sum(value)/29 as value FROM (SELECT service_name,le, ts, if(runningDifference(ts) <= 0, nan, if(runningDifference(value) < 0, (value) / runningDifference(ts), runningDifference(value) / runningDifference(ts))) as value FROM(SELECT fingerprint, service_name,le, toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, max(value) as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality IN ['Cumulative', 'Unspecified']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY fingerprint, service_name,le,ts ORDER BY fingerprint, service_name ASC,le ASC, ts) WHERE isNaN(value) = 0) GROUP BY service_name,le,ts HAVING isNaN(value) = 0 ORDER BY service_name ASC,le ASC, ts) GROUP BY service_name,ts ORDER BY service_name ASC, ts", }, } diff --git a/pkg/query-service/app/metrics/v3/delta.go b/pkg/query-service/app/metrics/v3/delta.go index ba3d2783ce..c2fa38507a 100644 --- a/pkg/query-service/app/metrics/v3/delta.go +++ b/pkg/query-service/app/metrics/v3/delta.go @@ -60,7 +60,7 @@ func buildDeltaMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableNam " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + @@ -160,7 +160,7 @@ func buildDeltaMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableNam " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + diff --git a/pkg/query-service/app/metrics/v3/delta_table.go b/pkg/query-service/app/metrics/v3/delta_table.go index 63cbaf72a2..7d98d27b1a 100644 --- a/pkg/query-service/app/metrics/v3/delta_table.go +++ b/pkg/query-service/app/metrics/v3/delta_table.go @@ -54,7 +54,7 @@ func buildDeltaMetricQueryForTable(start, end, _ int64, mq *v3.BuilderQuery, tab "SELECT %s toStartOfHour(now()) as ts," + // now() has no menaing & used as a placeholder for ts " %s as value" + " FROM " + constants.SIGNOZ_METRIC_DBNAME + "." + constants.SIGNOZ_SAMPLES_TABLENAME + - " GLOBAL INNER JOIN" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + diff --git a/pkg/query-service/app/metrics/v3/delta_table_test.go b/pkg/query-service/app/metrics/v3/delta_table_test.go index 5156c0b71d..d22807f2c1 100644 --- a/pkg/query-service/app/metrics/v3/delta_table_test.go +++ b/pkg/query-service/app/metrics/v3/delta_table_test.go @@ -38,7 +38,7 @@ func TestPanelTableForDelta(t *testing.T) { }, Expression: "A", }, - expected: "SELECT toStartOfHour(now()) as ts, sum(value)/1800 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']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY ts ORDER BY ts", + expected: "SELECT toStartOfHour(now()) as ts, sum(value)/1800 as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT fingerprint FROM signoz_metrics.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']) as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_count' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY ts ORDER BY ts", }, { name: "latency p50", @@ -61,7 +61,7 @@ func TestPanelTableForDelta(t *testing.T) { }, Expression: "A", }, - expected: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.500) as value FROM (SELECT le, toStartOfHour(now()) as ts, sum(value)/1800 as value FROM signoz_metrics.distributed_samples_v2 GLOBAL INNER JOIN (SELECT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND JSONExtractString(labels, 'service_name') = 'frontend') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY le,ts ORDER BY le ASC, ts) GROUP BY ts ORDER BY ts", + expected: "SELECT ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.500) as value FROM (SELECT le, toStartOfHour(now()) as ts, sum(value)/1800 as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.time_series_v2 WHERE metric_name = 'signoz_latency_bucket' AND temporality = 'Delta' AND JSONExtractString(labels, 'service_name') = 'frontend') as filtered_time_series USING fingerprint WHERE metric_name = 'signoz_latency_bucket' AND timestamp_ms >= 1689255866000 AND timestamp_ms <= 1689257640000 GROUP BY le,ts ORDER BY le ASC, ts) GROUP BY ts ORDER BY ts", }, { name: "latency p99 with group by", @@ -80,7 +80,7 @@ func TestPanelTableForDelta(t *testing.T) { }, Expression: "A", }, - expected: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name,le, toStartOfHour(now()) as ts, sum(value)/1800 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 >= 1689255866000 AND timestamp_ms <= 1689257640000 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", + expected: "SELECT service_name, ts, histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.990) as value FROM (SELECT service_name,le, toStartOfHour(now()) as ts, sum(value)/1800 as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.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 >= 1689255866000 AND timestamp_ms <= 1689257640000 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", }, } diff --git a/pkg/query-service/app/metrics/v3/query_builder.go b/pkg/query-service/app/metrics/v3/query_builder.go index 7641406c34..5ee4c24128 100644 --- a/pkg/query-service/app/metrics/v3/query_builder.go +++ b/pkg/query-service/app/metrics/v3/query_builder.go @@ -127,7 +127,7 @@ func buildMetricsTimeSeriesFilterQuery(fs *v3.FilterSet, groupTags []v3.Attribut } } - filterSubQuery := fmt.Sprintf("SELECT %s fingerprint FROM %s.%s WHERE %s", selectLabels, constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_TABLENAME, queryString) + filterSubQuery := fmt.Sprintf("SELECT %s fingerprint FROM %s.%s WHERE %s", selectLabels, constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_LOCAL_TABLENAME, queryString) return filterSubQuery, nil } @@ -176,7 +176,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + @@ -291,7 +291,7 @@ func buildMetricQuery(start, end, step int64, mq *v3.BuilderQuery, tableName str " 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" + + " INNER JOIN" + " (%s) as filtered_time_series" + " USING fingerprint" + " WHERE " + samplesTableTimeFilter + 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 595a8c4557..173645de78 100644 --- a/pkg/query-service/app/metrics/v3/query_builder_test.go +++ b/pkg/query-service/app/metrics/v3/query_builder_test.go @@ -243,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(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` + 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 INNER JOIN (SELECT fingerprint FROM signoz_metrics.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/parser.go b/pkg/query-service/app/parser.go index 4804847ea2..5c44ba19f4 100644 --- a/pkg/query-service/app/parser.go +++ b/pkg/query-service/app/parser.go @@ -726,6 +726,14 @@ func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) { return &req, nil } +func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) { + var req model.ApdexSettings + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return &req, nil +} + func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) { var req auth.RegisterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/pkg/query-service/app/parser_test.go b/pkg/query-service/app/parser_test.go index df2a4e79e6..dd668aef2a 100644 --- a/pkg/query-service/app/parser_test.go +++ b/pkg/query-service/app/parser_test.go @@ -30,7 +30,7 @@ func TestParseFilterSingleFilter(t *testing.T) { req, _ := http.NewRequest("POST", "", bytes.NewReader(postBody)) res, _ := parseFilterSet(req) query, _ := metrics.BuildMetricsTimeSeriesFilterQuery(res, []string{}, "table", model.NOOP) - So(query, ShouldContainSubstring, "signoz_metrics.distributed_time_series_v2 WHERE metric_name = 'table' AND JSONExtractString(labels, 'namespace') = 'a'") + So(query, ShouldContainSubstring, "WHERE metric_name = 'table' AND JSONExtractString(labels, 'namespace') = 'a'") }) } diff --git a/pkg/query-service/app/querier/querier.go b/pkg/query-service/app/querier/querier.go index 7284bddc9f..ea77fca4f3 100644 --- a/pkg/query-service/app/querier/querier.go +++ b/pkg/query-service/app/querier/querier.go @@ -235,7 +235,7 @@ func (q *querier) runBuilderQueries(ctx context.Context, params *v3.QueryRangePa // TODO: add support for logs and traces if builderQuery.DataSource == v3.DataSourceLogs { - query, err := logsV3.PrepareLogsQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery, "") + query, err := logsV3.PrepareLogsQuery(params.Start, params.End, params.CompositeQuery.QueryType, params.CompositeQuery.PanelType, builderQuery, logsV3.Options{}) if err != nil { errQueriesByName[queryName] = err.Error() continue diff --git a/pkg/query-service/app/queryBuilder/query_builder.go b/pkg/query-service/app/queryBuilder/query_builder.go index c3f8b0d4d0..bfa29395bb 100644 --- a/pkg/query-service/app/queryBuilder/query_builder.go +++ b/pkg/query-service/app/queryBuilder/query_builder.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/SigNoz/govaluate" + logsV3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" "go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/constants" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" @@ -40,7 +41,7 @@ var SupportedFunctions = []string{ var EvalFuncs = map[string]govaluate.ExpressionFunction{} type prepareTracesQueryFunc func(start, end int64, panelType v3.PanelType, bq *v3.BuilderQuery, keys map[string]v3.AttributeKey, graphLimitQtype string) (string, error) -type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, graphLimitQtype string) (string, error) +type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, options logsV3.Options) (string, error) type prepareMetricQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery) (string, error) type QueryBuilder struct { @@ -123,7 +124,7 @@ func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string, formulaSubQuery = strings.TrimSuffix(formulaSubQuery, " AND ") } if idx < len(variables)-1 { - formulaSubQuery += " GLOBAL INNER JOIN" + formulaSubQuery += " INNER JOIN " } prevVar = variable } @@ -131,6 +132,29 @@ func expressionToQuery(qp *v3.QueryRangeParamsV3, varToQuery map[string]string, return formulaQuery, nil } +func (qb *QueryBuilder) PrepareLiveTailQuery(params *v3.QueryRangeParamsV3) (string, error) { + var queryStr string + var err error + compositeQuery := params.CompositeQuery + + if compositeQuery != nil { + // There can only be a signle query and there is no concept of disabling queries + if len(compositeQuery.BuilderQueries) != 1 { + return "", fmt.Errorf("live tail is only supported for single query") + } + for queryName, query := range compositeQuery.BuilderQueries { + if query.Expression == queryName { + queryStr, err = qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, logsV3.Options{IsLivetailQuery: true}) + if err != nil { + return "", err + } + } + } + + } + return queryStr, nil +} + func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...interface{}) (map[string]string, error) { queries := make(map[string]string) @@ -169,18 +193,18 @@ func (qb *QueryBuilder) PrepareQueries(params *v3.QueryRangeParamsV3, args ...in case v3.DataSourceLogs: // for ts query with limit replace it as it is already formed if compositeQuery.PanelType == v3.PanelTypeGraph && query.Limit > 0 && len(query.GroupBy) > 0 { - limitQuery, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, constants.FirstQueryGraphLimit) + limitQuery, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, logsV3.Options{GraphLimitQtype: constants.FirstQueryGraphLimit}) if err != nil { return nil, err } - placeholderQuery, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, constants.SecondQueryGraphLimit) + placeholderQuery, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, logsV3.Options{GraphLimitQtype: constants.SecondQueryGraphLimit}) if err != nil { return nil, err } query := fmt.Sprintf(placeholderQuery, limitQuery) queries[queryName] = query } else { - queryString, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, "") + queryString, err := qb.options.BuildLogQuery(params.Start, params.End, compositeQuery.QueryType, compositeQuery.PanelType, query, logsV3.Options{}) if err != nil { return nil, err } diff --git a/pkg/query-service/app/queryBuilder/query_builder_test.go b/pkg/query-service/app/queryBuilder/query_builder_test.go index f2679f0aff..649556130c 100644 --- a/pkg/query-service/app/queryBuilder/query_builder_test.go +++ b/pkg/query-service/app/queryBuilder/query_builder_test.go @@ -250,7 +250,7 @@ func TestDeltaQueryBuilder(t *testing.T) { }, }, 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", + expected: "SELECT toStartOfInterval(toDateTime(intDiv(timestamp_ms, 1000)), INTERVAL 60 SECOND) as ts, sum(value)/60 as value FROM signoz_metrics.distributed_samples_v2 INNER JOIN (SELECT fingerprint FROM signoz_metrics.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", @@ -321,7 +321,7 @@ func TestDeltaQueryBuilder(t *testing.T) { }, }, 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", + 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 INNER JOIN (SELECT fingerprint FROM signoz_metrics.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 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 INNER JOIN (SELECT fingerprint FROM signoz_metrics.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", @@ -349,7 +349,7 @@ func TestDeltaQueryBuilder(t *testing.T) { }, }, 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", + 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 INNER JOIN (SELECT JSONExtractString(labels, 'service_name') as service_name, JSONExtractString(labels, 'le') as le, fingerprint FROM signoz_metrics.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", }, } diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 2b8b67510c..cae171fb1f 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -162,11 +162,12 @@ var GroupByColMap = map[string]struct{}{ } const ( - SIGNOZ_METRIC_DBNAME = "signoz_metrics" - SIGNOZ_SAMPLES_TABLENAME = "distributed_samples_v2" - SIGNOZ_TIMESERIES_TABLENAME = "distributed_time_series_v2" - SIGNOZ_TRACE_DBNAME = "signoz_traces" - SIGNOZ_SPAN_INDEX_TABLENAME = "distributed_signoz_index_v2" + SIGNOZ_METRIC_DBNAME = "signoz_metrics" + SIGNOZ_SAMPLES_TABLENAME = "distributed_samples_v2" + SIGNOZ_TIMESERIES_TABLENAME = "distributed_time_series_v2" + SIGNOZ_TRACE_DBNAME = "signoz_traces" + SIGNOZ_SPAN_INDEX_TABLENAME = "distributed_signoz_index_v2" + SIGNOZ_TIMESERIES_LOCAL_TABLENAME = "time_series_v2" ) var TimeoutExcludedRoutes = map[string]bool{ diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go index ceece7faef..c1bb852e20 100644 --- a/pkg/query-service/dao/interface.go +++ b/pkg/query-service/dao/interface.go @@ -32,6 +32,8 @@ type Queries interface { GetResetPasswordEntry(ctx context.Context, token string) (*model.ResetPasswordEntry, *model.ApiError) GetUsersByOrg(ctx context.Context, orgId string) ([]model.UserPayload, *model.ApiError) GetUsersByGroup(ctx context.Context, groupId string) ([]model.UserPayload, *model.ApiError) + + GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) } type Mutations interface { @@ -56,4 +58,6 @@ type Mutations interface { UpdateUserPassword(ctx context.Context, hash, userId string) *model.ApiError UpdateUserGroup(ctx context.Context, userId, groupId string) *model.ApiError + + SetApdexSettings(ctx context.Context, set *model.ApdexSettings) *model.ApiError } diff --git a/pkg/query-service/dao/sqlite/apdex.go b/pkg/query-service/dao/sqlite/apdex.go new file mode 100644 index 0000000000..8c74553fb8 --- /dev/null +++ b/pkg/query-service/dao/sqlite/apdex.go @@ -0,0 +1,74 @@ +package sqlite + +import ( + "context" + "fmt" + + "go.signoz.io/signoz/pkg/query-service/model" +) + +const defaultApdexThreshold = 0.5 + +func (mds *ModelDaoSqlite) GetApdexSettings(ctx context.Context, services []string) ([]model.ApdexSettings, *model.ApiError) { + var apdexSettings []model.ApdexSettings + var serviceName string + + for i, service := range services { + if i == 0 { + serviceName = fmt.Sprintf("'%s'", service) + } else { + serviceName = fmt.Sprintf("%s, '%s'", serviceName, service) + } + } + + query := fmt.Sprintf("SELECT * FROM apdex_settings WHERE service_name IN (%s)", serviceName) + + err := mds.db.Select(&apdexSettings, query) + if err != nil { + return nil, &model.ApiError{ + Err: err, + } + } + + // add default apdex settings for services that don't have any + for _, service := range services { + var found bool + for _, apdexSetting := range apdexSettings { + if apdexSetting.ServiceName == service { + found = true + break + } + } + + if !found { + apdexSettings = append(apdexSettings, model.ApdexSettings{ + ServiceName: service, + Threshold: defaultApdexThreshold, + }) + } + } + + return apdexSettings, nil +} + +func (mds *ModelDaoSqlite) SetApdexSettings(ctx context.Context, apdexSettings *model.ApdexSettings) *model.ApiError { + + fmt.Println("apdexSettings:", apdexSettings) + _, err := mds.db.NamedExec(` + INSERT OR REPLACE INTO apdex_settings ( + service_name, + threshold, + exclude_status_codes + ) VALUES ( + :service_name, + :threshold, + :exclude_status_codes + )`, apdexSettings) + if err != nil { + return &model.ApiError{ + Err: err, + } + } + + return nil +} diff --git a/pkg/query-service/dao/sqlite/connection.go b/pkg/query-service/dao/sqlite/connection.go index a1b7842bb3..dd113a2863 100644 --- a/pkg/query-service/dao/sqlite/connection.go +++ b/pkg/query-service/dao/sqlite/connection.go @@ -73,6 +73,11 @@ func InitDB(dataSourceName string) (*ModelDaoSqlite, error) { flags TEXT, FOREIGN KEY(user_id) REFERENCES users(id) ); + CREATE TABLE IF NOT EXISTS apdex_settings ( + service_name TEXT PRIMARY KEY, + threshold FLOAT NOT NULL, + exclude_status_codes TEXT NOT NULL + ); ` _, err = db.Exec(table_schema) diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 638de6e4d8..8b3697f89c 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -69,6 +69,7 @@ type Reader interface { // QB V3 metrics/traces/logs GetTimeSeriesResultV3(ctx context.Context, query string) ([]*v3.Series, error) GetListResultV3(ctx context.Context, query string) ([]*v3.Row, error) + LiveTailLogsV3(ctx context.Context, query string, timestampStart uint64, idStart string, client *v3.LogsLiveTailClient) GetTotalSpans(ctx context.Context) (uint64, error) GetSpansInLastHeartBeatInterval(ctx context.Context) (uint64, error) @@ -94,6 +95,8 @@ type Reader interface { QueryDashboardVars(ctx context.Context, query string) (*model.DashboardVar, error) CheckClickHouse(ctx context.Context) error + + GetLatencyMetricMetadata(context.Context, string, bool) (*v3.LatencyMetricMetadataResponse, error) } type Querier interface { diff --git a/pkg/query-service/model/db.go b/pkg/query-service/model/db.go index e043dd2ddd..9ae7270232 100644 --- a/pkg/query-service/model/db.go +++ b/pkg/query-service/model/db.go @@ -36,6 +36,12 @@ type User struct { GroupId string `json:"groupId,omitempty" db:"group_id"` } +type ApdexSettings struct { + ServiceName string `json:"serviceName" db:"service_name"` + Threshold float64 `json:"threshold" db:"threshold"` + ExcludeStatusCodes string `json:"excludeStatusCodes" db:"exclude_status_codes"` // sqlite doesn't support array type +} + type UserFlag map[string]string func (uf UserFlag) Value() (driver.Value, error) { diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 7647e5a75d..1fb4cf8e60 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "go.signoz.io/signoz/pkg/query-service/model" ) type DataSource string @@ -106,7 +107,8 @@ func (a AggregateOperator) RequireAttribute(dataSource DataSource) bool { switch dataSource { case DataSourceMetrics: switch a { - case AggregateOperatorNoOp: + case AggregateOperatorNoOp, + AggregateOperatorCount: return false default: return true @@ -584,10 +586,18 @@ type Result struct { List []*Row `json:"list"` } +type LogsLiveTailClient struct { + Name string + Logs chan *model.GetLogsResponse + Done chan *bool + Error chan error +} + type Series struct { - Labels map[string]string `json:"labels"` - Points []Point `json:"values"` - GroupingSetsPoint *Point `json:"-"` + Labels map[string]string `json:"labels"` + LabelsArray []map[string]string `json:"labelsArray"` + Points []Point `json:"values"` + GroupingSetsPoint *Point `json:"-"` } func (s *Series) SortPoints() { @@ -655,3 +665,8 @@ func (eq *ExplorerQuery) Validate() error { } return eq.CompositeQuery.Validate() } + +type LatencyMetricMetadataResponse struct { + Delta bool `json:"delta"` + Le []float64 `json:"le"` +}