You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/10/18 16:51:35 UTC

(spark) branch branch-4.3 updated: [SPARK-60311][CORE][UI] Respect UI root in History Server log page

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

dongjoon-hyun pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.3 by this push:
     new 0d304ea6796a [SPARK-60311][CORE][UI] Respect UI root in History Server log page
0d304ea6796a is described below

commit 0d304ea6796a0cd9f766e636255d42faad4e2c8a
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Sun Oct 18 09:51:04 2026 -0700

    [SPARK-60311][CORE][UI] Respect UI root in History Server log page
    
    ### What changes were proposed in this pull request?
    
    This PR aims to respect the UI root (`spark.ui.proxyBase`, `APPLICATION_WEB_PROXY_BASE`, or the Knox `X-Forwarded-Context` header) in the History Server log page.
    
    - `LogPage`: The `Back to Main page` link uses `UIUtils.prependBaseUri`.
    - `log-view.js`: The `Load More` and `Load New` buttons request `uiRoot + "/log"` instead of `/log`. The `proxy` branch for a Worker behind the Master's reverse proxy is unchanged.
    
    Since `log-view.js` is shared, the buttons of the Master and Driver log pages are fixed too.
    
    ### Why are the changes needed?
    
    Under a UI root, e.g., behind an ingress or Knox, the link and the buttons go outside of the UI root and don't work. This bug exists since Apache Spark [v4.0.0](https://github.com/apache/spark/releases/tag/v4.0.0) (2025-05-19), which added the History Server log page (SPARK-46903).
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, the `Back to Main page` link and the `Load More` and `Load New` buttons work under a UI root. There is no change when the UI root is empty (default).
    
    ### How was this patch tested?
    
    Pass the CIs with the updated `HistoryServerSuite` and the new `ui-test/tests/log-view.test.js`, which fail without this fix.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5.5
    
    Closes #59569 from dongjoon-hyun/SPARK-60311.
    
    Authored-by: Dongjoon Hyun <do...@apache.org>
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
    (cherry picked from commit 74dbd3a0740078d68b4ddf576868693d2490bfeb)
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
 .../org/apache/spark/ui/static/log-view.js         |  4 +-
 .../org/apache/spark/deploy/history/LogPage.scala  |  2 +-
 .../spark/deploy/history/HistoryServerSuite.scala  | 10 ++---
 ui-test/tests/log-view.test.js                     | 52 ++++++++++++++++++++++
 4 files changed, 60 insertions(+), 8 deletions(-)

diff --git a/core/src/main/resources/org/apache/spark/ui/static/log-view.js b/core/src/main/resources/org/apache/spark/ui/static/log-view.js
index f1fc6c915d5c..76e368f4ef34 100644
--- a/core/src/main/resources/org/apache/spark/ui/static/log-view.js
+++ b/core/src/main/resources/org/apache/spark/ui/static/log-view.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-/* global $ */
+/* global $, uiRoot */
 
 var baseParams;
 
@@ -63,7 +63,7 @@ function getRESTEndPoint() {
   if (ind > 0) {
     return words.slice(0, ind + 2).join('/') + "/log";
   }
-  return "/log"
+  return uiRoot + "/log";
 }
 
 /* eslint-disable no-unused-vars */
diff --git a/core/src/main/scala/org/apache/spark/deploy/history/LogPage.scala b/core/src/main/scala/org/apache/spark/deploy/history/LogPage.scala
index 9b14e76db37a..05cbda4070f2 100644
--- a/core/src/main/scala/org/apache/spark/deploy/history/LogPage.scala
+++ b/core/src/main/scala/org/apache/spark/deploy/history/LogPage.scala
@@ -63,7 +63,7 @@ private[history] class LogPage(conf: SparkConf) extends WebUIPage("logPage") wit
     val content =
       <script type="module" src={UIUtils.prependBaseUri(request, "/static/utils.js")}></script> ++
       <div>
-        <p><a href="/">Back to Main page</a></p>
+        <p><a href={UIUtils.prependBaseUri(request, "/")}>Back to Main page</a></p>
         {range}
         <div class="log-content overflow-auto p-1" style="height:80vh;">
           <div>{moreButton}</div>
