Logs: Fixed prettify JSON behavior with unescaped content (#105390)

* LogRowMessage: unescape after prettifying

* Add unit tests
This commit is contained in:
Matias Chomicki
2025-05-14 15:06:19 +02:00
committed by GitHub
parent d614d4f7d5
commit 815d17bd5c
3 changed files with 68 additions and 11 deletions
@@ -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;