Explore/Logs: Escaping of incorrectly escaped log lines (#31352)

* POC: Escaping of incorrectly escaped log lines

* Remove unused import

* Fix test, change copy

* Make escapedNewlines optional

* Fix typechecks

* Remove loading state from the escaping button

* Update namings
pull/31656/head
Ivana Huckova 4 years ago committed by GitHub
parent 43d4a593ae
commit 4c2e5fcbd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      packages/grafana-data/src/types/logs.ts
  2. 2
      packages/grafana-data/src/utils/logs.test.ts
  3. 3
      packages/grafana-data/src/utils/logs.ts
  4. 1
      packages/grafana-ui/src/components/Logs/LogDetails.test.tsx
  5. 28
      packages/grafana-ui/src/components/Logs/LogRow.tsx
  6. 1
      packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx
  7. 1
      packages/grafana-ui/src/components/Logs/LogRows.test.tsx
  8. 8
      packages/grafana-ui/src/components/Logs/LogRows.tsx
  9. 4
      public/app/core/logs_model.ts
  10. 1
      public/app/features/explore/LiveLogs.test.tsx
  11. 45
      public/app/features/explore/Logs.tsx
  12. 2
      public/app/features/explore/MetaInfoText.tsx
  13. 3
      public/app/features/explore/utils/decorators.test.ts

@ -62,6 +62,7 @@ export interface LogRowModel {
// Actual log line // Actual log line
entry: string; entry: string;
hasAnsi: boolean; hasAnsi: boolean;
hasUnescapedContent: boolean;
labels: Labels; labels: Labels;
logLevel: LogLevel; logLevel: LogLevel;
raw: string; raw: string;

@ -295,6 +295,7 @@ describe('sortLogsResult', () => {
dataFrame: new MutableDataFrame(), dataFrame: new MutableDataFrame(),
entry: '', entry: '',
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
logLevel: LogLevel.info, logLevel: LogLevel.info,
raw: '', raw: '',
@ -312,6 +313,7 @@ describe('sortLogsResult', () => {
dataFrame: new MutableDataFrame(), dataFrame: new MutableDataFrame(),
entry: '', entry: '',
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
logLevel: LogLevel.info, logLevel: LogLevel.info,
raw: '', raw: '',

@ -223,3 +223,6 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM
hasError: false, hasError: false,
}; };
}; };
export const escapeUnescapedString = (string: string) =>
string.replace(/\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n'));

@ -20,6 +20,7 @@ const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowMode
timeLocal: '', timeLocal: '',
timeUtc: '', timeUtc: '',
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
entry: '', entry: '',
raw: '', raw: '',
uid: '0', uid: '0',

@ -9,6 +9,7 @@ import {
GrafanaTheme, GrafanaTheme,
dateTimeFormat, dateTimeFormat,
checkLogsError, checkLogsError,
escapeUnescapedString,
} from '@grafana/data'; } from '@grafana/data';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { Tooltip } from '../Tooltip/Tooltip'; import { Tooltip } from '../Tooltip/Tooltip';
@ -42,6 +43,8 @@ interface Props extends Themeable {
timeZone: TimeZone; timeZone: TimeZone;
allowDetails?: boolean; allowDetails?: boolean;
logsSortOrder?: LogsSortOrder | null; logsSortOrder?: LogsSortOrder | null;
forceEscape?: boolean;
showDetectedFields?: string[];
getRows: () => LogRowModel[]; getRows: () => LogRowModel[];
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
@ -49,7 +52,6 @@ interface Props extends Themeable {
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>; getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
showContextToggle?: (row?: LogRowModel) => boolean; showContextToggle?: (row?: LogRowModel) => boolean;
showDetectedFields?: string[];
onClickShowDetectedField?: (key: string) => void; onClickShowDetectedField?: (key: string) => void;
onClickHideDetectedField?: (key: string) => void; onClickHideDetectedField?: (key: string) => void;
} }
@ -139,6 +141,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
wrapLogMessage, wrapLogMessage,
theme, theme,
getFieldLinks, getFieldLinks,
forceEscape,
} = this.props; } = this.props;
const { showDetails, showContext } = this.state; const { showDetails, showContext } = this.state;
const style = getLogRowStyles(theme, row.logLevel); const style = getLogRowStyles(theme, row.logLevel);
@ -148,12 +151,15 @@ class UnThemedLogRow extends PureComponent<Props, State> {
[styles.errorLogRow]: hasError, [styles.errorLogRow]: hasError,
}); });
const processedRow =
row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry) } : row;
return ( return (
<> <>
<tr className={logRowBackground} onClick={this.toggleDetails}> <tr className={logRowBackground} onClick={this.toggleDetails}>
{showDuplicates && ( {showDuplicates && (
<td className={style.logsRowDuplicates}> <td className={style.logsRowDuplicates}>
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} {processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
</td> </td>
)} )}
<td className={cx({ [style.logsRowLevel]: !hasError })}> <td className={cx({ [style.logsRowLevel]: !hasError })}>
@ -169,14 +175,14 @@ class UnThemedLogRow extends PureComponent<Props, State> {
</td> </td>
)} )}
{showTime && <td className={style.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>} {showTime && <td className={style.logsRowLocalTime}>{this.renderTimeStamp(row.timeEpochMs)}</td>}
{showLabels && row.uniqueLabels && ( {showLabels && processedRow.uniqueLabels && (
<td className={style.logsRowLabels}> <td className={style.logsRowLabels}>
<LogLabels labels={row.uniqueLabels} /> <LogLabels labels={processedRow.uniqueLabels} />
</td> </td>
)} )}
{showDetectedFields && showDetectedFields.length > 0 ? ( {showDetectedFields && showDetectedFields.length > 0 ? (
<LogRowMessageDetectedFields <LogRowMessageDetectedFields
row={row} row={processedRow}
showDetectedFields={showDetectedFields!} showDetectedFields={showDetectedFields!}
getFieldLinks={getFieldLinks} getFieldLinks={getFieldLinks}
wrapLogMessage={wrapLogMessage} wrapLogMessage={wrapLogMessage}
@ -184,7 +190,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
) : ( ) : (
<LogRowMessage <LogRowMessage
highlighterExpressions={highlighterExpressions} highlighterExpressions={highlighterExpressions}
row={row} row={processedRow}
getRows={getRows} getRows={getRows}
errors={errors} errors={errors}
hasMoreContextRows={hasMoreContextRows} hasMoreContextRows={hasMoreContextRows}
@ -207,7 +213,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
onClickShowDetectedField={onClickShowDetectedField} onClickShowDetectedField={onClickShowDetectedField}
onClickHideDetectedField={onClickHideDetectedField} onClickHideDetectedField={onClickHideDetectedField}
getRows={getRows} getRows={getRows}
row={row} row={processedRow}
wrapLogMessage={wrapLogMessage} wrapLogMessage={wrapLogMessage}
hasError={hasError} hasError={hasError}
showDetectedFields={showDetectedFields} showDetectedFields={showDetectedFields}
@ -219,16 +225,12 @@ class UnThemedLogRow extends PureComponent<Props, State> {
render() { render() {
const { showContext } = this.state; const { showContext } = this.state;
const { logsSortOrder } = this.props; const { logsSortOrder, row, getRowContext } = this.props;
if (showContext) { if (showContext) {
return ( return (
<> <>
<LogRowContextProvider <LogRowContextProvider row={row} getRowContext={getRowContext} logsSortOrder={logsSortOrder}>
row={this.props.row}
getRowContext={this.props.getRowContext}
logsSortOrder={logsSortOrder}
>
{({ result, errors, hasMoreContextRows, updateLimit }) => { {({ result, errors, hasMoreContextRows, updateLimit }) => {
return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}</>; return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}</>;
}} }}

@ -181,6 +181,7 @@ const row: LogRowModel = {
entry: '4', entry: '4',
labels: (null as any) as Labels, labels: (null as any) as Labels,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
raw: '4', raw: '4',
logLevel: LogLevel.info, logLevel: LogLevel.info,
timeEpochMs: 4, timeEpochMs: 4,

@ -155,6 +155,7 @@ const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
logLevel: LogLevel.debug, logLevel: LogLevel.debug,
entry, entry,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
raw: entry, raw: entry,
timeFromNow: '', timeFromNow: '',

@ -17,7 +17,6 @@ export interface Props extends Themeable {
deduplicatedRows?: LogRowModel[]; deduplicatedRows?: LogRowModel[];
dedupStrategy: LogsDedupStrategy; dedupStrategy: LogsDedupStrategy;
highlighterExpressions?: string[]; highlighterExpressions?: string[];
showContextToggle?: (row?: LogRowModel) => boolean;
showLabels: boolean; showLabels: boolean;
showTime: boolean; showTime: boolean;
wrapLogMessage: boolean; wrapLogMessage: boolean;
@ -28,11 +27,13 @@ export interface Props extends Themeable {
// Passed to fix problems with inactive scrolling in Logs Panel // Passed to fix problems with inactive scrolling in Logs Panel
// Can be removed when we unify scrolling for Panel and Explore // Can be removed when we unify scrolling for Panel and Explore
disableCustomHorizontalScroll?: boolean; disableCustomHorizontalScroll?: boolean;
forceEscape?: boolean;
showDetectedFields?: string[];
showContextToggle?: (row?: LogRowModel) => boolean;
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
showDetectedFields?: string[];
onClickShowDetectedField?: (key: string) => void; onClickShowDetectedField?: (key: string) => void;
onClickHideDetectedField?: (key: string) => void; onClickHideDetectedField?: (key: string) => void;
} }
@ -101,6 +102,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
showDetectedFields, showDetectedFields,
onClickShowDetectedField, onClickShowDetectedField,
onClickHideDetectedField, onClickHideDetectedField,
forceEscape,
} = this.props; } = this.props;
const { renderAll } = this.state; const { renderAll } = this.state;
const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme); const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme);
@ -151,6 +153,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
onClickHideDetectedField={onClickHideDetectedField} onClickHideDetectedField={onClickHideDetectedField}
getFieldLinks={getFieldLinks} getFieldLinks={getFieldLinks}
logsSortOrder={logsSortOrder} logsSortOrder={logsSortOrder}
forceEscape={forceEscape}
/> />
))} ))}
{hasData && {hasData &&
@ -175,6 +178,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
onClickHideDetectedField={onClickHideDetectedField} onClickHideDetectedField={onClickHideDetectedField}
getFieldLinks={getFieldLinks} getFieldLinks={getFieldLinks}
logsSortOrder={logsSortOrder} logsSortOrder={logsSortOrder}
forceEscape={forceEscape}
/> />
))} ))}
{hasData && !renderAll && ( {hasData && !renderAll && (

@ -361,6 +361,9 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
const message: string = typeof messageValue === 'string' ? messageValue : JSON.stringify(messageValue); const message: string = typeof messageValue === 'string' ? messageValue : JSON.stringify(messageValue);
const hasAnsi = textUtil.hasAnsiCodes(message); const hasAnsi = textUtil.hasAnsiCodes(message);
const hasUnescapedContent = !!message.match(/\\n|\\t|\\r/);
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : []; const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
let logLevel = LogLevel.unknown; let logLevel = LogLevel.unknown;
@ -383,6 +386,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
timeUtc: dateTimeFormat(ts, { timeZone: 'utc' }), timeUtc: dateTimeFormat(ts, { timeZone: 'utc' }),
uniqueLabels, uniqueLabels,
hasAnsi, hasAnsi,
hasUnescapedContent,
searchWords, searchWords,
entry: hasAnsi ? ansicolor.strip(message) : message, entry: hasAnsi ? ansicolor.strip(message) : message,
raw: message, raw: message,

@ -93,6 +93,7 @@ const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
logLevel: LogLevel.debug, logLevel: LogLevel.debug,
entry, entry,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
raw: entry, raw: entry,
timeFromNow: '', timeFromNow: '',

@ -29,6 +29,8 @@ import {
InlineSwitch, InlineSwitch,
withTheme, withTheme,
stylesFactory, stylesFactory,
Icon,
Tooltip,
} from '@grafana/ui'; } from '@grafana/ui';
import store from 'app/core/store'; import store from 'app/core/store';
import { ExploreGraphPanel } from './ExploreGraphPanel'; import { ExploreGraphPanel } from './ExploreGraphPanel';
@ -89,6 +91,8 @@ interface State {
logsSortOrder: LogsSortOrder | null; logsSortOrder: LogsSortOrder | null;
isFlipping: boolean; isFlipping: boolean;
showDetectedFields: string[]; showDetectedFields: string[];
hasUnescapedContent: boolean;
forceEscape: boolean;
} }
export class UnthemedLogs extends PureComponent<Props, State> { export class UnthemedLogs extends PureComponent<Props, State> {
@ -102,6 +106,8 @@ export class UnthemedLogs extends PureComponent<Props, State> {
logsSortOrder: null, logsSortOrder: null,
isFlipping: false, isFlipping: false,
showDetectedFields: [], showDetectedFields: [],
hasUnescapedContent: this.props.logRows.some((r) => r.hasUnescapedContent),
forceEscape: false,
}; };
componentWillUnmount() { componentWillUnmount() {
@ -123,6 +129,12 @@ export class UnthemedLogs extends PureComponent<Props, State> {
this.cancelFlippingTimer = setTimeout(() => this.setState({ isFlipping: false }), 1000); this.cancelFlippingTimer = setTimeout(() => this.setState({ isFlipping: false }), 1000);
}; };
onEscapeNewlines = () => {
this.setState((prevState) => ({
forceEscape: !prevState.forceEscape,
}));
};
onChangeDedup = (dedup: LogsDedupStrategy) => { onChangeDedup = (dedup: LogsDedupStrategy) => {
const { onDedupStrategyChange } = this.props; const { onDedupStrategyChange } = this.props;
if (this.props.dedupStrategy === dedup) { if (this.props.dedupStrategy === dedup) {
@ -237,7 +249,16 @@ export class UnthemedLogs extends PureComponent<Props, State> {
theme, theme,
} = this.props; } = this.props;
const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping, showDetectedFields } = this.state; const {
showLabels,
showTime,
wrapLogMessage,
logsSortOrder,
isFlipping,
showDetectedFields,
hasUnescapedContent,
forceEscape,
} = this.state;
const hasData = logRows && logRows.length > 0; const hasData = logRows && logRows.length > 0;
const dedupCount = dedupedRows const dedupCount = dedupedRows
@ -346,6 +367,27 @@ export class UnthemedLogs extends PureComponent<Props, State> {
/> />
)} )}
{hasUnescapedContent && (
<MetaInfoText
metaItems={[
{
label: 'Your logs might have incorrectly escaped content',
value: (
<Tooltip
content="We suggest to try to fix the escaping of your log lines first. This is an experimental feature, your logs might not be correctly escaped."
placement="right"
>
<Button variant="secondary" size="sm" onClick={this.onEscapeNewlines}>
<span>{forceEscape ? 'Remove escaping' : 'Escape newlines'}&nbsp;</span>
<Icon name="exclamation-triangle" className="muted" size="sm" />
</Button>
</Tooltip>
),
},
]}
/>
)}
<LogRows <LogRows
logRows={logRows} logRows={logRows}
deduplicatedRows={dedupedRows} deduplicatedRows={dedupedRows}
@ -357,6 +399,7 @@ export class UnthemedLogs extends PureComponent<Props, State> {
showContextToggle={showContextToggle} showContextToggle={showContextToggle}
showLabels={showLabels} showLabels={showLabels}
showTime={showTime} showTime={showTime}
forceEscape={forceEscape}
wrapLogMessage={wrapLogMessage} wrapLogMessage={wrapLogMessage}
timeZone={timeZone} timeZone={timeZone}
getFieldLinks={getFieldLinks} getFieldLinks={getFieldLinks}

@ -32,7 +32,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
export interface MetaItemProps { export interface MetaItemProps {
label?: string; label?: string;
value: string; value: string | JSX.Element;
} }
export const MetaInfoItem = memo(function MetaInfoItem(props: MetaItemProps) { export const MetaInfoItem = memo(function MetaInfoItem(props: MetaItemProps) {

@ -295,6 +295,7 @@ describe('decorateWithLogsResult', () => {
entry: 'this is a message', entry: 'this is a message',
entryFieldIndex: 3, entryFieldIndex: 3,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
logLevel: 'unknown', logLevel: 'unknown',
raw: 'this is a message', raw: 'this is a message',
@ -313,6 +314,7 @@ describe('decorateWithLogsResult', () => {
entry: 'third', entry: 'third',
entryFieldIndex: 3, entryFieldIndex: 3,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
logLevel: 'unknown', logLevel: 'unknown',
raw: 'third', raw: 'third',
@ -331,6 +333,7 @@ describe('decorateWithLogsResult', () => {
entry: 'second message', entry: 'second message',
entryFieldIndex: 3, entryFieldIndex: 3,
hasAnsi: false, hasAnsi: false,
hasUnescapedContent: false,
labels: {}, labels: {},
logLevel: 'unknown', logLevel: 'unknown',
raw: 'second message', raw: 'second message',

Loading…
Cancel
Save