diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 6cc46795fe6..7a573be38d1 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -40,6 +40,7 @@ interface Props extends Themeable { showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; + prettifyLogMessage: boolean; timeZone: TimeZone; enableLogDetails: boolean; logsSortOrder?: LogsSortOrder | null; @@ -139,6 +140,7 @@ class UnThemedLogRow extends PureComponent { showTime, showDetectedFields, wrapLogMessage, + prettifyLogMessage, theme, getFieldLinks, forceEscape, @@ -201,6 +203,7 @@ class UnThemedLogRow extends PureComponent { contextIsOpen={showContext} showContextToggle={showContextToggle} wrapLogMessage={wrapLogMessage} + prettifyLogMessage={prettifyLogMessage} onToggleContext={this.toggleContext} /> )} diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx index 4bd3cf868e0..c190de58ce7 100644 --- a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx @@ -3,6 +3,7 @@ import { isEqual } from 'lodash'; import tinycolor from 'tinycolor2'; import { css, cx } from '@emotion/css'; import { LogRowModel, findHighlightChunksInText, GrafanaTheme } from '@grafana/data'; +import memoizeOne from 'memoize-one'; // @ts-ignore import Highlighter from 'react-highlight-words'; @@ -23,6 +24,7 @@ interface Props extends Themeable { hasMoreContextRows?: HasMoreContextRows; contextIsOpen: boolean; wrapLogMessage: boolean; + prettifyLogMessage: boolean; errors?: LogRowContextQueryErrors; context?: LogRowContextRows; showContextToggle?: (row?: LogRowModel) => boolean; @@ -47,11 +49,46 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { `, horizontalScroll: css` label: verticalScroll; - white-space: nowrap; + white-space: pre; `, }; }); +function renderLogMessage( + hasAnsi: boolean, + entry: string, + highlights: string[] | undefined, + highlightClassName: string +) { + const needsHighlighter = + highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0 && entry.length < MAX_CHARACTERS; + if (needsHighlighter) { + return ( + + ); + } else if (hasAnsi) { + return ; + } else { + return entry; + } +} + +const restructureLog = memoizeOne((line: string, prettifyLogMessage: boolean): string => { + if (prettifyLogMessage) { + try { + return JSON.stringify(JSON.parse(line), undefined, 2); + } catch (error) { + return line; + } + } + return line; +}); + class UnThemedLogRowMessage extends PureComponent { onContextToggle = (e: React.SyntheticEvent) => { e.stopPropagation(); @@ -70,16 +107,16 @@ class UnThemedLogRowMessage extends PureComponent { contextIsOpen, showContextToggle, wrapLogMessage, + prettifyLogMessage, onToggleContext, } = this.props; const style = getLogRowStyles(theme, row.logLevel); - const { entry, hasAnsi, raw } = row; + const { hasAnsi, raw } = row; + const restructuredEntry = restructureLog(raw, prettifyLogMessage); const previewHighlights = highlighterExpressions?.length && !isEqual(highlighterExpressions, row.searchWords); const highlights = previewHighlights ? highlighterExpressions : row.searchWords; - const needsHighlighter = - highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0 && entry.length < MAX_CHARACTERS; const highlightClassName = previewHighlights ? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview]) : cx([style.logsRowMatchHighLight]); @@ -103,18 +140,7 @@ class UnThemedLogRowMessage extends PureComponent { /> )} - {needsHighlighter ? ( - - ) : hasAnsi ? ( - - ) : ( - entry - )} + {renderLogMessage(hasAnsi, restructuredEntry, highlights, highlightClassName)} {showContextToggle?.(row) && ( diff --git a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx index e473f057a67..29ae5d030dc 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.test.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.test.tsx @@ -16,6 +16,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} enableLogDetails={true} /> @@ -38,6 +39,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} previewLimit={1} enableLogDetails={true} @@ -69,6 +71,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} enableLogDetails={true} /> @@ -90,6 +93,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} enableLogDetails={true} /> @@ -112,6 +116,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} logsSortOrder={LogsSortOrder.Ascending} enableLogDetails={true} @@ -136,6 +141,7 @@ describe('LogRows', () => { showLabels={false} showTime={false} wrapLogMessage={true} + prettifyLogMessage={true} timeZone={'utc'} logsSortOrder={LogsSortOrder.Descending} enableLogDetails={true} diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index 2d2946769af..c17f1c296f9 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -20,6 +20,7 @@ export interface Props extends Themeable { showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; + prettifyLogMessage: boolean; timeZone: TimeZone; enableLogDetails: boolean; logsSortOrder?: LogsSortOrder | null; @@ -84,6 +85,7 @@ class UnThemedLogRows extends PureComponent { showLabels, showTime, wrapLogMessage, + prettifyLogMessage, logRows, deduplicatedRows, highlighterExpressions, @@ -135,6 +137,7 @@ class UnThemedLogRows extends PureComponent { showTime={showTime} showDetectedFields={showDetectedFields} wrapLogMessage={wrapLogMessage} + prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} enableLogDetails={enableLogDetails} onClickFilterLabel={onClickFilterLabel} @@ -160,6 +163,7 @@ class UnThemedLogRows extends PureComponent { showTime={showTime} showDetectedFields={showDetectedFields} wrapLogMessage={wrapLogMessage} + prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} enableLogDetails={enableLogDetails} onClickFilterLabel={onClickFilterLabel} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 85c867f1c35..c7348f5ab99 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -42,6 +42,7 @@ const SETTINGS_KEYS = { showLabels: 'grafana.explore.logs.showLabels', showTime: 'grafana.explore.logs.showTime', wrapLogMessage: 'grafana.explore.logs.wrapLogMessage', + prettifyLogMessage: 'grafana.explore.logs.prettifyLogMessage', }; interface Props { @@ -74,6 +75,7 @@ interface State { showLabels: boolean; showTime: boolean; wrapLogMessage: boolean; + prettifyLogMessage: boolean; dedupStrategy: LogsDedupStrategy; hiddenLogLevels: LogLevel[]; logsSortOrder: LogsSortOrder | null; @@ -91,6 +93,7 @@ export class UnthemedLogs extends PureComponent { showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), showTime: store.getBool(SETTINGS_KEYS.showTime, true), wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), + prettifyLogMessage: store.getBool(SETTINGS_KEYS.prettifyLogMessage, false), dedupStrategy: LogsDedupStrategy.none, hiddenLogLevels: [], logsSortOrder: null, @@ -166,6 +169,17 @@ export class UnthemedLogs extends PureComponent { } }; + onChangePrettifyLogMessage = (event?: React.SyntheticEvent) => { + const target = event && (event.target as HTMLInputElement); + if (target) { + const prettifyLogMessage = target.checked; + this.setState({ + prettifyLogMessage, + }); + store.set(SETTINGS_KEYS.prettifyLogMessage, prettifyLogMessage); + } + }; + onToggleLogLevel = (hiddenRawLevels: string[]) => { const hiddenLogLevels = hiddenRawLevels.map((level) => LogLevel[level as LogLevel]); this.setState({ hiddenLogLevels }); @@ -260,6 +274,7 @@ export class UnthemedLogs extends PureComponent { showLabels, showTime, wrapLogMessage, + prettifyLogMessage, dedupStrategy, hiddenLogLevels, logsSortOrder, @@ -305,6 +320,9 @@ export class UnthemedLogs extends PureComponent { + + + ({ @@ -356,6 +374,7 @@ export class UnthemedLogs extends PureComponent { enableLogDetails={true} forceEscape={forceEscape} wrapLogMessage={wrapLogMessage} + prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index a55c4401fad..1cfb4db2fda 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -12,7 +12,16 @@ interface LogsPanelProps extends PanelProps {} export const LogsPanel: React.FunctionComponent = ({ data, timeZone, - options: { showLabels, showTime, wrapLogMessage, showCommonLabels, sortOrder, dedupStrategy, enableLogDetails }, + options: { + showLabels, + showTime, + wrapLogMessage, + showCommonLabels, + prettifyLogMessage, + sortOrder, + dedupStrategy, + enableLogDetails, + }, title, }) => { const style = useStyles2(getStyles(title)); @@ -57,6 +66,7 @@ export const LogsPanel: React.FunctionComponent = ({ showLabels={showLabels} showTime={showTime} wrapLogMessage={wrapLogMessage} + prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} getFieldLinks={getFieldLinks} logsSortOrder={sortOrder} diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index 98fa6233e98..158ac5ffcf7 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -28,6 +28,12 @@ export const plugin = new PanelPlugin(LogsPanel).setPanelOptions((build description: '', defaultValue: false, }) + .addBooleanSwitch({ + path: 'prettifyLogMessage', + name: 'Prettify JSON', + description: '', + defaultValue: false, + }) .addBooleanSwitch({ path: 'enableLogDetails', name: 'Enable log details', diff --git a/public/app/plugins/panel/logs/types.ts b/public/app/plugins/panel/logs/types.ts index c117f9ba6d8..20756be50a1 100644 --- a/public/app/plugins/panel/logs/types.ts +++ b/public/app/plugins/panel/logs/types.ts @@ -5,6 +5,7 @@ export interface Options { showCommonLabels: boolean; showTime: boolean; wrapLogMessage: boolean; + prettifyLogMessage: boolean; enableLogDetails: boolean; sortOrder: LogsSortOrder; dedupStrategy: LogsDedupStrategy;