diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index f96778df2b..09d75871a3 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -55,6 +55,7 @@ export const REACT_QUERY_KEY = { // API Monitoring Query Keys GET_DOMAINS_LIST: 'GET_DOMAINS_LIST', GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN', + GET_TOP_ERRORS_BY_DOMAIN: 'GET_TOP_ERRORS_BY_DOMAIN', GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST', GET_ENDPOINT_METRICS_DATA: 'GET_ENDPOINT_METRICS_DATA', GET_ENDPOINT_STATUS_CODE_DATA: 'GET_ENDPOINT_STATUS_CODE_DATA', diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx index 0521299a00..93100d92a7 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.tsx @@ -21,6 +21,7 @@ import AllEndPoints from './AllEndPoints'; import DomainMetrics from './components/DomainMetrics'; import { VIEW_TYPES, VIEWS } from './constants'; import EndPointDetailsWrapper from './EndPointDetailsWrapper'; +import TopErrors from './TopErrors'; const TimeRangeOffset = 1000000000; @@ -181,6 +182,14 @@ function DomainDetails({ >
Endpoint Details
+ +
Top Errors
+
{selectedView === VIEW_TYPES.ALL_ENDPOINTS && ( @@ -203,6 +212,13 @@ function DomainDetails({ timeRange={modalTimeRange} /> )} + + {selectedView === VIEW_TYPES.TOP_ERRORS && ( + + )} )} diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx new file mode 100644 index 0000000000..484bafc144 --- /dev/null +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/TopErrors.tsx @@ -0,0 +1,120 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import { Spin, Table, Typography } from 'antd'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { + formatTopErrorsDataForTable, + getTopErrorsColumnsConfig, + getTopErrorsQueryPayload, + TopErrorsResponseRow, +} from 'container/ApiMonitoring/utils'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { useMemo } from 'react'; +import { useQueries } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; + +import ErrorState from './components/ErrorState'; + +function TopErrors({ + domainName, + timeRange, +}: { + domainName: string; + timeRange: { + startTime: number; + endTime: number; + }; +}): JSX.Element { + const { startTime: minTime, endTime: maxTime } = timeRange; + + const queryPayloads = useMemo( + () => getTopErrorsQueryPayload(domainName, minTime, maxTime), + [domainName, minTime, maxTime], + ); + + // Since only one query here + const topErrorsDataQueries = useQueries( + queryPayloads.map((payload) => ({ + queryKey: [ + REACT_QUERY_KEY.GET_TOP_ERRORS_BY_DOMAIN, + payload, + DEFAULT_ENTITY_VERSION, + ], + queryFn: (): Promise> => + GetMetricQueryRange(payload, DEFAULT_ENTITY_VERSION), + enabled: !!payload, + staleTime: 60 * 1000, // 1 minute stale time : optimize this part + })), + ); + + const topErrorsDataQuery = topErrorsDataQueries[0]; + const { + data: topErrorsData, + isLoading, + isRefetching, + isError, + refetch, + } = topErrorsDataQuery; + + const topErrorsColumnsConfig = useMemo(() => getTopErrorsColumnsConfig(), []); + + const formattedTopErrorsData = useMemo( + () => + formatTopErrorsDataForTable( + topErrorsData?.payload?.data?.result as TopErrorsResponseRow[], + ), + [topErrorsData], + ); + + if (isError) { + return ( +
+ +
+ ); + } + + console.log('uncaught topErrors Data', formattedTopErrorsData); + + return ( +
+
+
Top Errors
+ } />, + }} + dataSource={isLoading || isRefetching ? [] : formattedTopErrorsData} + locale={{ + emptyText: + isLoading || isRefetching ? null : ( +
+
+ thinking-emoji + + + This query had no results. Edit your query and try again! + +
+
+ ), + }} + scroll={{ x: true }} + tableLayout="fixed" + rowClassName={(_, index): string => + index % 2 === 0 ? 'table-row-dark' : 'table-row-light' + } + /> + + + ); +} + +export default TopErrors; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts index 9b3314d855..1fbaf85bdc 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/constants.ts @@ -1,9 +1,11 @@ export enum VIEWS { ALL_ENDPOINTS = 'all_endpoints', ENDPOINT_DETAILS = 'endpoint_details', + TOP_ERRORS = 'top_errors', } export const VIEW_TYPES = { ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS, ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS, + TOP_ERRORS: VIEWS.TOP_ERRORS, }; diff --git a/frontend/src/container/ApiMonitoring/utils.tsx b/frontend/src/container/ApiMonitoring/utils.tsx index f44fd5a939..c9cb2035e3 100644 --- a/frontend/src/container/ApiMonitoring/utils.tsx +++ b/frontend/src/container/ApiMonitoring/utils.tsx @@ -633,6 +633,158 @@ export const getEndPointsQueryPayload = ( ]; }; +export const getTopErrorsQueryPayload = ( + domainName: string, + start: number, + end: number, +): GetQueryResultsProps[] => [ + { + selectedTime: 'GLOBAL_TIME', + graphType: PANEL_TYPES.TABLE, + query: { + builder: { + queryData: [ + { + dataSource: DataSource.TRACES, + queryName: 'A', + aggregateOperator: 'count', + aggregateAttribute: { + id: '------false', + dataType: DataTypes.String, + key: '', + isColumn: false, + type: '', + isJSON: false, + }, + timeAggregation: 'rate', + spaceAggregation: 'sum', + functions: [], + filters: { + op: 'AND', + items: [ + { + id: '04da97bd', + key: { + key: 'kind_string', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'kind_string--string----true', + }, + op: '=', + value: 'Client', + }, + { + id: '75d65388', + key: { + key: 'status_message', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'status_message--string----true', + }, + op: 'exists', + value: '', + }, + { + id: 'b1af6bdb', + key: { + key: 'http.url', + dataType: DataTypes.String, + type: 'tag', + isColumn: false, + isJSON: false, + id: 'http.url--string--tag--false', + }, + op: 'exists', + value: '', + }, + { + id: '4872bf91', + key: { + key: 'net.peer.name', + dataType: DataTypes.String, + type: 'tag', + isColumn: false, + isJSON: false, + id: 'net.peer.name--string--tag--false', + }, + op: '=', + value: domainName, + }, + ], + }, + expression: 'A', + disabled: false, + stepInterval: 60, + having: [], + limit: 10, + orderBy: [ + { + columnName: 'timestamp', + order: 'desc', + }, + ], + groupBy: [ + { + key: 'http.url', + dataType: DataTypes.String, + type: 'tag', + isColumn: false, + isJSON: false, + id: 'http.url--string--tag--false', + }, + { + key: 'status_code', + dataType: DataTypes.Float64, + type: '', + isColumn: true, + isJSON: false, + id: 'status_code--float64----true', + }, + { + key: 'status_message', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'status_message--string----true', + }, + ], + legend: '', + reduceTo: 'avg', + }, + ], + queryFormulas: [], + }, + clickhouse_sql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2', + promql: [ + { + disabled: false, + legend: '', + name: 'A', + query: '', + }, + ], + queryType: EQueryType.QUERY_BUILDER, + }, + variables: {}, + start, + end, + step: 240, + }, +]; + export interface EndPointsTableRowData { key: string; endpointName: string; @@ -911,6 +1063,98 @@ export const formatEndPointsDataForTable = ( return formattedData; }; +export interface TopErrorsResponseRow { + metric: { + 'http.url': string; + status_code: string; + status_message: string; + }; + values: [number, string][]; + queryName: string; + legend: string; +} + +export interface TopErrorsTableRowData { + key: string; + endpointName: string; + statusCode: string; + statusMessage: string; + count: number | string; +} + +export const formatTopErrorsDataForTable = ( + data: TopErrorsResponseRow[] | undefined, +): TopErrorsTableRowData[] => { + if (!data) return []; + + return data.map((row) => ({ + key: v4(), + endpointName: + row.metric['http.url'] === 'n/a' || row.metric['http.url'] === undefined + ? '-' + : row.metric['http.url'], + statusCode: + row.metric.status_code === 'n/a' || row.metric.status_code === undefined + ? '-' + : row.metric.status_code, + statusMessage: + row.metric.status_message === 'n/a' || + row.metric.status_message === undefined + ? '-' + : row.metric.status_message, + count: + row.values && + row.values[0] && + row.values[0][1] !== undefined && + row.values[0][1] !== 'n/a' + ? row.values[0][1] + : '-', + })); +}; + +export const getTopErrorsColumnsConfig = (): ColumnType[] => [ + { + title:
Endpoint
, + dataIndex: 'endpointName', + key: 'endpointName', + width: 180, + ellipsis: true, + sorter: false, + className: 'column', + render: (text: string, record: TopErrorsTableRowData): React.ReactNode => ( +
{record.endpointName}
+ ), + }, + { + title:
Status code
, + dataIndex: 'statusCode', + key: 'statusCode', + width: 180, + ellipsis: true, + sorter: false, + align: 'right', + className: `column`, + }, + { + title:
Status message
, + dataIndex: 'statusMessage', + key: 'statusMessage', + width: 180, + ellipsis: true, + align: 'right', + className: `column`, + }, + { + title:
Count
, + dataIndex: 'count', + key: 'count', + width: 120, + sorter: false, + align: 'right', + className: `column`, + }, +]; + export const createFiltersForSelectedRowData = ( selectedRowData: EndPointsTableRowData, currentFilters?: IBuilderQuery['filters'],