This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 7a9de0307f9 UI: Add toggle to show or hide log levels in task logs 
(#72363)
7a9de0307f9 is described below

commit 7a9de0307f9437c1ce850c7a029cf1d4eae686b9
Author: PolitePp <[email protected]>
AuthorDate: Wed Sep 23 17:51:13 2026 +0300

    UI: Add toggle to show or hide log levels in task logs (#72363)
    
    * issue-72360: UI: Add toggle to show or hide log levels in task logs
    
    * issue-72360: UI: update icon for the button
    
    * issue-72360: fix statis check, move renderWithLevel to the outer scope
    
    ---------
    
    Co-authored-by: Vladimir Krokodilov <[email protected]>
---
 .../airflow/ui/public/i18n/locales/en/common.json  |  6 ++++
 .../ui/src/components/renderStructuredLog.test.tsx | 33 ++++++++++++++++++++++
 .../ui/src/components/renderStructuredLog.tsx      |  4 ++-
 .../src/airflow/ui/src/constants/localStorage.ts   |  1 +
 .../ui/src/context/keyboardShortcuts/shortcuts.ts  |  5 ++++
 .../ui/src/pages/TaskInstance/Logs/Logs.tsx        | 17 ++++++++++-
 .../src/pages/TaskInstance/Logs/TaskLogHeader.tsx  |  9 ++++++
 .../ui/src/pages/TaskInstance/Logs/utils.test.ts   | 13 +++++++++
 .../ui/src/pages/TaskInstance/Logs/utils.ts        |  3 ++
 .../src/airflow/ui/src/queries/useLogs.test.ts     | 12 ++++++++
 .../src/airflow/ui/src/queries/useLogs.tsx         | 19 ++++++++++++-
 .../airflow/ui/tests/e2e/specs/task-logs.spec.ts   |  6 ++++
 12 files changed, 125 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
index 0f5d63160f1..fe8b8b93d0d 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json
@@ -168,6 +168,11 @@
   "generateToken": "Generate Token",
   "includedIn": "Included in",
   "key": "Key",
+  "logLevel": {
+    "hide": "Hide Log Levels",
+    "hotkey": "l",
+    "show": "Show Log Levels"
+  },
   "logicalDate": "Logical Date",
   "logout": "Logout",
   "logoutConfirmation": "You are about to logout from the application.",
@@ -352,6 +357,7 @@
       "toggleExpand": "Expand or collapse all groups",
       "toggleFullscreen": "Toggle fullscreen",
       "toggleGraphGrid": "Toggle graph / grid view",
+      "toggleLogLevel": "Toggle log levels",
       "toggleSource": "Toggle source",
       "toggleTaskGroup": "Expand or collapse $t(taskGroup_one)",
       "toggleTimestamp": "Toggle timestamps",
diff --git 
a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
index 655522ef45e..af86bc0a853 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
@@ -26,6 +26,23 @@ import { renderStructuredLog, renderTIContextPreamble, 
tiContextFields } from ".
 
 const translate = (key: string) => key;
 
+const renderWithLevel = (showLogLevel: boolean) => {
+  const result = renderStructuredLog({
+    index: 0,
+    logLink: "",
+    logMessage: {
+      event: "Task started",
+      level: "info",
+      timestamp: "2026-01-01T00:00:00Z",
+    },
+    renderingMode: "jsx",
+    showLogLevel,
+    translate: translate as never,
+  });
+
+  return render(<Wrapper>{result}</Wrapper>);
+};
+
 describe("tiContextFields", () => {
   it("contains the six fields bound via bind_contextvars", () => {
     expect(tiContextFields).toEqual(
@@ -76,6 +93,22 @@ describe("renderStructuredLog — traceback frame 
highlighting", () => {
   });
 });
 
+describe("renderStructuredLog — log level visibility", () => {
+  it("renders the level when showLogLevel is true", () => {
+    renderWithLevel(true);
+
+    expect(screen.getByText("INFO")).toBeInTheDocument();
+    expect(screen.getByText("Task started")).toBeInTheDocument();
+  });
+
+  it("omits the level when showLogLevel is false", () => {
+    renderWithLevel(false);
+
+    expect(screen.queryByText("INFO")).toBeNull();
+    expect(screen.getByText("Task started")).toBeInTheDocument();
+  });
+});
+
 describe("renderStructuredLog — TI context field stripping", () => {
   it("does not render TI context fields as per-line structured attributes", () 
=> {
     const result = renderStructuredLog({
diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
index dd7fc9a84dc..37fdb5d1c15 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -52,6 +52,7 @@ type RenderStructuredLogProps = {
   logLink: string;
   logMessage: string | StructuredLogMessage;
   renderingMode?: "jsx" | "text";
+  showLogLevel?: boolean;
   showSource?: boolean;
   showTimestamp?: boolean;
   sourceFilters?: Array<string>;
@@ -181,6 +182,7 @@ const renderStructuredLogImpl = ({
   logLink,
   logMessage,
   renderingMode = "jsx",
+  showLogLevel = true,
   showSource = true,
   showTimestamp = true,
   sourceFilters,
@@ -227,7 +229,7 @@ const renderStructuredLogImpl = ({
     }
   }
 
-  if (typeof level === "string") {
+  if (typeof level === "string" && showLogLevel) {
     const formattedLevel = level.toUpperCase();
 
     if (renderingMode === "text") {
diff --git a/airflow-core/src/airflow/ui/src/constants/localStorage.ts 
b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
index 16926fe8c66..1152dd06f5b 100644
--- a/airflow-core/src/airflow/ui/src/constants/localStorage.ts
+++ b/airflow-core/src/airflow/ui/src/constants/localStorage.ts
@@ -26,6 +26,7 @@ export const CALENDAR_VIEW_MODE_KEY = "calendar-view-mode";
 export const LOG_WRAP_KEY = "log_wrap";
 export const LOG_SHOW_TIMESTAMP_KEY = "log_show_timestamp";
 export const LOG_SHOW_SOURCE_KEY = "log_show_source";
+export const LOG_SHOW_LOG_LEVEL_KEY = "log_show_log_level";
 export const VERSION_INDICATOR_DISPLAY_MODE_KEY = 
"version_indicator_display_mode";
 export const COLLAPSED_UI_ALERTS_KEY = "collapsed_ui_alerts";
 export const SHOW_ALL_DEPENDENCIES_KEY = "show_all_dependencies";
diff --git 
a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts 
b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts
index 1271037a99f..4482f944eac 100644
--- a/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts
+++ b/airflow-core/src/airflow/ui/src/context/keyboardShortcuts/shortcuts.ts
@@ -101,6 +101,11 @@ export const SHORTCUTS = {
       descriptionKey: "shortcuts.descriptions.toggleFullscreen",
       keys: "f",
     },
+    toggleLogLevel: {
+      category: "logs",
+      descriptionKey: "shortcuts.descriptions.toggleLogLevel",
+      keys: "l",
+    },
     toggleSource: { category: "logs", descriptionKey: 
"shortcuts.descriptions.toggleSource", keys: "s" },
     toggleTimestamp: {
       category: "logs",
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
index c64fbe9191f..65a23edebf7 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
@@ -27,7 +27,12 @@ import { useTaskInstanceServiceGetMappedTaskInstance } from 
"openapi/queries";
 
 import { Modal } from "src/system-components";
 
-import { LOG_SHOW_SOURCE_KEY, LOG_SHOW_TIMESTAMP_KEY, LOG_WRAP_KEY } from 
"src/constants/localStorage";
+import {
+  LOG_SHOW_LOG_LEVEL_KEY,
+  LOG_SHOW_SOURCE_KEY,
+  LOG_SHOW_TIMESTAMP_KEY,
+  LOG_WRAP_KEY,
+} from "src/constants/localStorage";
 import { SearchParamsKeys } from "src/constants/searchParams";
 import { SHORTCUTS } from "src/context/keyboardShortcuts";
 import { useShortcut } from "src/hooks/useShortcut";
@@ -82,6 +87,7 @@ export const Logs = () => {
   const [wrap, setWrap] = useLocalStorage<boolean>(LOG_WRAP_KEY, defaultWrap);
   const [showTimestamp, setShowTimestamp] = 
useLocalStorage<boolean>(LOG_SHOW_TIMESTAMP_KEY, true);
   const [showSource, setShowSource] = 
useLocalStorage<boolean>(LOG_SHOW_SOURCE_KEY, false);
+  const [showLogLevel, setShowLogLevel] = 
useLocalStorage<boolean>(LOG_SHOW_LOG_LEVEL_KEY, true);
   const [fullscreen, setFullscreen] = useState(false);
   const [expanded, setExpanded] = useState(false);
 
@@ -93,6 +99,7 @@ export const Logs = () => {
   } = useLogs({
     dagId,
     logLevelFilters,
+    showLogLevel,
     showSource,
     showTimestamp,
     sourceFilters,
@@ -103,6 +110,7 @@ export const Logs = () => {
   const downloadTextLines = getDownloadText({
     fetchedData,
     logLevelFilters,
+    showLogLevel,
     showSource,
     showTimestamp,
     sourceFilters,
@@ -160,6 +168,7 @@ export const Logs = () => {
 
   const toggleWrap = () => setWrap(!wrap);
   const toggleTimestamp = () => setShowTimestamp(!showTimestamp);
+  const toggleLogLevel = () => setShowLogLevel(!showLogLevel);
   const toggleSource = () => setShowSource(!showSource);
   const toggleFullscreen = () => setFullscreen(!fullscreen);
   const toggleExpanded = () => setExpanded((act) => !act);
@@ -180,6 +189,10 @@ export const Logs = () => {
     ...SHORTCUTS.logs.toggleTimestamp,
     callback: toggleTimestamp,
   });
+  useShortcut({
+    ...SHORTCUTS.logs.toggleLogLevel,
+    callback: toggleLogLevel,
+  });
   useShortcut({
     ...SHORTCUTS.logs.toggleSource,
     callback: toggleSource,
@@ -209,12 +222,14 @@ export const Logs = () => {
       searchQuery,
       totalMatches: searchMatchIndices.length,
     },
+    showLogLevel,
     showSource,
     showTimestamp,
     sourceOptions: parsedData.sources,
     taskInstance,
     toggleExpanded,
     toggleFullscreen,
+    toggleLogLevel,
     toggleSource,
     toggleTimestamp,
     toggleWrap,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
index c351c5f666e..108587ab864 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogHeader.tsx
@@ -24,6 +24,7 @@ import {
   MdCompress,
   MdExpand,
   MdOutlineFileDownload,
+  MdOutlineLabel,
   MdOutlineOpenInFull,
   MdSettings,
   MdWrapText,
@@ -49,12 +50,14 @@ export type TaskLogHeaderProps = {
   readonly isFullscreen?: boolean;
   readonly onSelectTryNumber: (tryNumber: number) => void;
   readonly search: LogSearchInputProps;
+  readonly showLogLevel: boolean;
   readonly showSource: boolean;
   readonly showTimestamp: boolean;
   readonly sourceOptions?: Array<string>;
   readonly taskInstance?: TaskInstanceResponse;
   readonly toggleExpanded?: () => void;
   readonly toggleFullscreen: () => void;
+  readonly toggleLogLevel: () => void;
   readonly toggleSource: () => void;
   readonly toggleTimestamp: () => void;
   readonly toggleWrap: () => void;
@@ -69,12 +72,14 @@ export const TaskLogHeader = ({
   isFullscreen = false,
   onSelectTryNumber,
   search,
+  showLogLevel,
   showSource,
   showTimestamp,
   sourceOptions,
   taskInstance,
   toggleExpanded,
   toggleFullscreen,
+  toggleLogLevel,
   toggleSource,
   toggleTimestamp,
   toggleWrap,
@@ -217,6 +222,10 @@ export const TaskLogHeader = ({
                 <MdAccessTime /> {showTimestamp ? translate("timestamp.hide") 
: translate("timestamp.show")}
                 
<Menu.ItemCommand>{translate("timestamp.hotkey")}</Menu.ItemCommand>
               </Menu.Item>
+              <Menu.Item data-testid="log-settings-log-level" 
onClick={toggleLogLevel} value="log-level">
+                <MdOutlineLabel /> {showLogLevel ? translate("logLevel.hide") 
: translate("logLevel.show")}
+                
<Menu.ItemCommand>{translate("logLevel.hotkey")}</Menu.ItemCommand>
+              </Menu.Item>
               <Menu.Item data-testid="log-settings-expand" 
onClick={toggleExpanded} value="expand">
                 {expanded ? (
                   <>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
index 6bb68d7192a..eb0d6567edd 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.test.ts
@@ -38,6 +38,7 @@ const tiLine = (event: string, timestamp: string) => ({
 describe("getDownloadText", () => {
   const baseOptions = {
     logLevelFilters: [],
+    showLogLevel: true,
     showSource: false,
     showTimestamp: false,
     sourceFilters: [],
@@ -103,6 +104,18 @@ describe("getDownloadText", () => {
 
     expect(lines.every((line) => !line.includes("Task Identity"))).toBe(true);
   });
+
+  it("omits log level prefix when showLogLevel is false", () => {
+    const fetchedData = {
+      content: [{ event: "Task started", level: "info", timestamp: 
"2026-01-01T00:00:00Z" }],
+      continuation_token: null,
+    };
+
+    const lines = getDownloadText({ ...baseOptions, fetchedData, showLogLevel: 
false });
+
+    expect(lines).toContain("Task started");
+    expect(lines.every((line) => !line.includes("INFO -"))).toBe(true);
+  });
 });
 
 describe("getHighlightColor", () => {
diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
index ad88b22e6d6..e3040081940 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/utils.ts
@@ -34,6 +34,7 @@ export const getGroupHeaderMarker = (isExpanded: boolean): 
string => (isExpanded
 type GetDownloadTextOptions = {
   fetchedData: TaskInstancesLogResponse | undefined;
   logLevelFilters: Array<string>;
+  showLogLevel: boolean;
   showSource: boolean;
   showTimestamp: boolean;
   sourceFilters: Array<string>;
@@ -48,6 +49,7 @@ type GetDownloadTextOptions = {
 export const getDownloadText = ({
   fetchedData,
   logLevelFilters,
+  showLogLevel,
   showSource,
   showTimestamp,
   sourceFilters,
@@ -63,6 +65,7 @@ export const getDownloadText = ({
       logLink: "",
       logMessage: line,
       renderingMode: "text",
+      showLogLevel,
       showSource,
       showTimestamp,
       sourceFilters,
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts 
b/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
index 2f56c48e608..4671c69c138 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.test.ts
@@ -50,6 +50,18 @@ describe("getLogLineText", () => {
     );
   });
 
+  it("omits the level when showLogLevel is false", () => {
+    const logMessage = {
+      event: "task done",
+      level: "info",
+      timestamp: "2026-01-01T00:00:00Z",
+    } as StructuredLogMessage;
+
+    expect(
+      getLogLineText({ logMessage, showLogLevel: false, showSource: false, 
showTimestamp: true, translate }),
+    ).toBe("[2026-01-01T00:00:00Z] task done");
+  });
+
   it("strips ANSI codes from plain string lines", () => {
     expect(getLogLineText({ logMessage: "plain \u001B[32mok\u001B[0m line", 
translate })).toBe(
       "plain ok line",
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 143d47a3b19..6e102a0f33b 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -53,6 +53,7 @@ export type ParsedLogEntry = {
 type GetLogLineTextOptions = {
   logLevelFilters?: Array<string>;
   logMessage: string | StructuredLogMessage;
+  showLogLevel?: boolean;
   showSource?: boolean;
   showTimestamp?: boolean;
   sourceFilters?: Array<string>;
@@ -67,6 +68,7 @@ type GetLogLineTextOptions = {
 export const getLogLineText = ({
   logLevelFilters,
   logMessage,
+  showLogLevel,
   showSource,
   showTimestamp,
   sourceFilters,
@@ -79,6 +81,7 @@ export const getLogLineText = ({
       logLink: "",
       logMessage,
       renderingMode: "text",
+      showLogLevel,
       showSource,
       showTimestamp,
       sourceFilters,
@@ -91,6 +94,7 @@ type Props = {
   dagId: string;
   limit?: number;
   logLevelFilters?: Array<string>;
+  showLogLevel?: boolean;
   showSource?: boolean;
   showTimestamp?: boolean;
   sourceFilters?: Array<string>;
@@ -101,6 +105,7 @@ type Props = {
 type ParseLogsProps = {
   data: TaskInstancesLogResponse["content"];
   logLevelFilters?: Array<string>;
+  showLogLevel?: boolean;
   showSource?: boolean;
   showTimestamp?: boolean;
   sourceFilters?: Array<string>;
@@ -112,6 +117,7 @@ type ParseLogsProps = {
 const parseLogs = ({
   data,
   logLevelFilters,
+  showLogLevel,
   showSource,
   showTimestamp,
   sourceFilters,
@@ -157,6 +163,7 @@ const parseLogs = ({
             logLink,
             logMessage: datum,
             renderingMode: "jsx",
+            showLogLevel,
             showSource,
             showTimestamp,
             sourceFilters,
@@ -186,7 +193,15 @@ const parseLogs = ({
     parsedLines.forEach(({ element, lineNumber, logMessage }) => {
       const text = innerText(element);
       const getPlainText = () =>
-        getLogLineText({ logLevelFilters, logMessage, showSource, 
showTimestamp, sourceFilters, translate });
+        getLogLineText({
+          logLevelFilters,
+          logMessage,
+          showLogLevel,
+          showSource,
+          showTimestamp,
+          sourceFilters,
+          translate,
+        });
 
       if (text.includes("::group::")) {
         const groupName = text.split("::group::")[1] as string;
@@ -294,6 +309,7 @@ export const useLogs = (
     dagId,
     limit,
     logLevelFilters,
+    showLogLevel,
     showSource,
     showTimestamp,
     sourceFilters,
@@ -329,6 +345,7 @@ export const useLogs = (
   const parsedData = parseLogs({
     data: parseStreamingLogContent(truncateData(data, limit)),
     logLevelFilters,
+    showLogLevel,
     showSource,
     showTimestamp,
     sourceFilters,
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
index 0f60a5400b9..6c12f45af03 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/task-logs.spec.ts
@@ -78,6 +78,12 @@ test.describe("Verify task logs display", () => {
     await page.getByTestId("log-settings-timestamp").click();
     await expect(virtualizedList).not.toContainText(/\[\d{4}-\d{2}-\d{2} 
\d{2}:\d{2}:\d{2}]/);
 
+    await expect(virtualizedList).toContainText(/INFO\s-/);
+
+    await page.getByTestId("log-settings-button").click();
+    await page.getByTestId("log-settings-log-level").click();
+    await expect(virtualizedList).not.toContainText(/INFO\s-/);
+
     await page.getByTestId("log-settings-button").click();
     await page.getByTestId("log-settings-source").click();
     await expect(virtualizedList).toContainText(/source/);

Reply via email to