Commit 6a01bab6 by Fredrik Enestad Committed by GitHub

Allow toggling of derived fields (#27148)

parent f5ee1f93
...@@ -18,7 +18,7 @@ import { getLogRowStyles } from './getLogRowStyles'; ...@@ -18,7 +18,7 @@ import { getLogRowStyles } from './getLogRowStyles';
import { stylesFactory } from '../../themes/stylesFactory'; import { stylesFactory } from '../../themes/stylesFactory';
import { selectThemeVariant } from '../../themes/selectThemeVariant'; import { selectThemeVariant } from '../../themes/selectThemeVariant';
import { parseMessage, FieldDef } from './logParser'; import { getAllFields } from './logParser';
//Components //Components
import { LogDetailsRow } from './LogDetailsRow'; import { LogDetailsRow } from './LogDetailsRow';
...@@ -61,61 +61,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -61,61 +61,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
class UnThemedLogDetails extends PureComponent<Props> { class UnThemedLogDetails extends PureComponent<Props> {
getParser = memoizeOne(getParser); getParser = memoizeOne(getParser);
getDerivedFields = memoizeOne((row: LogRowModel): FieldDef[] => {
return (
row.dataFrame.fields
.map((field, index) => ({ ...field, index }))
// Remove Id which we use for react key and entry field which we are showing as the log message. Also remove hidden fields.
.filter(
(field, index) => !('id' === field.name || row.entryFieldIndex === index || field.config.custom?.hidden)
)
// Filter out fields without values. For example in elastic the fields are parsed from the document which can
// have different structure per row and so the dataframe is pretty sparse.
.filter(field => {
const value = field.values.get(row.rowIndex);
// Not sure exactly what will be the empty value here. And we want to keep 0 as some values can be non
// string.
return value !== null && value !== undefined;
})
.map(field => {
const { getFieldLinks } = this.props;
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex) : [];
return {
key: field.name,
value: field.values.get(row.rowIndex).toString(),
links: links,
fieldIndex: field.index,
};
})
);
});
/**
* Returns all fields for log row which consists of fields we parse from the message itself and any derived fields
* setup in data source config.
*/
getAllFields = memoizeOne((row: LogRowModel) => {
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
// value is in the dataFrame it will be without the quotes. We treat them here as the same value.
const value = field.value.replace(/(^")|("$)/g, '');
const fieldHash = `${field.key}=${value}`;
if (acc[fieldHash]) {
acc[fieldHash].links = [...(acc[fieldHash].links || []), ...(field.links || [])];
} else {
acc[fieldHash] = field;
}
return acc;
}, {} as { [key: string]: FieldDef });
const allFields = Object.values(fieldsMap);
allFields.sort(sortFieldsLinkFirst);
return allFields;
});
getStatsForParsedField = (key: string) => { getStatsForParsedField = (key: string) => {
const matcher = this.getParser(this.props.row.entry)!.buildMatcher(key); const matcher = this.getParser(this.props.row.entry)!.buildMatcher(key);
return calculateFieldStats(this.props.getRows(), matcher); return calculateFieldStats(this.props.getRows(), matcher);
...@@ -135,12 +80,13 @@ class UnThemedLogDetails extends PureComponent<Props> { ...@@ -135,12 +80,13 @@ class UnThemedLogDetails extends PureComponent<Props> {
onClickShowParsedField, onClickShowParsedField,
onClickHideParsedField, onClickHideParsedField,
showParsedFields, showParsedFields,
getFieldLinks,
} = this.props; } = this.props;
const style = getLogRowStyles(theme, row.logLevel); const style = getLogRowStyles(theme, row.logLevel);
const styles = getStyles(theme); const styles = getStyles(theme);
const labels = row.labels ? row.labels : {}; const labels = row.labels ? row.labels : {};
const labelsAvailable = Object.keys(labels).length > 0; const labelsAvailable = Object.keys(labels).length > 0;
const fields = this.getAllFields(row); const fields = getAllFields(row, getFieldLinks);
const parsedFieldsAvailable = fields && fields.length > 0; const parsedFieldsAvailable = fields && fields.length > 0;
return ( return (
...@@ -219,15 +165,5 @@ class UnThemedLogDetails extends PureComponent<Props> { ...@@ -219,15 +165,5 @@ class UnThemedLogDetails extends PureComponent<Props> {
} }
} }
function sortFieldsLinkFirst(fieldA: FieldDef, fieldB: FieldDef) {
if (fieldA.links?.length && !fieldB.links?.length) {
return -1;
}
if (!fieldA.links?.length && fieldB.links?.length) {
return 1;
}
return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0;
}
export const LogDetails = withTheme(UnThemedLogDetails); export const LogDetails = withTheme(UnThemedLogDetails);
LogDetails.displayName = 'LogDetails'; LogDetails.displayName = 'LogDetails';
...@@ -183,7 +183,7 @@ class UnThemedLogRow extends PureComponent<Props, State> { ...@@ -183,7 +183,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
</td> </td>
)} )}
{showParsedFields && showParsedFields.length > 0 ? ( {showParsedFields && showParsedFields.length > 0 ? (
<LogRowMessageParsed row={row} showParsedFields={showParsedFields!} /> <LogRowMessageParsed row={row} showParsedFields={showParsedFields!} getFieldLinks={getFieldLinks} />
) : ( ) : (
<LogRowMessage <LogRowMessage
highlighterExpressions={highlighterExpressions} highlighterExpressions={highlighterExpressions}
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { LogRowModel } from '@grafana/data'; import { LogRowModel, Field, LinkModel } from '@grafana/data';
import { Themeable } from '../../types/theme'; import { Themeable } from '../../types/theme';
import { withTheme } from '../../themes/index'; import { withTheme } from '../../themes/index';
import { parseMessage } from './logParser'; import { getAllFields } from './logParser';
export interface Props extends Themeable { export interface Props extends Themeable {
row: LogRowModel; row: LogRowModel;
showParsedFields: string[]; showParsedFields: string[];
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
} }
class UnThemedLogRowMessageParsed extends PureComponent<Props> { class UnThemedLogRowMessageParsed extends PureComponent<Props> {
render() { render() {
const { row, showParsedFields } = this.props; const { row, showParsedFields, getFieldLinks } = this.props;
const fields = parseMessage(row.entry); const fields = getAllFields(row, getFieldLinks);
const line = showParsedFields const line = showParsedFields
.map(parsedKey => { .map(parsedKey => {
......
import { Field, getParser, LinkModel } from '@grafana/data'; import { Field, getParser, LinkModel, LogRowModel } from '@grafana/data';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { MAX_CHARACTERS } from './LogRowMessage'; import { MAX_CHARACTERS } from './LogRowMessage';
const memoizedGetParser = memoizeOne(getParser); const memoizedGetParser = memoizeOne(getParser);
export type FieldDef = { type FieldDef = {
key: string; key: string;
value: string; value: string;
links?: Array<LinkModel<Field>>; links?: Array<LinkModel<Field>>;
fieldIndex?: number; fieldIndex?: number;
}; };
export const parseMessage = memoizeOne((rowEntry): FieldDef[] => { /**
* Returns all fields for log row which consists of fields we parse from the message itself and any derived fields
* setup in data source config.
*/
export const getAllFields = memoizeOne(
(row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>) => {
const fields = parseMessage(row.entry);
const derivedFields = getDerivedFields(row, getFieldLinks);
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
// value is in the dataFrame it will be without the quotes. We treat them here as the same value.
const value = field.value.replace(/(^")|("$)/g, '');
const fieldHash = `${field.key}=${value}`;
if (acc[fieldHash]) {
acc[fieldHash].links = [...(acc[fieldHash].links || []), ...(field.links || [])];
} else {
acc[fieldHash] = field;
}
return acc;
}, {} as { [key: string]: FieldDef });
const allFields = Object.values(fieldsMap);
allFields.sort(sortFieldsLinkFirst);
return allFields;
}
);
const parseMessage = memoizeOne((rowEntry): FieldDef[] => {
if (rowEntry.length > MAX_CHARACTERS) { if (rowEntry.length > MAX_CHARACTERS) {
return []; return [];
} }
...@@ -30,3 +58,43 @@ export const parseMessage = memoizeOne((rowEntry): FieldDef[] => { ...@@ -30,3 +58,43 @@ export const parseMessage = memoizeOne((rowEntry): FieldDef[] => {
return fields; return fields;
}); });
const getDerivedFields = memoizeOne(
(row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>): FieldDef[] => {
return (
row.dataFrame.fields
.map((field, index) => ({ ...field, index }))
// Remove Id which we use for react key and entry field which we are showing as the log message. Also remove hidden fields.
.filter(
(field, index) => !('id' === field.name || row.entryFieldIndex === index || field.config.custom?.hidden)
)
// Filter out fields without values. For example in elastic the fields are parsed from the document which can
// have different structure per row and so the dataframe is pretty sparse.
.filter(field => {
const value = field.values.get(row.rowIndex);
// Not sure exactly what will be the empty value here. And we want to keep 0 as some values can be non
// string.
return value !== null && value !== undefined;
})
.map(field => {
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex) : [];
return {
key: field.name,
value: field.values.get(row.rowIndex).toString(),
links: links,
fieldIndex: field.index,
};
})
);
}
);
function sortFieldsLinkFirst(fieldA: FieldDef, fieldB: FieldDef) {
if (fieldA.links?.length && !fieldB.links?.length) {
return -1;
}
if (!fieldA.links?.length && fieldB.links?.length) {
return 1;
}
return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment