Logs: Fixed prettify JSON behavior with unescaped content (#105390)
* LogRowMessage: unescape after prettifying * Add unit tests
This commit is contained in:
@@ -105,10 +105,7 @@ export const LogRow = ({
|
||||
);
|
||||
const levelStyles = useMemo(() => getLogLevelStyles(theme, row.logLevel), [row.logLevel, theme]);
|
||||
const processedRow = useMemo(
|
||||
() =>
|
||||
row.hasUnescapedContent && forceEscape
|
||||
? { ...row, entry: escapeUnescapedString(row.entry), raw: escapeUnescapedString(row.raw) }
|
||||
: row,
|
||||
() => (row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry) } : row),
|
||||
[forceEscape, row]
|
||||
);
|
||||
const errorMessage = checkLogsError(row);
|
||||
@@ -292,6 +289,7 @@ export const LogRow = ({
|
||||
mouseIsOver={mouseIsOver}
|
||||
onBlur={onMouseLeave}
|
||||
expanded={showDetails}
|
||||
forceEscape={forceEscape}
|
||||
logRowMenuIconsBefore={logRowMenuIconsBefore}
|
||||
logRowMenuIconsAfter={logRowMenuIconsAfter}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, getDefaultNormalizer } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
@@ -234,4 +234,53 @@ line3`;
|
||||
expect(screen.queryByText(/1 more/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Incorrectly escaped content in JSON logs', () => {
|
||||
let entry = '';
|
||||
beforeEach(() => {
|
||||
entry = `{"stacktrace":"stack line 1\\n\\tat stack line 2\\n\\tat stack line 3\\n\\tat stack line 4\\n\\t..."}`;
|
||||
});
|
||||
it('Displays unprettified JSON logs with incorrectly escaped content', async () => {
|
||||
setup({
|
||||
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000, hasUnescapedContent: true }),
|
||||
prettifyLogMessage: false,
|
||||
forceEscape: false,
|
||||
});
|
||||
expect(screen.getByText(entry)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays prettified JSON logs with unescaped content', async () => {
|
||||
setup({
|
||||
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000, hasUnescapedContent: true }),
|
||||
prettifyLogMessage: true,
|
||||
forceEscape: false,
|
||||
});
|
||||
expect(screen.queryByText(entry)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays prettified JSON logs with escaped content', async () => {
|
||||
setup({
|
||||
row: createLogRow({ entry, logLevel: LogLevel.error, timeEpochMs: 1546297200000, hasUnescapedContent: true }),
|
||||
prettifyLogMessage: true,
|
||||
forceEscape: true,
|
||||
});
|
||||
expect(screen.queryByText(entry)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/"stacktrace": "stack line 1/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/\tat stack line 2/, {
|
||||
normalizer: getDefaultNormalizer({ collapseWhitespace: false, trim: true }),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/\tat stack line 3/, {
|
||||
normalizer: getDefaultNormalizer({ collapseWhitespace: false, trim: true }),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/\tat stack line 4/, {
|
||||
normalizer: getDefaultNormalizer({ collapseWhitespace: false, trim: true }),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DataQuery } from '@grafana/schema';
|
||||
import { PopoverContent, useTheme2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { escapeUnescapedString } from '../utils';
|
||||
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogRowMenuCell } from './LogRowMenuCell';
|
||||
import { LogRowStyles } from './getLogRowStyles';
|
||||
@@ -36,6 +38,7 @@ interface Props {
|
||||
expanded?: boolean;
|
||||
logRowMenuIconsBefore?: ReactNode[];
|
||||
logRowMenuIconsAfter?: ReactNode[];
|
||||
forceEscape?: boolean;
|
||||
}
|
||||
|
||||
interface LogMessageProps {
|
||||
@@ -115,16 +118,22 @@ const getEllipsisStyles = (theme: GrafanaTheme2) => ({
|
||||
});
|
||||
|
||||
const restructureLog = (
|
||||
line: string,
|
||||
row: LogRowModel,
|
||||
prettifyLogMessage: boolean,
|
||||
wrapLogMessage: boolean,
|
||||
expanded: boolean
|
||||
expanded: boolean,
|
||||
forceEscape: boolean
|
||||
): string => {
|
||||
let line = row.raw;
|
||||
if (prettifyLogMessage) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(line), undefined, 2);
|
||||
line = JSON.stringify(JSON.parse(line), undefined, 2);
|
||||
return row.hasUnescapedContent && forceEscape ? escapeUnescapedString(line) : line;
|
||||
} catch (error) {}
|
||||
}
|
||||
if (row.hasUnescapedContent && forceEscape) {
|
||||
line = escapeUnescapedString(line);
|
||||
}
|
||||
// With wrapping disabled, we want to turn it into a single-line log entry unless the line is expanded
|
||||
if (!wrapLogMessage && !expanded) {
|
||||
line = line.replace(/(\r\n|\n|\r)/g, '');
|
||||
@@ -151,11 +160,12 @@ export const LogRowMessage = memo((props: Props) => {
|
||||
expanded,
|
||||
logRowMenuIconsBefore,
|
||||
logRowMenuIconsAfter,
|
||||
forceEscape,
|
||||
} = props;
|
||||
const { hasAnsi, raw } = row;
|
||||
const { hasAnsi } = row;
|
||||
const restructuredEntry = useMemo(
|
||||
() => restructureLog(raw, prettifyLogMessage, wrapLogMessage, Boolean(expanded)),
|
||||
[raw, prettifyLogMessage, wrapLogMessage, expanded]
|
||||
() => restructureLog(row, prettifyLogMessage, wrapLogMessage, Boolean(expanded), Boolean(forceEscape)),
|
||||
[expanded, forceEscape, prettifyLogMessage, row, wrapLogMessage]
|
||||
);
|
||||
const shouldShowMenu = mouseIsOver || pinned;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user