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

yao pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-3.5 by this push:
     new 63088872745 [SPARK-44367][SQL][UI] Show error message on UI for each 
failed query
63088872745 is described below

commit 630888727451d2ec7ba620a303d12bbda05d3801
Author: Kent Yao <[email protected]>
AuthorDate: Thu Jul 20 13:51:39 2023 +0800

    [SPARK-44367][SQL][UI] Show error message on UI for each failed query
    
    ### What changes were proposed in this pull request?
    
    This PR adds an 'error message' col to the failed query execution table on 
the SQL/DataFrame tab of UI.
    
    ### Why are the changes needed?
    
    The SQL tab of UI is not helping to detect SQL errors. This PR will provide 
users with a clear understanding of why their queries have failed.
    
    ### Does this PR introduce _any_ user-facing change?
    
    SQL tab of UI shows errors for failed queries
    
    ### How was this patch tested?
    
    built and tested locally
    
    
![image](https://github.com/apache/spark/assets/8326978/cf25e347-bb99-47f8-accd-aabaf8d3a9d8)
    
    Closes #41951 from yaooqinn/SPARK-44367.
    
    Authored-by: Kent Yao <[email protected]>
    Signed-off-by: Kent Yao <[email protected]>
    (cherry picked from commit 399705512a417de460529843eb047d5c2e8f9e22)
    Signed-off-by: Kent Yao <[email protected]>
---
 .../main/scala/org/apache/spark/ui/UIUtils.scala   | 28 ++++++++++++
 .../scala/org/apache/spark/ui/jobs/StagePage.scala | 15 +------
 .../org/apache/spark/ui/jobs/StageTable.scala      | 18 +-------
 .../scala/org/apache/spark/ui/UIUtilsSuite.scala   | 24 ++++++++++-
 .../apache/spark/sql/execution/SQLExecution.scala  |  2 +-
 .../spark/sql/execution/ui/AllExecutionsPage.scala | 50 +++++++++++++++++++---
 .../hive/thriftserver/ui/ThriftServerPage.scala    | 17 --------
 7 files changed, 97 insertions(+), 57 deletions(-)

diff --git a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala 
b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
index 0ce647d12c5..f0f8cf1310f 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -31,6 +31,7 @@ import scala.util.control.NonFatal
 import scala.xml._
 import scala.xml.transform.{RewriteRule, RuleTransformer}
 
+import org.apache.commons.text.StringEscapeUtils
 import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap
 
 import org.apache.spark.internal.Logging
@@ -708,4 +709,31 @@ private[spark] object UIUtils extends Logging {
       Seq.empty[Node]
     }
   }
+
+  private final val ERROR_CLASS_REGEX = 
"""\[(?<errorClass>[A-Z][A-Z_.]+[A-Z])]""".r
+
+  private def errorSummary(errorMessage: String): (String, Boolean) = {
+    var isMultiline = true
+    val maybeErrorClass =
+      
ERROR_CLASS_REGEX.findFirstMatchIn(errorMessage).map(_.group("errorClass"))
+    val errorClassOrBrief = if (maybeErrorClass.nonEmpty && 
maybeErrorClass.get.nonEmpty) {
+      maybeErrorClass.get
+    } else if (errorMessage.indexOf('\n') >= 0) {
+      errorMessage.substring(0, errorMessage.indexOf('\n'))
+    } else if (errorMessage.indexOf(":") >= 0) {
+      errorMessage.substring(0, errorMessage.indexOf(":"))
+    } else {
+      isMultiline = false
+      errorMessage
+    }
+
+    val errorSummary = StringEscapeUtils.escapeHtml4(errorClassOrBrief)
+    (errorSummary, isMultiline)
+  }
+
+  def errorMessageCell(errorMessage: String): Seq[Node] = {
+    val (summary, isMultiline) = errorSummary(errorMessage)
+    val details = detailsUINode(isMultiline, errorMessage)
+    <td>{summary}{details}</td>
+  }
 }
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala 
b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
index 1934e9e58e6..02aece6e50a 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/StagePage.scala
@@ -696,7 +696,7 @@ private[ui] class TaskPagedTable(
         <td>{formatBytes(task.taskMetrics.map(_.memoryBytesSpilled))}</td>
         <td>{formatBytes(task.taskMetrics.map(_.diskBytesSpilled))}</td>
       }}
