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

pierrejeambrun 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 60c7db4637d Render stacktraces properly (#47965)
60c7db4637d is described below

commit 60c7db4637d04ac00d3c6592842cf379c067ede1
Author: Aritra Basu <[email protected]>
AuthorDate: Wed Mar 26 19:08:49 2025 +0530

    Render stacktraces properly (#47965)
    
    * Render stacktraces properly
    Closes: #47921
    
    * Update airflow/ui/src/components/RenderStructuredLog.tsx
    
    Co-authored-by: Jens Scheffler <[email protected]>
    
    * Fixes ut, resolves comments
    
    Fixed the UT which was failing due to a
    mistaken id, and removed the usage of the
    quotes as escaped characters and usage of delete
    
    * Remove unsed import
    
    Co-authored-by: Jens Scheffler <[email protected]>
    
    * Remove delete statement
    
    ---------
    
    Co-authored-by: Jens Scheffler <[email protected]>
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../ui/src/components/renderStructuredLog.tsx      | 179 +++++++++++++++++++++
 .../src/airflow/ui/src/queries/useLogs.tsx         | 118 +-------------
 2 files changed, 182 insertions(+), 115 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx 
b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
new file mode 100644
index 00000000000..87d364aa466
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/renderStructuredLog.tsx
@@ -0,0 +1,179 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { chakra, Code } from "@chakra-ui/react";
+import { Link } from "react-router-dom";
+
+import type { StructuredLogMessage } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { LogLevel, logLevelColorMapping } from "src/utils/logs";
+
+type Frame = {
+  filename: string;
+  lineno: number;
+  name: string;
+};
+
+type ErrorDetail = {
+  exc_notes: Array<string>;
+  exc_type: string;
+  exc_value: string;
+  frames: Array<Frame>;
+  is_cause: boolean;
+  syntax_error: string | null;
+};
+
+type RenderStructuredLogProps = {
+  index: number;
+  logLevelFilters?: Array<string>;
+  logLink: string;
+  logMessage: string | StructuredLogMessage;
+  sourceFilters?: Array<string>;
+};
+
+export const renderStructuredLog = ({
+  index,
+  logLevelFilters,
+  logLink,
+  logMessage,
+  sourceFilters,
+}: RenderStructuredLogProps) => {
+  if (typeof logMessage === "string") {
+    return (
+      <chakra.span key={index} lineHeight={1.5}>
+        {logMessage}
+      </chakra.span>
+    );
+  }
+
+  const { event, level = undefined, timestamp, ...structured } = logMessage;
+
+  const elements = [];
+
+  if (
+    logLevelFilters !== undefined &&
+    Boolean(logLevelFilters.length) &&
+    ((typeof level === "string" && !logLevelFilters.includes(level)) || 
!Boolean(level))
+  ) {
+    return "";
+  }
+
+  if (
+    sourceFilters !== undefined &&
+    Boolean(sourceFilters.length) &&
+    (("logger" in structured && !sourceFilters.includes(structured.logger as 
string)) ||
+      !("logger" in structured))
+  ) {
+    return "";
+  }
+
+  elements.push(
+    <Link
+      id={index.toString()}
+      key={`line_${index}`}
+      style={{
+        display: "inline-block",
+        marginRight: "10px",
+        paddingRight: "5px",
+        textAlign: "right",
+        userSelect: "none",
+        WebkitUserSelect: "none",
+        width: "3em",
+      }}
+      to={`${logLink}#${index}`}
+    >
+      {index}
+    </Link>,
+  );
+
+  if (Boolean(timestamp)) {
+    elements.push("[", <Time datetime={timestamp} key={0} />, "] ");
+  }
+
+  if (typeof level === "string") {
+    elements.push(
+      <Code
+        colorPalette={level.toUpperCase() in LogLevel ? 
logLevelColorMapping[level as LogLevel] : undefined}
+        key={1}
+        lineHeight={1.5}
+        minH={0}
+        px={0}
+      >
+        {level.toUpperCase()}
+      </Code>,
+      " - ",
+    );
+  }
+
+  const { error_detail: errorDetail, ...reStructured } = structured;
+  let details;
+
+  if (errorDetail !== undefined) {
+    details = (errorDetail as Array<ErrorDetail>).map((error) => {
+      const errorLines = error.frames.map((frame) => (
+        <chakra.p key="test">
+          File{" "}
+          <chakra.span color="fg.info" key="test">
+            {JSON.stringify(frame.filename)}
+          </chakra.span>
+          , line {frame.lineno} in {frame.name}
+        </chakra.p>
+      ));
+
+      return (
+        <details key={error.exc_type} open={true} style={{ marginLeft: "20em" 
}}>
+          <summary data-testid={`summary-${error.exc_type}`}>
+            <chakra.span color="fg.info" cursor="pointer">
+              {error.exc_type}: {error.exc_value}
+            </chakra.span>
+          </summary>
+          {errorLines}
+        </details>
+      );
+    });
+  }
+
+  elements.push(
+    <chakra.span className="event" key={2} style={{ whiteSpace: "pre-wrap" }}>
+      {event}
+    </chakra.span>,
+  );
+
+  for (const key in reStructured) {
+    if (Object.hasOwn(reStructured, key)) {
+      elements.push(
+        ": ",
+        <chakra.span color={key === "logger" ? "fg.info" : undefined} 
key={`prop_${key}`}>
+          {key === "logger" ? "source" : 
key}={JSON.stringify(reStructured[key])}
+        </chakra.span>,
+      );
+    }
+  }
+
+  elements.push(
+    <chakra.span className="event" key={3} style={{ whiteSpace: "pre-wrap" }}>
+      {details}
+    </chakra.span>,
+  );
+
+  return (
+    <chakra.p key={index} lineHeight={1.5}>
+      {elements}
+    </chakra.p>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx 
b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
index 7e9128a624b..3007deee70d 100644
--- a/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useLogs.tsx
@@ -16,22 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { chakra, Code } from "@chakra-ui/react";
+import { chakra } from "@chakra-ui/react";
 import type { UseQueryOptions } from "@tanstack/react-query";
 import dayjs from "dayjs";
 import innerText from "react-innertext";
-import { Link } from "react-router-dom";
 
 import { useTaskInstanceServiceGetLog } from "openapi/queries";
-import type {
-  StructuredLogMessage,
-  TaskInstanceResponse,
-  TaskInstancesLogResponse,
-} from "openapi/requests/types.gen";
-import Time from "src/components/Time";
+import type { TaskInstanceResponse, TaskInstancesLogResponse } from 
"openapi/requests/types.gen";
+import { renderStructuredLog } from "src/components/renderStructuredLog";
 import { isStatePending, useAutoRefresh } from "src/utils";
 import { getTaskInstanceLink } from "src/utils/links";
-import { LogLevel, logLevelColorMapping } from "src/utils/logs";
 
 type Props = {
   dagId: string;
@@ -49,112 +43,6 @@ type ParseLogsProps = {
   tryNumber: number;
 };
 
-type RenderStructuredLogProps = {
-  index: number;
-  logLevelFilters?: Array<string>;
-  logLink: string;
-  logMessage: string | StructuredLogMessage;
-  sourceFilters?: Array<string>;
-};
-
-const renderStructuredLog = ({
-  index,
-  logLevelFilters,
-  logLink,
-  logMessage,
-  sourceFilters,
-}: RenderStructuredLogProps) => {
-  if (typeof logMessage === "string") {
-    return (
-      <chakra.span key={index} lineHeight={1.5}>
-        {logMessage}
-      </chakra.span>
-    );
-  }
-
-  const { event, level = undefined, timestamp, ...structured } = logMessage;
-
-  const elements = [];
-
-  if (
-    logLevelFilters !== undefined &&
-    Boolean(logLevelFilters.length) &&
-    ((typeof level === "string" && !logLevelFilters.includes(level)) || 
!Boolean(level))
-  ) {
-    return "";
-  }
-
-  if (
-    sourceFilters !== undefined &&
-    Boolean(sourceFilters.length) &&
-    (("logger" in structured && !sourceFilters.includes(structured.logger as 
string)) ||
-      !("logger" in structured))
-  ) {
-    return "";
-  }
-
-  elements.push(
-    <Link
-      id={index.toString()}
-      key={`line_${index}`}
-      style={{
-        display: "inline-block",
-        marginRight: "10px",
-        paddingRight: "5px",
-        textAlign: "right",
-        userSelect: "none",
-        WebkitUserSelect: "none",
-        width: "3em",
-      }}
-      to={`${logLink}#${index}`}
-    >
-      {index}
-    </Link>,
-  );
-
-  if (Boolean(timestamp)) {
-    elements.push("[", <Time datetime={timestamp} key={0} />, "] ");
-  }
-
-  if (typeof level === "string") {
-    elements.push(
-      <Code
-        colorPalette={level.toUpperCase() in LogLevel ? 
logLevelColorMapping[level as LogLevel] : undefined}
-        key={1}
-        lineHeight={1.5}
-        minH={0}
-        px={0}
-      >
-        {level.toUpperCase()}
-      </Code>,
-      " - ",
-    );
-  }
-
-  elements.push(
-    <chakra.span className="event" key={2} style={{ whiteSpace: "pre-wrap" }}>
-      {event}
-    </chakra.span>,
-  );
-
-  for (const key in structured) {
-    if (Object.hasOwn(structured, key)) {
-      elements.push(
-        " ",
-        <chakra.span color={key === "logger" ? "fg.info" : undefined} 
key={`prop_${key}`}>
-          {key === "logger" ? "source" : key}={JSON.stringify(structured[key])}
-        </chakra.span>,
-      );
-    }
-  }
-
-  return (
-    <chakra.p key={index} lineHeight={1.5}>
-      {elements}
-    </chakra.p>
-  );
-};
-
 const parseLogs = ({ data, logLevelFilters, sourceFilters, taskInstance, 
tryNumber }: ParseLogsProps) => {
   let warning;
   let parsedLines;

Reply via email to