Logs panel: Add option to show common labels (#36166)

* Add common labels to Logs panel

* Clean up and add tests

* Update documentation

* Update public/app/plugins/panel/logs/LogsPanel.tsx

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>

* Fix type error

Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
This commit is contained in:
Ivana Huckova
2021-07-01 08:06:58 -04:00
committed by GitHub
parent f152180dc3
commit 742c737784
7 changed files with 158 additions and 19 deletions

View File

@@ -33,6 +33,7 @@ Use these settings to refine your visualization:
- **Time -** Show or hide the time column. This is the timestamp associated with the log line as reported from the data source. - **Time -** Show or hide the time column. This is the timestamp associated with the log line as reported from the data source.
- **Unique labels -** Show or hide the unique labels column, which shows only non-common labels. - **Unique labels -** Show or hide the unique labels column, which shows only non-common labels.
- **Common labels -** Show or hide the common labels.
- **Wrap lines -** Toggle line wrapping. - **Wrap lines -** Toggle line wrapping.
- **Enable log details -** Toggle option to see the log details view for each log row. The default setting is true. - **Enable log details -** Toggle option to see the log details view for each log row. The default setting is true.
- **Order -** Display results in descending or ascending time order. The default is **Descending**, showing the newest logs first. Set to **Ascending** to show the oldest log lines first. - **Order -** Display results in descending or ascending time order. The default is **Descending**, showing the newest logs first. Set to **Ascending** to show the oldest log lines first.

View File

@@ -16,6 +16,7 @@ import {
logSeriesToLogsModel, logSeriesToLogsModel,
filterLogLevels, filterLogLevels,
LIMIT_LABEL, LIMIT_LABEL,
COMMON_LABELS,
} from './logs_model'; } from './logs_model';
describe('dedupLogRows()', () => { describe('dedupLogRows()', () => {
@@ -322,7 +323,7 @@ describe('dataFrameToLogsModel', () => {
]); ]);
expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: COMMON_LABELS,
value: series[0].fields[1].labels, value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap, kind: LogsMetaKind.LabelsMap,
}); });
@@ -392,7 +393,7 @@ describe('dataFrameToLogsModel', () => {
expect(logsModel.series).toHaveLength(2); expect(logsModel.series).toHaveLength(2);
expect(logsModel.meta).toHaveLength(3); expect(logsModel.meta).toHaveLength(3);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: COMMON_LABELS,
value: series[0].fields[1].labels, value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap, kind: LogsMetaKind.LabelsMap,
}); });
@@ -537,7 +538,7 @@ describe('dataFrameToLogsModel', () => {
]); ]);
expect(logsModel.meta).toHaveLength(1); expect(logsModel.meta).toHaveLength(1);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: COMMON_LABELS,
value: { value: {
foo: 'bar', foo: 'bar',
}, },
@@ -686,7 +687,7 @@ describe('dataFrameToLogsModel', () => {
const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 }); const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 });
expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({ expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels', label: COMMON_LABELS,
value: series[0].fields[1].labels, value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap, kind: LogsMetaKind.LabelsMap,
}); });
@@ -798,7 +799,7 @@ describe('logSeriesToLogsModel', () => {
const logsModel = dataFrameToLogsModel(logSeries, 0); const logsModel = dataFrameToLogsModel(logSeries, 0);
expect(logsModel.meta).toMatchObject([ expect(logsModel.meta).toMatchObject([
{ kind: 2, label: 'Common labels', value: { foo: 'bar', level: 'dbug' } }, { kind: 2, label: COMMON_LABELS, value: { foo: 'bar', level: 'dbug' } },
{ kind: 0, label: LIMIT_LABEL, value: 2000 }, { kind: 0, label: LIMIT_LABEL, value: 2000 },
{ kind: 1, label: 'Total bytes processed', value: '194 kB' }, { kind: 1, label: 'Total bytes processed', value: '194 kB' },
]); ]);

View File

@@ -31,6 +31,7 @@ import { getThemeColor } from 'app/core/utils/colors';
import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters'; import { SIPrefix } from '@grafana/data/src/valueFormats/symbolFormatters';
export const LIMIT_LABEL = 'Line limit'; export const LIMIT_LABEL = 'Line limit';
export const COMMON_LABELS = 'Common labels';
export const LogLevelColor = { export const LogLevelColor = {
[LogLevel.critical]: colors[7], [LogLevel.critical]: colors[7],
@@ -395,7 +396,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
const meta: LogsMetaItem[] = []; const meta: LogsMetaItem[] = [];
if (size(commonLabels) > 0) { if (size(commonLabels) > 0) {
meta.push({ meta.push({
label: 'Common labels', label: COMMON_LABELS,
value: commonLabels, value: commonLabels,
kind: LogsMetaKind.LabelsMap, kind: LogsMetaKind.LabelsMap,
}); });

View File

@@ -0,0 +1,109 @@
import React, { ComponentProps } from 'react';
import { render, screen } from '@testing-library/react';
import { LoadingState, MutableDataFrame, FieldType } from '@grafana/data';
import { LogsPanel } from './LogsPanel';
type LogsPanelProps = ComponentProps<typeof LogsPanel>;
describe('LogsPanel', () => {
describe('when returned series include common labels', () => {
const seriesWithCommonLabels = [
new MutableDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
},
{
name: 'message',
type: FieldType.string,
values: [
't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
],
labels: {
app: 'common_app',
job: 'common_job',
},
},
],
}),
];
it('shows common labels when showCommonLabels is set to true', () => {
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: true } });
expect(screen.getByText(/common labels:/i)).toBeInTheDocument();
expect(screen.getByText(/common_app/i)).toBeInTheDocument();
expect(screen.getByText(/common_job/i)).toBeInTheDocument();
});
it('does not show common labels when showCommonLabels is set to false', () => {
setup({ data: { series: seriesWithCommonLabels }, options: { showCommonLabels: false } });
expect(screen.queryByText(/common labels:/i)).not.toBeInTheDocument();
expect(screen.queryByText(/common_app/i)).not.toBeInTheDocument();
expect(screen.queryByText(/common_job/i)).not.toBeInTheDocument();
});
});
describe('when returned series does not include common labels', () => {
const seriesWithoutCommonLabels = [
new MutableDataFrame({
fields: [
{
name: 'time',
type: FieldType.time,
values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
},
{
name: 'message',
type: FieldType.string,
values: [
't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
],
},
],
}),
];
it('shows (no common labels) when showCommonLabels is set to true', () => {
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: true } });
expect(screen.getByText(/common labels:/i)).toBeInTheDocument();
expect(screen.getByText(/(no common labels)/i)).toBeInTheDocument();
});
it('does not show common labels when showCommonLabels is set to false', () => {
setup({ data: { series: seriesWithoutCommonLabels }, options: { showCommonLabels: false } });
expect(screen.queryByText(/common labels:/i)).not.toBeInTheDocument();
expect(screen.queryByText(/(no common labels)/i)).not.toBeInTheDocument();
});
});
});
const setup = (propsOverrides?: {}) => {
const props = ({
data: {
error: undefined,
request: {
panelId: 4,
dashboardId: 123,
app: 'dashboard',
requestId: 'A',
timezone: 'browser',
interval: '30s',
intervalMs: 30000,
maxDataPoints: 823,
targets: [],
range: {},
},
series: [],
state: LoadingState.Done,
},
timeZone: 'utc',
options: {},
title: 'Logs panel',
id: 1,
...propsOverrides,
} as unknown) as LogsPanelProps;
return render(<LogsPanel {...props} />);
};

