Explore/Loki: Toggle parsed fields in logs view (#26178)

* POC for toggling parsed fields in Explore/Loki

* fixed issues

* add clear button for toggled parsed fields

* fix test-frontend failures

* use blue eye instead of eye/eye-slash and update tooltips

* break out parseMessage

* move indicator to meta data section

* clean up LogRowMessageParsed

* better label

* clean up after making optional
This commit is contained in:
Fredrik Enestad 2020-08-20 16:28:10 +02:00 committed by GitHub
parent 9f2386a219
commit e962f02fbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 45 deletions

View File

@ -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<LinkModel<Field>>;
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<LinkModel<Field>>;
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<Props> {
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<Props> {
* 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<Props> {
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<Props> {
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}
/>
);
})}

View File

@ -20,6 +20,9 @@ export interface Props extends Themeable {
onClickFilterOutLabel?: (key: string, value: string) => void;
links?: Array<LinkModel<Field>>;
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<Props, State> {
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<Props, State> {
}
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) ? (
<IconButton name="eye" className={styles.showingField} title="Hide this field" onClick={this.hideField} />
) : (
<IconButton name="eye" title="Show this field instead of the message" onClick={this.showField} />
);
return (
<tr className={cx(style.logDetailsValue, { [styles.noHoverBackground]: showFieldsStats })}>
{/* Action buttons - show stats/filter results */}
<td className={style.logsDetailsIcon} colSpan={isLabel ? undefined : 3}>
<td className={style.logsDetailsIcon} colSpan={isLabel ? undefined : 2}>
<IconButton name="signal" title={'Ad-hoc statistics'} onClick={this.showStats} />
</td>
@ -109,6 +136,12 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
</>
)}
{!isLabel && (
<>
<td className={style.logsDetailsIcon}>{toggleFieldButton}</td>
</>
)}
{/* Key - value columns */}
<td className={style.logDetailsLabel}>{parsedKey}</td>
<td className={styles.wordBreakAll}>

View File

@ -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<DataQueryResponse>;
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
showContextToggle?: (row?: LogRowModel) => boolean;
showParsedFields?: string[];
onClickShowParsedField?: (key: string) => void;
onClickHideParsedField?: (key: string) => void;
}
interface State {
@ -133,6 +137,8 @@ class UnThemedLogRow extends PureComponent<Props, State> {
getRows,
onClickFilterLabel,
onClickFilterOutLabel,
onClickShowParsedField,
onClickHideParsedField,
highlighterExpressions,
allowDetails,
row,
@ -141,6 +147,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
showContextToggle,
showLabels,
showTime,
showParsedFields,
wrapLogMessage,
theme,
getFieldLinks,
@ -175,19 +182,23 @@ class UnThemedLogRow extends PureComponent<Props, State> {
<LogLabels labels={row.uniqueLabels} />
</td>
)}
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={row}
getRows={getRows}
errors={errors}
hasMoreContextRows={hasMoreContextRows}
updateLimit={updateLimit}
context={context}
contextIsOpen={showContext}
showContextToggle={showContextToggle}
wrapLogMessage={wrapLogMessage}
onToggleContext={this.toggleContext}
/>
{showParsedFields && showParsedFields.length > 0 ? (
<LogRowMessageParsed row={row} showParsedFields={showParsedFields!} />
) : (
<LogRowMessage
highlighterExpressions={highlighterExpressions}
row={row}
getRows={getRows}
errors={errors}
hasMoreContextRows={hasMoreContextRows}
updateLimit={updateLimit}
context={context}
contextIsOpen={showContext}
showContextToggle={showContextToggle}
wrapLogMessage={wrapLogMessage}
onToggleContext={this.toggleContext}
/>
)}
</tr>
{this.state.showDetails && (
<LogDetails
@ -198,8 +209,11 @@ class UnThemedLogRow extends PureComponent<Props, State> {
getFieldLinks={getFieldLinks}
onClickFilterLabel={onClickFilterLabel}
onClickFilterOutLabel={onClickFilterOutLabel}
onClickShowParsedField={onClickShowParsedField}
onClickHideParsedField={onClickHideParsedField}
getRows={getRows}
row={row}
showParsedFields={showParsedFields}
/>
)}
</>

View File

@ -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<Props> {
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 <td>{line}</td>;
}
}
export const LogRowMessageParsed = withTheme(UnThemedLogRowMessageParsed);
LogRowMessageParsed.displayName = 'LogRowMessageParsed';

View File

@ -34,6 +34,9 @@ export interface Props extends Themeable {
onClickFilterOutLabel?: (key: string, value: string) => void;
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
showParsedFields?: string[];
onClickShowParsedField?: (key: string) => void;
onClickHideParsedField?: (key: string) => void;
}
interface State {
@ -99,6 +102,9 @@ class UnThemedLogRows extends PureComponent<Props, State> {
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<Props, State> {
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<Props, State> {
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}
/>

View File

@ -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<LinkModel<Field>>;
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;
});

View File

@ -77,18 +77,20 @@ interface State {
wrapLogMessage: boolean;
logsSortOrder: LogsSortOrder | null;
isFlipping: boolean;
showParsedFields: string[];
}
export class Logs extends PureComponent<Props, State> {
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<Props, State> {
}
};
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<Props, State> {
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<Props, State> {
/>
)}
{showParsedFields && showParsedFields.length > 0 && (
<MetaInfoText
metaItems={[
{
label: 'Showing only parsed fields',
value: renderMetaItem(showParsedFields, LogsMetaKind.LabelsMap),
},
{
label: '',
value: (
<Button variant="secondary" size="sm" onClick={this.clearParsedFields}>
Show all parsed fields
</Button>
),
},
]}
/>
)}
<LogRows
logRows={logRows}
deduplicatedRows={dedupedRows}
@ -306,6 +358,9 @@ export class Logs extends PureComponent<Props, State> {
timeZone={timeZone}
getFieldLinks={getFieldLinks}
logsSortOrder={logsSortOrder}
showParsedFields={showParsedFields}
onClickShowParsedField={this.showParsedField}
onClickHideParsedField={this.hideParsedField}
/>
{!loading && !hasData && !scanning && (