diff --git a/core/src/test/scala/org/apache/spark/deploy/history/HistoryServerSuite.scala b/core/src/test/scala/org/apache/spark/deploy/history/HistoryServerSuite.scala
index 79e0001fa22f..835f8943dd64 100644
--- a/core/src/test/scala/org/apache/spark/deploy/history/HistoryServerSuite.scala
+++ b/core/src/test/scala/org/apache/spark/deploy/history/HistoryServerSuite.scala
@@ -353,11 +353,11 @@ abstract class HistoryServerSuite extends SparkFunSuite with BeforeAndAfter with
       "spark.ui.proxyBase is defined but it should not for this UT")
     assert(sys.env.get("APPLICATION_WEB_PROXY_BASE").isEmpty,
       "APPLICATION_WEB_PROXY_BASE is defined but it should not for this UT")
-    val page = new HistoryPage(server)
+    val pages = Seq(new HistoryPage(server), new LogPage(new SparkConf(false)))
     val requestThroughKnox = mock[HttpServletRequest]
     val knoxBaseUrl = "/gateway/default/sparkhistoryui"
     when(requestThroughKnox.getHeader("X-Forwarded-Context")).thenReturn(knoxBaseUrl)
-    val responseThroughKnox = page.render(requestThroughKnox)
+    val responseThroughKnox = pages.flatMap(_.render(requestThroughKnox))
 
     val urlsThroughKnox = responseThroughKnox \\ "@href" map (_.toString)
     val siteRelativeLinksThroughKnox = urlsThroughKnox filter (_.startsWith("/"))
@@ -366,7 +366,7 @@ abstract class HistoryServerSuite extends SparkFunSuite with BeforeAndAfter with
     }
 
     val directRequest = mock[HttpServletRequest]
-    val directResponse = page.render(directRequest)
+    val directResponse = pages.flatMap(_.render(directRequest))
 
     val directUrls = directResponse \\ "@href" map (_.toString)
     val directSiteRelativeLinks = directUrls filter (_.startsWith("/"))
@@ -377,12 +377,12 @@ abstract class HistoryServerSuite extends SparkFunSuite with BeforeAndAfter with
 
   test("static relative links are prefixed with uiRoot (spark.ui.proxyBase)") {
     val uiRoot = Option(System.getenv("APPLICATION_WEB_PROXY_BASE")).getOrElse("/testwebproxybase")
-    val page = new HistoryPage(server)
+    val pages = Seq(new HistoryPage(server), new LogPage(new SparkConf(false)))
     val request = mock[HttpServletRequest]
 
     // when
     System.setProperty("spark.ui.proxyBase", uiRoot)
-    val response = page.render(request)
+    val response = pages.flatMap(_.render(request))
 
     // then
     val urls = response \\ "@href" map (_.toString)
diff --git a/ui-test/tests/log-view.test.js b/ui-test/tests/log-view.test.js
new file mode 100644
index 000000000000..892d6fdc6100
--- /dev/null
+++ b/ui-test/tests/log-view.test.js
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+/**
+ * @jest-environment jsdom
+ */
+
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+// log-view.js is a classic (non-module) script, so read it and return the helper under test.
+// The script reads the global uiRoot, which webui.js sets from the server-rendered UI root.
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const logViewPath = join(
+  __dirname, '../../core/src/main/resources/org/apache/spark/ui/static/log-view.js');
+const { getRESTEndPoint } = new Function(
+  readFileSync(logViewPath, 'utf8') + '\nreturn { getRESTEndPoint };')();
+
+function openLogPage(root, path) {
+  window.uiRoot = root;
+  window.history.replaceState(null, '', path);
+}
+
+// A History Server behind an ingress (spark.ui.proxyBase) or Knox (X-Forwarded-Context) is only
+// reachable under the UI root.
+test('getRESTEndPoint prefixes the UI root', function () {
+  openLogPage('/gateway/default/sparkhistoryui',
+    '/gateway/default/sparkhistoryui/logPage/?self&logType=out');
+  expect(getRESTEndPoint()).toBe('/gateway/default/sparkhistoryui/log');
+});
+
+// A worker's UI root is the reverse proxy URL without the /proxy/<workerId> part.
+test('getRESTEndPoint keeps the reverse proxy path to a worker', function () {
+  openLogPage('/path/to/spark',
+    '/path/to/spark/proxy/worker-1/logPage/?appId=app-1&executorId=0&logType=stdout');
+  expect(getRESTEndPoint()).toBe('http://localhost/path/to/spark/proxy/worker-1/log');
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org