From 86a888a6a2265edbb5be3d66c3ac7dd6b1e9c196 Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:43:02 +0530 Subject: [PATCH] chore: metrics explorer improvements (#7285) --- .../api/metricsExplorer/getMetricDetails.ts | 8 +- .../metricsExplorer/updateMetricMetadata.ts | 5 +- frontend/src/constants/localStorage.ts | 1 + .../Explorer/Explorer.styles.scss | 13 ++- .../MetricsExplorer/Explorer/Explorer.tsx | 4 +- .../Explorer/RelatedMetrics.tsx | 91 ++++++++----------- .../Explorer/RelatedMetricsCard.tsx | 24 +++-- .../MetricsExplorer/Explorer/types.ts | 2 - .../MetricDetails/AllAttributes.tsx | 27 ++++-- .../MetricDetails/Metadata.tsx | 59 +++++++++--- .../MetricDetails/MetricDetails.tsx | 44 +++++++-- .../MetricDetails/constants.ts | 1 + .../MetricsExplorer/MetricDetails/types.ts | 2 +- .../MetricsExplorer/MetricDetails/utils.tsx | 63 +++++++++++++ .../MetricsLoading/MetricsLoading.styles.scss | 19 ++++ .../MetricsLoading/MetricsLoading.tsx | 24 +++++ .../MetricsExplorer/Summary/Summary.tsx | 11 ++- .../TimeSeriesView/TimeSeriesView.tsx | 10 +- .../MetricsExplorer/MetricsExplorerPage.tsx | 4 +- 19 files changed, 301 insertions(+), 111 deletions(-) create mode 100644 frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.styles.scss create mode 100644 frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.tsx diff --git a/frontend/src/api/metricsExplorer/getMetricDetails.ts b/frontend/src/api/metricsExplorer/getMetricDetails.ts index 3f206167aa..8ce73c67f2 100644 --- a/frontend/src/api/metricsExplorer/getMetricDetails.ts +++ b/frontend/src/api/metricsExplorer/getMetricDetails.ts @@ -16,15 +16,21 @@ export interface MetricDetails { timeSeriesActive: number; lastReceived: string; attributes: MetricDetailsAttribute[]; - metadata: { + metadata?: { metric_type: MetricType; description: string; unit: string; + temporality: Temporality; }; alerts: MetricDetailsAlert[] | null; dashboards: MetricDetailsDashboard[] | null; } +export enum Temporality { + CUMULATIVE = 'cumulative', + DELTA = 'delta', +} + export interface MetricDetailsAttribute { key: string; value: string[]; diff --git a/frontend/src/api/metricsExplorer/updateMetricMetadata.ts b/frontend/src/api/metricsExplorer/updateMetricMetadata.ts index 712bffd43d..c841f9c9a1 100644 --- a/frontend/src/api/metricsExplorer/updateMetricMetadata.ts +++ b/frontend/src/api/metricsExplorer/updateMetricMetadata.ts @@ -1,12 +1,15 @@ import axios from 'api'; import { ErrorResponse, SuccessResponse } from 'types/api'; +import { Temporality } from './getMetricDetails'; import { MetricType } from './getMetricsList'; export interface UpdateMetricMetadataProps { description: string; unit: string; - type: MetricType; + metricType: MetricType; + temporality: Temporality; + isMonotonic?: boolean; } export interface UpdateMetricMetadataResponse { diff --git a/frontend/src/constants/localStorage.ts b/frontend/src/constants/localStorage.ts index 806aa87379..7614a670d7 100644 --- a/frontend/src/constants/localStorage.ts +++ b/frontend/src/constants/localStorage.ts @@ -26,4 +26,5 @@ export enum LOCALSTORAGE { UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT', CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS', DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING', + METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS', } diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss b/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss index 5424491a2c..8e0836fcb7 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.styles.scss @@ -67,7 +67,6 @@ .related-metrics-container { width: 100%; min-height: 300px; - max-height: 450px; display: flex; flex-direction: column; gap: 10px; @@ -102,19 +101,23 @@ } .related-metrics-body { - padding: 10px 0; + margin-top: 20px; + max-height: 650px; + overflow-y: scroll; .related-metrics-card-container { - min-height: 300px; - margin-bottom: 25px; + margin-bottom: 20px; + min-height: 640px; + .related-metrics-card { - // height: 400px; display: flex; flex-direction: column; gap: 16px; .related-metrics-card-error { padding-top: 10px; + height: fit-content; + width: fit-content; } } } diff --git a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx index 9a741107ed..bba15b9476 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/Explorer.tsx @@ -38,8 +38,8 @@ function Explorer(): JSX.Element { const { notifications } = useNotifications(); const { mutate: updateDashboard, isLoading } = useUpdateDashboard(); const { options } = useOptionsMenu({ - storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS, - dataSource: DataSource.TRACES, + storageKey: LOCALSTORAGE.METRICS_LIST_OPTIONS, + dataSource: DataSource.METRICS, aggregateOperator: 'noop', }); diff --git a/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx index f5df0f3cd9..a293f9d4f3 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetrics.tsx @@ -1,9 +1,5 @@ import { Color } from '@signozhq/design-tokens'; -import { Card, Col, Input, Row, Select, Skeleton } from 'antd'; -import { useIsDarkMode } from 'hooks/useDarkMode'; -import { useResizeObserver } from 'hooks/useDimensions'; -import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; -import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; +import { Card, Col, Empty, Input, Row, Select, Skeleton } from 'antd'; import { Gauge } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -15,9 +11,7 @@ import { RelatedMetricsProps, RelatedMetricWithQueryResult } from './types'; import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs'; function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element { - const isDarkMode = useIsDarkMode(); const graphRef = useRef(null); - const dimensions = useResizeObserver(graphRef); const { maxTime, minTime } = useSelector( (state) => state.globalTime, ); @@ -41,13 +35,15 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element { } }, [metricNames]); - const { relatedMetrics, isRelatedMetricsLoading } = useGetRelatedMetricsGraphs( - { - selectedMetricName, - startMs, - endMs, - }, - ); + const { + relatedMetrics, + isRelatedMetricsLoading, + isRelatedMetricsError, + } = useGetRelatedMetricsGraphs({ + selectedMetricName, + startMs, + endMs, + }); const metricNamesSelectOptions = useMemo( () => @@ -91,31 +87,6 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element { return filteredMetrics; }, [relatedMetrics, selectedRelatedMetric, searchValue]); - const chartData = useMemo( - () => - filteredRelatedMetrics.map(({ queryResult }) => - getUPlotChartData(queryResult.data?.payload), - ), - [filteredRelatedMetrics], - ); - - const options = useMemo( - () => - filteredRelatedMetrics.map(({ queryResult }) => - getUPlotChartOptions({ - apiResponse: queryResult.data?.payload, - isDarkMode, - dimensions, - yAxisUnit: '', - softMax: null, - softMin: null, - minTimeScale: startMs, - maxTimeScale: endMs, - }), - ), - [filteredRelatedMetrics, isDarkMode, dimensions, startMs, endMs], - ); - return (
@@ -145,20 +116,34 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
{isRelatedMetricsLoading && } - - {filteredRelatedMetrics.map((relatedMetricWithQueryResult, index) => ( - - - - - - ))} - + {isRelatedMetricsError && ( + + )} + {!isRelatedMetricsLoading && + !isRelatedMetricsError && + filteredRelatedMetrics.length === 0 && ( + + )} + {!isRelatedMetricsLoading && + !isRelatedMetricsError && + filteredRelatedMetrics.length > 0 && ( + + {filteredRelatedMetrics.map((relatedMetricWithQueryResult) => ( + + + + + + ))} + + )}
); diff --git a/frontend/src/container/MetricsExplorer/Explorer/RelatedMetricsCard.tsx b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetricsCard.tsx index 1f4bec08e2..2c2c57499a 100644 --- a/frontend/src/container/MetricsExplorer/Explorer/RelatedMetricsCard.tsx +++ b/frontend/src/container/MetricsExplorer/Explorer/RelatedMetricsCard.tsx @@ -1,14 +1,11 @@ -import { Skeleton, Typography } from 'antd'; -import Uplot from 'components/Uplot'; +import { Empty, Skeleton, Typography } from 'antd'; +import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; +import { DataSource } from 'types/common/queryBuilder'; import DashboardsAndAlertsPopover from '../MetricDetails/DashboardsAndAlertsPopover'; import { RelatedMetricsCardProps } from './types'; -function RelatedMetricsCard({ - metric, - options, - chartData, -}: RelatedMetricsCardProps): JSX.Element { +function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element { const { queryResult } = metric; if (queryResult.isLoading) { @@ -27,13 +24,20 @@ function RelatedMetricsCard({ {metric.name} {queryResult.isLoading ? : null} - {queryResult.error ? ( + {queryResult.isError ? (
- Something went wrong +
) : null} {!queryResult.isLoading && !queryResult.error && ( - + )} { - // TODO: Implement this when explore page is ready - console.log(metricName, attribute); + (key: string, value: string) => { + const compositeQuery = getMetricDetailsQuery(metricName, { key, value }); + const encodedCompositeQuery = JSON.stringify(compositeQuery); + safeNavigate( + `${ROUTES.METRICS_EXPLORER_EXPLORER}?compositeQuery=${encodedCompositeQuery}`, + ); }, - [metricName], + [metricName, safeNavigate], ); const filteredAttributes = useMemo( @@ -40,7 +48,10 @@ function AllAttributes({ label: attribute.key, contribution: attribute.valueCount, }, - value: attribute.value, + value: { + key: attribute.key, + value: attribute.value, + }, })) : [], [filteredAttributes], @@ -70,14 +81,14 @@ function AllAttributes({ align: 'left', ellipsis: true, className: 'metric-metadata-value', - render: (attributes: string[]): JSX.Element => ( + render: (field: { key: string; value: string[] }): JSX.Element => (
- {attributes.map((attribute) => ( + {field.value.map((attribute) => ( @@ -93,9 +115,11 @@ function MetricDetails({ destroyOnClose closeIcon={} > - {isMetricDetailsLoading ? ( - - ) : ( + {isMetricDetailsLoading && } + {isMetricDetailsError && !isMetricDetailsLoading && ( + + )} + {!isMetricDetailsLoading && !isMetricDetailsError && (
diff --git a/frontend/src/container/MetricsExplorer/MetricDetails/constants.ts b/frontend/src/container/MetricsExplorer/MetricDetails/constants.ts index d00ce5ddb0..586abb5538 100644 --- a/frontend/src/container/MetricsExplorer/MetricDetails/constants.ts +++ b/frontend/src/container/MetricsExplorer/MetricDetails/constants.ts @@ -2,4 +2,5 @@ export const METRIC_METADATA_KEYS = { description: 'Description', unit: 'Unit', metric_type: 'Metric Type', + temporality: 'Temporality', }; diff --git a/frontend/src/container/MetricsExplorer/MetricDetails/types.ts b/frontend/src/container/MetricsExplorer/MetricDetails/types.ts index 3a5f54772a..9dad275610 100644 --- a/frontend/src/container/MetricsExplorer/MetricDetails/types.ts +++ b/frontend/src/container/MetricsExplorer/MetricDetails/types.ts @@ -19,7 +19,7 @@ export interface DashboardsAndAlertsPopoverProps { export interface MetadataProps { metricName: string; - metadata: MetricDetails['metadata']; + metadata: MetricDetails['metadata'] | undefined; refetchMetricDetails: () => void; } diff --git a/frontend/src/container/MetricsExplorer/MetricDetails/utils.tsx b/frontend/src/container/MetricsExplorer/MetricDetails/utils.tsx index bb91f8bd80..f8013a4c7f 100644 --- a/frontend/src/container/MetricsExplorer/MetricDetails/utils.tsx +++ b/frontend/src/container/MetricsExplorer/MetricDetails/utils.tsx @@ -1,3 +1,10 @@ +import { Temporality } from 'api/metricsExplorer/getMetricDetails'; +import { MetricType } from 'api/metricsExplorer/getMetricsList'; +import { initialQueriesMap } from 'constants/queryBuilder'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; + export function formatTimestampToReadableDate(timestamp: string): string { const date = new Date(timestamp); // Extracting date components @@ -19,3 +26,59 @@ export function formatNumberToCompactFormat(num: number): string { maximumFractionDigits: 1, }).format(num); } + +export function determineIsMonotonic( + metricType: MetricType, + temporality: Temporality, +): boolean { + if (metricType === MetricType.HISTOGRAM) { + return true; + } + if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) { + return false; + } + if (metricType === MetricType.SUM) { + return temporality === Temporality.CUMULATIVE; + } + return false; +} + +export function getMetricDetailsQuery( + metricName: string, + filter?: { key: string; value: string }, +): Query { + return { + ...initialQueriesMap[DataSource.METRICS], + builder: { + queryData: [ + { + ...initialQueriesMap[DataSource.METRICS].builder.queryData[0], + aggregateAttribute: { + key: metricName, + type: DataTypes.String, + id: `${metricName}----string--`, + }, + timeAggregation: 'rate', + spaceAggregation: 'sum', + filters: { + op: 'AND', + items: filter + ? [ + { + op: '=', + id: filter.key, + value: filter.value, + key: { + key: filter.key, + type: DataTypes.String, + }, + }, + ] + : [], + }, + }, + ], + queryFormulas: [], + }, + }; +} diff --git a/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.styles.scss b/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.styles.scss new file mode 100644 index 0000000000..e5f2cda226 --- /dev/null +++ b/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.styles.scss @@ -0,0 +1,19 @@ +.loading-metrics { + padding: 24px 0; + height: 240px; + + display: flex; + justify-content: center; + align-items: flex-start; + + .loading-metrics-content { + display: flex; + align-items: flex-start; + flex-direction: column; + + .loading-gif { + height: 72px; + margin-left: -24px; + } + } +} diff --git a/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.tsx b/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.tsx new file mode 100644 index 0000000000..db457d53ac --- /dev/null +++ b/frontend/src/container/MetricsExplorer/MetricsLoading/MetricsLoading.tsx @@ -0,0 +1,24 @@ +import './MetricsLoading.styles.scss'; + +import { Typography } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { DataSource } from 'types/common/queryBuilder'; + +export function MetricsLoading(): JSX.Element { + const { t } = useTranslation('common'); + return ( +
+
+ wait-icon + + + {t('pending_data_placeholder', { dataSource: DataSource.METRICS })} + +
+
+ ); +} diff --git a/frontend/src/container/MetricsExplorer/Summary/Summary.tsx b/frontend/src/container/MetricsExplorer/Summary/Summary.tsx index a44e43d5d9..45d1fe8d02 100644 --- a/frontend/src/container/MetricsExplorer/Summary/Summary.tsx +++ b/frontend/src/container/MetricsExplorer/Summary/Summary.tsx @@ -7,7 +7,7 @@ import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; @@ -99,6 +99,15 @@ function Summary(): JSX.Element { enabled: !!metricsTreemapQuery, }); + // Reset the filters when the component mounts + useEffect(() => { + handleChangeQueryData('filters', { + op: 'AND', + items: [], + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleFilterChange = useCallback( (value: TagFilter) => { handleChangeQueryData('filters', value); diff --git a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx index 34b6dc4032..68871d80b4 100644 --- a/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx +++ b/frontend/src/container/TimeSeriesView/TimeSeriesView.tsx @@ -6,6 +6,7 @@ import { QueryParams } from 'constants/query'; import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch'; import LogsError from 'container/LogsError/LogsError'; import { LogsLoading } from 'container/LogsLoading/LogsLoading'; +import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading'; import NoLogs from 'container/NoLogs/NoLogs'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading'; @@ -131,6 +132,10 @@ function TimeSeriesView({ logEvent('Logs Explorer: Data present', { panelType: 'TIME_SERIES', }); + } else if (dataSource === DataSource.METRICS) { + logEvent('Metrics Explorer: Data present', { + panelType: 'TIME_SERIES', + }); } } }, [isLoading, isError, chartData, dataSource]); @@ -164,8 +169,9 @@ function TimeSeriesView({ ref={graphRef} data-testid="time-series-graph" > - {isLoading && - (dataSource === DataSource.LOGS ? : )} + {isLoading && dataSource === DataSource.LOGS && } + {isLoading && dataSource === DataSource.TRACES && } + {isLoading && dataSource === DataSource.METRICS && } {chartData && chartData[0] && diff --git a/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx b/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx index 7a13a65ac9..1b416d4f64 100644 --- a/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx +++ b/frontend/src/pages/MetricsExplorer/MetricsExplorerPage.tsx @@ -5,12 +5,12 @@ import { TabRoutes } from 'components/RouteTab/types'; import history from 'lib/history'; import { useLocation } from 'react-use'; -import { Explorer, Summary, Views } from './constants'; +import { Explorer, Summary } from './constants'; function MetricsExplorerPage(): JSX.Element { const { pathname } = useLocation(); - const routes: TabRoutes[] = [Summary, Explorer, Views]; + const routes: TabRoutes[] = [Summary, Explorer]; return (