mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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' },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
109
public/app/plugins/panel/logs/LogsPanel.test.tsx
Normal file
109
public/app/plugins/panel/logs/LogsPanel.test.tsx
Normal 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} />);
|
||||||
|
};
|
||||||
@@ -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};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user