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