Repository: incubator-livy Updated Branches: refs/heads/master 4fb5516d2 -> 0442eb01a
[LIVY-495] Add thriftserver UI ## What changes were proposed in this pull request? The PR adds a new table in the Session tab dedicated to thrift-server sessions. The table contains the active sessions with a link to the corresponding Livy session. Moreover, the same information is also exposed through a REST endpoint (`"thriftserver/sessions"`). ## How was this patch tested? Manual tests. A screenshot of the UI is: ![screen shot 2018-10-09 at 11 32 18 am](https://user-images.githubusercontent.com/8821783/46660327-11082400-cbb7-11e8-9779-e8d85483dc97.png) Author: Marco Gaido <mga...@hortonworks.com> Closes #114 from mgaido91/LIVY-495. Project: http://git-wip-us.apache.org/repos/asf/incubator-livy/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-livy/commit/0442eb01 Tree: http://git-wip-us.apache.org/repos/asf/incubator-livy/tree/0442eb01 Diff: http://git-wip-us.apache.org/repos/asf/incubator-livy/diff/0442eb01 Branch: refs/heads/master Commit: 0442eb01aa293d540fdf23ddd86b86c4278b83b9 Parents: 4fb5516 Author: Marco Gaido <mga...@hortonworks.com> Authored: Thu Oct 25 12:44:20 2018 -0700 Committer: Marcelo Vanzin <van...@cloudera.com> Committed: Thu Oct 25 12:44:20 2018 -0700 ---------------------------------------------------------------------- .../livy/server/ui/static/js/all-sessions.js | 4 +- .../org/apache/livy/server/LivyServer.scala | 16 +++-- .../livy/server/ThriftServerFactory.scala | 6 ++ .../org/apache/livy/server/ui/UIServlet.scala | 64 ++++++++++++-------- .../ui/static/html/thrift-sessions-table.html | 51 ++++++++++++++++ .../livy/server/ui/static/js/thrift-sessions.js | 50 +++++++++++++++ .../thriftserver/ThriftServerFactoryImpl.scala | 7 +++ .../thriftserver/ui/ThriftJsonServlet.scala | 55 +++++++++++++++++ 8 files changed, 222 insertions(+), 31 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js ---------------------------------------------------------------------- diff --git a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js index 64b06df..90de331 100644 --- a/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js +++ b/server/src/main/resources/org/apache/livy/server/ui/static/js/all-sessions.js @@ -26,7 +26,7 @@ function loadSessionsTable(sessions) { tdWrap(session.kind) + tdWrap(session.state) + tdWrap(logLinks(session, "session")) + - "</tr>" + "</tr>" ); }); } @@ -39,7 +39,7 @@ function loadBatchesTable(sessions) { tdWrap(appIdLink(session)) + tdWrap(session.state) + tdWrap(logLinks(session, "batch")) + - "</tr>" + "</tr>" ); }); } http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/server/src/main/scala/org/apache/livy/server/LivyServer.scala ---------------------------------------------------------------------- diff --git a/server/src/main/scala/org/apache/livy/server/LivyServer.scala b/server/src/main/scala/org/apache/livy/server/LivyServer.scala index 525f560..5f3bfd1 100644 --- a/server/src/main/scala/org/apache/livy/server/LivyServer.scala +++ b/server/src/main/scala/org/apache/livy/server/LivyServer.scala @@ -93,6 +93,12 @@ class LivyServer extends Logging { livyConf.set(LIVY_SPARK_SCALA_VERSION.key, sparkScalaVersion(formattedSparkVersion, scalaVersionFromSparkSubmit, livyConf)) + val thriftServerFactory = if (livyConf.getBoolean(LivyConf.THRIFT_SERVER_ENABLED)) { + Some(ThriftServerFactory.getInstance) + } else { + None + } + if (UserGroupInformation.isSecurityEnabled) { // If Hadoop security is enabled, run kinit periodically. runKinit() should be called // before any Hadoop operation, otherwise Kerberos exception will be thrown. @@ -211,10 +217,13 @@ class LivyServer extends Logging { mount(context, batchServlet, "/batches/*") if (livyConf.getBoolean(UI_ENABLED)) { - val uiServlet = new UIServlet(basePath) + val uiServlet = new UIServlet(basePath, livyConf) mount(context, uiServlet, "/ui/*") mount(context, staticResourceServlet, "/static/*") mount(context, uiRedirectServlet(basePath + "/ui/"), "/*") + thriftServerFactory.foreach { factory => + mount(context, factory.getServlet(basePath), factory.getServletMappings: _*) + } } else { mount(context, uiRedirectServlet(basePath + "/metrics"), "/*") } @@ -278,9 +287,8 @@ class LivyServer extends Logging { } }) - if (livyConf.getBoolean(LivyConf.THRIFT_SERVER_ENABLED)) { - ThriftServerFactory.getInstance.start( - livyConf, interactiveSessionManager, sessionStore, accessManager) + thriftServerFactory.foreach { + _.start(livyConf, interactiveSessionManager, sessionStore, accessManager) } _serverUrl = Some(s"${server.protocol}://${server.host}:${server.port}") http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/server/src/main/scala/org/apache/livy/server/ThriftServerFactory.scala ---------------------------------------------------------------------- diff --git a/server/src/main/scala/org/apache/livy/server/ThriftServerFactory.scala b/server/src/main/scala/org/apache/livy/server/ThriftServerFactory.scala index b6f7d9d..00c3c9f 100644 --- a/server/src/main/scala/org/apache/livy/server/ThriftServerFactory.scala +++ b/server/src/main/scala/org/apache/livy/server/ThriftServerFactory.scala @@ -17,6 +17,8 @@ package org.apache.livy.server +import javax.servlet.Servlet + import org.apache.livy.LivyConf import org.apache.livy.server.recovery.SessionStore import org.apache.livy.sessions.InteractiveSessionManager @@ -30,6 +32,10 @@ trait ThriftServerFactory { livySessionManager: InteractiveSessionManager, sessionStore: SessionStore, accessManager: AccessManager): Unit + + def getServlet(basePath: String): Servlet + + def getServletMappings: Seq[String] } object ThriftServerFactory { http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala ---------------------------------------------------------------------- diff --git a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala index 47d6eae..7cbd135 100644 --- a/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala +++ b/server/src/main/scala/org/apache/livy/server/ui/UIServlet.scala @@ -21,18 +21,40 @@ import scala.xml.Node import org.scalatra.ScalatraServlet -class UIServlet(val basePath: String) extends ScalatraServlet { +import org.apache.livy.LivyConf + +class UIServlet(val basePath: String, livyConf: LivyConf) extends ScalatraServlet { before() { contentType = "text/html" } - sealed trait Page { val name: String } + private trait Page { + val name: String + def getNavCrumbs: Seq[Node] = Seq.empty + } private case class SimplePage(name: String) extends Page - private case class AllSessionsPage(name: String = "Sessions") extends Page + + private case class AllSessionsPage(name: String = "Sessions") extends Page { + override def getNavCrumbs: Seq[Node] = <li class="active"><a href="#">Sessions</a></li> + } private case class SessionPage(id: Int) extends Page { val name: String = "Session " + id + override def getNavCrumbs: Seq[Node] = { + <li><a href={basePath + "/ui"}>Sessions</a></li> ++ + <li class="active"><a href="#">{name}</a></li> + } } private case class LogPage(sessionType: String, id: Int) extends Page { val sessionName: String = sessionType + " " + id val name: String = sessionName + " Log" + override def getNavCrumbs: Seq[Node] = { + val sessionLink = if (sessionType == "Session") { + basePath + "/ui/session/" + id + } else { + "#" + } + <li><a href={basePath + "/ui"}>Sessions</a></li> ++ + <li><a href={sessionLink}>{sessionName}</a></li> ++ + <li class="active"><a href="#">Log</a></li> + } } private def getHeader(pageName: String): Seq[Node] = @@ -55,7 +77,7 @@ class UIServlet(val basePath: String) extends ScalatraServlet { <title>Livy - {pageName}</title> </head> - private def wrapNavTabs(tabs: Seq[Node]): Seq[Node] = + private def wrapNavCrumbs(crumbs: Seq[Node]): Seq[Node] = <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> @@ -65,32 +87,12 @@ class UIServlet(val basePath: String) extends ScalatraServlet { </div> <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> - {tabs} + {crumbs} </ul> </div> </div> </nav> - private def getNavBar(page: Page): Seq[Node] = { - val tabs: Seq[Node] = page match { - case _: AllSessionsPage => <li class="active"><a href="#">Sessions</a></li> - case sessionPage: SessionPage => { - <li><a href={basePath + "/ui"}>Sessions</a></li> ++ - <li class="active"><a href="#">{sessionPage.name}</a></li> - } - case logPage: LogPage => { - val sessionLink = if (logPage.sessionType == "Session") { - basePath + "/ui/session/" + logPage.id - } else "#" - <li><a href={basePath + "/ui"}>Sessions</a></li> ++ - <li><a href={sessionLink}>{logPage.sessionName}</a></li> ++ - <li class="active"><a href="#">Log</a></li> - } - case _ => Seq.empty - } - wrapNavTabs(tabs) - } - private def createPage(pageInfo: Page, pageContents: Seq[Node]): Seq[Node] = <html> {getHeader(pageInfo.name)} @@ -102,15 +104,27 @@ class UIServlet(val basePath: String) extends ScalatraServlet { </body> </html> + private def getNavBar(page: Page): Seq[Node] = wrapNavCrumbs(page.getNavCrumbs) + notFound { createPage(SimplePage("404"), <h3>404 No Such Page</h3>) } + private def thriftSessionsTable: Seq[Node] = { + if (livyConf.getBoolean(LivyConf.THRIFT_SERVER_ENABLED)) { + <div id="thrift-sessions"></div> ++ + <script src={s"$basePath/static/js/thrift-sessions.js"}></script> + } else { + Seq.empty + } + } + get("/") { val content = <div id="all-sessions"> <div id="interactive-sessions"></div> <div id="batches"></div> + {thriftSessionsTable} <script src={basePath + "/static/js/all-sessions.js"}></script> </div> http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/html/thrift-sessions-table.html ---------------------------------------------------------------------- diff --git a/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/html/thrift-sessions-table.html b/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/html/thrift-sessions-table.html new file mode 100644 index 0000000..243d74d --- /dev/null +++ b/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/html/thrift-sessions-table.html @@ -0,0 +1,51 @@ +<!-- + 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. +--> + +<h4 class="sessions-template">JDBC/ODBC Open Sessions</h4> + +<table id="thrift-sessions-table" + class="table table-striped sessions-table sessions-template"> + <thead class="sessions-table-head"> + <tr> + <th> + <span data-toggle="tooltip" + title="JDBC/ODBC session Id for the session"> + Session Id + </span> + </th> + <th> + <span data-toggle="tooltip" + title="Livy Interactive Session Id for this session. Links to the Session Summary Page"> + Livy Session Id + </span> + </th> + <th> + <span data-toggle="tooltip" title="Remote user who submitted this session"> + Owner + </span> + </th> + <th> + <span data-toggle="tooltip" + title="Creation time of the session"> + Created At + </span> + </th> + </tr> + </thead> + <tbody class="sessions-table-body"> + </tbody> +</table> http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/js/thrift-sessions.js ---------------------------------------------------------------------- diff --git a/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/js/thrift-sessions.js b/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/js/thrift-sessions.js new file mode 100644 index 0000000..74f8572 --- /dev/null +++ b/thriftserver/server/src/main/resources/org/apache/livy/server/ui/static/js/thrift-sessions.js @@ -0,0 +1,50 @@ +/* + * 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. + */ + +function loadThriftSessionsTable(sessions) { + $.each(sessions, function(index, session) { + $("#thrift-sessions-table .sessions-table-body").append( + "<tr>" + + tdWrap(session.sessionId) + + tdWrap(uiLink("session/" + session.livySessionId, session.livySessionId)) + + tdWrap(session.owner) + + tdWrap(session.createdAt) + + "</tr>" + ); + }); +} + +var numSessions = 0; + +$(document).ready(function () { + var sessionsReq = $.getJSON(location.origin + prependBasePath("/thriftserver/sessions"), function(response) { + if (response && response.total > 0) { + $("#thrift-sessions").load(prependBasePath("/static/html/thrift-sessions-table.html .sessions-template"), function() { + loadThriftSessionsTable(response.sessions); + $("#thrift-sessions-table").DataTable(); + $('#thrift-sessions [data-toggle="tooltip"]').tooltip(); + }); + } + numSessions = response.total; + }); + + $.when(sessionsReq).done(function () { + if (numSessions == 0) { + $("#thrift-sessions").append('<h4>No open JDBC/ODBC sessions.</h4>'); + } + }); +}); http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ThriftServerFactoryImpl.scala ---------------------------------------------------------------------- diff --git a/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ThriftServerFactoryImpl.scala b/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ThriftServerFactoryImpl.scala index 5cc5707..7669580 100644 --- a/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ThriftServerFactoryImpl.scala +++ b/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ThriftServerFactoryImpl.scala @@ -17,10 +17,13 @@ package org.apache.livy.thriftserver +import javax.servlet.Servlet + import org.apache.livy.LivyConf import org.apache.livy.server.{AccessManager, ThriftServerFactory} import org.apache.livy.server.recovery.SessionStore import org.apache.livy.sessions.InteractiveSessionManager +import org.apache.livy.thriftserver.ui.ThriftJsonServlet class ThriftServerFactoryImpl extends ThriftServerFactory { override def start( @@ -34,4 +37,8 @@ class ThriftServerFactoryImpl extends ThriftServerFactory { } LivyThriftServer.start(livyConf, livySessionManager, sessionStore, accessManager) } + + override def getServlet(basePath: String): Servlet = new ThriftJsonServlet(basePath) + + override def getServletMappings: Seq[String] = Seq("/thriftserver/*") } http://git-wip-us.apache.org/repos/asf/incubator-livy/blob/0442eb01/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ui/ThriftJsonServlet.scala ---------------------------------------------------------------------- diff --git a/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ui/ThriftJsonServlet.scala b/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ui/ThriftJsonServlet.scala new file mode 100644 index 0000000..a7db3e9 --- /dev/null +++ b/thriftserver/server/src/main/scala/org/apache/livy/thriftserver/ui/ThriftJsonServlet.scala @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.apache.livy.thriftserver.ui + +import java.text.SimpleDateFormat + +import org.apache.livy.server.JsonServlet +import org.apache.livy.thriftserver.LivyThriftServer + + +class ThriftJsonServlet(val basePath: String) extends JsonServlet { + + private val df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z") + + case class SessionInfo( + sessionId: String, + livySessionId: String, + owner: String, + createdAt: String) + + get("/sessions") { + val thriftSessions = LivyThriftServer.getInstance.map { server => + val sessionManager = server.getSessionManager() + sessionManager.getSessions.map { sessionHandle => + val info = sessionManager.getSessionInfo(sessionHandle) + SessionInfo(sessionHandle.getSessionId.toString, + sessionManager.livySessionId(sessionHandle).map(_.toString).getOrElse(""), + info.username, + df.format(info.creationTime)) + }.toSeq + }.getOrElse(Seq.empty) + val from = params.get("from").map(_.toInt).getOrElse(0) + val size = params.get("size").map(_.toInt).getOrElse(100) + + Map( + "from" -> from, + "total" -> thriftSessions.length, + "sessions" -> thriftSessions.view(from, from + size)) + } +}