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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.
- **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.
- **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.

View File

@ -16,6 +16,7 @@ import {
logSeriesToLogsModel,
filterLogLevels,
LIMIT_LABEL,
COMMON_LABELS,
} from './logs_model';
describe('dedupLogRows()', () => {
@ -322,7 +323,7 @@ describe('dataFrameToLogsModel', () => {
]);
expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels',
label: COMMON_LABELS,
value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap,
});
@ -392,7 +393,7 @@ describe('dataFrameToLogsModel', () => {
expect(logsModel.series).toHaveLength(2);
expect(logsModel.meta).toHaveLength(3);
expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels',
label: COMMON_LABELS,
value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap,
});
@ -537,7 +538,7 @@ describe('dataFrameToLogsModel', () => {
]);
expect(logsModel.meta).toHaveLength(1);
expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels',
label: COMMON_LABELS,
value: {
foo: 'bar',
},
@ -686,7 +687,7 @@ describe('dataFrameToLogsModel', () => {
const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 });
expect(logsModel.meta).toHaveLength(2);
expect(logsModel.meta![0]).toMatchObject({
label: 'Common labels',
label: COMMON_LABELS,
value: series[0].fields[1].labels,
kind: LogsMetaKind.LabelsMap,
});
@ -798,7 +799,7 @@ describe('logSeriesToLogsModel', () => {
const logsModel = dataFrameToLogsModel(logSeries, 0);
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: 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';
export const LIMIT_LABEL = 'Line limit';
export const COMMON_LABELS = 'Common labels';
export const LogLevelColor = {
[LogLevel.critical]: colors[7],
@ -395,7 +396,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
const meta: LogsMetaItem[] = [];
if (size(commonLabels) > 0) {
meta.push({
label: 'Common labels',
label: COMMON_LABELS,
value: commonLabels,
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 { css } from '@emotion/css';
import { LogRows, CustomScrollbar, useTheme2 } from '@grafana/ui';
import { PanelProps, Field } from '@grafana/data';
import { LogRows, CustomScrollbar, LogLabels, useStyles2 } from '@grafana/ui';
import { PanelProps, Field, Labels, GrafanaTheme2 } from '@grafana/data';
import { Options } from './types';
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { COMMON_LABELS } from '../../../core/logs_model';
interface LogsPanelProps extends PanelProps<Options> {}
export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
data,
timeZone,
options: { showLabels, showTime, wrapLogMessage, sortOrder, dedupStrategy, enableLogDetails },
options: { showLabels, showTime, wrapLogMessage, showCommonLabels, sortOrder, dedupStrategy, enableLogDetails },
title,
}) => {
const theme = useTheme2();
const style = useStyles2(getStyles(title));
// 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 logRows = newResults?.rows || [];
const commonLabels = newResults?.meta?.find((m) => m.label === COMMON_LABELS);
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
return [logRows, deduplicatedRows];
return [logRows, deduplicatedRows, commonLabels];
}, [data, dedupStrategy]);
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 (
<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}
deduplicatedRows={deduplicatedRows}
@ -64,3 +66,21 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
</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: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'showCommonLabels',
name: 'Common labels',
description: '',
defaultValue: false,
})
.addBooleanSwitch({
path: 'wrapLogMessage',
name: 'Wrap lines',

View File

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