mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
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:
parent
9f2386a219
commit
e962f02fbc
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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}>
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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';
|
@ -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}
|
||||
/>
|
||||
|
32
packages/grafana-ui/src/components/Logs/logParser.ts
Normal file
32
packages/grafana-ui/src/components/Logs/logParser.ts
Normal 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;
|
||||
});
|
@ -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 && (
|
||||
|
Loading…
Reference in New Issue
Block a user