diff --git a/docs/sources/panels/visualizations/logs-panel.md b/docs/sources/panels/visualizations/logs-panel.md index aa01ebbf228..66121d25229 100644 --- a/docs/sources/panels/visualizations/logs-panel.md +++ b/docs/sources/panels/visualizations/logs-panel.md @@ -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. diff --git a/public/app/core/logs_model.test.ts b/public/app/core/logs_model.test.ts index 1fc68c61359..d9a5366f290 100644 --- a/public/app/core/logs_model.test.ts +++ b/public/app/core/logs_model.test.ts @@ -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' }, ]); diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 2dd5eae8ae0..6c70b26e278 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -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, }); diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx new file mode 100644 index 00000000000..9274d668cda --- /dev/null +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -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; + +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(); +}; diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 20767c9bed2..a55c4401fad 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -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 {} export const LogsPanel: React.FunctionComponent = ({ 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 = ({ ); } - 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 ( -
+
+ {showCommonLabels && ( +
+ Common labels: + +
+ )} = ({ ); }; + +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}; + `, +}); diff --git a/public/app/plugins/panel/logs/module.tsx b/public/app/plugins/panel/logs/module.tsx index e0233faf775..98fa6233e98 100644 --- a/public/app/plugins/panel/logs/module.tsx +++ b/public/app/plugins/panel/logs/module.tsx @@ -16,6 +16,12 @@ export const plugin = new PanelPlugin(LogsPanel).setPanelOptions((build description: '', defaultValue: false, }) + .addBooleanSwitch({ + path: 'showCommonLabels', + name: 'Common labels', + description: '', + defaultValue: false, + }) .addBooleanSwitch({ path: 'wrapLogMessage', name: 'Wrap lines', diff --git a/public/app/plugins/panel/logs/types.ts b/public/app/plugins/panel/logs/types.ts index d1513ca7942..c117f9ba6d8 100644 --- a/public/app/plugins/panel/logs/types.ts +++ b/public/app/plugins/panel/logs/types.ts @@ -2,6 +2,7 @@ import { LogsSortOrder, LogsDedupStrategy } from '@grafana/data'; export interface Options { showLabels: boolean; + showCommonLabels: boolean; showTime: boolean; wrapLogMessage: boolean; enableLogDetails: boolean;