mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 00:05:53 +08:00
feat: show warning when the top level operations count is more than 2500 (#5193)
This commit is contained in:
parent
dbfa4e80bb
commit
34750aba84
@ -11,6 +11,13 @@ export const ServiceMetricsPage = Loadable(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ServiceTopLevelOperationsPage = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "ServiceMetricsPage" */ 'pages/ServiceTopLevelOperations'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const ServiceMapPage = Loadable(
|
export const ServiceMapPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||||
);
|
);
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
ServiceMapPage,
|
ServiceMapPage,
|
||||||
ServiceMetricsPage,
|
ServiceMetricsPage,
|
||||||
ServicesTablePage,
|
ServicesTablePage,
|
||||||
|
ServiceTopLevelOperationsPage,
|
||||||
SettingsPage,
|
SettingsPage,
|
||||||
ShortcutsPage,
|
ShortcutsPage,
|
||||||
SignupPage,
|
SignupPage,
|
||||||
@ -84,6 +85,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'SERVICE_METRICS',
|
key: 'SERVICE_METRICS',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
|
||||||
|
exact: true,
|
||||||
|
component: ServiceTopLevelOperationsPage,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'SERVICE_TOP_LEVEL_OPERATIONS',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.SERVICE_MAP,
|
path: ROUTES.SERVICE_MAP,
|
||||||
component: ServiceMapPage,
|
component: ServiceMapPage,
|
||||||
|
@ -2,6 +2,7 @@ const ROUTES = {
|
|||||||
SIGN_UP: '/signup',
|
SIGN_UP: '/signup',
|
||||||
LOGIN: '/login',
|
LOGIN: '/login',
|
||||||
SERVICE_METRICS: '/services/:servicename',
|
SERVICE_METRICS: '/services/:servicename',
|
||||||
|
SERVICE_TOP_LEVEL_OPERATIONS: '/services/:servicename/top-level-operations',
|
||||||
SERVICE_MAP: '/service-map',
|
SERVICE_MAP: '/service-map',
|
||||||
TRACE: '/trace',
|
TRACE: '/trace',
|
||||||
TRACE_DETAIL: '/trace/:id',
|
TRACE_DETAIL: '/trace/:id',
|
||||||
|
@ -1,13 +1,26 @@
|
|||||||
|
import '../ServiceApplication.styles.scss';
|
||||||
|
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { Popconfirm, PopconfirmProps } from 'antd';
|
||||||
import type { ColumnType } from 'antd/es/table';
|
import type { ColumnType } from 'antd/es/table';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { routeConfig } from 'container/SideNav/config';
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
import { getQueryString } from 'container/SideNav/helper';
|
import { getQueryString } from 'container/SideNav/helper';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ServicesList } from 'types/api/metrics/getService';
|
import { ServicesList } from 'types/api/metrics/getService';
|
||||||
|
|
||||||
import { filterDropdown } from '../Filter/FilterDropdown';
|
import { filterDropdown } from '../Filter/FilterDropdown';
|
||||||
import { Name } from '../styles';
|
|
||||||
|
const MAX_TOP_LEVEL_OPERATIONS = 2500;
|
||||||
|
|
||||||
|
const highTopLevelOperationsPopoverDesc = (metrics: string): JSX.Element => (
|
||||||
|
<div className="popover-description">
|
||||||
|
The service `{metrics}` has too many top level operations. It makes the
|
||||||
|
dashboard slow to load.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const getColumnSearchProps = (
|
export const getColumnSearchProps = (
|
||||||
dataIndex: keyof ServicesList,
|
dataIndex: keyof ServicesList,
|
||||||
@ -15,24 +28,61 @@ export const getColumnSearchProps = (
|
|||||||
): ColumnType<ServicesList> => ({
|
): ColumnType<ServicesList> => ({
|
||||||
filterDropdown,
|
filterDropdown,
|
||||||
filterIcon: <SearchOutlined />,
|
filterIcon: <SearchOutlined />,
|
||||||
onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
|
onFilter: (
|
||||||
record[dataIndex]
|
value: string | number | boolean,
|
||||||
.toString()
|
record: ServicesList,
|
||||||
.toLowerCase()
|
): boolean => {
|
||||||
.includes(value.toString().toLowerCase()),
|
if (record[dataIndex]) {
|
||||||
render: (metrics: string): JSX.Element => {
|
record[dataIndex]
|
||||||
|
?.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(value.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
render: (metrics: string, record: ServicesList): JSX.Element => {
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
|
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
|
||||||
const queryString = getQueryString(avialableParams, urlParams);
|
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 (
|
return (
|
||||||
<Link
|
<div className={`serviceName ${hasHighTopLevelOperations ? 'error' : ''} `}>
|
||||||
to={`${ROUTES.APPLICATION}/${encodeURIComponent(
|
{hasHighTopLevelOperations && (
|
||||||
metrics,
|
<Popconfirm
|
||||||
)}?${queryString.join('')}`}
|
title="Too Many Top Level Operations"
|
||||||
>
|
description={highTopLevelOperationsPopoverDesc(metrics)}
|
||||||
<Name>{metrics}</Name>
|
placement="right"
|
||||||
</Link>
|
overlayClassName="service-high-top-level-operations"
|
||||||
|
onConfirm={handleShowTopLevelOperations}
|
||||||
|
trigger={['hover']}
|
||||||
|
showCancel={false}
|
||||||
|
okText="Show Top Level Operations"
|
||||||
|
>
|
||||||
|
<Info size={14} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to={`${ROUTES.APPLICATION}/${encodeURIComponent(
|
||||||
|
metrics,
|
||||||
|
)}?${queryString.join('')}`}
|
||||||
|
>
|
||||||
|
{metrics}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,11 +15,19 @@ export const getColumnSearchProps = (
|
|||||||
): ColumnType<ServicesList> => ({
|
): ColumnType<ServicesList> => ({
|
||||||
filterDropdown,
|
filterDropdown,
|
||||||
filterIcon: <SearchOutlined />,
|
filterIcon: <SearchOutlined />,
|
||||||
onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
|
onFilter: (
|
||||||
record[dataIndex]
|
value: string | number | boolean,
|
||||||
.toString()
|
record: ServicesList,
|
||||||
.toLowerCase()
|
): boolean => {
|
||||||
.includes(value.toString().toLowerCase()),
|
if (record[dataIndex]) {
|
||||||
|
record[dataIndex]
|
||||||
|
?.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(value.toString().toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
render: (metrics: string): JSX.Element => {
|
render: (metrics: string): JSX.Element => {
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
|
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
|
||||||
|
@ -202,6 +202,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.INTEGRATIONS,
|
ROUTES.INTEGRATIONS,
|
||||||
ROUTES.DASHBOARD,
|
ROUTES.DASHBOARD,
|
||||||
ROUTES.DASHBOARD_WIDGET,
|
ROUTES.DASHBOARD_WIDGET,
|
||||||
|
ROUTES.SERVICE_TOP_LEVEL_OPERATIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
@ -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;
|
||||||
|
}
|
142
frontend/src/pages/ServiceTopLevelOperations/index.tsx
Normal file
142
frontend/src/pages/ServiceTopLevelOperations/index.tsx
Normal file
@ -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<IServiceName>();
|
||||||
|
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<string[]>([]);
|
||||||
|
|
||||||
|
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 => (
|
||||||
|
<div className="">
|
||||||
|
SigNoz calculates the RED metrics for a service using the entry-point spans.
|
||||||
|
For more details, you can check out our
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/metrics/#open-the-services-section"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
docs
|
||||||
|
</a>
|
||||||
|
. 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{' '}
|
||||||
|
<a
|
||||||
|
href="https://opentelemetry.io/docs/specs/otel/trace/api/#span"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
here
|
||||||
|
</a>{' '}
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'Top Level Operation',
|
||||||
|
key: 'top-level-operation',
|
||||||
|
render: (operation: string): JSX.Element => (
|
||||||
|
<div className="top-level-operations-list-item" key={operation}>
|
||||||
|
<Typography.Text> {operation} </Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<Typography.Title level={5} className="top-level-operations-header">
|
||||||
|
<Link to={ROUTES.APPLICATION}>
|
||||||
|
<span className="breadcrumb">
|
||||||
|
{' '}
|
||||||
|
<BarChart2 size={12} /> services{' '}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<div className="divider">/</div>
|
||||||
|
<Link to={`${ROUTES.APPLICATION}/${servicename}`}>
|
||||||
|
<span className="breadcrumb">{servicename} </span>
|
||||||
|
</Link>
|
||||||
|
</Typography.Title>
|
||||||
|
|
||||||
|
<div className="info-alert">
|
||||||
|
<Alert message={alertDesc()} type="info" showIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="loading-top-level-operations">
|
||||||
|
<Typography.Title level={5}>
|
||||||
|
<SyncOutlined spin /> Loading ...
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && (
|
||||||
|
<div className="top-level-operations-list">
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
bordered
|
||||||
|
title={(): string => '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}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -15,6 +15,9 @@ export interface ServicesList {
|
|||||||
callRate: number;
|
callRate: number;
|
||||||
numErrors: number;
|
numErrors: number;
|
||||||
errorRate: number;
|
errorRate: number;
|
||||||
|
dataWarning?: {
|
||||||
|
topLevelOps?: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayloadProps = ServicesList[];
|
export type PayloadProps = ServicesList[];
|
||||||
|
@ -97,4 +97,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
OLD_LOGS_EXPLORER: [],
|
OLD_LOGS_EXPLORER: [],
|
||||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user