feat: Error exception (#979)

* feat: error expection page is made
This commit is contained in:
palash-signoz 2022-04-22 20:03:08 +05:30 committed by GitHub
parent 95311db543
commit 5424c7714f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 600 additions and 4 deletions

View File

@ -0,0 +1,7 @@
{
"see_trace_graph": "See what happened before and after this error in a trace graph",
"see_error_in_trace_graph": "See the error in trace graph",
"stack_trace": "Stacktrace",
"older": "Older",
"newer": "Newer"
}

View File

@ -11,7 +11,8 @@
"n_a": "N/A",
"routes": {
"general": "General",
"alert_channels": "Alert Channels"
"alert_channels": "Alert Channels",
"all_errors": "All Errors"
},
"settings": {
"total_retention_period": "Total Retention Period",

View File

@ -0,0 +1,7 @@
{
"see_trace_graph": "See what happened before and after this error in a trace graph",
"see_error_in_trace_graph": "See the error in trace graph",
"stack_trace": "Stacktrace",
"older": "Older",
"newer": "Newer"
}

View File

@ -11,7 +11,8 @@
"n_a": "N/A",
"routes": {
"general": "General",
"alert_channels": "Alert Channels"
"alert_channels": "Alert Channels",
"all_errors": "All Errors"
},
"settings": {
"total_retention_period": "Total Retention Period",

View File

@ -86,6 +86,14 @@ export const AllAlertChannels = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
);
export const AllErrors = Loadable(
/* webpackChunkName: "All Errors" */ () => import('pages/AllErrors'),
);
export const ErrorDetails = Loadable(
() => import(/* webpackChunkName: "Error Details" */ 'pages/ErrorDetails'),
);
export const StatusPage = Loadable(
() => import(/* webpackChunkName: "All Status" */ 'pages/Status'),
);

View File

@ -4,11 +4,13 @@ import { RouteProps } from 'react-router-dom';
import {
AllAlertChannels,
AllErrors,
CreateAlertChannelAlerts,
CreateNewAlerts,
DashboardPage,
EditAlertChannelsAlerts,
EditRulesPage,
ErrorDetails,
InstrumentationPage,
ListAllALertsPage,
NewDashboardPage,
@ -114,6 +116,16 @@ const routes: AppRoutes[] = [
exact: true,
component: AllAlertChannels,
},
{
path: ROUTES.ALL_ERROR,
exact: true,
component: AllErrors,
},
{
path: ROUTES.ERROR_DETAIL,
exact: true,
component: ErrorDetails,
},
{
path: ROUTES.VERSION,
exact: true,

View File

@ -0,0 +1,30 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getAll';
const getAll = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errors?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
})}`,
);
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getAll;

View File

@ -0,0 +1,32 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getByErrorTypeAndService';
const getByErrorType = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errorWithType?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
serviceName: props.serviceName,
errorType: props.errorType,
})}`,
);
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getByErrorType;

View File

