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 79d3afc82b5 Show source code lines in task log tracebacks (#75100)
79d3afc82b5 is described below

commit 79d3afc82b5f9873cffcb9b6f2e8a992fe1c1d52
Author: Souradeep Biswas <[email protected]>
AuthorDate: Thu Oct 22 01:51:13 2026 +0530

    Show source code lines in task log tracebacks (#75100)
    
    structlog's ExceptionDictTransformer only records each frame's filename,
    line number and function name, so tracebacks in task logs (which are always
    written as JSON) no longer showed the line of code that was running. Add it
    via linecache, as the stdlib traceback module does, and show it under each
    frame in the UI log view.
    
    closes: #55910
---
 .../ui/src/components/renderStructuredLog.test.tsx | 30 ++++++++++++++++++++++
 .../ui/src/components/renderStructuredLog.tsx      | 10 +++++++-
 .../src/airflow_shared/logging/structlog.py        | 15 ++++++++++-
 shared/logging/tests/logging/test_structlog.py     |  1 +
 4 files changed, 54 insertions(+), 2 deletions(-)

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 321b9476394..4efab10141e 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.test.tsx
@@ -98,6 +98,36 @@ describe("renderStructuredLog — traceback frame 
highlighting", () => {
   });
 });
 
+describe("renderStructuredLog — traceback source lines", () => {
+  const props = {
+    index: 0,
+    logLink: "",
+    logMessage: {
+      error_detail: [
+        {
+          exc_notes: [],
+          exc_type: "ZeroDivisionError",
+          exc_value: "division by zero",
+          frames: [{ filename: "/dags/my_dag.py", line: "return 1 / 0", 
lineno: 12, name: "divide" }],
+          is_cause: false,
+          syntax_error: null,
+        },
+      ],
+      event: "Task failed with exception",
+    },
+    translate: translate as never,
+  };
+
+  it("shows the source line under its frame", () => {
+    render(<Wrapper>{renderStructuredLog({ ...props, renderingMode: "jsx" 
})}</Wrapper>);
+
+    expect(screen.getByText("return 1 / 0")).toBeInTheDocument();
+    expect(renderStructuredLog({ ...props, renderingMode: "text" })).toContain(
+      "components:logs.location\n      return 1 / 0\n",
+    );
+  });
+});
+
 describe("renderStructuredLog — log level visibility", () => {
   it("renders the level when showLogLevel is true", () => {
     renderWithLevel(true);
diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
index 8744cccee62..bcd18fab2cb 100644
--- a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -33,6 +33,7 @@ import { isUserCodeFrame, LogLevel, logLevelColorMapping } 
from "src/utils/logs"
 
 type Frame = {
   filename: string;
+  line?: string;
   lineno: number;
   name: string;
 };
@@ -258,7 +259,9 @@ const renderStructuredLogImpl = ({
     details = (errorDetail as Array<ErrorDetail>).map((error) => {
       const errorLines = error.frames.map((frame) => {
         if (renderingMode === "text") {
-          return `    ${translate("components:logs.file")} ${frame.filename}, 
${translate("components:logs.location", { line: frame.lineno, name: frame.name 
})}\n`;
+          const sourceLine = frame.line === undefined ? "" : `      
${frame.line}\n`;
+
+          return `    ${translate("components:logs.file")} ${frame.filename}, 
${translate("components:logs.location", { line: frame.lineno, name: frame.name 
})}\n${sourceLine}`;
         }
 
         // Highlight user-code frames (DAG bundle, plugins, local files) in 
the info blue,
@@ -276,6 +279,11 @@ const renderStructuredLogImpl = ({
               {JSON.stringify(frame.filename)}
             </chakra.span>
             , {translate("components:logs.location", { line: frame.lineno, 
name: frame.name })}
+            {frame.line === undefined ? undefined : (
+              <chakra.span display="block" ps={4} whiteSpace="pre-wrap">
+                {frame.line}
+              </chakra.span>
+            )}
           </chakra.p>
         );
       });
diff --git a/shared/logging/src/airflow_shared/logging/structlog.py 
b/shared/logging/src/airflow_shared/logging/structlog.py
index de3ca21db50..82dd2010c92 100644
--- a/shared/logging/src/airflow_shared/logging/structlog.py
+++ b/shared/logging/src/airflow_shared/logging/structlog.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 import codecs
 import io
 import itertools
+import linecache
 import logging
 import os
 import re
@@ -363,6 +364,16 @@ def _make_safe_enc_hook(default):
     return safe_enc_hook
 
 
+def _add_source_lines(stacks: list[dict[str, Any]]) -> list[dict[str, Any]]:
+    """Add each frame's source code ``line``, which structlog's 
``ExceptionDictTransformer`` omits."""
+    for stack in stacks:
+        for frame in stack["frames"]:
+            linecache.checkcache(frame["filename"])
+            if line := linecache.getline(frame["filename"], 
frame["lineno"]).strip():
+                frame["line"] = line
+    return stacks
+
+
 @cache
 def structlog_processors(
     json_output: bool,
@@ -436,7 +447,9 @@ def structlog_processors(
             use_rich=False, show_locals=False, suppress=suppress
         )
 
-        dict_tracebacks = 
structlog.processors.ExceptionRenderer(dict_exc_formatter)
+        dict_tracebacks = structlog.processors.ExceptionRenderer(
+            lambda exc_info: _add_source_lines(dict_exc_formatter(exc_info))
+        )
 
         import msgspec
 
diff --git a/shared/logging/tests/logging/test_structlog.py 
b/shared/logging/tests/logging/test_structlog.py
index 0f8df20fd39..30f7386ad8a 100644
--- a/shared/logging/tests/logging/test_structlog.py
+++ b/shared/logging/tests/logging/test_structlog.py
@@ -436,6 +436,7 @@ def test_json_exc(structlog_config, get_logger, 
monkeypatch):
                         "filename": __file__,
                         "lineno": lineno,
                         "name": "test_json_exc",
+                        "line": "1 / 0",
                     },
                 ],
                 "is_cause": False,

Reply via email to