[SPARK-8171][WEB UI] Javascript based infinite scrolling for the log page

Updated the log page by replacing the current pagination with a javascript-based infinite scroll solution

Author: Alex Bozarth <ajbozart@us.ibm.com>

Closes #10910 from ajbozarth/spark8171.
This commit is contained in:
Alex Bozarth 2016-04-20 21:24:11 +09:00 committed by Kousuke Saruta
parent ed9d803854
commit 834277884f
5 changed files with 174 additions and 43 deletions

View file

@ -0,0 +1,129 @@
/*
* 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.
*/
var baseParams;
var curLogLength;
var startByte;
var endByte;
var totalLogLength;
var byteLength;
function setLogScroll(oldHeight) {
var logContent = $(".log-content");
logContent.scrollTop(logContent[0].scrollHeight - oldHeight);
}
function tailLog() {
var logContent = $(".log-content");
logContent.scrollTop(logContent[0].scrollHeight);
}
function setLogData() {
$('#log-data').html("Showing " + curLogLength + " Bytes: " + startByte
+ " - " + endByte + " of " + totalLogLength);
}
function disableMoreButton() {
var moreBtn = $(".log-more-btn");
moreBtn.attr("disabled", "disabled");
moreBtn.html("Top of Log");
}
function noNewAlert() {
var alert = $(".no-new-alert");
alert.css("display", "block");
window.setTimeout(function () {alert.css("display", "none");}, 4000);
}
function loadMore() {
var offset = Math.max(startByte - byteLength, 0);
var moreByteLength = Math.min(byteLength, startByte);
$.ajax({
type: "GET",
url: "/log" + baseParams + "&offset=" + offset + "&byteLength=" + moreByteLength,
success: function (data) {
var oldHeight = $(".log-content")[0].scrollHeight;
var newlineIndex = data.indexOf('\n');
var dataInfo = data.substring(0, newlineIndex).match(/\d+/g);
var retStartByte = dataInfo[0];
var retLogLength = dataInfo[2];
var cleanData = data.substring(newlineIndex + 1);
if (retStartByte == 0) {
disableMoreButton();
}
$("pre", ".log-content").prepend(cleanData);
curLogLength = curLogLength + (startByte - retStartByte);
startByte = retStartByte;
totalLogLength = retLogLength;
setLogScroll(oldHeight);
setLogData();
}
});
}
function loadNew() {
$.ajax({
type: "GET",
url: "/log" + baseParams + "&byteLength=0",
success: function (data) {
var dataInfo = data.substring(0, data.indexOf('\n')).match(/\d+/g);
var newDataLen = dataInfo[2] - totalLogLength;
if (newDataLen != 0) {
$.ajax({
type: "GET",
url: "/log" + baseParams + "&byteLength=" + newDataLen,
success: function (data) {
var newlineIndex = data.indexOf('\n');
var dataInfo = data.substring(0, newlineIndex).match(/\d+/g);
var retStartByte = dataInfo[0];
var retEndByte = dataInfo[1];
var retLogLength = dataInfo[2];
var cleanData = data.substring(newlineIndex + 1);
$("pre", ".log-content").append(cleanData);
curLogLength = curLogLength + (retEndByte - retStartByte);
endByte = retEndByte;
totalLogLength = retLogLength;
tailLog();
setLogData();
}
});
} else {
noNewAlert();
}
}
});
}
function initLogPage(params, logLen, start, end, totLogLen, defaultLen) {
baseParams = params;
curLogLength = logLen;
startByte = start;
endByte = end;
totalLogLength = totLogLen;
byteLength = defaultLen;
tailLog();
if (startByte == 0) {
disableMoreButton();
}
}

View file

@ -237,3 +237,13 @@ a.expandbutton {
color: #333;
text-decoration: none;
}
.log-more-btn, .log-new-btn {
width: 100%
}
.no-new-alert {
text-align: center;
margin: 0;
padding: 4px 0;
}

View file

