From 6a01bab67c47b6e7c4328efd227d62b8a69fbe84 Mon Sep 17 00:00:00 2001 From: Fredrik Enestad Date: Tue, 25 Aug 2020 11:37:19 +0200 Subject: [PATCH] Allow toggling of derived fields (#27148) --- .../src/components/Logs/LogDetails.tsx | 70 +----------------- .../grafana-ui/src/components/Logs/LogRow.tsx | 2 +- .../components/Logs/LogRowMessageParsed.tsx | 9 ++- .../src/components/Logs/logParser.ts | 74 ++++++++++++++++++- 4 files changed, 80 insertions(+), 75 deletions(-) diff --git a/packages/grafana-ui/src/components/Logs/LogDetails.tsx b/packages/grafana-ui/src/components/Logs/LogDetails.tsx index da4a6b93747..71541ab0453 100644 --- a/packages/grafana-ui/src/components/Logs/LogDetails.tsx +++ b/packages/grafana-ui/src/components/Logs/LogDetails.tsx @@ -18,7 +18,7 @@ import { getLogRowStyles } from './getLogRowStyles'; import { stylesFactory } from '../../themes/stylesFactory'; import { selectThemeVariant } from '../../themes/selectThemeVariant'; -import { parseMessage, FieldDef } from './logParser'; +import { getAllFields } from './logParser'; //Components import { LogDetailsRow } from './LogDetailsRow'; @@ -61,61 +61,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { class UnThemedLogDetails extends PureComponent { 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) => { const matcher = this.getParser(this.props.row.entry)!.buildMatcher(key); return calculateFieldStats(this.props.getRows(), matcher); @@ -135,12 +80,13 @@ class UnThemedLogDetails extends PureComponent { onClickShowParsedField, onClickHideParsedField, showParsedFields, + getFieldLinks, } = this.props; const style = getLogRowStyles(theme, row.logLevel); const styles = getStyles(theme); const labels = row.labels ? row.labels : {}; const labelsAvailable = Object.keys(labels).length > 0; - const fields = this.getAllFields(row); + const fields = getAllFields(row, getFieldLinks); const parsedFieldsAvailable = fields && fields.length > 0; return ( @@ -219,15 +165,5 @@ class UnThemedLogDetails extends PureComponent { } } -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); LogDetails.displayName = 'LogDetails'; diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 0aabe42de22..49ed31ec276 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -183,7 +183,7 @@ class UnThemedLogRow extends PureComponent { )} {showParsedFields && showParsedFields.length > 0 ? ( - + ) : ( Array>; } class UnThemedLogRowMessageParsed extends PureComponent { render() { - const { row, showParsedFields } = this.props; - const fields = parseMessage(row.entry); + const { row, showParsedFields, getFieldLinks } = this.props; + const fields = getAllFields(row, getFieldLinks); const line = showParsedFields .map(parsedKey => { diff --git a/packages/grafana-ui/src/components/Logs/logParser.ts b/packages/grafana-ui/src/components/Logs/logParser.ts index fd0d6886ca2..2fa39cd5823 100644 --- a/packages/grafana-ui/src/components/Logs/logParser.ts +++ b/packages/grafana-ui/src/components/Logs/logParser.ts @@ -1,18 +1,46 @@ -import { Field, getParser, LinkModel } from '@grafana/data'; +import { Field, getParser, LinkModel, LogRowModel } from '@grafana/data'; import memoizeOne from 'memoize-one'; import { MAX_CHARACTERS } from './LogRowMessage'; const memoizedGetParser = memoizeOne(getParser); -export type FieldDef = { +type FieldDef = { key: string; value: string; links?: Array>; 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>) => { + 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) { return []; } @@ -30,3 +58,43 @@ export const parseMessage = memoizeOne((rowEntry): FieldDef[] => { return fields; }); + +const getDerivedFields = memoizeOne( + (row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number) => Array>): 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; +}