mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-27 14:22:01 +08:00
fix: colored logs in new logs explorer (#5749)
* fix: colored logs in new logs explorer * fix: handle escapes better * fix: handle escapes better * chore: add code comments * chore: added back text to copy to the body
This commit is contained in:
parent
706f967246
commit
22f2e68db2
@ -2,6 +2,7 @@
|
|||||||
import './LogDetails.styles.scss';
|
import './LogDetails.styles.scss';
|
||||||
|
|
||||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||||
import { RadioChangeEvent } from 'antd/lib';
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
@ -10,8 +11,13 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
import Overview from 'container/LogDetailedView/Overview';
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
import {
|
||||||
|
aggregateAttributesResourcesToString,
|
||||||
|
removeEscapeCharacters,
|
||||||
|
unescapeString,
|
||||||
|
} from 'container/LogDetailedView/utils';
|
||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
|
import dompurify from 'dompurify';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -28,11 +34,14 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import { VIEW_TYPES, VIEWS } from './constants';
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||||
|
|
||||||
|
const convert = new Convert();
|
||||||
|
|
||||||
function LogDetail({
|
function LogDetail({
|
||||||
log,
|
log,
|
||||||
onClose,
|
onClose,
|
||||||
@ -90,6 +99,17 @@ function LogDetail({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const htmlBody = useMemo(
|
||||||
|
() => ({
|
||||||
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(unescapeString(log?.body || ''), {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[log?.body],
|
||||||
|
);
|
||||||
|
|
||||||
const handleJSONCopy = (): void => {
|
const handleJSONCopy = (): void => {
|
||||||
copyToClipboard(LogJsonData);
|
copyToClipboard(LogJsonData);
|
||||||
notifications.success({
|
notifications.success({
|
||||||
@ -127,8 +147,8 @@ function LogDetail({
|
|||||||
>
|
>
|
||||||
<div className="log-detail-drawer__log">
|
<div className="log-detail-drawer__log">
|
||||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||||
<Tooltip title={log?.body} placement="left">
|
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||||
<Typography.Text className="log-body">{log?.body}</Typography.Text>
|
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div className="log-overflow-shadow"> </div>
|
<div className="log-overflow-shadow"> </div>
|
||||||
|
@ -6,6 +6,7 @@ import { Typography } from 'antd';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
@ -56,7 +57,7 @@ function LogGeneralField({
|
|||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(fieldValue, {
|
dompurify.sanitize(unescapeString(fieldValue), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
|
|||||||
import { DrawerProps } from 'antd';
|
import { DrawerProps } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
@ -145,7 +146,9 @@ function RawLogView({
|
|||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(text, { FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS] }),
|
dompurify.sanitize(unescapeString(text), {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
[text],
|
[text],
|
||||||
|
@ -4,6 +4,7 @@ import Convert from 'ansi-to-html';
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
@ -115,7 +116,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
<TableBodyContent
|
<TableBodyContent
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: convert.toHtml(
|
__html: convert.toHtml(
|
||||||
dompurify.sanitize(field, {
|
dompurify.sanitize(unescapeString(field), {
|
||||||
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -22,6 +22,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|||||||
|
|
||||||
import { ActionItemProps } from './ActionItem';
|
import { ActionItemProps } from './ActionItem';
|
||||||
import TableView from './TableView';
|
import TableView from './TableView';
|
||||||
|
import { removeEscapeCharacters } from './utils';
|
||||||
|
|
||||||
interface OverviewProps {
|
interface OverviewProps {
|
||||||
logData: ILog;
|
logData: ILog;
|
||||||
@ -124,7 +125,7 @@ function Overview({
|
|||||||
children: (
|
children: (
|
||||||
<div className="logs-body-content">
|
<div className="logs-body-content">
|
||||||
<MEditor
|
<MEditor
|
||||||
value={logData.body}
|
value={removeEscapeCharacters(logData.body)}
|
||||||
language="json"
|
language="json"
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(): void => {}}
|
onChange={(): void => {}}
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import './TableViewActions.styles.scss';
|
import './TableViewActions.styles.scss';
|
||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
|
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
|
||||||
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import dompurify from 'dompurify';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import { DataType } from '../TableView';
|
import { DataType } from '../TableView';
|
||||||
import {
|
import {
|
||||||
@ -19,6 +22,7 @@ import {
|
|||||||
jsonToDataNodes,
|
jsonToDataNodes,
|
||||||
recursiveParseJSON,
|
recursiveParseJSON,
|
||||||
removeEscapeCharacters,
|
removeEscapeCharacters,
|
||||||
|
unescapeString,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
interface ITableViewActionsProps {
|
interface ITableViewActionsProps {
|
||||||
@ -39,6 +43,8 @@ interface ITableViewActionsProps {
|
|||||||
) => () => void;
|
) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const convert = new Convert();
|
||||||
|
|
||||||
export function TableViewActions(
|
export function TableViewActions(
|
||||||
props: ITableViewActionsProps,
|
props: ITableViewActionsProps,
|
||||||
): React.ReactElement {
|
): React.ReactElement {
|
||||||
@ -71,11 +77,33 @@ export function TableViewActions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bodyHtml =
|
||||||
|
record.field === 'body'
|
||||||
|
? {
|
||||||
|
__html: convert.toHtml(
|
||||||
|
dompurify.sanitize(unescapeString(record.value), {
|
||||||
|
FORBID_TAGS: [...FORBID_DOM_PURIFY_TAGS],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: { __html: '' };
|
||||||
|
|
||||||
const fieldFilterKey = filterKeyForField(fieldData.field);
|
const fieldFilterKey = filterKeyForField(fieldData.field);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
<div className={cx('value-field', isOpen ? 'open-popover' : '')}>
|
||||||
|
{record.field === 'body' ? (
|
||||||
|
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: Color.BG_SIENNA_400,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
tabSize: 4,
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={bodyHtml}
|
||||||
|
/>
|
||||||
|
</CopyClipboardHOC>
|
||||||
|
) : (
|
||||||
<CopyClipboardHOC textToCopy={textToCopy}>
|
<CopyClipboardHOC textToCopy={textToCopy}>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@ -87,6 +115,7 @@ export function TableViewActions(
|
|||||||
{removeEscapeCharacters(fieldData.value)}
|
{removeEscapeCharacters(fieldData.value)}
|
||||||
</span>
|
</span>
|
||||||
</CopyClipboardHOC>
|
</CopyClipboardHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isListViewPanel && (
|
{!isListViewPanel && (
|
||||||
<span className="action-btn">
|
<span className="action-btn">
|
||||||
|
@ -250,19 +250,37 @@ export const getDataTypes = (value: unknown): DataTypes => {
|
|||||||
return determineType(value);
|
return determineType(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
|
||||||
|
// the log line readable
|
||||||
export const removeEscapeCharacters = (str: string): string =>
|
export const removeEscapeCharacters = (str: string): string =>
|
||||||
str.replace(/\\([ntfr'"\\])/g, (_: string, char: string) => {
|
str
|
||||||
const escapeMap: Record<string, string> = {
|
.replace(/\\x1[bB][[0-9;]*m/g, '')
|
||||||
n: '\n',
|
.replace(/\\u001[bB][[0-9;]*m/g, '')
|
||||||
t: '\t',
|
.replace(/\\x[0-9A-Fa-f]{2}/g, '')
|
||||||
f: '\f',
|
.replace(/\\u[0-9A-Fa-f]{4}/g, '')
|
||||||
r: '\r',
|
.replace(/\\[btnfrv0'"\\]/g, '');
|
||||||
"'": "'",
|
|
||||||
'"': '"',
|
// we need to remove the escape from the escaped characters as some recievers like file log escape the unicode escape characters.
|
||||||
'\\': '\\',
|
// example: Log [\u001B[32;1mThis is bright green\u001B[0m] is being sent as [\\u001B[32;1mThis is bright green\\u001B[0m]
|
||||||
};
|
//
|
||||||
return escapeMap[char as keyof typeof escapeMap];
|
// so we need to remove this escapes to render the color properly
|
||||||
});
|
export const unescapeString = (str: string): string =>
|
||||||
|
str
|
||||||
|
.replace(/\\n/g, '\n') // Replaces escaped newlines
|
||||||
|
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
|
||||||
|
.replace(/\\t/g, '\t') // Replaces escaped tabs
|
||||||
|
.replace(/\\b/g, '\b') // Replaces escaped backspaces
|
||||||
|
.replace(/\\f/g, '\f') // Replaces escaped form feeds
|
||||||
|
.replace(/\\v/g, '\v') // Replaces escaped vertical tabs
|
||||||
|
.replace(/\\'/g, "'") // Replaces escaped single quotes
|
||||||
|
.replace(/\\"/g, '"') // Replaces escaped double quotes
|
||||||
|
.replace(/\\\\/g, '\\') // Replaces escaped backslashes
|
||||||
|
.replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) =>
|
||||||
|
String.fromCharCode(parseInt(hex, 16)),
|
||||||
|
) // Replaces hexadecimal escape sequences
|
||||||
|
.replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) =>
|
||||||
|
String.fromCharCode(parseInt(hex, 16)),
|
||||||
|
); // Replaces Unicode escape sequences
|
||||||
|
|
||||||
export function removeExtraSpaces(input: string): string {
|
export function removeExtraSpaces(input: string): string {
|
||||||
return input.replace(/\s+/g, ' ').trim();
|
return input.replace(/\s+/g, ' ').trim();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user