-      {errorMessageCell(task.errorMessage.getOrElse(""))}
+      {UIUtils.errorMessageCell(task.errorMessage.getOrElse(""))}
     </tr>
   }
 
@@ -713,19 +713,6 @@ private[ui] class TaskPagedTable(
   private def metricInfo(task: TaskData)(fn: TaskMetrics => Seq[Node]): 
Seq[Node] = {
     task.taskMetrics.map(fn).getOrElse(Nil)
   }
-
-  private def errorMessageCell(error: String): Seq[Node] = {
-    val isMultiline = error.indexOf('\n') >= 0
-    // Display the first line by default
-    val errorSummary = StringEscapeUtils.escapeHtml4(
-      if (isMultiline) {
-        error.substring(0, error.indexOf('\n'))
-      } else {
-        error
-      })
-    val details = UIUtils.detailsUINode(isMultiline, error)
-    <td>{errorSummary}{details}</td>
-  }
 }
 
 private[spark] object ApiHelper {
diff --git a/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala 
b/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
index 9e6eb418fe1..9e78f29e92e 100644
--- a/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
+++ b/core/src/main/scala/org/apache/spark/ui/jobs/StageTable.scala
@@ -24,8 +24,6 @@ import javax.servlet.http.HttpServletRequest
 
 import scala.xml._
 
-import org.apache.commons.text.StringEscapeUtils
-
 import org.apache.spark.status.AppStatusStore
 import org.apache.spark.status.api.v1
 import org.apache.spark.ui._
@@ -217,7 +215,7 @@ private[ui] class StagePagedTable(
         <td>{data.shuffleWriteWithUnit}</td> ++
         {
           if (isFailedStage) {
-            failureReasonHtml(info)
+            UIUtils.errorMessageCell(info.failureReason.getOrElse(""))
           } else {
             Seq.empty
           }
@@ -225,20 +223,6 @@ private[ui] class StagePagedTable(
     }
   }
 
-  private def failureReasonHtml(s: v1.StageData): Seq[Node] = {
-    val failureReason = s.failureReason.getOrElse("")
-    val isMultiline = failureReason.indexOf('\n') >= 0
-    // Display the first line by default
-    val failureReasonSummary = StringEscapeUtils.escapeHtml4(
-      if (isMultiline) {
-        failureReason.substring(0, failureReason.indexOf('\n'))
-      } else {
-        failureReason
-      })
-    val details = UIUtils.detailsUINode(isMultiline, failureReason)
-    <td valign="middle">{failureReasonSummary}{details}</td>
-  }
-
   private def makeDescription(s: v1.StageData, descriptionOption: 
Option[String]): Seq[Node] = {
     val basePathUri = UIUtils.prependBaseUri(request, basePath)
 
diff --git a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala 
b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
index 9d040bb4e1e..aecd25f6c8d 100644
--- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
@@ -20,7 +20,7 @@ package org.apache.spark.ui
 import scala.xml.{Node, Text}
 import scala.xml.Utility.trim
 
-import org.apache.spark.SparkFunSuite
+import org.apache.spark.{ErrorMessageFormat, SparkException, SparkFunSuite, 
SparkThrowableHelper}
 
 class UIUtilsSuite extends SparkFunSuite {
   import UIUtils._
@@ -189,4 +189,26 @@ class UIUtilsSuite extends SparkFunSuite {
     assert(generated.sameElements(expected),
       s"\n$errorMsg\n\nExpected:\n$expected\nGenerated:\n$generated")
   }
+
+  // scalastyle:off line.size.limit
+  test("SPARK-44367: Extract errorClass from errorMsg with errorMessageCell") {
+    val e1 = "Job aborted due to stage failure: Task 0 in stage 1.0 failed 1 
times, most recent failure: Lost task 0.0 in stage 1.0 (TID 1) (10.221.98.22 
executor driver): org.apache.spark.SparkArithmeticException: [DIVIDE_BY_ZERO] 
Division by zero. Use `try_divide` to tolerate divisor being 0 and return NULL 
instead. If necessary set \"spark.sql.ansi.enabled\" to \"false\" to bypass 
this error.\n== SQL(line 1, position 8) ==\nselect a/b from src\n       
^^^\n\n\tat org.apache.spark.sql. [...]
+    val cell1 = UIUtils.errorMessageCell(e1)
+    assert(cell1 === <td>{"DIVIDE_BY_ZERO"}{UIUtils.detailsUINode(isMultiline 
= true, e1)}</td>)
+
+    val e2 = SparkException.internalError("test")
+    val cell2 = UIUtils.errorMessageCell(e2.getMessage)
+    assert(cell2 === <td>{"INTERNAL_ERROR"}{UIUtils.detailsUINode(isMultiline 
= true, e2.getMessage)}</td>)
+
+    val e3 = new SparkException(
+      errorClass = "CANNOT_CAST_DATATYPE",
+      messageParameters = Map("sourceType" -> "long", "targetType" -> "int"), 
cause = null)
+    val cell3 = UIUtils.errorMessageCell(SparkThrowableHelper.getMessage(e3, 
ErrorMessageFormat.PRETTY))
+    assert(cell3 === 
<td>{"CANNOT_CAST_DATATYPE"}{UIUtils.detailsUINode(isMultiline = true, 
e3.getMessage)}</td>)
+
+    val e4 = "java.lang.RuntimeException: random text"
+    val cell4 = UIUtils.errorMessageCell(e4)
+    assert(cell4 === 
<td>{"java.lang.RuntimeException"}{UIUtils.detailsUINode(isMultiline = true, 
e4)}</td>)
+  }
+  // scalastyle:on line.size.limit
 }
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SQLExecution.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SQLExecution.scala
index eeca1669e74..68b29e9e216 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/execution/SQLExecution.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/SQLExecution.scala
@@ -124,7 +124,7 @@ object SQLExecution {
           val endTime = System.nanoTime()
           val errorMessage = ex.map {
             case e: SparkThrowable =>
-              SparkThrowableHelper.getMessage(e, ErrorMessageFormat.MINIMAL)
+              SparkThrowableHelper.getMessage(e, ErrorMessageFormat.PRETTY)
             case e =>
               // unexpected behavior
               SparkThrowableHelper.getMessage(e)
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
index a69ca1bbc80..2e088ec8e4b 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
@@ -75,8 +75,16 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends 
WebUIPage("") with L
 
       if (running.nonEmpty) {
         val runningPageTable =
-          executionsTable(request, "running", running.toSeq,
-            executionIdToSubExecutions.mapValues(_.toSeq).toMap, currentTime, 
true, true, true)
+          executionsTable(
+            request,
+            "running",
+            running.toSeq,
+            executionIdToSubExecutions.mapValues(_.toSeq).toMap,
+            currentTime,
+            showErrorMessage = false,
+            showRunningJobs = true,
+            showSucceededJobs = true,
+            showFailedJobs = true)
 
         _content ++=
           <span id="running" class="collapse-aggregated-runningExecutions 
collapse-table"
@@ -93,9 +101,16 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends 
WebUIPage("") with L
       }
 
       if (completed.nonEmpty) {
-        val completedPageTable =
-          executionsTable(request, "completed", completed.toSeq,
-            executionIdToSubExecutions.mapValues(_.toSeq).toMap, currentTime, 
false, true, false)
+        val completedPageTable = executionsTable(
+          request,
+          "completed",
+          completed.toSeq,
+          executionIdToSubExecutions.mapValues(_.toSeq).toMap,
+          currentTime,
+          showErrorMessage = false,
+          showRunningJobs = false,
+          showSucceededJobs = true,
+          showFailedJobs = false)
 
         _content ++=
           <span id="completed" class="collapse-aggregated-completedExecutions 
collapse-table"
@@ -113,8 +128,16 @@ private[ui] class AllExecutionsPage(parent: SQLTab) 
extends WebUIPage("") with L
 
       if (failed.nonEmpty) {
         val failedPageTable =
-          executionsTable(request, "failed", failed.toSeq,
-            executionIdToSubExecutions.mapValues(_.toSeq).toMap, currentTime, 
false, true, true)
+          executionsTable(
+            request,
+            "failed",
+            failed.toSeq,
+            executionIdToSubExecutions.mapValues(_.toSeq).toMap,
+            currentTime,
+            showErrorMessage = true,
+            showRunningJobs = false,
+            showSucceededJobs = true,
+            showFailedJobs = true)
 
         _content ++=
           <span id="failed" class="collapse-aggregated-failedExecutions 
collapse-table"
@@ -176,6 +199,7 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends 
WebUIPage("") with L
     executionData: Seq[SQLExecutionUIData],
     executionIdToSubExecutions: Map[Long, Seq[SQLExecutionUIData]],
     currentTime: Long,
+    showErrorMessage: Boolean,
     showRunningJobs: Boolean,
     showSucceededJobs: Boolean,
     showFailedJobs: Boolean): Seq[Node] = {
@@ -195,6 +219,7 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends 
WebUIPage("") with L
         UIUtils.prependBaseUri(request, parent.basePath),
         "SQL", // subPath
         currentTime,
+        showErrorMessage,
         showRunningJobs,
         showSucceededJobs,
         showFailedJobs,
@@ -220,6 +245,7 @@ private[ui] class ExecutionPagedTable(
     basePath: String,
     subPath: String,
     currentTime: Long,
+    showErrorMessage: Boolean,
     showRunningJobs: Boolean,
     showSucceededJobs: Boolean,
     showFailedJobs: Boolean,
@@ -287,6 +313,12 @@ private[ui] class ExecutionPagedTable(
       } else {
         Seq(("Job IDs", true, None))
       }
+    } ++ {
+      if (showErrorMessage) {
+        Seq(("Error Message", true, None))
+      } else {
+        Nil
+      }
     } ++ {
       if (showSubExecutions) {
         Seq(("Sub Execution IDs", true, None))
@@ -363,6 +395,9 @@ private[ui] class ExecutionPagedTable(
             {jobLinks(executionTableRow.failedJobData)}
           </td>
         }}
+        {if (showErrorMessage) {
+          UIUtils.errorMessageCell(executionUIData.errorMessage.getOrElse(""))
+        }}
         {if (showSubExecutions) {
           <td>
             
{executionLinks(executionTableRow.subExecutionData.map(_.executionUIData.executionId))}
@@ -536,6 +571,7 @@ private[ui] class ExecutionDataSource(
       case "Job IDs" | "Succeeded Job IDs" => Ordering by 
(_.completedJobData.headOption)
       case "Running Job IDs" => Ordering.by(_.runningJobData.headOption)
       case "Failed Job IDs" => Ordering.by(_.failedJobData.headOption)
+      case "Error Message" => Ordering.by(_.executionUIData.errorMessage)
       case unknownColumn => throw 
QueryExecutionErrors.unknownColumnError(unknownColumn)
     }
     if (desc) {
diff --git 
a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
 
b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
index d0378efd646..d47a99466a5 100644
--- 
a/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
+++ 
b/sql/hive-thriftserver/src/main/scala/org/apache/spark/sql/hive/thriftserver/ui/ThriftServerPage.scala
@@ -23,8 +23,6 @@ import javax.servlet.http.HttpServletRequest
 
 import scala.xml.Node
 
-import org.apache.commons.text.StringEscapeUtils
-
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.hive.thriftserver.ui.ToolTips._
 import org.apache.spark.ui._
@@ -274,21 +272,6 @@ private[ui] class SqlStatsPagedTable(
     </tr>
   }
 
-
-  private def errorMessageCell(errorMessage: String): Seq[Node] = {
-    val isMultiline = errorMessage.indexOf('\n') >= 0
-    val errorSummary = StringEscapeUtils.escapeHtml4(
-      if (isMultiline) {
-        errorMessage.substring(0, errorMessage.indexOf('\n'))
-      } else {
-        errorMessage
-      })
-    val details = detailsUINode(isMultiline, errorMessage)
-    <td>
-      {errorSummary}{details}
-    </td>
-  }
-
   private def jobURL(request: HttpServletRequest, jobId: String): String =
     "%s/jobs/job/?id=%s".format(UIUtils.prependBaseUri(request, 
parent.basePath), jobId)
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to