mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
logs: log-details: handle dataplane-compliant dataframes (#71935)
* logs: log-details: handle dataplane-compliant dataframes * lint fix, removed unused import
This commit is contained in:
parent
2dea069443
commit
0da199324a
@ -667,7 +667,6 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
|||||||
<div className={styles.logRows} data-testid="logRowsTable">
|
<div className={styles.logRows} data-testid="logRowsTable">
|
||||||
{/* Width should be full width minus logsnavigation and padding */}
|
{/* Width should be full width minus logsnavigation and padding */}
|
||||||
<LogsTable
|
<LogsTable
|
||||||
rows={logRows}
|
|
||||||
logsSortOrder={this.state.logsSortOrder}
|
logsSortOrder={this.state.logsSortOrder}
|
||||||
range={this.props.range}
|
range={this.props.range}
|
||||||
splitOpen={this.props.splitOpen}
|
splitOpen={this.props.splitOpen}
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import React, { ComponentProps } from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
|
|
||||||
import {
|
import { FieldType, LogRowModel, LogsSortOrder, standardTransformersRegistry, toUtc } from '@grafana/data';
|
||||||
FieldType,
|
|
||||||
LogLevel,
|
|
||||||
LogRowModel,
|
|
||||||
LogsSortOrder,
|
|
||||||
MutableDataFrame,
|
|
||||||
standardTransformersRegistry,
|
|
||||||
toUtc,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
|
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
|
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
|
||||||
@ -68,7 +60,6 @@ describe('LogsTable', () => {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<LogsTable
|
<LogsTable
|
||||||
rows={[makeLog({})]}
|
|
||||||
logsSortOrder={LogsSortOrder.Descending}
|
logsSortOrder={LogsSortOrder.Descending}
|
||||||
splitOpen={() => undefined}
|
splitOpen={() => undefined}
|
||||||
timeZone={'utc'}
|
timeZone={'utc'}
|
||||||
@ -140,26 +131,3 @@ describe('LogsTable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
|
||||||
const uid = overrides.uid || '1';
|
|
||||||
const entry = `log message ${uid}`;
|
|
||||||
return {
|
|
||||||
uid,
|
|
||||||
entryFieldIndex: 0,
|
|
||||||
rowIndex: 0,
|
|
||||||
dataFrame: new MutableDataFrame(),
|
|
||||||
logLevel: LogLevel.debug,
|
|
||||||
entry,
|
|
||||||
hasAnsi: false,
|
|
||||||
hasUnescapedContent: false,
|
|
||||||
labels: {},
|
|
||||||
raw: entry,
|
|
||||||
timeFromNow: '',
|
|
||||||
timeEpochMs: 1,
|
|
||||||
timeEpochNs: '1000000',
|
|
||||||
timeLocal: '',
|
|
||||||
timeUtc: '',
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
applyFieldOverrides,
|
applyFieldOverrides,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
Field,
|
||||||
LogRowModel,
|
|
||||||
LogsSortOrder,
|
LogsSortOrder,
|
||||||
sortDataFrame,
|
sortDataFrame,
|
||||||
SplitOpen,
|
SplitOpen,
|
||||||
@ -16,7 +15,7 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { Table } from '@grafana/ui';
|
import { Table } from '@grafana/ui';
|
||||||
import { shouldRemoveField } from 'app/features/logs/components/logParser';
|
import { separateVisibleFields } from 'app/features/logs/components/logParser';
|
||||||
import { parseLogsFrame } from 'app/features/logs/logsFrame';
|
import { parseLogsFrame } from 'app/features/logs/logsFrame';
|
||||||
|
|
||||||
import { getFieldLinksForExplore } from '../utils/links';
|
import { getFieldLinksForExplore } from '../utils/links';
|
||||||
@ -28,7 +27,6 @@ interface Props {
|
|||||||
splitOpen: SplitOpen;
|
splitOpen: SplitOpen;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
logsSortOrder: LogsSortOrder;
|
logsSortOrder: LogsSortOrder;
|
||||||
rows: LogRowModel[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => {
|
const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => {
|
||||||
@ -40,7 +38,7 @@ const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const LogsTable: React.FunctionComponent<Props> = (props) => {
|
export const LogsTable: React.FunctionComponent<Props> = (props) => {
|
||||||
const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames, rows } = props;
|
const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames } = props;
|
||||||
|
|
||||||
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
|
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
|
||||||
|
|
||||||
@ -129,9 +127,9 @@ export const LogsTable: React.FunctionComponent<Props> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// remove fields that should not be displayed
|
// remove fields that should not be displayed
|
||||||
dataFrame.fields.forEach((field: Field, index: number) => {
|
|
||||||
const row = rows[0]; // we just take the first row as the relevant row
|
const hiddenFields = separateVisibleFields(dataFrame, { keepBody: true, keepTimestamp: true }).hidden;
|
||||||
if (shouldRemoveField(field, index, row, false, false)) {
|
hiddenFields.forEach((field: Field, index: number) => {
|
||||||
transformations.push({
|
transformations.push({
|
||||||
id: 'organize',
|
id: 'organize',
|
||||||
options: {
|
options: {
|
||||||
@ -140,7 +138,6 @@ export const LogsTable: React.FunctionComponent<Props> = (props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (transformations.length > 0) {
|
if (transformations.length > 0) {
|
||||||
const [transformedDataFrame] = await lastValueFrom(transformDataFrame(transformations, [dataFrame]));
|
const [transformedDataFrame] = await lastValueFrom(transformDataFrame(transformations, [dataFrame]));
|
||||||
@ -150,7 +147,7 @@ export const LogsTable: React.FunctionComponent<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
prepare();
|
prepare();
|
||||||
}, [prepareTableFrame, logsFrames, logsSortOrder, rows]);
|
}, [prepareTableFrame, logsFrames, logsSortOrder]);
|
||||||
|
|
||||||
if (!tableFrame) {
|
if (!tableFrame) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
import { DataFrameType, Field, FieldType, LogRowModel, MutableDataFrame } from '@grafana/data';
|
||||||
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
import { createLogRow } from './__mocks__/logRow';
|
import { createLogRow } from './__mocks__/logRow';
|
||||||
@ -199,6 +199,211 @@ describe('logParser', () => {
|
|||||||
expect(fields.length).toBe(1);
|
expect(fields.length).toBe(1);
|
||||||
expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined);
|
expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dataplane frames', () => {
|
||||||
|
const makeLogRow = (fields: Field[], entryFieldIndex: number): LogRowModel =>
|
||||||
|
createLogRow({
|
||||||
|
entryFieldIndex,
|
||||||
|
rowIndex: 0,
|
||||||
|
dataFrame: {
|
||||||
|
refId: 'A',
|
||||||
|
fields,
|
||||||
|
length: fields[0]?.values.length,
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.LogLines,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectHasField = (defs: FieldDef[], name: string): void => {
|
||||||
|
expect(defs.find((field) => field.keys[0] === name)).not.toBe(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should filter out fields with data links that have a nullish value', () => {
|
||||||
|
const createScenario = (value: unknown) =>
|
||||||
|
makeLogRow(
|
||||||
|
[
|
||||||
|
testTimeField,
|
||||||
|
testLineField,
|
||||||
|
{
|
||||||
|
name: 'link',
|
||||||
|
type: FieldType.string,
|
||||||
|
config: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'link1',
|
||||||
|
url: 'https://example.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
values: [value],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getAllFields(createScenario(null))).toHaveLength(0);
|
||||||
|
expect(getAllFields(createScenario(undefined))).toHaveLength(0);
|
||||||
|
expect(getAllFields(createScenario(''))).toHaveLength(1);
|
||||||
|
expect(getAllFields(createScenario('test'))).toHaveLength(1);
|
||||||
|
// technically this is a field-type-string, but i will add more
|
||||||
|
// falsy-values, just to be sure
|
||||||
|
expect(getAllFields(createScenario(false))).toHaveLength(1);
|
||||||
|
expect(getAllFields(createScenario(NaN))).toHaveLength(1);
|
||||||
|
expect(getAllFields(createScenario(0))).toHaveLength(1);
|
||||||
|
expect(getAllFields(createScenario(-0))).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out system-fields without data-links, but should keep severity', () => {
|
||||||
|
const row = makeLogRow(
|
||||||
|
[
|
||||||
|
testTimeField,
|
||||||
|
testLineField,
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'id',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['id1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'attributes',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: [{ a: 1, b: 2 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: {},
|
||||||
|
name: 'severity',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['info'],
|
||||||
|
},
|
||||||
|
testStringField,
|
||||||
|
],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = getAllFields(row);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(2);
|
||||||
|
expectHasField(output, 'test_field_string');
|
||||||
|
expectHasField(output, 'severity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep system fields with data-links', () => {
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
title: 'link1',
|
||||||
|
url: 'https://example.com',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const row = makeLogRow(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...testTimeField,
|
||||||
|
config: { links },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...testLineField,
|
||||||
|
config: { links },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { links },
|
||||||
|
name: 'id',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['id1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { links },
|
||||||
|
name: 'attributes',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: [{ a: 1, b: 2 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: { links },
|
||||||
|
name: 'severity',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['info'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = getAllFields(row);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(5);
|
||||||
|
expectHasField(output, 'timestamp');
|
||||||
|
expectHasField(output, 'body');
|
||||||
|
expectHasField(output, 'id');
|
||||||
|
expectHasField(output, 'attributes');
|
||||||
|
expectHasField(output, 'severity');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out config-hidden fields', () => {
|
||||||
|
const row = makeLogRow(
|
||||||
|
[
|
||||||
|
testTimeField,
|
||||||
|
testLineField,
|
||||||
|
{
|
||||||
|
...testStringField,
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = getAllFields(row);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out fields with null values', () => {
|
||||||
|
const row = makeLogRow(
|
||||||
|
[
|
||||||
|
testTimeField,
|
||||||
|
testLineField,
|
||||||
|
{
|
||||||
|
// null-value
|
||||||
|
config: {},
|
||||||
|
type: FieldType.string,
|
||||||
|
name: 'test1',
|
||||||
|
values: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// null-value and data-link
|
||||||
|
config: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'link1',
|
||||||
|
url: 'https://example.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
type: FieldType.string,
|
||||||
|
name: 'test2',
|
||||||
|
values: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// normal value
|
||||||
|
config: {},
|
||||||
|
type: FieldType.string,
|
||||||
|
name: 'test3',
|
||||||
|
values: ['testvalue'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = getAllFields(row);
|
||||||
|
|
||||||
|
expect(output).toHaveLength(1);
|
||||||
|
expectHasField(output, 'test3');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createLogLineLinks', () => {
|
describe('createLogLineLinks', () => {
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import { partition } from 'lodash';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
|
|
||||||
import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data';
|
import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data';
|
||||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||||
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
|
import { parseLogsFrame } from '../logsFrame';
|
||||||
|
|
||||||
export type FieldDef = {
|
export type FieldDef = {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
values: string[];
|
values: string[];
|
||||||
@ -65,10 +68,9 @@ export const getDataframeFields = memoizeOne(
|
|||||||
row: LogRowModel,
|
row: LogRowModel,
|
||||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
|
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
|
||||||
): FieldDef[] => {
|
): FieldDef[] => {
|
||||||
return row.dataFrame.fields
|
const visibleFields = separateVisibleFields(row.dataFrame).visible;
|
||||||
.map((field, index) => ({ ...field, index }))
|
const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null);
|
||||||
.filter((field, index) => !shouldRemoveField(field, index, row))
|
return nonEmptyVisibleFields.map((field) => {
|
||||||
.map((field) => {
|
|
||||||
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
|
const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
|
||||||
const fieldVal = field.values[row.rowIndex];
|
const fieldVal = field.values[row.rowIndex];
|
||||||
const outputVal =
|
const outputVal =
|
||||||
@ -85,56 +87,64 @@ export const getDataframeFields = memoizeOne(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export function shouldRemoveField(
|
type VisOptions = {
|
||||||
field: Field,
|
keepTimestamp?: boolean;
|
||||||
index: number,
|
keepBody?: boolean;
|
||||||
row: LogRowModel,
|
};
|
||||||
shouldRemoveLine = true,
|
|
||||||
shouldRemoveTime = true
|
// return the fields (their indices to be exact) that should be visible
|
||||||
) {
|
// based on the logs dataframe structure
|
||||||
// field that has empty value (we want to keep 0 or empty string)
|
function getVisibleFieldIndices(frame: DataFrame, opts: VisOptions): Set<number> {
|
||||||
if (field.values[row.rowIndex] == null) {
|
const logsFrame = parseLogsFrame(frame);
|
||||||
return true;
|
if (logsFrame === null) {
|
||||||
|
// should not really happen
|
||||||
|
return new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
// hidden field, remove
|
// we want to show every "extra" field
|
||||||
if (field.config.custom?.hidden) {
|
const visibleFieldIndices = new Set(logsFrame.extraFields.map((f) => f.index));
|
||||||
return true;
|
|
||||||
|
// we always show the severity field
|
||||||
|
if (logsFrame.severityField !== null) {
|
||||||
|
visibleFieldIndices.add(logsFrame.severityField.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
// field with data-links, keep
|
if (opts.keepBody) {
|
||||||
if ((field.config.links ?? []).length > 0) {
|
visibleFieldIndices.add(logsFrame.bodyField.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.keepTimestamp) {
|
||||||
|
visibleFieldIndices.add(logsFrame.timeField.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleFieldIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the dataframe's fields into visible and hidden arrays.
|
||||||
|
// note: does not do any row-level checks,
|
||||||
|
// for example does not check if the field's values are nullish
|
||||||
|
// or not at a givn row.
|
||||||
|
export function separateVisibleFields(
|
||||||
|
frame: DataFrame,
|
||||||
|
opts?: VisOptions
|
||||||
|
): { visible: FieldWithIndex[]; hidden: FieldWithIndex[] } {
|
||||||
|
const fieldsWithIndex: FieldWithIndex[] = frame.fields.map((field, index) => ({ ...field, index }));
|
||||||
|
|
||||||
|
const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {});
|
||||||
|
|
||||||
|
const [visible, hidden] = partition(fieldsWithIndex, (f) => {
|
||||||
|
// hidden fields are always hidden
|
||||||
|
if (f.config.custom?.hidden) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// the remaining checks use knowledge of how we parse logs-dataframes
|
|
||||||
|
|
||||||
// Remove field if it is:
|
// fields with data-links are visible
|
||||||
// "labels" field that is in Loki used to store all labels
|
if ((f.config.links ?? []).length > 0) {
|
||||||
if (field.name === 'labels' && field.type === FieldType.other) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// id and tsNs are arbitrary added fields in the backend and should be hidden in the UI
|
|
||||||
if (field.name === 'id' || field.name === 'tsNs') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (shouldRemoveTime) {
|
|
||||||
const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time);
|
|
||||||
if (
|
|
||||||
field.name === firstTimeField?.name &&
|
|
||||||
field.type === FieldType.time &&
|
|
||||||
field.values[0] === firstTimeField.values[0]
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRemoveLine) {
|
return visibleFieldIndices.has(f.index);
|
||||||
// first string-field is the log-line
|
});
|
||||||
const firstStringFieldIndex = row.dataFrame.fields.findIndex((f) => f.type === FieldType.string);
|
|
||||||
if (firstStringFieldIndex === index) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return { visible, hidden };
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user