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);