diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.tsx index 9d77a159a5d..da4a6b93747 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.tsx @@ -18,16 +18,10 @@ import { getLogRowStyles } from './getLogRowStyles'; import { stylesFactory } from '../../themes/stylesFactory'; import { selectThemeVariant } from '../../themes/selectThemeVariant'; +import { parseMessage, FieldDef } from './logParser'; + //Components import { LogDetailsRow } from './LogDetailsRow'; -import { MAX_CHARACTERS } from './LogRowMessage'; - -type FieldDef = { - key: string; - value: string; - links?: Array>; - fieldIndex?: number; -}; export interface Props extends Themeable { row: LogRowModel; @@ -39,6 +33,9 @@ export interface Props extends Themeable { onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; getFieldLinks?: (field: Field, rowIndex: number) => Array>; + showParsedFields?: string[]; + onClickShowParsedField?: (key: string) => void; + onClickHideParsedField?: (key: string) => void; } const getStyles = stylesFactory((theme: GrafanaTheme) => { @@ -64,25 +61,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { class UnThemedLogDetails extends PureComponent { getParser = memoizeOne(getParser); - parseMessage = memoizeOne((rowEntry): FieldDef[] => { - if (rowEntry.length > MAX_CHARACTERS) { - return []; - } - const parser = this.getParser(rowEntry); - if (!parser) { - return []; - } - // Use parser to highlight detected fields - const parsedFields = parser.getFields(rowEntry); - const fields = parsedFields.map(field => { - const key = parser.getLabelFromField(field); - const value = parser.getValueFromField(field); - return { key, value }; - }); - - return fields; - }); - getDerivedFields = memoizeOne((row: LogRowModel): FieldDef[] => { return ( row.dataFrame.fields @@ -117,7 +95,7 @@ class UnThemedLogDetails extends PureComponent { * setup in data source config. */ getAllFields = memoizeOne((row: LogRowModel) => { - const fields = this.parseMessage(row.entry); + const fields = parseMessage(row.entry); const derivedFields = this.getDerivedFields(row); const fieldsMap = [...derivedFields, ...fields].reduce((acc, field) => { // Strip enclosing quotes for hashing. When values are parsed from log line the quotes are kept, but if same @@ -154,6 +132,9 @@ class UnThemedLogDetails extends PureComponent { className, onMouseEnter, onMouseLeave, + onClickShowParsedField, + onClickHideParsedField, + showParsedFields, } = this.props; const style = getLogRowStyles(theme, row.logLevel); const styles = getStyles(theme); @@ -211,11 +192,14 @@ class UnThemedLogDetails extends PureComponent { parsedKey={key} parsedValue={value} links={links} + onClickShowParsedField={onClickShowParsedField} + onClickHideParsedField={onClickHideParsedField} getStats={() => fieldIndex === undefined ? this.getStatsForParsedField(key) : calculateStats(row.dataFrame.fields[fieldIndex].values.toArray()) } + showParsedFields={showParsedFields} /> ); })} diff --git a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx index 5fa8ee4d461..f5b3cfaadab 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetailsRow.tsx @@ -20,6 +20,9 @@ export interface Props extends Themeable { onClickFilterOutLabel?: (key: string, value: string) => void; links?: Array>; getStats: () => LogLabelStatsModel[] | null; + showParsedFields?: string[]; + onClickShowParsedField?: (key: string) => void; + onClickHideParsedField?: (key: string) => void; } interface State { @@ -44,6 +47,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { label: wordBreakAll; word-break: break-all; `, + showingField: css` + color: ${theme.palette.blue95}; + `, }; }); @@ -54,6 +60,20 @@ class UnThemedLogDetailsRow extends PureComponent { fieldStats: null, }; + showField = () => { + const { onClickShowParsedField, parsedKey } = this.props; + if (onClickShowParsedField) { + onClickShowParsedField(parsedKey); + } + }; + + hideField = () => { + const { onClickHideParsedField, parsedKey } = this.props; + if (onClickHideParsedField) { + onClickHideParsedField(parsedKey); + } + }; + filterLabel = () => { const { onClickFilterLabel, parsedKey, parsedValue } = this.props; if (onClickFilterLabel) { @@ -87,14 +107,21 @@ class UnThemedLogDetailsRow extends PureComponent { } render() { - const { theme, parsedKey, parsedValue, isLabel, links } = this.props; + const { theme, parsedKey, parsedValue, isLabel, links, showParsedFields } = this.props; const { showFieldsStats, fieldStats, fieldCount } = this.state; const styles = getStyles(theme); const style = getLogRowStyles(theme); + const toggleFieldButton = + !isLabel && showParsedFields && showParsedFields.includes(parsedKey) ? ( + + ) : ( + + ); + return ( {/* Action buttons - show stats/filter results */} - + @@ -109,6 +136,12 @@ class UnThemedLogDetailsRow extends PureComponent { )} + {!isLabel && ( + <> + {toggleFieldButton} + + )} + {/* Key - value columns */} {parsedKey} diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 6f472b9bccd..0aabe42de22 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -27,6 +27,7 @@ import { selectThemeVariant } from '../../themes/selectThemeVariant'; //Components import { LogDetails } from './LogDetails'; +import { LogRowMessageParsed } from './LogRowMessageParsed'; import { LogRowMessage } from './LogRowMessage'; import { LogLabels } from './LogLabels'; @@ -47,6 +48,9 @@ interface Props extends Themeable { getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks?: (field: Field, rowIndex: number) => Array>; showContextToggle?: (row?: LogRowModel) => boolean; + showParsedFields?: string[]; + onClickShowParsedField?: (key: string) => void; + onClickHideParsedField?: (key: string) => void; } interface State { @@ -133,6 +137,8 @@ class UnThemedLogRow extends PureComponent { getRows, onClickFilterLabel, onClickFilterOutLabel, + onClickShowParsedField, + onClickHideParsedField, highlighterExpressions, allowDetails, row, @@ -141,6 +147,7 @@ class UnThemedLogRow extends PureComponent { showContextToggle, showLabels, showTime, + showParsedFields, wrapLogMessage, theme, getFieldLinks, @@ -175,19 +182,23 @@ class UnThemedLogRow extends PureComponent { )} - + {showParsedFields && showParsedFields.length > 0 ? ( + + ) : ( + + )} {this.state.showDetails && ( { getFieldLinks={getFieldLinks} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} + onClickShowParsedField={onClickShowParsedField} + onClickHideParsedField={onClickHideParsedField} getRows={getRows} row={row} + showParsedFields={showParsedFields} /> )} diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessageParsed.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessageParsed.tsx new file mode 100644 index 00000000000..0f6c0214b1c --- /dev/null +++ b/packages/grafana-ui/src/components/Logs/LogRowMessageParsed.tsx @@ -0,0 +1,40 @@ +import React, { PureComponent } from 'react'; +import { LogRowModel } from '@grafana/data'; + +import { Themeable } from '../../types/theme'; +import { withTheme } from '../../themes/index'; + +import { parseMessage } from './logParser'; + +export interface Props extends Themeable { + row: LogRowModel; + showParsedFields: string[]; +} + +class UnThemedLogRowMessageParsed extends PureComponent { + render() { + const { row, showParsedFields } = this.props; + const fields = parseMessage(row.entry); + + const line = showParsedFields + .map(parsedKey => { + const field = fields.find(field => { + const { key } = field; + return key === parsedKey; + }); + + if (field) { + return `${parsedKey}=${field.value}`; + } + + return null; + }) + .filter(s => s !== null) + .join(' '); + + return {line}; + } +} + +export const LogRowMessageParsed = withTheme(UnThemedLogRowMessageParsed); +LogRowMessageParsed.displayName = 'LogRowMessageParsed'; diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index b13a3ddcb1f..29cd85b6dbf 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -34,6 +34,9 @@ export interface Props extends Themeable { onClickFilterOutLabel?: (key: string, value: string) => void; getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise; getFieldLinks?: (field: Field, rowIndex: number) => Array>; + showParsedFields?: string[]; + onClickShowParsedField?: (key: string) => void; + onClickHideParsedField?: (key: string) => void; } interface State { @@ -99,6 +102,9 @@ class UnThemedLogRows extends PureComponent { getFieldLinks, disableCustomHorizontalScroll, logsSortOrder, + showParsedFields, + onClickShowParsedField, + onClickHideParsedField, } = this.props; const { renderAll } = this.state; const { logsRowsTable, logsRowsHorizontalScroll } = getLogRowStyles(theme); @@ -140,11 +146,14 @@ class UnThemedLogRows extends PureComponent { showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} + showParsedFields={showParsedFields} wrapLogMessage={wrapLogMessage} timeZone={timeZone} allowDetails={allowDetails} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} + onClickShowParsedField={onClickShowParsedField} + onClickHideParsedField={onClickHideParsedField} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} /> @@ -161,11 +170,14 @@ class UnThemedLogRows extends PureComponent { showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} + showParsedFields={showParsedFields} wrapLogMessage={wrapLogMessage} timeZone={timeZone} allowDetails={allowDetails} onClickFilterLabel={onClickFilterLabel} onClickFilterOutLabel={onClickFilterOutLabel} + onClickShowParsedField={onClickShowParsedField} + onClickHideParsedField={onClickHideParsedField} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} /> diff --git a/packages/grafana-ui/src/components/Logs/logParser.ts b/packages/grafana-ui/src/components/Logs/logParser.ts new file mode 100644 index 00000000000..fd0d6886ca2 --- /dev/null +++ b/packages/grafana-ui/src/components/Logs/logParser.ts @@ -0,0 +1,32 @@ +import { Field, getParser, LinkModel } from '@grafana/data'; +import memoizeOne from 'memoize-one'; + +import { MAX_CHARACTERS } from './LogRowMessage'; + +const memoizedGetParser = memoizeOne(getParser); + +export type FieldDef = { + key: string; + value: string; + links?: Array>; + fieldIndex?: number; +}; + +export const parseMessage = memoizeOne((rowEntry): FieldDef[] => { + if (rowEntry.length > MAX_CHARACTERS) { + return []; + } + const parser = memoizedGetParser(rowEntry); + if (!parser) { + return []; + } + // Use parser to highlight detected fields + const parsedFields = parser.getFields(rowEntry); + const fields = parsedFields.map(field => { + const key = parser.getLabelFromField(field); + const value = parser.getValueFromField(field); + return { key, value }; + }); + + return fields; +}); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index dd4d1ce970d..6125f713dab 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -77,18 +77,20 @@ interface State { wrapLogMessage: boolean; logsSortOrder: LogsSortOrder | null; isFlipping: boolean; + showParsedFields: string[]; } export class Logs extends PureComponent { flipOrderTimer: NodeJS.Timeout; cancelFlippingTimer: NodeJS.Timeout; - state = { + state: State = { showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), showTime: store.getBool(SETTINGS_KEYS.showTime, true), wrapLogMessage: store.getBool(SETTINGS_KEYS.wrapLogMessage, true), logsSortOrder: null, isFlipping: false, + showParsedFields: [], }; componentWillUnmount() { @@ -170,6 +172,37 @@ export class Logs extends PureComponent { } }; + showParsedField = (key: string) => { + const index = this.state.showParsedFields.indexOf(key); + + if (index === -1) { + this.setState(state => { + return { + showParsedFields: state.showParsedFields.concat(key), + }; + }); + } + }; + + hideParsedField = (key: string) => { + const index = this.state.showParsedFields.indexOf(key); + if (index > -1) { + this.setState(state => { + return { + showParsedFields: state.showParsedFields.filter(k => key !== k), + }; + }); + } + }; + + clearParsedFields = () => { + this.setState(state => { + return { + showParsedFields: [], + }; + }); + }; + render() { const { logRows, @@ -195,7 +228,7 @@ export class Logs extends PureComponent { return null; } - const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping } = this.state; + const { showLabels, showTime, wrapLogMessage, logsSortOrder, isFlipping, showParsedFields } = this.state; const { dedupStrategy } = this.props; const hasData = logRows && logRows.length > 0; const dedupCount = dedupedRows @@ -290,6 +323,25 @@ export class Logs extends PureComponent { /> )} + {showParsedFields && showParsedFields.length > 0 && ( + + Show all parsed fields + + ), + }, + ]} + /> + )} + { timeZone={timeZone} getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} + showParsedFields={showParsedFields} + onClickShowParsedField={this.showParsedField} + onClickHideParsedField={this.hideParsedField} /> {!loading && !hasData && !scanning && (