diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index a93fe6263f..bee46680c0 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -409,10 +409,10 @@ const routes: AppRoutes[] = [ isPrivate: true, }, { - path: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW, + path: ROUTES.MESSAGING_QUEUES_OVERVIEW, exact: true, component: MessagingQueues, - key: 'MESSAGING_QUEUES_CELERY_OVERVIEW', + key: 'MESSAGING_QUEUES_OVERVIEW', isPrivate: true, }, { diff --git a/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.styles.scss b/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.styles.scss index 6d9be3a665..83dcabd43f 100644 --- a/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.styles.scss +++ b/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.styles.scss @@ -10,6 +10,9 @@ .config-select-option { width: 100%; + min-width: 160px; + max-width: fit-content; + .ant-select-selector { display: flex; min-height: 32px; @@ -23,6 +26,10 @@ } } } + + .copy-url-btn { + flex-shrink: 0; + } } .lightMode { diff --git a/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.tsx b/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.tsx index 22f2743410..c50f379fda 100644 --- a/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.tsx +++ b/frontend/src/components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions.tsx @@ -1,7 +1,7 @@ import './CeleryOverviewConfigOptions.styles.scss'; import { Color } from '@signozhq/design-tokens'; -import { Button, Select, Spin, Tooltip } from 'antd'; +import { Button, Row, Select, Spin, Tooltip } from 'antd'; import { getValuesFromQueryParams, setQueryParamsFromOptions, @@ -18,7 +18,7 @@ import { useCopyToClipboard } from 'react-use'; interface SelectOptionConfig { placeholder: string; queryParam: QueryParams; - filterType: 'serviceName' | 'spanName' | 'msgSystem'; + filterType: string | string[]; } function FilterSelect({ @@ -29,13 +29,14 @@ function FilterSelect({ const { handleSearch, isFetching, options } = useCeleryFilterOptions( filterType, ); + const urlQuery = useUrlQuery(); const history = useHistory(); const location = useLocation(); return ( + setSelectedKeys(e.target.value ? [e.target.value] : []) + } + onPressEnter={(): void => handleSearch(selectedKeys as string[], confirm)} + style={{ marginBottom: 8, display: 'block' }} + /> + + + + + + + ), + filterIcon: (filtered: boolean): JSX.Element => ( + + ), + onFilter: (value, record): boolean => + record[dataIndex || ''] + .toString() + .toLowerCase() + .includes((value as string).toLowerCase()), +}); + function getColumns(data: RowData[]): TableColumnsType { if (data?.length === 0) { return []; @@ -235,12 +317,11 @@ type FilterConfig = { function makeFilters(urlQuery: URLSearchParams): Filter[] { const filterConfigs: FilterConfig[] = [ - { paramName: 'destination', key: 'destination', operator: 'in' }, - { paramName: 'queue', key: 'queue', operator: 'in' }, - { paramName: 'kind_string', key: 'kind_string', operator: 'in' }, - { paramName: 'service', key: 'service.name', operator: 'in' }, - { paramName: 'span_name', key: 'span_name', operator: 'in' }, - { paramName: 'messaging_system', key: 'messaging_system', operator: 'in' }, + { paramName: QueryParams.destination, key: 'destination', operator: 'in' }, + { paramName: QueryParams.msgSystem, key: 'queue', operator: 'in' }, + { paramName: QueryParams.kindString, key: 'kind_string', operator: 'in' }, + { paramName: QueryParams.service, key: 'service.name', operator: 'in' }, + { paramName: QueryParams.spanName, key: 'name', operator: 'in' }, ]; return filterConfigs @@ -260,7 +341,11 @@ function makeFilters(urlQuery: URLSearchParams): Filter[] { .filter((filter): filter is Filter => filter !== null); } -export default function CeleryOverviewTable(): JSX.Element { +export default function CeleryOverviewTable({ + onRowClick, +}: { + onRowClick: (record: RowData) => void; +}): JSX.Element { const [tableData, setTableData] = useState([]); const { minTime, maxTime } = useSelector( @@ -271,6 +356,8 @@ export default function CeleryOverviewTable(): JSX.Element { onSuccess: (data) => { if (data?.payload) { setTableData(getTableData(data?.payload)); + } else if (isEmpty(data?.payload)) { + setTableData([]); } }, }); @@ -293,11 +380,42 @@ export default function CeleryOverviewTable(): JSX.Element { LOCALSTORAGE.CELERY_OVERVIEW_COLUMNS, ); + const [searchText, setSearchText] = useState(''); + const searchInput = useRef(null); + + const handleSearch = ( + selectedKeys: string[], + confirm: FilterDropdownProps['confirm'], + ): void => { + confirm(); + setSearchText(selectedKeys[0]); + }; + + const handleReset = ( + clearFilters: () => void, + confirm: FilterDropdownProps['confirm'], + ): void => { + clearFilters(); + setSearchText(''); + confirm(); + }; + const columns = useMemo( - () => getDraggedColumns(getColumns(tableData), draggedColumns), + () => + getDraggedColumns( + getColumns(tableData).map((item) => ({ + ...item, + ...getColumnSearchProps( + searchInput, + handleReset, + handleSearch, + item.key?.toString(), + ), + })), + draggedColumns, + ), [tableData, draggedColumns], ); - const handleDragColumn = useCallback( (fromIndex: number, toIndex: number) => onDragColumns(columns, fromIndex, toIndex), @@ -316,17 +434,44 @@ export default function CeleryOverviewTable(): JSX.Element { ); const handleRowClick = (record: RowData): void => { - console.log(record); + onRowClick(record); }; + const getFilteredData = useCallback( + (data: RowData[]): RowData[] => { + if (!searchText) return data; + + const searchLower = searchText.toLowerCase(); + return data.filter((record) => + Object.values(record).some( + (value) => + value !== undefined && + value.toString().toLowerCase().includes(searchLower), + ), + ); + }, + [searchText], + ); + + const filteredData = useMemo(() => getFilteredData(tableData), [ + getFilteredData, + tableData, + ]); + return (
+ setSearchText(e.target.value)} + value={searchText} + allowClear + /> { - const { payload } = await getAttributesValues({ - aggregateOperator: 'noop', - dataSource: DataSource.TRACES, - aggregateAttribute: '', - attributeKey, - searchText: searchText ?? '', - filterAttributeKeyDataType: DataTypes.String, - tagType: 'tag', - }); - - if (payload) { - const values = Object.values(payload).find((el) => !!el) || []; - const options: DefaultOptionType[] = values.map((val: string) => ({ - label: val, - value: val, - })); - return options; - } - return []; + const keys = Array.isArray(attributeKey) ? attributeKey : [attributeKey]; + + const responses = await Promise.all( + keys.map((key) => + getAttributesValues({ + aggregateOperator: 'noop', + dataSource: DataSource.TRACES, + aggregateAttribute: '', + attributeKey: key, + searchText: searchText ?? '', + filterAttributeKeyDataType: DataTypes.String, + tagType: 'tag', + }), + ), + ); + + const uniqueValues = [ + ...new Set( + responses.flatMap( + ({ payload }) => Object.values(payload || {}).find((el) => !!el) || [], + ), + ), + ]; + + return uniqueValues.map((val: string) => ({ + label: val, + value: val, + })); }, ); diff --git a/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx index 20989c57fc..ae5ba5ffb6 100644 --- a/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx +++ b/frontend/src/components/CeleryTask/CeleryTaskDetail/CeleryTaskDetail.tsx @@ -4,14 +4,11 @@ import { Color, Spacing } from '@signozhq/design-tokens'; import { Divider, Drawer, Typography } from 'antd'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; -import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; -import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useIsDarkMode } from 'hooks/useDarkMode'; import useUrlQuery from 'hooks/useUrlQuery'; -import { RowData } from 'lib/query/createTableColumnsFromQuery'; import { X } from 'lucide-react'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory, useLocation } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; @@ -19,12 +16,11 @@ import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; -import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder'; import { GlobalReducer } from 'types/reducer/globalTime'; import { v4 as uuidv4 } from 'uuid'; import CeleryTaskGraph from '../CeleryTaskGraph/CeleryTaskGraph'; +import { useNavigateToTraces } from '../useNavigateToTraces'; export type CeleryTaskData = { entity: string; @@ -143,46 +139,7 @@ export default function CeleryTaskDetail({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const { currentQuery } = useQueryBuilder(); - - const prepareQuery = useCallback( - (selectedFilters: TagFilterItem[]): Query => ({ - ...currentQuery, - builder: { - ...currentQuery.builder, - queryData: currentQuery.builder.queryData.map((item) => ({ - ...item, - dataSource: DataSource.TRACES, - aggregateOperator: MetricAggregateOperator.NOOP, - filters: { - ...item.filters, - items: selectedFilters, - }, - })), - }, - }), - [currentQuery], - ); - - const navigateToTrace = (data: RowData): void => { - const { entity, value } = taskData; - const selectedFilters = createFiltersFromData({ ...data, [entity]: value }); - const urlParams = new URLSearchParams(); - urlParams.set(QueryParams.startTime, (minTime / 1000000).toString()); - urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString()); - - const JSONCompositeQuery = encodeURIComponent( - JSON.stringify(prepareQuery(selectedFilters)), - ); - - const newTraceExplorerPath = `${ - ROUTES.TRACES_EXPLORER - }?${urlParams.toString()}&${ - QueryParams.compositeQuery - }=${JSONCompositeQuery}`; - - window.open(newTraceExplorerPath, '_blank'); - }; + const navigateToTrace = useNavigateToTraces(); return ( { + const filters = createFiltersFromData({ + ...rowData, + [taskData.entity]: taskData.value, + }); + navigateToTrace(filters); + }} /> ); diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx index 278cb31494..d0a0dd1f7e 100644 --- a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx +++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskBar.tsx @@ -140,7 +140,6 @@ function CeleryTaskBar({ }); const customSeries = (data: QueryData[]): uPlot.Series[] => { - console.log(data); const configurations: uPlot.Series[] = [ { label: 'Timestamp', stroke: 'purple' }, ]; diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss index dfb66e2f49..55d17b8034 100644 --- a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss +++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraph.style.scss @@ -24,7 +24,7 @@ .widget-graph-container { &.bar { - height: calc(100% - 85px); + height: calc(100% - 105px); } } } @@ -45,9 +45,12 @@ height: calc(100% - 18px); .widget-graph-container { - &.bar, + &.bar { + height: calc(100% - 105px); + } + &.graph { - height: calc(100% - 85px); + height: calc(100% - 80px); } } } @@ -93,7 +96,10 @@ } &__label-wrapper { - margin-bottom: 8px; + margin-bottom: 4px; + display: flex; + flex-direction: column; + gap: 8px; } &__label { diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts index 2e963c9740..a82274430d 100644 --- a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts +++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils.ts @@ -917,3 +917,215 @@ export const celeryTimeSeriesTablesWidgetData = ( columnUnits: { A: 'ns' }, }), ); + +// State Count Widget +export const celeryAllStateCountWidgetData = getWidgetQueryBuilder( + getWidgetQuery({ + title: 'All State Count', + description: 'Represents the all state count.', + panelTypes: PANEL_TYPES.VALUE, + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'count', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: uuidv4(), + key: { + dataType: DataTypes.String, + id: 'celery.task_name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'celery.task_name', + type: 'tag', + }, + op: '=', + value: 'tasks.tasks.divide', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + }), +); + +export const celerySuccessStateCountWidgetData = getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Successful State Count', + description: 'Represents the successful state count.', + panelTypes: PANEL_TYPES.VALUE, + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'count', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: uuidv4(), + key: { + dataType: DataTypes.String, + id: 'celery.state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'celery.state', + type: 'tag', + }, + op: '=', + value: 'SUCCESS', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + }), +); + +export const celeryFailedStateCountWidgetData = getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Failed State Count', + description: 'Represents the failed state count.', + panelTypes: PANEL_TYPES.VALUE, + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'count', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: uuidv4(), + key: { + dataType: DataTypes.String, + id: 'celery.state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'celery.state', + type: 'tag', + }, + op: '=', + value: 'FAILURE', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + }), +); + +export const celeryRetryStateCountWidgetData = getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Retry State Count', + description: 'Represents the retry state count.', + panelTypes: PANEL_TYPES.VALUE, + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + key: '', + type: '', + }, + aggregateOperator: 'count', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + { + id: uuidv4(), + key: { + dataType: DataTypes.String, + id: 'celery.state--string--tag--false', + isColumn: false, + isJSON: false, + key: 'celery.state', + type: 'tag', + }, + op: '=', + value: 'RETRY', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + }), +); diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx index 5ae07a341d..21d8180d16 100644 --- a/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx +++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/CeleryTaskStateGraphConfig.tsx @@ -1,8 +1,17 @@ +/* eslint-disable no-nested-ternary */ import './CeleryTaskGraph.style.scss'; import { Col, Row } from 'antd'; import { Dispatch, SetStateAction } from 'react'; +import { + celeryAllStateCountWidgetData, + celeryFailedStateCountWidgetData, + celeryRetryStateCountWidgetData, + celerySuccessStateCountWidgetData, +} from './CeleryTaskGraphUtils'; +import { useGetValueFromWidget } from './useGetValueFromWidget'; + interface TabData { label: string; key: string; @@ -33,6 +42,16 @@ function CeleryTaskStateGraphConfig({ setBarState(key as CeleryTaskState); }; + const { values, isLoading, isError } = useGetValueFromWidget( + [ + celeryAllStateCountWidgetData, + celeryFailedStateCountWidgetData, + celeryRetryStateCountWidgetData, + celerySuccessStateCountWidgetData, + ], + ['celery-task-states'], + ); + return ( {tabs.map((tab, index) => ( @@ -46,6 +65,9 @@ function CeleryTaskStateGraphConfig({ >
{tab.label}
+
+ {isLoading ? '-' : isError ? '-' : values[index]} +
{tab.key === barState &&
} diff --git a/frontend/src/components/CeleryTask/CeleryTaskGraph/useGetValueFromWidget.ts b/frontend/src/components/CeleryTask/CeleryTaskGraph/useGetValueFromWidget.ts new file mode 100644 index 0000000000..87f1c4f343 --- /dev/null +++ b/frontend/src/components/CeleryTask/CeleryTaskGraph/useGetValueFromWidget.ts @@ -0,0 +1,63 @@ +import { ENTITY_VERSION_V4 } from 'constants/app'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils'; +import { useCallback } from 'react'; +import { useQueries } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +interface UseGetValueResult { + values: string[]; + isLoading: boolean; + isError: boolean; +} + +export const useGetValueFromWidget = ( + widgetsData: Widgets | Widgets[], + queryKey: string[], +): UseGetValueResult => { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const queryPayloads = useCallback( + () => + getQueryPayloadFromWidgetsData({ + start: Math.floor(minTime / 1000000000), + end: Math.floor(maxTime / 1000000000), + widgetsData: Array.isArray(widgetsData) ? widgetsData : [widgetsData], + panelType: PANEL_TYPES.VALUE, + }), + [minTime, maxTime, widgetsData], + ); + + const queries = useQueries( + queryPayloads().map((payload) => ({ + queryKey: [...queryKey, payload, ENTITY_VERSION_V4], + queryFn: (): Promise> => + GetMetricQueryRange(payload, ENTITY_VERSION_V4), + enabled: !!payload, + })), + ); + + const isLoading = queries.some((query) => query.isLoading); + const isError = queries.some((query) => query.isError); + + const values = queries.map((query) => { + if (query.isLoading) return 'Loading...'; + if (query.isError) return 'Error'; + + const value = parseFloat( + query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value || 'NaN', + ); + return Number.isNaN(value) ? 'NaN' : value.toFixed(2); + }); + + return { values, isLoading, isError }; +}; diff --git a/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts b/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts index 96b50de2fc..a2cc262803 100644 --- a/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts +++ b/frontend/src/components/CeleryTask/useCeleryFilterOptions.ts @@ -5,7 +5,7 @@ import { useState } from 'react'; import { useGetAllFilters } from './CeleryTaskConfigOptions/useGetCeleryFilters'; export const useCeleryFilterOptions = ( - type: string, + type: string | string[], ): { searchText: string; handleSearch: (value: string) => void; diff --git a/frontend/src/components/CeleryTask/useNavigateToTraces.ts b/frontend/src/components/CeleryTask/useNavigateToTraces.ts new file mode 100644 index 0000000000..9bb8db9cf5 --- /dev/null +++ b/frontend/src/components/CeleryTask/useNavigateToTraces.ts @@ -0,0 +1,65 @@ +import { QueryParams } from 'constants/query'; +import ROUTES from 'constants/routes'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +export function useNavigateToTraces(): ( + filters: TagFilterItem[], + startTime?: number, + endTime?: number, +) => void { + const { currentQuery } = useQueryBuilder(); + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + + const prepareQuery = useCallback( + (selectedFilters: TagFilterItem[]): Query => ({ + ...currentQuery, + builder: { + ...currentQuery.builder, + queryData: currentQuery.builder.queryData.map((item) => ({ + ...item, + dataSource: DataSource.TRACES, + aggregateOperator: MetricAggregateOperator.NOOP, + filters: { + ...item.filters, + items: selectedFilters, + }, + })), + }, + }), + [currentQuery], + ); + + return useCallback( + (filters: TagFilterItem[], startTime?: number, endTime?: number): void => { + const urlParams = new URLSearchParams(); + if (startTime && endTime) { + urlParams.set(QueryParams.startTime, startTime.toString()); + urlParams.set(QueryParams.endTime, endTime.toString()); + } else { + urlParams.set(QueryParams.startTime, (minTime / 1000000).toString()); + urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString()); + } + + const JSONCompositeQuery = encodeURIComponent( + JSON.stringify(prepareQuery(filters)), + ); + + const newTraceExplorerPath = `${ + ROUTES.TRACES_EXPLORER + }?${urlParams.toString()}&${ + QueryParams.compositeQuery + }=${JSONCompositeQuery}`; + + window.open(newTraceExplorerPath, '_blank'); + }, + [minTime, maxTime, prepareQuery], + ); +} diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 393fedb55a..1d9ddc2ceb 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -44,4 +44,6 @@ export enum QueryParams { taskName = 'taskName', spanName = 'spanName', msgSystem = 'msgSystem', + destination = 'destination', + kindString = 'kindString', } diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 5818c82ce2..b90cd9b916 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -64,7 +64,7 @@ const ROUTES = { INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts', INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes', MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task', - MESSAGING_QUEUES_CELERY_OVERVIEW: '/messaging-queues/celery-overview', + MESSAGING_QUEUES_OVERVIEW: '/messaging-queues/overview', } as const; export default ROUTES; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 24d9c2b968..1b8ba43ac5 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -290,7 +290,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { routeKey === 'MESSAGING_QUEUES' || routeKey === 'MESSAGING_QUEUES_DETAIL' || routeKey === 'MESSAGING_QUEUES_CELERY_TASK' || - routeKey === 'MESSAGING_QUEUES_CELERY_OVERVIEW'; + routeKey === 'MESSAGING_QUEUES_OVERVIEW'; const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY'; diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index 00c32604b2..875fb31b6b 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -52,7 +52,7 @@ export const routeConfig: Record = { [ROUTES.MESSAGING_QUEUES]: [QueryParams.resourceAttributes], [ROUTES.MESSAGING_QUEUES_DETAIL]: [QueryParams.resourceAttributes], [ROUTES.MESSAGING_QUEUES_CELERY_TASK]: [QueryParams.resourceAttributes], - [ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW]: [QueryParams.resourceAttributes], + [ROUTES.MESSAGING_QUEUES_OVERVIEW]: [QueryParams.resourceAttributes], [ROUTES.INFRASTRUCTURE_MONITORING_HOSTS]: [QueryParams.resourceAttributes], [ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES]: [ QueryParams.resourceAttributes, diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts index 18a7b57ad8..f844d7f07d 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts @@ -215,7 +215,7 @@ export const routesToSkip = [ ROUTES.MESSAGING_QUEUES, ROUTES.MESSAGING_QUEUES_DETAIL, ROUTES.MESSAGING_QUEUES_CELERY_TASK, - ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW, + ROUTES.MESSAGING_QUEUES_OVERVIEW, ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, ROUTES.SOMETHING_WENT_WRONG, ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES, diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx b/frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx index 134c897bc5..9d3728516e 100644 --- a/frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverview.tsx @@ -1,20 +1,41 @@ import './CeleryOverview.styles.scss'; import CeleryOverviewConfigOptions from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions'; -import CeleryOverviewTable from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable'; +import CeleryOverviewTable, { + RowData, +} from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; +import { useState } from 'react'; + +import CeleryOverviewDetails from './CeleryOverviewDetail/CeleryOverviewDetails'; export default function CeleryOverview(): JSX.Element { + const [details, setDetails] = useState(null); + + const onRowClick = (record: RowData): void => { + setDetails(record); + }; + return (
-

Celery Overview

+

+ Messaging Queue Overview +

- +
+ {details && ( + { + setDetails(null); + }} + /> + )}
); } diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.styles.scss b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.styles.scss new file mode 100644 index 0000000000..8196461828 --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.styles.scss @@ -0,0 +1,30 @@ +.celery-overview-detail-container { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px 8px; +} + +.overview-right-panel-graph-card { + border-radius: 8px; + border: 1px solid #262626 !important; + background: #0a0a0a; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.1), + 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + + .request-rate-card, + .error-rate-card, + .avg-latency-card { + height: 320px !important; + padding: 10px; + width: 100%; + box-sizing: border-box; + border-bottom: 0.75px solid #262626; + margin-bottom: 12px; + position: relative; + + .widget-graph-container { + height: 260px; + } + } +} diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.tsx b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.tsx new file mode 100644 index 0000000000..7fd5ab0b47 --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/CeleryOverviewDetails.tsx @@ -0,0 +1,99 @@ +import './CeleryOverviewDetails.styles.scss'; + +import { Color, Spacing } from '@signozhq/design-tokens'; +import { Divider, Drawer, Typography } from 'antd'; +import { RowData } from 'components/CeleryOverview/CeleryOverviewTable/CeleryOverviewTable'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { X } from 'lucide-react'; +import { useMemo } from 'react'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; + +import { getFiltersFromKeyValue } from '../CeleryOverviewUtils'; +import OverviewRightPanelGraph from './OverviewRightPanelGraph'; +import ValueInfo from './ValueInfo'; + +export default function CeleryOverviewDetails({ + details, + onClose, +}: { + details: RowData; + onClose: () => void; +}): JSX.Element { + const isDarkMode = useIsDarkMode(); + + const filters: TagFilterItem[] = useMemo(() => { + const keyValues = Object.entries(details).map(([key, value]) => { + switch (key) { + case 'service_name': + return getFiltersFromKeyValue('service.name', value, 'resource'); + case 'span_name': + return getFiltersFromKeyValue('name', value, ''); + case 'messaging_system': + return getFiltersFromKeyValue('messaging.system', value, 'tag'); + case 'destination': + return getFiltersFromKeyValue( + details.messaging_system === 'celery' + ? 'messaging.destination' + : 'messaging.destination.name', + value, + 'tag', + ); + case 'kind_string': + return getFiltersFromKeyValue('kind_string', value, ''); + default: + return undefined; + } + }); + + return keyValues.filter((item) => item !== undefined) as TagFilterItem[]; + }, [details]); + + const groupByFilter = useMemo( + () => + getFiltersFromKeyValue('messaging.destination.partition.id', '', 'tag').key, + [], + ); + + return ( + + {details.service_name} +
+ + {details.span_name} + + + + {details.messaging_system} + + + + {details.destination} + + + + {details.kind_string} + +
+
+ } + placement="right" + onClose={onClose} + open={!!details} + style={{ + overscrollBehavior: 'contain', + background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100, + }} + className="celery-task-detail-drawer" + destroyOnClose + closeIcon={} + > +
+ + +
+ + ); +} diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/OverviewRightPanelGraph.tsx b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/OverviewRightPanelGraph.tsx new file mode 100644 index 0000000000..6516e1f39a --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/OverviewRightPanelGraph.tsx @@ -0,0 +1,169 @@ +import { Card } from 'antd'; +import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces'; +import { QueryParams } from 'constants/query'; +import { ViewMenuAction } from 'container/GridCardLayout/config'; +import GridCard from 'container/GridCardLayout/GridCard'; +import { Button } from 'container/MetricsApplication/Tabs/styles'; +import { onGraphClickHandler } from 'container/MetricsApplication/Tabs/util'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin'; +import { useCallback, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory, useLocation } from 'react-router-dom'; +import { UpdateTimeInterval } from 'store/actions'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { + celeryOverviewAvgLatencyGraphData, + celeryOverviewErrorRateGraphData, + celeryOverviewRequestRateGraphData, +} from '../CeleryOverviewGraphUtils'; + +export default function OverviewRightPanelGraph({ + groupByFilter, + filters, +}: { + groupByFilter?: BaseAutocompleteData; + filters?: TagFilterItem[]; +}): JSX.Element { + const history = useHistory(); + const { pathname } = useLocation(); + const dispatch = useDispatch(); + const urlQuery = useUrlQuery(); + + const onDragSelect = useCallback( + (start: number, end: number) => { + const startTimestamp = Math.trunc(start); + const endTimestamp = Math.trunc(end); + + urlQuery.set(QueryParams.startTime, startTimestamp.toString()); + urlQuery.set(QueryParams.endTime, endTimestamp.toString()); + const generatedUrl = `${pathname}?${urlQuery.toString()}`; + history.push(generatedUrl); + + if (startTimestamp !== endTimestamp) { + dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp])); + } + }, + [dispatch, history, pathname, urlQuery], + ); + + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + + const requestRateWidget = useMemo( + () => + celeryOverviewRequestRateGraphData(minTime, maxTime, filters, groupByFilter), + [minTime, maxTime, filters, groupByFilter], + ); + + const errorRateWidget = useMemo( + () => + celeryOverviewErrorRateGraphData(minTime, maxTime, filters, groupByFilter), + [minTime, maxTime, filters, groupByFilter], + ); + + const avgLatencyWidget = useMemo( + () => + celeryOverviewAvgLatencyGraphData(minTime, maxTime, filters, groupByFilter), + [minTime, maxTime, filters, groupByFilter], + ); + const [selectedTimeStamp, setSelectedTimeStamp] = useState(0); + + const handleSetTimeStamp = useCallback((selectTime: number) => { + setSelectedTimeStamp(selectTime); + }, []); + + const navigateToTraces = useNavigateToTraces(); + + const handleGraphClick = useCallback( + (type: string): OnClickPluginOpts['onClick'] => ( + xValue, + yValue, + mouseX, + mouseY, + ): Promise => + onGraphClickHandler(handleSetTimeStamp)( + xValue, + yValue, + mouseX, + mouseY, + type, + ), + [handleSetTimeStamp], + ); + + const goToTraces = useCallback( + (widget: Widgets) => { + const { stepInterval } = widget?.query?.builder?.queryData?.[0] ?? {}; + navigateToTraces( + filters ?? [], + selectedTimeStamp, + selectedTimeStamp + (stepInterval ?? 60), + ); + }, + [navigateToTraces, filters, selectedTimeStamp], + ); + return ( + +
+ + +
+
+ + +
+
+ + +
+
+ ); +} + +OverviewRightPanelGraph.defaultProps = { + groupByFilter: undefined, + filters: undefined, +}; diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.styles.scss b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.styles.scss new file mode 100644 index 0000000000..8d42c449d6 --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.styles.scss @@ -0,0 +1,63 @@ +.value-info-card { + border-radius: 8px; + border: 1px solid #262626 !important; + background: #0a0a0a; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.1), + 0px 1px 2px -1px rgba(0, 0, 0, 0.1); + + .metric-column { + .metric-title { + color: #fafafa; + font-size: 14px; + font-weight: 500; + } + + .metric-value-container { + display: flex; + align-items: baseline; + gap: 6px; + } + + .metric-value { + font-size: 24px; + font-weight: 400; + + &.red { + color: #f87171; + } + + &.green { + color: #4ade80; + } + + &.loading { + opacity: 0.5; + } + } + + .metric-unit { + color: #a3a3a3; + font-size: 14px; + } + + .metric-reference { + color: #a3a3a3; + font-size: 12px; + } + + .trace-button { + margin-top: 8px; + background: #262626; + border: none; + + &:hover { + background: #404040; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } +} diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.tsx b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.tsx new file mode 100644 index 0000000000..97ba9b752d --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewDetail/ValueInfo.tsx @@ -0,0 +1,209 @@ +import './ValueInfo.styles.scss'; + +import { FileSearchOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Row } from 'antd'; +import { useNavigateToTraces } from 'components/CeleryTask/useNavigateToTraces'; +import { ENTITY_VERSION_V4 } from 'constants/app'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { useCallback, useMemo } from 'react'; +import { useQueries } from 'react-query'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { v4 as uuidv4 } from 'uuid'; + +import { + celeryOverviewAvgLatencyWidgetData, + celeryOverviewErrorRateWidgetData, + celeryOverviewRequestRateWidgetData, +} from '../CeleryOverviewGraphUtils'; +import { getQueryPayloadFromWidgetsData } from '../CeleryOverviewUtils'; + +export default function ValueInfo({ + filters, +}: { + filters?: TagFilterItem[]; +}): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + // get data from api + const queryPayloads = useMemo( + () => + getQueryPayloadFromWidgetsData({ + start: Math.floor(minTime / 1000000000), + end: Math.floor(maxTime / 1000000000), + widgetsData: [ + celeryOverviewRequestRateWidgetData(filters), + celeryOverviewErrorRateWidgetData(filters), + celeryOverviewAvgLatencyWidgetData(filters), + ], + panelType: PANEL_TYPES.VALUE, + }), + [minTime, maxTime, filters], + ); + + const queries = useQueries( + queryPayloads.map((payload) => ({ + queryKey: [ + 'overview-detail', + payload, + ENTITY_VERSION_V4, + 'overview-right-panel', + ], + queryFn: (): Promise> => + GetMetricQueryRange(payload, ENTITY_VERSION_V4), + enabled: !!payload, + })), + ); + + const getValues = useCallback( + () => + queries.map((query) => { + const value = parseFloat( + query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0] + ?.values?.[0]?.value || 'NaN', + ); + return Number.isNaN(value) ? 'NaN' : value.toFixed(2); + }), + [queries], + ); + + const isLoading = queries.some((query) => query.isLoading); + const [requestRate, errorRate, avgLatency] = useMemo( + () => (isLoading ? ['0', '0', '0'] : getValues()), + [isLoading, getValues], + ); + + const navigateToTrace = useNavigateToTraces(); + + const avgLatencyInMs = useMemo(() => { + if (avgLatency === 'NaN') return 'NaN'; + const numericValue = parseFloat(avgLatency); + return (numericValue / 1000000).toFixed(2); + }, [avgLatency]); + + const getColorBasedOnValue = (value: string): string => { + const numericValue = parseFloat(value); + if (value === 'NaN') return 'gray'; + if (numericValue < 3) return 'green'; + if (numericValue < 8) return 'yellow'; + return 'red'; + }; + + const getColorForLatency = (value: string): string => { + const numericValue = parseFloat(value); + if (value === 'NaN') return 'gray'; + if (numericValue < 100) return 'green'; + if (numericValue < 200) return 'yellow'; + return 'red'; + }; + + const getColorForErrorRate = (value: string): string => { + const numericValue = parseFloat(value); + if (value === 'NaN') return 'gray'; + if (numericValue < 60) return 'green'; + if (numericValue < 90) return 'yellow'; + return 'red'; + }; + + return ( + + + +
Request Rate
+
+
+ {requestRate === 'NaN' ? '0' : requestRate} +
+
req/s
+
+ + + +
Error Rate
+
+
+ {errorRate === 'NaN' ? '0' : errorRate} +
+
%
+
+ + + +
Average Latency
+
+
+ {avgLatencyInMs === 'NaN' ? '0' : avgLatencyInMs} +
+
ms
+
+ + +
+
+ ); +} + +ValueInfo.defaultProps = { + filters: undefined, +}; diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewGraphUtils.ts b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewGraphUtils.ts new file mode 100644 index 0000000000..ad66080bf8 --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewGraphUtils.ts @@ -0,0 +1,407 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { getStepInterval } from 'components/CeleryTask/CeleryTaskGraph/CeleryTaskGraphUtils'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory'; +import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { + BaseAutocompleteData, + DataTypes, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; +import { v4 as uuidv4 } from 'uuid'; + +export const celeryOverviewRequestRateWidgetData = ( + filters?: TagFilterItem[], +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Request Rate', + description: 'Represents request rate of the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + panelTypes: PANEL_TYPES.VALUE, + }), + ); + +export const celeryOverviewErrorRateWidgetData = ( + filters?: TagFilterItem[], +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Error', + description: 'Represents Error in the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + ...(filters ?? []), + { + id: uuidv4(), + key: { + dataType: DataTypes.bool, + id: 'has_error--bool----true', + isColumn: true, + isJSON: false, + key: 'has_error', + type: '', + }, + op: '=', + value: 'true', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: '', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + ], + panelTypes: PANEL_TYPES.VALUE, + }), + ); + +export const celeryOverviewAvgLatencyWidgetData = ( + filters?: TagFilterItem[], +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Avg Latency', + description: 'Represents Avg Latency of the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'duration_nano--float64----true', + isColumn: true, + isJSON: false, + key: 'duration_nano', + type: '', + }, + aggregateOperator: 'p95', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: [], + having: [], + legend: 'p95', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'p95', + }, + ], + panelTypes: PANEL_TYPES.VALUE, + }), + ); + +export const celeryOverviewRequestRateGraphData = ( + startTime: number, + endTime: number, + filters?: TagFilterItem[], + groupByFilter?: BaseAutocompleteData, +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Request rate', + description: 'Represents Request rate of the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'Request Rate', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'rate', + }, + ], + panelTypes: PANEL_TYPES.BAR, + }), + ); + +export const celeryOverviewErrorRateGraphData = ( + startTime: number, + endTime: number, + filters?: TagFilterItem[], + groupByFilter?: BaseAutocompleteData, +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Error', + description: 'Represents Error in the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'A', + filters: { + items: [ + ...(filters ?? []), + { + id: uuidv4(), + key: { + dataType: DataTypes.bool, + id: 'has_error--bool----true', + isColumn: true, + isJSON: false, + key: 'has_error', + type: '', + }, + op: '=', + value: 'true', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'True', + limit: null, + orderBy: [], + queryName: 'A', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'rate', + }, + { + aggregateAttribute: { + dataType: DataTypes.EMPTY, + id: '------false', + isColumn: false, + isJSON: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'B', + filters: { + items: [ + ...(filters ?? []), + { + id: uuidv4(), + key: { + dataType: DataTypes.bool, + id: 'has_error--bool----true', + isColumn: true, + isJSON: false, + key: 'has_error', + type: '', + }, + op: '=', + value: 'false', + }, + ], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'False', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'rate', + }, + ], + panelTypes: PANEL_TYPES.BAR, + }), + ); + +export const celeryOverviewAvgLatencyGraphData = ( + startTime: number, + endTime: number, + filters?: TagFilterItem[], + groupByFilter?: BaseAutocompleteData, +): Widgets => + getWidgetQueryBuilder( + getWidgetQuery({ + title: 'Latency', + description: 'Represents Latency of the service', + queryData: [ + { + aggregateAttribute: { + dataType: DataTypes.ArrayFloat64, + id: 'duration_nano--float64----true', + isColumn: true, + isJSON: false, + key: 'duration_nano', + type: '', + }, + aggregateOperator: 'p90', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'C', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'p90', + limit: null, + orderBy: [], + queryName: 'C', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'p90', + }, + { + aggregateAttribute: { + dataType: DataTypes.ArrayFloat64, + id: 'duration_nano--float64----true', + isColumn: true, + isJSON: false, + key: 'duration_nano', + type: '', + }, + aggregateOperator: 'p95', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'D', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'p95', + limit: null, + orderBy: [], + queryName: 'D', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'p95', + }, + { + aggregateAttribute: { + dataType: DataTypes.ArrayFloat64, + id: 'duration_nano--float64----true', + isColumn: true, + isJSON: false, + key: 'duration_nano', + type: '', + }, + aggregateOperator: 'p99', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'E', + filters: { + items: filters ?? [], + op: 'AND', + }, + functions: [], + groupBy: groupByFilter ? [groupByFilter] : [], + having: [], + legend: 'p99', + limit: null, + orderBy: [], + queryName: 'E', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: getStepInterval(startTime, endTime), + timeAggregation: 'p99', + }, + ], + panelTypes: PANEL_TYPES.TIME_SERIES, + yAxisUnit: 'ns', + }), + ); diff --git a/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewUtils.ts b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewUtils.ts new file mode 100644 index 0000000000..98d735c4d3 --- /dev/null +++ b/frontend/src/pages/Celery/CeleryOverview/CeleryOverviewUtils.ts @@ -0,0 +1,47 @@ +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuidv4 } from 'uuid'; + +export const getQueryPayloadFromWidgetsData = ({ + start, + end, + widgetsData, + panelType, +}: { + start: number; + end: number; + widgetsData: Widgets[]; + panelType: PANEL_TYPES; +}): GetQueryResultsProps[] => + widgetsData.map((widget) => ({ + start, + end, + graphType: panelType, + query: widget.query, + selectedTime: 'GLOBAL_TIME', + formatForWeb: false, + variables: {}, + })); + +export const getFiltersFromKeyValue = ( + key: string, + value: string | number, + type?: string, + op?: string, + dataType?: DataTypes, +): TagFilterItem => ({ + id: uuidv4(), + key: { + key, + dataType: dataType || DataTypes.String, + type: type || 'tag', + isColumn: false, + isJSON: false, + id: `${key}--${dataType || DataTypes.String}--${type || 'tag'}--false`, + }, + op: op || '=', + value: value.toString(), +}); diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx index 15fee0bb78..a3cd2bc6cb 100644 --- a/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx +++ b/frontend/src/pages/MessagingQueues/MessagingQueuesMainPage.tsx @@ -37,11 +37,11 @@ export const Overview: TabRoutes = { Component: CeleryOverview, name: (
- Celery Overview + Overview
), - route: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW, - key: ROUTES.MESSAGING_QUEUES_CELERY_OVERVIEW, + route: ROUTES.MESSAGING_QUEUES_OVERVIEW, + key: ROUTES.MESSAGING_QUEUES_OVERVIEW, }; export default function MessagingQueuesMainPage(): JSX.Element { diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index dcfe2f2524..02096cae9d 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -109,5 +109,5 @@ export const routePermission: Record = { INFRASTRUCTURE_MONITORING_HOSTS: ['ADMIN', 'EDITOR', 'VIEWER'], INFRASTRUCTURE_MONITORING_KUBERNETES: ['ADMIN', 'EDITOR', 'VIEWER'], MESSAGING_QUEUES_CELERY_TASK: ['ADMIN', 'EDITOR', 'VIEWER'], - MESSAGING_QUEUES_CELERY_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'], + MESSAGING_QUEUES_OVERVIEW: ['ADMIN', 'EDITOR', 'VIEWER'], };