mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -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:
parent
f152180dc3
commit
742c737784
@ -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.
|
||||
|
@ -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' },
|
||||
]);
|
||||
|
@ -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,
|
||||
});
|
||||
|
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 { 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};
|
||||
`,
|
||||
});
|
||||
|
@ -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',
|
||||
|
@ -2,6 +2,7 @@ import { LogsSortOrder, LogsDedupStrategy } from '@grafana/data';
|
||||
|
||||
export interface Options {
|
||||
showLabels: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showTime: boolean;
|
||||
wrapLogMessage: boolean;
|
||||
enableLogDetails: boolean;
|
||||
|
Loading…
Reference in New Issue
Block a user