@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getById';
const getById = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errorWithId?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
errorId: props.errorId,
})}`,
);
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getById;

View File

@ -1,13 +1,13 @@
import MEditor from '@monaco-editor/react';
import React from 'react';
function Editor({ value }: EditorProps): JSX.Element {
function Editor({ value, readOnly = false }: EditorProps): JSX.Element {
return (
<MEditor
theme="vs-dark"
defaultLanguage="yaml"
value={value.current}
options={{ fontSize: 16, automaticLayout: true }}
options={{ fontSize: 16, automaticLayout: true, readOnly }}
height="40vh"
onChange={(newValue): void => {
if (value.current && newValue) {
@ -21,6 +21,11 @@ function Editor({ value }: EditorProps): JSX.Element {
interface EditorProps {
value: React.MutableRefObject<string>;
readOnly?: boolean;
}
Editor.defaultProps = {
readOnly: false,
};
export default Editor;

View File

@ -17,6 +17,8 @@ const ROUTES = {
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
ALL_ERROR: '/errors',
ERROR_DETAIL: '/errors/:serviceName/:errorType',
VERSION: '/status',
};

View File

@ -0,0 +1,104 @@
import { Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import getAll from 'api/errors/getAll';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import React from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { generatePath, Link } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Exception } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
function AllErrors(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], {
queryFn: () =>
getAll({
end: maxTime,
start: minTime,
}),
});
const getDateValue = (value: string): JSX.Element => {
return (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
);
};
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
dataIndex: 'exceptionType',
key: 'exceptionType',
render: (value, record): JSX.Element => (
<Link
to={generatePath(ROUTES.ERROR_DETAIL, {
serviceName: record.serviceName,
errorType: record.exceptionType,
})}
>
{value}
</Link>
),
sorter: (a, b): number => a.exceptionType.length - b.exceptionType.length,
},
{
title: 'Error Message',
dataIndex: 'exceptionMessage',
key: 'exceptionMessage',
render: (value): JSX.Element => (
<Typography.Paragraph
ellipsis={{
rows: 2,
}}
>
{value}
</Typography.Paragraph>
),
},
{
title: 'Count',
dataIndex: 'exceptionCount',
key: 'exceptionCount',
sorter: (a, b): number => a.exceptionCount - b.exceptionCount,
},
{
title: 'Last Seen',
dataIndex: 'lastSeen',
key: 'lastSeen',
render: getDateValue,
sorter: (a, b): number =>
dayjs(a.lastSeen).isBefore(dayjs(b.lastSeen)) === true ? 1 : 0,
},
{
title: 'First Seen',
dataIndex: 'firstSeen',
key: 'firstSeen',
render: getDateValue,
sorter: (a, b): number =>
dayjs(a.firstSeen).isBefore(dayjs(b.firstSeen)) === true ? 1 : 0,
},
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
sorter: (a, b): number => a.serviceName.length - b.serviceName.length,
},
];
return (
<Table
tableLayout="fixed"
dataSource={data?.payload as Exception[]}
columns={columns}
loading={isLoading || false}
/>
);
}
export default AllErrors;

View File

@ -0,0 +1,157 @@
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
import Editor from 'components/Editor';
import dayjs from 'dayjs';
import history from 'lib/history';
import React, { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { PayloadProps } from 'types/api/errors/getById';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
const { idPayload } = props;
const [isLoading, setLoading] = useState<boolean>(false);
const { t } = useTranslation(['errorDetails', 'common']);
const { search } = useLocation();
const params = new URLSearchParams(search);
const queryErrorId = params.get('errorId');
const errorDetail = idPayload;
const stackTraceValue = useRef(errorDetail.excepionStacktrace);
const columns = useMemo(
() => [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
},
{
title: 'Value',
dataIndex: 'value',
key: 'value',
},
],
[],
);
const keyToExclude = useMemo(
() => [
'excepionStacktrace',
'exceptionType',
'errorId',
'timestamp',
'exceptionMessage',
'newerErrorId',
'olderErrorId',
],
[],
);
const onClickErrorIdHandler = async (id: string): Promise<void> => {
try {
setLoading(true);
if (id.length === 0) {
notification.error({
message: 'Error Id cannot be empty',
});
setLoading(false);
return;
}
history.push(`${history.location.pathname}?errorId=${id}`);
setLoading(false);
} catch (error) {
notification.error({
message: t('something_went_wrong'),
});
setLoading(false);
}
};
const timeStamp = dayjs(errorDetail.timestamp);
const data: { key: string; value: string }[] = Object.keys(errorDetail)
.filter((e) => !keyToExclude.includes(e))
.map((key) => ({
key,
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
}));
const onClickTraceHandler = (): void => {
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
};
return (
<>
<Typography>{errorDetail.exceptionType}</Typography>
<Typography>{errorDetail.exceptionMessage}</Typography>
<Divider />
<EventContainer>
<div>
<Typography>Event {errorDetail.errorId}</Typography>
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography>
</div>
<div>
<Space align="end" direction="horizontal">
{/* <Button icon={<LeftOutlined />} /> */}
<Button
loading={isLoading}
disabled={
errorDetail.olderErrorId.length === 0 ||
queryErrorId === errorDetail.olderErrorId
}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.olderErrorId)
}
>
{t('older')}
</Button>
<Button
loading={isLoading}
disabled={
errorDetail.newerErrorId.length === 0 ||
queryErrorId === errorDetail.newerErrorId
}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.newerErrorId)
}
>
{t('newer')}
</Button>
{/* <Button icon={<RightOutlined />} /> */}
</Space>
</div>
</EventContainer>
<DashedContainer>
<Typography>{t('see_trace_graph')}</Typography>
<Button onClick={onClickTraceHandler} type="primary">
{t('see_error_in_trace_graph')}
</Button>
</DashedContainer>
<Typography.Title level={4}>{t('stack_trace')}</Typography.Title>
<Editor value={stackTraceValue} readOnly />
<EditorContainer>
<Space direction="vertical">
<Table tableLayout="fixed" columns={columns} dataSource={data} />
</Space>
</EditorContainer>
</>
);
}
interface ErrorDetailsProps {
idPayload: PayloadProps;
}
export default ErrorDetails;

View File

@ -0,0 +1,28 @@
import { grey } from '@ant-design/colors';
import styled from 'styled-components';
export const DashedContainer = styled.div`
border: ${`1px dashed ${grey[0]}`};
box-sizing: border-box;
border-radius: 0.25rem;
display: flex;
justify-content: space-between;
padding: 1rem;
margin-top: 1.875rem;
margin-bottom: 1.625rem;
align-items: center;
`;
export const ButtonContainer = styled.div`
display: flex;
gap: 1rem;
`;
export const EventContainer = styled.div`
display: flex;
justify-content: space-between;
`;
export const EditorContainer = styled.div`
margin-top: 1.5rem;
`;

View File

@ -11,6 +11,7 @@ const breadcrumbNameMap = {
[ROUTES.INSTRUMENTATION]: 'Add instrumentation',
[ROUTES.SETTINGS]: 'Settings',
[ROUTES.DASHBOARD]: 'Dashboard',
[ROUTES.ALL_ERROR]: 'Errors',
[ROUTES.VERSION]: 'Status',
};

View File

@ -3,6 +3,7 @@ import {
AlignLeftOutlined,
ApiOutlined,
BarChartOutlined,
BugOutlined,
DashboardFilled,
DeploymentUnitOutlined,
LineChartOutlined,
@ -31,6 +32,11 @@ const menus: SidebarMenu[] = [
to: ROUTES.LIST_ALL_ALERT,
name: 'Alerts',
},
{
Icon: BugOutlined,
to: ROUTES.ALL_ERROR,
name: 'Errors',
},
{
to: ROUTES.SERVICE_MAP,
name: 'Service Map',

View File

@ -0,0 +1,26 @@
import RouteTab from 'components/RouteTab';
import ROUTES from 'constants/routes';
import AllErrorsContainer from 'container/AllError';
import React from 'react';
import { useTranslation } from 'react-i18next';
function AllErrors(): JSX.Element {
const { t } = useTranslation();
return (
<RouteTab
{...{
routes: [
{
Component: AllErrorsContainer,
name: t('routes.all_errors'),
route: ROUTES.ALL_ERROR,
},
],
activeKey: t('routes.all_errors'),
}}
/>
);
}
export default AllErrors;

View File

@ -0,0 +1,88 @@
import { Typography } from 'antd';
import getByErrorType from 'api/errors/getByErrorTypeAndService';
import getById from 'api/errors/getById';
import Spinner from 'components/Spinner';
import ErrorDetailsContainer from 'container/ErrorDetails';
import React from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/errors/getById';
import { GlobalReducer } from 'types/reducer/globalTime';
function ErrorDetails(): JSX.Element {
const { errorType, serviceName } = useParams<ErrorDetailsParams>();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { search } = useLocation();
const params = new URLSearchParams(search);
const errorId = params.get('errorId');
const { data, status } = useQuery(
[
'errorByType',
errorType,
'serviceName',
serviceName,
maxTime,
minTime,
errorId,
],
{
queryFn: () =>
getByErrorType({
end: maxTime,
errorType,
serviceName,
start: minTime,
}),
enabled: errorId === null,
cacheTime: 5000,
},
);
const { status: ErrorIdStatus, data: errorIdPayload } = useQuery(
[
'errorByType',
errorType,
'serviceName',
serviceName,
maxTime,
minTime,
'errorId',
errorId,
],
{
queryFn: () =>
getById({
end: maxTime,
errorId: errorId || data?.payload?.errorId || '',
start: minTime,
}),
enabled: errorId !== null || status === 'success',
cacheTime: 5000,
},
);
if (status === 'loading' || ErrorIdStatus === 'loading') {
return <Spinner tip="Loading.." />;
}
if (status === 'error' || ErrorIdStatus === 'error') {
return <Typography>{data?.error || errorIdPayload?.error}</Typography>;
}
return (
<ErrorDetailsContainer idPayload={errorIdPayload?.payload as PayloadProps} />
);
}
export interface ErrorDetailsParams {
errorType: string;
serviceName: string;
}
export default ErrorDetails;

View File

@ -0,0 +1,17 @@
import { GlobalTime } from 'types/actions/globalTime';
export interface Props {
start: GlobalTime['minTime'];
end: GlobalTime['maxTime'];
}
export interface Exception {
exceptionType: string;
exceptionMessage: string;
exceptionCount: number;
lastSeen: string;
firstSeen: string;
serviceName: string;
}
export type PayloadProps = Exception[];

View File

@ -0,0 +1,22 @@
import { GlobalTime } from 'types/actions/globalTime';
export interface Props {
start: GlobalTime['minTime'];
end: GlobalTime['maxTime'];
serviceName: string;
errorType: string;
}
export interface PayloadProps {
errorId: string;
exceptionType: string;
excepionStacktrace: string;
exceptionEscaped: string;
exceptionMessage: string;
timestamp: string;
spanID: string;
traceID: string;
serviceName: Props['serviceName'];
newerErrorId: string;
olderErrorId: string;
}

View File

@ -0,0 +1,11 @@
import { GlobalTime } from 'types/actions/globalTime';
import { PayloadProps as Payload } from './getByErrorTypeAndService';
export type PayloadProps = Payload;
export type Props = {
start: GlobalTime['minTime'];
end: GlobalTime['minTime'];
errorId: string;
};