diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts
index bda390afbf..9275e7d6f6 100644
--- a/frontend/src/AppRoutes/pageComponents.ts
+++ b/frontend/src/AppRoutes/pageComponents.ts
@@ -11,6 +11,13 @@ export const ServiceMetricsPage = Loadable(
),
);
+export const ServiceTopLevelOperationsPage = Loadable(
+ () =>
+ import(
+ /* webpackChunkName: "ServiceMetricsPage" */ 'pages/ServiceTopLevelOperations'
+ ),
+);
+
export const ServiceMapPage = Loadable(
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
);
diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts
index fed77f186e..4fd421ffba 100644
--- a/frontend/src/AppRoutes/routes.ts
+++ b/frontend/src/AppRoutes/routes.ts
@@ -33,6 +33,7 @@ import {
ServiceMapPage,
ServiceMetricsPage,
ServicesTablePage,
+ ServiceTopLevelOperationsPage,
SettingsPage,
ShortcutsPage,
SignupPage,
@@ -84,6 +85,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'SERVICE_METRICS',
},
+ {
+ path: ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
+ exact: true,
+ component: ServiceTopLevelOperationsPage,
+ isPrivate: true,
+ key: 'SERVICE_TOP_LEVEL_OPERATIONS',
+ },
{
path: ROUTES.SERVICE_MAP,
component: ServiceMapPage,
diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts
index cbeb672a5c..243bdd0bba 100644
--- a/frontend/src/constants/routes.ts
+++ b/frontend/src/constants/routes.ts
@@ -2,6 +2,7 @@ const ROUTES = {
SIGN_UP: '/signup',
LOGIN: '/login',
SERVICE_METRICS: '/services/:servicename',
+ SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
SERVICE_MAP: '/service-map',
TRACE: '/trace',
TRACE_DETAIL: '/trace/:id',
diff --git a/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx b/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx
index b272a39475..372f301008 100644
--- a/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx
+++ b/frontend/src/container/ServiceApplication/Columns/GetColumnSearchProps.tsx
@@ -1,13 +1,26 @@
+import '../ServiceApplication.styles.scss';
+
import { SearchOutlined } from '@ant-design/icons';
+import { Popconfirm, PopconfirmProps } from 'antd';
import type { ColumnType } from 'antd/es/table';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
+import history from 'lib/history';
+import { Info } from 'lucide-react';
import { Link } from 'react-router-dom';
import { ServicesList } from 'types/api/metrics/getService';
import { filterDropdown } from '../Filter/FilterDropdown';
-import { Name } from '../styles';
+
+const MAX_TOP_LEVEL_OPERATIONS = 2500;
+
+const highTopLevelOperationsPopoverDesc = (metrics: string): JSX.Element => (
+
+ The service `{metrics}` has too many top level operations. It makes the
+ dashboard slow to load.
+
+);
export const getColumnSearchProps = (
dataIndex: keyof ServicesList,
@@ -15,24 +28,61 @@ export const getColumnSearchProps = (
): ColumnType => ({
filterDropdown,
filterIcon: ,
- onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
- record[dataIndex]
- .toString()
- .toLowerCase()
- .includes(value.toString().toLowerCase()),
- render: (metrics: string): JSX.Element => {
+ onFilter: (
+ value: string | number | boolean,
+ record: ServicesList,
+ ): boolean => {
+ if (record[dataIndex]) {
+ record[dataIndex]
+ ?.toString()
+ .toLowerCase()
+ .includes(value.toString().toLowerCase());
+ }
+
+ return false;
+ },
+ render: (metrics: string, record: ServicesList): JSX.Element => {
const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
const queryString = getQueryString(avialableParams, urlParams);
+ const topLevelOperations = record?.dataWarning?.topLevelOps || [];
+
+ const handleShowTopLevelOperations: PopconfirmProps['onConfirm'] = () => {
+ history.push(
+ `${ROUTES.APPLICATION}/${encodeURIComponent(metrics)}/top-level-operations`,
+ );
+ };
+
+ const hasHighTopLevelOperations =
+ topLevelOperations &&
+ Array.isArray(topLevelOperations) &&
+ topLevelOperations.length > MAX_TOP_LEVEL_OPERATIONS;
return (
-
- {metrics}
-
+
+ {hasHighTopLevelOperations && (
+
+
+
+ )}
+
+
+ {metrics}
+
+
);
},
});
diff --git a/frontend/src/container/ServiceApplication/ServiceApplication.styles.scss b/frontend/src/container/ServiceApplication/ServiceApplication.styles.scss
new file mode 100644
index 0000000000..9cdbd2329c
--- /dev/null
+++ b/frontend/src/container/ServiceApplication/ServiceApplication.styles.scss
@@ -0,0 +1,25 @@
+.serviceName {
+ color: #4e74f8;
+ font-weight: 600;
+ cursor: pointer;
+
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.error {
+ color: var(--bg-cherry-500);
+
+ a {
+ color: var(--bg-cherry-500);
+ }
+}
+
+.service-high-top-level-operations {
+ width: 300px;
+
+ .popover-description {
+ padding: 16px 0;
+ }
+}
diff --git a/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx b/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx
index 4257dc57ec..059063e5d9 100644
--- a/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx
+++ b/frontend/src/container/ServiceTable/Columns/GetColumnSearchProps.tsx
@@ -15,11 +15,19 @@ export const getColumnSearchProps = (
): ColumnType => ({
filterDropdown,
filterIcon: ,
- onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
- record[dataIndex]
- .toString()
- .toLowerCase()
- .includes(value.toString().toLowerCase()),
+ onFilter: (
+ value: string | number | boolean,
+ record: ServicesList,
+ ): boolean => {
+ if (record[dataIndex]) {
+ record[dataIndex]
+ ?.toString()
+ .toLowerCase()
+ .includes(value.toString().toLowerCase());
+ }
+
+ return false;
+ },
render: (metrics: string): JSX.Element => {
const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
index 7909984592..7543e02a47 100644
--- a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
+++ b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts
@@ -202,6 +202,7 @@ export const routesToSkip = [
ROUTES.INTEGRATIONS,
ROUTES.DASHBOARD,
ROUTES.DASHBOARD_WIDGET,
+ ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
diff --git a/frontend/src/pages/ServiceTopLevelOperations/ServiceTopLevelOperations.styles.scss b/frontend/src/pages/ServiceTopLevelOperations/ServiceTopLevelOperations.styles.scss
new file mode 100644
index 0000000000..e7c5dfcda4
--- /dev/null
+++ b/frontend/src/pages/ServiceTopLevelOperations/ServiceTopLevelOperations.styles.scss
@@ -0,0 +1,26 @@
+.title {
+ font-weight: 500;
+ font-size: 14px;
+
+ margin: 16px 0;
+}
+
+.info-alert {
+ margin: 16px 0;
+}
+
+.top-level-operations-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.breadcrumb {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.top-level-operations-list {
+ margin-top: 16px;
+}
diff --git a/frontend/src/pages/ServiceTopLevelOperations/index.tsx b/frontend/src/pages/ServiceTopLevelOperations/index.tsx
new file mode 100644
index 0000000000..38a2ae2807
--- /dev/null
+++ b/frontend/src/pages/ServiceTopLevelOperations/index.tsx
@@ -0,0 +1,142 @@
+import './ServiceTopLevelOperations.styles.scss';
+
+import { SyncOutlined } from '@ant-design/icons';
+import { Alert, Table, Typography } from 'antd';
+import ROUTES from 'constants/routes';
+import { IServiceName } from 'container/MetricsApplication/Tabs/types';
+import useErrorNotification from 'hooks/useErrorNotification';
+import { useQueryService } from 'hooks/useQueryService';
+import useResourceAttribute from 'hooks/useResourceAttribute';
+import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
+import { BarChart2 } from 'lucide-react';
+import { ReactNode, useEffect, useMemo, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { Link, useParams } from 'react-router-dom';
+import { AppState } from 'store/reducers';
+import { GlobalReducer } from 'types/reducer/globalTime';
+import { Tags } from 'types/reducer/trace';
+
+export default function ServiceTopLevelOperations(): JSX.Element {
+ const { servicename: encodedServiceName } = useParams();
+ const { maxTime, minTime, selectedTime } = useSelector<
+ AppState,
+ GlobalReducer
+ >((state) => state.globalTime);
+ const servicename = decodeURIComponent(encodedServiceName);
+ const { queries } = useResourceAttribute();
+ const selectedTags = useMemo(
+ () => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
+ [queries],
+ );
+
+ const [topLevelOperations, setTopLevelOperations] = useState([]);
+
+ const { data, error, isLoading } = useQueryService({
+ minTime,
+ maxTime,
+ selectedTime,
+ selectedTags,
+ });
+
+ useErrorNotification(error);
+
+ useEffect(() => {
+ const selectedService = data?.find(
+ (service) => service.serviceName === servicename,
+ );
+
+ setTopLevelOperations(selectedService?.dataWarning?.topLevelOps || []);
+ }, [servicename, data]);
+
+ const alertDesc = (): ReactNode => (
+
+ SigNoz calculates the RED metrics for a service using the entry-point spans.
+ For more details, you can check out our
+
+ {' '}
+ docs
+
+ . We expect the number of unique entry-point operations to be no more than
+ 2500. The high number of top level operations might be due to an
+ instrumentation issue in your service. Below table shows the sample top level
+ operations. Please refer to official docs for span name guidelines{' '}
+
+ {' '}
+ here
+ {' '}
+ and update the instrumentation to to follow the guidelines. If there are any
+ dynamic IDs in the span name, make sure to use the span attributes instead.
+ If you have more questions, please reach out to us via support.
+
+ );
+
+ const columns = [
+ {
+ title: 'Top Level Operation',
+ key: 'top-level-operation',
+ render: (operation: string): JSX.Element => (
+
+ {operation}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+ {' '}
+ services{' '}
+
+
+ /
+
+ {servicename}
+
+
+
+
+
+ {isLoading && (
+
+
+ Loading ...
+
+
+ )}
+
+ {!isLoading && (
+
+
'Top Level Operations'}
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ dataSource={topLevelOperations}
+ loading={isLoading}
+ showHeader={false}
+ pagination={{
+ pageSize: 100,
+ hideOnSinglePage: true,
+ showTotal: (total: number, range: number[]): string =>
+ `${range[0]}-${range[1]} of ${total}`,
+ }}
+ />
+
+ )}
+
+ );
+}
diff --git a/frontend/src/types/api/metrics/getService.ts b/frontend/src/types/api/metrics/getService.ts
index bb9d6db941..d931197433 100644
--- a/frontend/src/types/api/metrics/getService.ts
+++ b/frontend/src/types/api/metrics/getService.ts
@@ -15,6 +15,9 @@ export interface ServicesList {
callRate: number;
numErrors: number;
errorRate: number;
+ dataWarning?: {
+ topLevelOps?: string[];
+ };
}
export type PayloadProps = ServicesList[];
diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts
index 1c992965d4..44757e3508 100644
--- a/frontend/src/utils/permission/index.ts
+++ b/frontend/src/utils/permission/index.ts
@@ -97,4 +97,5 @@ export const routePermission: Record = {
OLD_LOGS_EXPLORER: [],
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
+ SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
};