1import { Field, getParser, LinkModel, LogRowModel } from '@grafana/data'; 2import memoizeOne from 'memoize-one'; 3 4import { MAX_CHARACTERS } from './LogRowMessage'; 5 6const memoizedGetParser = memoizeOne(getParser); 7 8type FieldDef = { 9 key: string; 10 value: string; 11 links?: Array<LinkModel<Field>>; 12 fieldIndex?: number; 13}; 14 15/** 16 * Returns all fields for log row which consists of fields we parse from the message itself and any derived fields 17 * setup in data source config. 18 */ 19export const getAllFields = memoizeOne( 20 (row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>) => { 21 const fields = parseMessage(row.entry); 22 const derivedFields = getDerivedFields(row, getFieldLinks); 23 const fieldsMap = [...derivedFields, ...fields].reduce((acc, field) => { 24 // Strip enclosing quotes for hashing. When values are parsed from log line the quotes are kept, but if same 25 // value is in the dataFrame it will be without the quotes. We treat them here as the same value. 26 const value = field.value.replace(/(^")|("$)/g, ''); 27 const fieldHash = `${field.key}=${value}`; 28 if (acc[fieldHash]) { 29 acc[fieldHash].links = [...(acc[fieldHash].links || []), ...(field.links || [])]; 30 } else { 31 acc[fieldHash] = field; 32 } 33 return acc; 34 }, {} as { [key: string]: FieldDef }); 35 36 const allFields = Object.values(fieldsMap); 37 allFields.sort(sortFieldsLinkFirst); 38 39 return allFields; 40 } 41); 42 43const parseMessage = memoizeOne((rowEntry): FieldDef[] => { 44 if (rowEntry.length > MAX_CHARACTERS) { 45 return []; 46 } 47 const parser = memoizedGetParser(rowEntry); 48 if (!parser) { 49 return []; 50 } 51 // Use parser to highlight detected fields 52 const detectedFields = parser.getFields(rowEntry); 53 const fields = detectedFields.map((field) => { 54 const key = parser.getLabelFromField(field); 55 const value = parser.getValueFromField(field); 56 return { key, value }; 57 }); 58 59 return fields; 60}); 61 62const getDerivedFields = memoizeOne( 63 (row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>): FieldDef[] => { 64 return ( 65 row.dataFrame.fields 66 .map((field, index) => ({ ...field, index })) 67 // Remove Id which we use for react key and entry field which we are showing as the log message. Also remove hidden fields. 68 .filter( 69 (field, index) => !('id' === field.name || row.entryFieldIndex === index || field.config.custom?.hidden) 70 ) 71 // Filter out fields without values. For example in elastic the fields are parsed from the document which can 72 // have different structure per row and so the dataframe is pretty sparse. 73 .filter((field) => { 74 const value = field.values.get(row.rowIndex); 75 // Not sure exactly what will be the empty value here. And we want to keep 0 as some values can be non 76 // string. 77 return value !== null && value !== undefined; 78 }) 79 .map((field) => { 80 const links = getFieldLinks ? getFieldLinks(field, row.rowIndex) : []; 81 return { 82 key: field.name, 83 value: field.values.get(row.rowIndex).toString(), 84 links: links, 85 fieldIndex: field.index, 86 }; 87 }) 88 ); 89 } 90); 91 92function sortFieldsLinkFirst(fieldA: FieldDef, fieldB: FieldDef) { 93 if (fieldA.links?.length && !fieldB.links?.length) { 94 return -1; 95 } 96 if (!fieldA.links?.length && fieldB.links?.length) { 97 return 1; 98 } 99 return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0; 100} 101