From 4c2e5fcbd096e436fac05a2d798afce0855b3a94 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 3 Mar 2021 18:32:27 +0100 Subject: [PATCH] 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 --- packages/grafana-data/src/types/logs.ts | 1 + packages/grafana-data/src/utils/logs.test.ts | 2 + packages/grafana-data/src/utils/logs.ts | 3 ++ .../src/components/Logs/LogDetails.test.tsx | 1 + .../grafana-ui/src/components/Logs/LogRow.tsx | 28 ++++++------ .../Logs/LogRowContextProvider.test.tsx | 1 + .../src/components/Logs/LogRows.test.tsx | 1 + .../src/components/Logs/LogRows.tsx | 8 +++- public/app/core/logs_model.ts | 4 ++ public/app/features/explore/LiveLogs.test.tsx | 1 + public/app/features/explore/Logs.tsx | 45 ++++++++++++++++++- public/app/features/explore/MetaInfoText.tsx | 2 +- .../features/explore/utils/decorators.test.ts | 3 ++ 13 files changed, 83 insertions(+), 17 deletions(-) diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 737cba83ce9..ac141711073 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -62,6 +62,7 @@ export interface LogRowModel { // Actual log line entry: string; hasAnsi: boolean; + hasUnescapedContent: boolean; labels: Labels; logLevel: LogLevel; raw: string; diff --git a/packages/grafana-data/src/utils/logs.test.ts b/packages/grafana-data/src/utils/logs.test.ts index d6569a3ca8e..2a0b336f30d 100644 --- a/packages/grafana-data/src/utils/logs.test.ts +++ b/packages/grafana-data/src/utils/logs.test.ts @@ -295,6 +295,7 @@ describe('sortLogsResult', () => { dataFrame: new MutableDataFrame(), entry: '', hasAnsi: false, + hasUnescapedContent: false, labels: {}, logLevel: LogLevel.info, raw: '', @@ -312,6 +313,7 @@ describe('sortLogsResult', () => { dataFrame: new MutableDataFrame(), entry: '', hasAnsi: false, + hasUnescapedContent: false, labels: {}, logLevel: LogLevel.info, raw: '', diff --git a/packages/grafana-data/src/utils/logs.ts b/packages/grafana-data/src/utils/logs.ts index a8590e9e567..fb3ed84706d 100644 --- a/packages/grafana-data/src/utils/logs.ts +++ b/packages/grafana-data/src/utils/logs.ts @@ -223,3 +223,6 @@ export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorM hasError: false, }; }; + +export const escapeUnescapedString = (string: string) => + string.replace(/\\n|\\t|\\r/g, (match: string) => (match.slice(1) === 't' ? '\t' : '\n')); diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx index 3fbb6505edf..d40fe942261 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.test.tsx @@ -20,6 +20,7 @@ const setup = (propOverrides?: Partial, rowOverrides?: Partial LogRowModel[]; onClickFilterLabel?: (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; getFieldLinks?: (field: Field, rowIndex: number) => Array>; showContextToggle?: (row?: LogRowModel) => boolean; - showDetectedFields?: string[]; onClickShowDetectedField?: (key: string) => void; onClickHideDetectedField?: (key: string) => void; } @@ -139,6 +141,7 @@ class UnThemedLogRow extends PureComponent { wrapLogMessage, theme, getFieldLinks, + forceEscape, } = this.props; const { showDetails, showContext } = this.state; const style = getLogRowStyles(theme, row.logLevel); @@ -148,12 +151,15 @@ class UnThemedLogRow extends PureComponent { [styles.errorLogRow]: hasError, }); + const processedRow = + row.hasUnescapedContent && forceEscape ? { ...row, entry: escapeUnescapedString(row.entry) } : row; + return ( <> {showDuplicates && ( - {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} + {processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null} )} @@ -169,14 +175,14 @@ class UnThemedLogRow extends PureComponent { )} {showTime && {this.renderTimeStamp(row.timeEpochMs)}} - {showLabels && row.uniqueLabels && ( + {showLabels && processedRow.uniqueLabels && ( - + )} {showDetectedFields && showDetectedFields.length > 0 ? ( { ) : ( { onClickShowDetectedField={onClickShowDetectedField} onClickHideDetectedField={onClickHideDetectedField} getRows={getRows} - row={row} + row={processedRow} wrapLogMessage={wrapLogMessage} hasError={hasError} showDetectedFields={showDetectedFields} @@ -219,16 +225,12 @@ class UnThemedLogRow extends PureComponent { render() { const { showContext } = this.state; - const { logsSortOrder } = this.props; + const { logsSortOrder, row, getRowContext } = this.props; if (showContext) { return ( <> - + {({ result, errors, hasMoreContextRows, updateLimit }) => { return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}; }} diff --git a/packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx b/packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx index df1f37dd78d..a1f912d45db 100644 --- a/packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRowContextProvider.test.tsx @@ -181,6 +181,7 @@ const row: LogRowModel = { entry: '4', labels: (null as any) as Labels, hasAnsi: false, + hasUnescapedContent: false, raw: '4', logLevel: LogLevel.info, timeEpochMs: 4, diff --git a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx index 9bf507c9d23..95301d20c0d 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx @@ -155,6 +155,7 @@ const makeLog = (overrides: Partial): LogRowModel => { logLevel: LogLevel.debug, entry, hasAnsi: false, + hasUnescapedContent: false, labels: {}, raw: entry, timeFromNow: '', diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index eba3ff43476..5c63cf0dd37 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -17,7 +17,6 @@ export interface Props extends Themeable { deduplicatedRows?: LogRowModel[]; dedupStrategy: LogsDedupStrategy; highlighterExpressions?: string[]; - showContextToggle?: (row?: LogRowModel) => boolean; showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; @@ -28,11 +27,13 @@ export interface Props extends Themeable { // Passed to fix problems with inactive scrolling in Logs Panel // Can be removed when we unify scrolling for Panel and Explore disableCustomHorizontalScroll?: boolean; + forceEscape?: boolean; + showDetectedFields?: string[]; + showContextToggle?: (row?: LogRowModel) => boolean; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks?: (field: Field, rowIndex: number) => Array>; - showDetectedFields?: string[]; onClickShowDetectedField?: (key: string) => void; onClickHideDetectedField?: (key: string) => void; } @@ -101,6 +102,7 @@ class UnThemedLogRows extends PureComponent { showDetectedFields, onClickShowDetectedField, onClickHideDetectedField, + forceEscape, } = this.props; const { renderAll } = this.state; const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme); @@ -151,6 +153,7 @@ class UnThemedLogRows extends PureComponent { onClickHideDetectedField={onClickHideDetectedField} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} + forceEscape={forceEscape} /> ))} {hasData && @@ -175,6 +178,7 @@ class UnThemedLogRows extends PureComponent { onClickHideDetectedField={onClickHideDetectedField} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} + forceEscape={forceEscape} /> ))} {hasData && !renderAll && ( diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 7c613d4f309..3df00d2cdf1 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -361,6 +361,9 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi const message: string = typeof messageValue === 'string' ? messageValue : JSON.stringify(messageValue); const hasAnsi = textUtil.hasAnsiCodes(message); + + const hasUnescapedContent = !!message.match(/\\n|\\t|\\r/); + const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : []; let logLevel = LogLevel.unknown; @@ -383,6 +386,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi timeUtc: dateTimeFormat(ts, { timeZone: 'utc' }), uniqueLabels, hasAnsi, + hasUnescapedContent, searchWords, entry: hasAnsi ? ansicolor.strip(message) : message, raw: message, diff --git a/public/app/features/explore/LiveLogs.test.tsx b/public/app/features/explore/LiveLogs.test.tsx index d082b42fe25..7690d59f54b 100644 --- a/public/app/features/explore/LiveLogs.test.tsx +++ b/public/app/features/explore/LiveLogs.test.tsx @@ -93,6 +93,7 @@ const makeLog = (overrides: Partial): LogRowModel => { logLevel: LogLevel.debug, entry, hasAnsi: false, + hasUnescapedContent: false, labels: {}, raw: entry, timeFromNow: '', diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index b0ed8dd00a0..e8b8d0cf1df 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -29,6 +29,8 @@ import { InlineSwitch, withTheme, stylesFactory, + Icon, + Tooltip, } from '@grafana/ui'; import store from 'app/core/store'; import { ExploreGraphPanel } from './ExploreGraphPanel'; @@ -89,6 +91,8 @@ interface State { logsSortOrder: LogsSortOrder | null; isFlipping: boolean; showDetectedFields: string[]; + hasUnescapedContent: boolean; + forceEscape: boolean; } export class UnthemedLogs extends PureComponent { @@ -102,6 +106,8 @@ export class UnthemedLogs extends PureComponent { logsSortOrder: null, isFlipping: false, showDetectedFields: [], + hasUnescapedContent: this.props.logRows.some((r) => r.hasUnescapedContent), + forceEscape: false, }; componentWillUnmount() { @@ -123,6 +129,12 @@ export class UnthemedLogs extends PureComponent { this.cancelFlippingTimer = setTimeout(() => this.setState({ isFlipping: false }), 1000); }; + onEscapeNewlines = () => { + this.setState((prevState) => ({ + forceEscape: !prevState.forceEscape, + })); + }; + onChangeDedup = (dedup: LogsDedupStrategy) => { const { onDedupStrategyChange } = this.props; if (this.props.dedupStrategy === dedup) { @@ -237,7 +249,16 @@ export class UnthemedLogs extends PureComponent { theme, } = 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 dedupCount = dedupedRows @@ -346,6 +367,27 @@ export class UnthemedLogs extends PureComponent { /> )} + {hasUnescapedContent && ( + + + + ), + }, + ]} + /> + )} + { showContextToggle={showContextToggle} showLabels={showLabels} showTime={showTime} + forceEscape={forceEscape} wrapLogMessage={wrapLogMessage} timeZone={timeZone} getFieldLinks={getFieldLinks} diff --git a/public/app/features/explore/MetaInfoText.tsx b/public/app/features/explore/MetaInfoText.tsx index 3dad0ac9645..9d3fff1964a 100644 --- a/public/app/features/explore/MetaInfoText.tsx +++ b/public/app/features/explore/MetaInfoText.tsx @@ -32,7 +32,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({ export interface MetaItemProps { label?: string; - value: string; + value: string | JSX.Element; } export const MetaInfoItem = memo(function MetaInfoItem(props: MetaItemProps) { diff --git a/public/app/features/explore/utils/decorators.test.ts b/public/app/features/explore/utils/decorators.test.ts index 93469e33175..088faa979da 100644 --- a/public/app/features/explore/utils/decorators.test.ts +++ b/public/app/features/explore/utils/decorators.test.ts @@ -295,6 +295,7 @@ describe('decorateWithLogsResult', () => { entry: 'this is a message', entryFieldIndex: 3, hasAnsi: false, + hasUnescapedContent: false, labels: {}, logLevel: 'unknown', raw: 'this is a message', @@ -313,6 +314,7 @@ describe('decorateWithLogsResult', () => { entry: 'third', entryFieldIndex: 3, hasAnsi: false, + hasUnescapedContent: false, labels: {}, logLevel: 'unknown', raw: 'third', @@ -331,6 +333,7 @@ describe('decorateWithLogsResult', () => { entry: 'second message', entryFieldIndex: 3, hasAnsi: false, + hasUnescapedContent: false, labels: {}, logLevel: 'unknown', raw: 'second message',