diff --git a/public/app/features/explore/Logs/LogsMetaRow.test.tsx b/public/app/features/explore/Logs/LogsMetaRow.test.tsx index 4210df96dff..a40c3c27cb9 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.test.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.test.tsx @@ -48,7 +48,7 @@ describe('LogsMetaRow', () => { }); it('renders the show original line button', () => { - setup({ displayedFields: ['test'] }); + setup({ displayedFields: ['test'], defaultDisplayedFields: ['Time', 'detected_level', '___LOG_LINE_BODY___'] }); expect( screen.getByRole('button', { name: 'Show original line', @@ -66,13 +66,20 @@ describe('LogsMetaRow', () => { }); it('renders the displayed fields', async () => { - setup({ displayedFields: ['testField1234'] }); + setup({ + displayedFields: ['testField1234'], + defaultDisplayedFields: ['Time', 'detected_level', '___LOG_LINE_BODY___'], + }); expect(await screen.findByText('testField1234')).toBeInTheDocument(); }); it('renders a button to clear displayedfields', () => { const clearSpy = jest.fn(); - setup({ displayedFields: ['testField1234'], clearDisplayedFields: clearSpy }); + setup({ + displayedFields: ['testField1234'], + defaultDisplayedFields: ['Time', 'detected_level', '___LOG_LINE_BODY___'], + clearDisplayedFields: clearSpy, + }); fireEvent( screen.getByRole('button', { name: 'Show original line', diff --git a/public/app/features/explore/Logs/LogsTableWrap.test.tsx b/public/app/features/explore/Logs/LogsTableWrap.test.tsx index cccef55e349..a7df442d37a 100644 --- a/public/app/features/explore/Logs/LogsTableWrap.test.tsx +++ b/public/app/features/explore/Logs/LogsTableWrap.test.tsx @@ -84,7 +84,7 @@ describe('LogsTableWrap', () => { await waitFor(() => { expect(updatePanelState).toBeCalledWith({ visualisationType: 'table', - columns: { 0: 'app', 1: 'Line', 2: 'Time' }, + displayedFields: ['app', '___LOG_LINE_BODY___', 'Time'], labelFieldName: 'labels', }); }); @@ -97,7 +97,7 @@ describe('LogsTableWrap', () => { await waitFor(() => { expect(updatePanelState).toBeCalledWith({ visualisationType: 'table', - columns: { 0: 'Line', 1: 'Time' }, + displayedFields: ['___LOG_LINE_BODY___', 'Time'], labelFieldName: 'labels', }); }); diff --git a/public/app/features/logs/components/LogDetailsBody.tsx b/public/app/features/logs/components/LogDetailsBody.tsx index aa23f9cea5a..5a67704fe4a 100644 --- a/public/app/features/logs/components/LogDetailsBody.tsx +++ b/public/app/features/logs/components/LogDetailsBody.tsx @@ -35,6 +35,7 @@ export const LOG_LINE_BODY_FIELD_NAME = '___LOG_LINE_BODY___'; export const TABLE_TIME_FIELD_NAME = 'Time'; export const TABLE_LINE_FIELD_NAME = 'Line'; export const TABLE_DETECTED_LEVEL_FIELD_NAME = 'detected_level'; +export const TABLE_LEVEL_FIELD_NAME = 'level'; export const LogDetailsBody = (props: Props) => { const showField = () => { diff --git a/public/app/features/logs/components/otel/formats.ts b/public/app/features/logs/components/otel/formats.ts index 23e6ec3559f..c8d7b7e7c9e 100644 --- a/public/app/features/logs/components/otel/formats.ts +++ b/public/app/features/logs/components/otel/formats.ts @@ -42,10 +42,7 @@ function getDisplayedFieldsForLanguages(logs: LogListModel[] | LogRowModel[], la }); return displayedFields.filter( - (field) => - field === LOG_LINE_BODY_FIELD_NAME || - field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME || - logs.some((log) => log.labels[field] !== undefined) + (field) => field === OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME || logs.some((log) => log.labels[field] !== undefined) ); } diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx index 468cd63a3b3..0131f9e5bed 100644 --- a/public/app/features/logs/components/panel/LogList.test.tsx +++ b/public/app/features/logs/components/panel/LogList.test.tsx @@ -15,7 +15,7 @@ import { import { config, reportInteraction } from '@grafana/runtime'; import { disablePopoverMenu, enablePopoverMenu, isPopoverMenuDisabled } from '../../utils'; -import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody'; +import { LOG_LINE_BODY_FIELD_NAME, TABLE_TIME_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME } from '../LogDetailsBody'; import { createLogLine, createLogRow } from '../mocks/logRow'; import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, OTEL_PROBE_FIELD } from '../otel/formats'; @@ -129,8 +129,18 @@ describe('LogList', () => { ); expect(screen.getByText('log message 1')).toBeInTheDocument(); - expect(onLogOptionsChange).not.toHaveBeenCalled(); - expect(setDisplayedFields).not.toHaveBeenCalled(); + // Even when OTel is disabled, we still report table defaults + expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', [ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + LOG_LINE_BODY_FIELD_NAME, + ]); + // setDisplayedFields is called with the default fields + expect(setDisplayedFields).toHaveBeenCalledWith([ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + LOG_LINE_BODY_FIELD_NAME, + ]); config.featureToggles.otelLogsFormatting = originalState; }); @@ -144,10 +154,19 @@ describe('LogList', () => { ); expect(screen.getByText('log message 1')).toBeInTheDocument(); - expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', []); + // For non-OTel logs, we report table defaults only (no OTel attributes field) + expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', [ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + LOG_LINE_BODY_FIELD_NAME, + ]); - // No fields to display, no call - expect(setDisplayedFields).not.toHaveBeenCalled(); + // setDisplayedFields is called with the default fields + expect(setDisplayedFields).toHaveBeenCalledWith([ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + LOG_LINE_BODY_FIELD_NAME, + ]); config.featureToggles.otelLogsFormatting = originalState; }); @@ -168,11 +187,19 @@ describe('LogList', () => { /> ); expect(screen.getByText('log message 1')).toBeInTheDocument(); + // For OTel logs, we report table defaults + OTel fields expect(onLogOptionsChange).toHaveBeenCalledWith('defaultDisplayedFields', [ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + LOG_LINE_BODY_FIELD_NAME, + OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, + ]); + expect(setDisplayedFields).toHaveBeenCalledWith([ + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, LOG_LINE_BODY_FIELD_NAME, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, ]); - expect(setDisplayedFields).toHaveBeenCalledWith([LOG_LINE_BODY_FIELD_NAME, OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME]); config.featureToggles.otelLogsFormatting = originalState; }); diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 44712978fe1..6533d3e0aa8 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -27,9 +27,14 @@ import { config, getDataSourceSrv } from '@grafana/runtime'; import { PopoverContent } from '@grafana/ui'; import { checkLogsError, checkLogsSampled, downloadLogs as download, DownloadFormat } from '../../utils'; -import { LOG_LINE_BODY_FIELD_NAME, TABLE_TIME_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME } from '../LogDetailsBody'; +import { + LOG_LINE_BODY_FIELD_NAME, + TABLE_TIME_FIELD_NAME, + TABLE_DETECTED_LEVEL_FIELD_NAME, + TABLE_LEVEL_FIELD_NAME, +} from '../LogDetailsBody'; import { getFieldSelectorState } from '../fieldSelector/FieldSelector'; -import { OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME, getDisplayedFieldsForLogs } from '../otel/formats'; +import { getDisplayedFieldsForLogs } from '../otel/formats'; import { getDefaultDetailsMode, getDetailsWidth } from './LogDetailsContext'; import { LogLineTimestampResolution } from './LogLine'; @@ -121,11 +126,23 @@ export const useLogIsPermalinked = (log: LogListModel) => { /** * Get default table fields. - * Always returns the table field constants: Time and detected_level (excluding Line). + * Always returns Time, and detected_level if it exists in the logs (excluding Line). */ function getTableDefaultFields(logs: LogRowModel[]): string[] { - // Always return the table field constants (Time and detected_level, excluding Line) - return [TABLE_TIME_FIELD_NAME, TABLE_DETECTED_LEVEL_FIELD_NAME]; + const fields: string[] = [TABLE_TIME_FIELD_NAME]; + + // Check if detected_level exists in any log's labels, fall back to level if not found + const hasDetectedLevel = logs.some((log) => log.labels?.[TABLE_DETECTED_LEVEL_FIELD_NAME] !== undefined); + const hasLevel = !hasDetectedLevel && logs.some((log) => log.labels?.[TABLE_LEVEL_FIELD_NAME] !== undefined); + + if (hasDetectedLevel) { + fields.push(TABLE_DETECTED_LEVEL_FIELD_NAME); + } else if (hasLevel) { + // Fall back to level if detected_level is not present + fields.push(TABLE_LEVEL_FIELD_NAME); + } + + return fields; } export type LogListState = Pick< @@ -273,11 +290,14 @@ export const LogListContextProvider = ({ }, []); const otelDisplayedFields = useMemo(() => { - if (!config.featureToggles.otelLogsFormatting || !setDisplayedFields || showLogAttributes === false) { + if (!config.featureToggles.otelLogsFormatting) { + return []; + } + if (showLogAttributes === false) { return []; } return getDisplayedFieldsForLogs(logs); - }, [logs, setDisplayedFields, showLogAttributes]); + }, [logs, showLogAttributes]); // Get table default fields const tableDefaultFields = useMemo(() => { @@ -287,34 +307,10 @@ export const LogListContextProvider = ({ // Combine table defaults with OTel defaults in specific order: // ['Time', 'detected_level', '___LOG_LINE_BODY___', '___OTEL_LOG_ATTRIBUTES___'] const defaultDisplayedFields = useMemo(() => { - const orderedFields: string[] = []; - - // 1. Add Time from table defaults - if (tableDefaultFields.includes(TABLE_TIME_FIELD_NAME)) { - orderedFields.push(TABLE_TIME_FIELD_NAME); - } - - // 2. Add detected_level from table defaults - if (tableDefaultFields.includes(TABLE_DETECTED_LEVEL_FIELD_NAME)) { - orderedFields.push(TABLE_DETECTED_LEVEL_FIELD_NAME); - } - - // 3. Always add LOG_LINE_BODY + const orderedFields: string[] = tableDefaultFields; + // Always add LOG_LINE_BODY orderedFields.push(LOG_LINE_BODY_FIELD_NAME); - - // 4. Always add OTEL_LOG_ATTRIBUTES - orderedFields.push(OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME); - - // 5. Add any other OTel fields that aren't already included - otelDisplayedFields.forEach((field) => { - if ( - field !== LOG_LINE_BODY_FIELD_NAME && - field !== OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME && - !orderedFields.includes(field) - ) { - orderedFields.push(field); - } - }); + orderedFields.push(...otelDisplayedFields); return orderedFields; }, [tableDefaultFields, otelDisplayedFields]); diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index f5325d21108..07353f6656a 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -223,6 +223,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(OLDEST_LOGS_LABEL_REGEX)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('sortOrder', LogsSortOrder.Descending); @@ -235,6 +237,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(DEDUPE_LABEL_COPY)); await userEvent.click(screen.getByText('Numbers')); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); @@ -286,6 +290,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(SHOW_TIMESTAMP_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('showTime', true); @@ -298,6 +304,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(WRAP_LINES_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('wrapLogMessage', true); @@ -318,6 +326,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText('Wrap disabled')); await userEvent.click(screen.getByText('Enable line wrapping')); @@ -353,6 +363,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(TIMESTAMP_LABEL_COPY)); await userEvent.click(screen.getByText('Show millisecond timestamps')); @@ -381,6 +393,8 @@ describe('LogListControls', () => { ); + // Clear any initial calls (e.g., from defaultDisplayedFields) + onLogOptionsChange.mockClear(); await userEvent.click(screen.getByLabelText(ENABLE_HIGHLIGHTING_LABEL_COPY)); expect(onLogOptionsChange).toHaveBeenCalledTimes(1); expect(onLogOptionsChange).toHaveBeenCalledWith('syntaxHighlighting', true);