@ -20,7 +20,7 @@ package org.apache.spark.deploy.worker.ui
import java.io.File
import javax.servlet.http.HttpServletRequest
import scala.xml.Node
import scala.xml.{Node, Unparsed}
import org.apache.spark.internal.Logging
import org.apache.spark.ui.{UIUtils, WebUIPage}
@ -31,10 +31,9 @@ private[ui] class LogPage(parent: WorkerWebUI) extends WebUIPage("logPage") with
private val worker = parent.worker
private val workDir = new File(parent.workDir.toURI.normalize().getPath)
private val supportedLogTypes = Set("stderr", "stdout")
private val defaultBytes = 100 * 1024
def renderLog(request: HttpServletRequest): String = {
val defaultBytes = 100 * 1024
val appId = Option(request.getParameter("appId"))
val executorId = Option(request.getParameter("executorId"))
val driverId = Option(request.getParameter("driverId"))
@ -44,9 +43,9 @@ private[ui] class LogPage(parent: WorkerWebUI) extends WebUIPage("logPage") with
val logDir = (appId, executorId, driverId) match {
case (Some(a), Some(e), None) =>
s"${workDir.getPath}/$appId/$executorId/"
s"${workDir.getPath}/$a/$e/"
case (None, None, Some(d)) =>
s"${workDir.getPath}/$driverId/"
s"${workDir.getPath}/$d/"
case _ =>
throw new Exception("Request must specify either application or driver identifiers")
}
@ -57,7 +56,6 @@ private[ui] class LogPage(parent: WorkerWebUI) extends WebUIPage("logPage") with
}
def render(request: HttpServletRequest): Seq[Node] = {
val defaultBytes = 100 * 1024
val appId = Option(request.getParameter("appId"))
val executorId = Option(request.getParameter("executorId"))
val driverId = Option(request.getParameter("driverId"))
@ -76,49 +74,44 @@ private[ui] class LogPage(parent: WorkerWebUI) extends WebUIPage("logPage") with
val (logText, startByte, endByte, logLength) = getLog(logDir, logType, offset, byteLength)
val linkToMaster = <p><a href={worker.activeMasterWebUiUrl}>Back to Master</a></p>
val range = <span>Bytes {startByte.toString} - {endByte.toString} of {logLength}</span>
val curLogLength = endByte - startByte
val range =
<span id="log-data">
Showing {curLogLength} Bytes: {startByte.toString} - {endByte.toString} of {logLength}
</span>
val backButton =
if (startByte > 0) {
<a href={"?%s&logType=%s&offset=%s&byteLength=%s"
.format(params, logType, math.max(startByte - byteLength, 0), byteLength)}>
<button type="button" class="btn btn-default">
Previous {Utils.bytesToString(math.min(byteLength, startByte))}
val moreButton =
<button type="button" onclick={"loadMore()"} class="log-more-btn btn btn-default">
Load More
</button>
</a>
} else {
<button type="button" class="btn btn-default" disabled="disabled">
Previous 0 B
</button>
}
val nextButton =
if (endByte < logLength) {
<a href={"?%s&logType=%s&offset=%s&byteLength=%s".
format(params, logType, endByte, byteLength)}>
<button type="button" class="btn btn-default">
Next {Utils.bytesToString(math.min(byteLength, logLength - endByte))}
val newButton =
<button type="button" onclick={"loadNew()"} class="log-new-btn btn btn-default">
Load New
</button>
</a>
} else {
<button type="button" class="btn btn-default" disabled="disabled">
Next 0 B
</button>
}
val alert =
<div class="no-new-alert alert alert-info" style="display: none;">
End of Log
</div>
val logParams = "?%s&logType=%s".format(params, logType)
val jsOnload = "window.onload = " +
s"initLogPage('$logParams', $curLogLength, $startByte, $endByte, $logLength, $byteLength);"
val content =
<div>
{linkToMaster}
<div>
<div style="float:left; margin-right:10px">{backButton}</div>
<div style="float:left;">{range}</div>
<div style="float:right; margin-left:10px">{nextButton}</div>
</div>
<br />
<div style="height:500px; overflow:auto; padding:5px;">
{range}
<div class="log-content" style="height:80vh; overflow:auto; padding:5px;">
<div>{moreButton}</div>
<pre>{logText}</pre>
{alert}
<div>{newButton}</div>
</div>
<script>{Unparsed(jsOnload)}</script>
</div>
UIUtils.basicSparkPage(content, logType + " log page for " + pageName)
}

View file

@ -84,9 +84,7 @@ private[spark] object JettyUtils extends Logging {
val result = servletParams.responder(request)
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
response.setHeader("X-Frame-Options", xFrameOptionsValue)
// scalastyle:off println
response.getWriter.println(servletParams.extractFn(result))
// scalastyle:on println
response.getWriter.print(servletParams.extractFn(result))
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")

View file

@ -168,6 +168,7 @@ private[spark] object UIUtils extends Logging {
<script src={prependBaseUri("/static/table.js")}></script>
<script src={prependBaseUri("/static/additional-metrics.js")}></script>
<script src={prependBaseUri("/static/timeline-view.js")}></script>
<script src={prependBaseUri("/static/log-view.js")}></script>
}
def vizHeaderNodes: Seq[Node] = {