mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-28 06:22:01 +08:00
feat: trace to logs and logs to trace is added (#2699)
* feat: trace to logs and logs to trace is added * chore: icons and spanId is updated * chore: feedback changes are updated --------- Co-authored-by: Ankit Nayan <ankit@signoz.io>
This commit is contained in:
parent
5637188e72
commit
a0c320e47e
@ -1,13 +1,14 @@
|
|||||||
import { Button, Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||||
import React, { memo, useCallback, useMemo } from 'react';
|
import React, { memo, useCallback, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
|
import { ButtonContainer } from './styles';
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
@ -16,7 +17,6 @@ function AddToQueryHOC({
|
|||||||
const {
|
const {
|
||||||
searchFilter: { queryString },
|
searchFilter: { queryString },
|
||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
|
||||||
|
|
||||||
const generatedQuery = useMemo(
|
const generatedQuery = useMemo(
|
||||||
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
||||||
@ -31,24 +31,20 @@ function AddToQueryHOC({
|
|||||||
} else {
|
} else {
|
||||||
updatedQueryString += ` AND ${generatedQuery}`;
|
updatedQueryString += ` AND ${generatedQuery}`;
|
||||||
}
|
}
|
||||||
dispatch({
|
|
||||||
type: SET_SEARCH_QUERY_STRING,
|
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||||
payload: {
|
}, [generatedQuery, queryString]);
|
||||||
searchQueryString: updatedQueryString,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [dispatch, generatedQuery, queryString]);
|
|
||||||
|
|
||||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||||
fieldKey,
|
fieldKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button size="small" type="text" onClick={handleQueryAdd}>
|
<ButtonContainer size="small" type="text" onClick={handleQueryAdd}>
|
||||||
<Popover placement="top" content={popOverContent}>
|
<Popover placement="top" content={popOverContent}>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
</Button>
|
</ButtonContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
frontend/src/components/Logs/styles.ts
Normal file
8
frontend/src/components/Logs/styles.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const ButtonContainer = styled(Button)`
|
||||||
|
&&& {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
`;
|
@ -1,14 +1,22 @@
|
|||||||
import { blue, orange } from '@ant-design/colors';
|
import { blue, orange } from '@ant-design/colors';
|
||||||
import { Input } from 'antd';
|
import { LinkOutlined } from '@ant-design/icons';
|
||||||
|
import { Input, Space, Tooltip } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
|
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
|
||||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import flatten from 'flat';
|
import flatten from 'flat';
|
||||||
|
import history from 'lib/history';
|
||||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { generatePath } from 'react-router-dom';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
import ActionItem from './ActionItem';
|
import ActionItem from './ActionItem';
|
||||||
@ -23,6 +31,8 @@ interface TableViewProps {
|
|||||||
function TableView({ logData }: TableViewProps): JSX.Element | null {
|
function TableView({ logData }: TableViewProps): JSX.Element | null {
|
||||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||||
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const flattenLogData: Record<string, never> | null = useMemo(
|
const flattenLogData: Record<string, never> | null = useMemo(
|
||||||
() => (logData ? flatten(logData) : null),
|
() => (logData ? flatten(logData) : null),
|
||||||
[logData],
|
[logData],
|
||||||
@ -41,6 +51,29 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
value: JSON.stringify(flattenLogData[key]),
|
value: JSON.stringify(flattenLogData[key]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const onTraceHandler = (record: DataType) => (): void => {
|
||||||
|
if (flattenLogData === null) return;
|
||||||
|
|
||||||
|
const traceId = flattenLogData[record.field];
|
||||||
|
|
||||||
|
const spanId = flattenLogData?.span_id;
|
||||||
|
|
||||||
|
if (traceId) {
|
||||||
|
dispatch({
|
||||||
|
type: SET_DETAILED_LOG_DATA,
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const basePath = generatePath(ROUTES.TRACE_DETAIL, {
|
||||||
|
id: traceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = spanId ? `${basePath}?spanId=${spanId}` : basePath;
|
||||||
|
|
||||||
|
history.push(route);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -62,11 +95,38 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
dataIndex: 'field',
|
dataIndex: 'field',
|
||||||
key: 'field',
|
key: 'field',
|
||||||
width: 30,
|
width: 30,
|
||||||
|
align: 'left',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (field: string): JSX.Element => {
|
render: (field: string, record): JSX.Element => {
|
||||||
const fieldKey = field.split('.').slice(-1);
|
const fieldKey = field.split('.').slice(-1);
|
||||||
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
|
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
|
||||||
|
|
||||||
|
if (record.field === 'trace_id') {
|
||||||
|
const traceId = flattenLogData[record.field];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space size="middle">
|
||||||
|
{renderedField}
|
||||||
|
|
||||||
|
{traceId && (
|
||||||
|
<Tooltip title="Inspect in Trace">
|
||||||
|
<div
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
role="presentation"
|
||||||
|
onClick={onTraceHandler(record)}
|
||||||
|
>
|
||||||
|
<LinkOutlined
|
||||||
|
style={{
|
||||||
|
width: '15px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
return (
|
return (
|
||||||
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
|
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
|
||||||
|
@ -3,7 +3,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { parseQuery } from 'lib/logql';
|
import { parseQuery } from 'lib/logql';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -28,7 +28,7 @@ export function useSearchParser(): {
|
|||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
const parsedFilters = useMemo(() => urlQuery.get('q'), [urlQuery]);
|
const parsedFilters = urlQuery.get('q');
|
||||||
|
|
||||||
const { minTime, maxTime, selectedTime } = useSelector<
|
const { minTime, maxTime, selectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -62,16 +62,12 @@ export function useSearchParser(): {
|
|||||||
},
|
},
|
||||||
// need to hide this warning as we don't want to update the query string on every change
|
// need to hide this warning as we don't want to update the query string on every change
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[dispatch, parsedQuery, selectedTime],
|
[dispatch, parsedQuery, selectedTime, queryString],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!queryString && parsedFilters) {
|
updateQueryString(parsedFilters || '');
|
||||||
updateQueryString(parsedFilters);
|
}, [parsedFilters, updateQueryString]);
|
||||||
} else if (queryString) {
|
|
||||||
updateQueryString(queryString);
|
|
||||||
}
|
|
||||||
}, [queryString, updateQueryString, parsedFilters]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queryString,
|
queryString,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Modal, Tabs, Tooltip, Typography } from 'antd';
|
import { Button, Modal, Tabs, Tooltip, Typography } from 'antd';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { StyledSpace } from 'components/Styled';
|
import { StyledSpace } from 'components/Styled';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import history from 'lib/history';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||||
|
|
||||||
import Events from './Events';
|
import Events from './Events';
|
||||||
@ -18,6 +21,8 @@ import Tags from './Tags';
|
|||||||
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||||
const { tree, firstSpanStartTime } = props;
|
const { tree, firstSpanStartTime } = props;
|
||||||
|
|
||||||
|
const { id: traceId } = useParams<Params>();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
|
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
|
||||||
@ -69,6 +74,12 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const onLogsHandler = (): void => {
|
||||||
|
const query = encodeURIComponent(`trace_id IN ('${traceId}')`);
|
||||||
|
|
||||||
|
history.push(`${ROUTES.LOGS}?q=${query}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer>
|
<CardContainer>
|
||||||
<StyledSpace
|
<StyledSpace
|
||||||
@ -78,6 +89,7 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
|||||||
<Typography.Text strong> Details for selected Span </Typography.Text>
|
<Typography.Text strong> Details for selected Span </Typography.Text>
|
||||||
|
|
||||||
<CustomTitle>Service</CustomTitle>
|
<CustomTitle>Service</CustomTitle>
|
||||||
|
|
||||||
<Tooltip overlay={OverLayComponentServiceName}>
|
<Tooltip overlay={OverLayComponentServiceName}>
|
||||||
<CustomText ellipsis>{tree.serviceName}</CustomText>
|
<CustomText ellipsis>{tree.serviceName}</CustomText>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -86,6 +98,8 @@ function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
|||||||
<Tooltip overlay={OverLayComponentName}>
|
<Tooltip overlay={OverLayComponentName}>
|
||||||
<CustomText ellipsis>{tree.name}</CustomText>
|
<CustomText ellipsis>{tree.name}</CustomText>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Button onClick={onLogsHandler}>Go to Related logs</Button>
|
||||||
</StyledSpace>
|
</StyledSpace>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@ -125,4 +139,8 @@ export interface ModalText {
|
|||||||
subText: string;
|
subText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default SelectedSpanDetails;
|
export default SelectedSpanDetails;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user