mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 11:09:09 +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(
|
||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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 => (
|
||||
<div className="popover-description">
|
||||
The service `{metrics}` has too many top level operations. It makes the
|
||||
dashboard slow to load.
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getColumnSearchProps = (
|
||||
dataIndex: keyof ServicesList,
|
||||
@ -15,24 +28,61 @@ export const getColumnSearchProps = (
|
||||
): ColumnType<ServicesList> => ({
|
||||
filterDropdown,
|
||||
filterIcon: <SearchOutlined />,
|
||||
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 (
|
||||
<Link
|
||||
to={`${ROUTES.APPLICATION}/${encodeURIComponent(
|
||||
metrics,
|
||||
)}?${queryString.join('')}`}
|
||||
>
|
||||
<Name>{metrics}</Name>
|
||||
</Link>
|
||||
<div className={`serviceName ${hasHighTopLevelOperations ? 'error' : ''} `}>
|
||||
{hasHighTopLevelOperations && (
|
||||
<Popconfirm
|
||||
title="Too Many Top Level Operations"
|
||||
description={highTopLevelOperationsPopoverDesc(metrics)}
|
||||
placement="right"
|
||||
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> => ({
|
||||
filterDropdown,
|
||||
filterIcon: <SearchOutlined />,
|
||||
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];
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
numErrors: number;
|
||||
errorRate: number;
|
||||
dataWarning?: {
|
||||
topLevelOps?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type PayloadProps = ServicesList[];
|
||||
|
@ -97,4 +97,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
OLD_LOGS_EXPLORER: [],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SERVICE_TOP_LEVEL_OPERATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user