View File

@@ -1,27 +1,29 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { LogRows, CustomScrollbar, useTheme2 } from '@grafana/ui'; import { LogRows, CustomScrollbar, LogLabels, useStyles2 } from '@grafana/ui';
import { PanelProps, Field } from '@grafana/data'; import { PanelProps, Field, Labels, GrafanaTheme2 } from '@grafana/data';
import { Options } from './types'; import { Options } from './types';
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model'; import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { COMMON_LABELS } from '../../../core/logs_model';
interface LogsPanelProps extends PanelProps<Options> {} interface LogsPanelProps extends PanelProps<Options> {}
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
data, data,
timeZone, timeZone,
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy, enableLogDetails }, options: { showLabels, showTime, wrapLogMessage, showCommonLabels, sortOrder, dedupStrategy, enableLogDetails },
title, title,
}) => { }) => {
const theme = useTheme2(); const style = useStyles2(getStyles(title));
// Important to memoize stuff here, as panel rerenders a lot for example when resizing. // Important to memoize stuff here, as panel rerenders a lot for example when resizing.
const [logRows, deduplicatedRows] = useMemo(() => { const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null; const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
const logRows = newResults?.rows || []; const logRows = newResults?.rows || [];
const commonLabels = newResults?.meta?.find((m) => m.label === COMMON_LABELS);
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy); const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
return [logRows, deduplicatedRows]; return [logRows, deduplicatedRows, commonLabels];
}, [data, dedupStrategy]); }, [data, dedupStrategy]);
const getFieldLinks = useCallback( const getFieldLinks = useCallback(
@@ -39,15 +41,15 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
); );
} }
const spacing = css`
margin-bottom: ${theme.spacing(1.5)};
//We can remove this hot-fix when we fix panel menu with no title overflowing top of all panels
margin-top: ${theme.spacing(!title ? 2.5 : 0)};
`;
return ( return (
<CustomScrollbar autoHide> <CustomScrollbar autoHide>
<div className={spacing}> <div className={style.container}>
{showCommonLabels && (
<div className={style.labelContainer}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
</div>
)}
<LogRows <LogRows
logRows={logRows} logRows={logRows}
deduplicatedRows={deduplicatedRows} deduplicatedRows={deduplicatedRows}
@@ -64,3 +66,21 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
</CustomScrollbar> </CustomScrollbar>
); );
}; };
const getStyles = (title: string) => (theme: GrafanaTheme2) => ({
container: css`
margin-bottom: ${theme.spacing(1.5)};
//We can remove this hot-fix when we fix panel menu with no title overflowing top of all panels
margin-top: ${theme.spacing(!title ? 2.5 : 0)};
`,
labelContainer: css`
margin: ${theme.spacing(0, 0, 0.5, 0.5)};
display: flex;
align-items: center;
`,
label: css`
margin-right: ${theme.spacing(0.5)};
font-size: ${theme.typography.bodySmall.fontSize};
font-weight: ${theme.typography.fontWeightMedium};
`,
});

View File

@@ -16,6 +16,12 @@ export const plugin = new PanelPlugin<Options>(LogsPanel).setPanelOptions((build
description: '', description: '',
defaultValue: false, defaultValue: false,
}) })
.addBooleanSwitch({
path: 'showCommonLabels',
name: 'Common labels',
description: '',
defaultValue: false,
})
.addBooleanSwitch({ .addBooleanSwitch({
path: 'wrapLogMessage', path: 'wrapLogMessage',
name: 'Wrap lines', name: 'Wrap lines',

View File

@@ -2,6 +2,7 @@ import { LogsSortOrder, LogsDedupStrategy } from '@grafana/data';
export interface Options { export interface Options {
showLabels: boolean; showLabels: boolean;
showCommonLabels: boolean;
showTime: boolean; showTime: boolean;
wrapLogMessage: boolean; wrapLogMessage: boolean;
enableLogDetails: boolean; enableLogDetails: boolean;