[KYUUBI #4439][FOLLOWUP] Add dto class for operation data

### _Why are the changes needed?_

- add dto for operation data, which is programming friendly
- show exception for session data

### _How was this patch tested?_
- [ ] Add some test cases that check the changes thoroughly including negative and positive cases if possible

- [ ] Add screenshots for manual tests if appropriate

- [x] [Run test](https://kyuubi.readthedocs.io/en/master/develop_tools/testing.html#running-tests) locally before make a pull request

Closes #4468 from turboFei/session_op_dto.

Closes #4439

fa905e70c [fwang12] fix ut
5c1c7c845 [fwang12] save
2d20215a0 [fwang12] comments
46cd2384e [fwang12] saev

Authored-by: fwang12 <fwang12@ebay.com>
Signed-off-by: fwang12 <fwang12@ebay.com>
This commit is contained in:
fwang12 2023-03-07 15:43:23 +08:00
parent 39eac5a780
commit 222a3345f2
7 changed files with 258 additions and 34 deletions

View File

@ -387,4 +387,19 @@ object Utils extends Logging {
Option(Thread.currentThread().getContextClassLoader).getOrElse(getKyuubiClassLoader)
def isOnK8s: Boolean = Files.exists(Paths.get("/var/run/secrets/kubernetes.io"))
/**
* Return a nice string representation of the exception. It will call "printStackTrace" to
* recursively generate the stack trace including the exception and its causes.
*/
def prettyPrint(e: Throwable): String = {
if (e == null) {
""
} else {
// Use e.printStackTrace here because e.getStackTrace doesn't include the cause
val stringWriter = new StringWriter()
e.printStackTrace(new PrintWriter(stringWriter))
stringWriter.toString
}
}
}

View File

@ -0,0 +1,158 @@
/*
* 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.kyuubi.client.api.v1.dto;
import java.util.Objects;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
public class OperationData {
private String identifier;
private String statement;
private String state;
private Long createTime;
private Long startTime;
private Long completeTime;
private String exception;
private String sessionId;
private String sessionUser;
private String sessionType;
public OperationData() {}
public OperationData(
String identifier,
String statement,
String state,
Long createTime,
Long startTime,
Long completeTime,
String exception,
String sessionId,
String sessionUser,
String sessionType) {
this.identifier = identifier;
this.statement = statement;
this.state = state;
this.createTime = createTime;
this.startTime = startTime;
this.completeTime = completeTime;
this.exception = exception;
this.sessionId = sessionId;
this.sessionUser = sessionUser;
this.sessionType = sessionType;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public String getStatement() {
return statement;
}
public void setStatement(String statement) {
this.statement = statement;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getStartTime() {
return startTime;
}
public void setStartTime(Long startTime) {
this.startTime = startTime;
}
public Long getCompleteTime() {
return completeTime;
}
public void setCompleteTime(Long completeTime) {
this.completeTime = completeTime;
}
public String getException() {
return exception;
}
public void setException(String exception) {
this.exception = exception;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getSessionUser() {
return sessionUser;
}
public void setSessionUser(String sessionUser) {
this.sessionUser = sessionUser;
}
public String getSessionType() {
return sessionType;
}
public void setSessionType(String sessionType) {
this.sessionType = sessionType;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SessionData that = (SessionData) o;
return Objects.equals(getIdentifier(), that.getIdentifier());
}
@Override
public int hashCode() {
return Objects.hash(getIdentifier());
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.JSON_STYLE);
}
}

View File

@ -31,6 +31,7 @@ public class SessionData {
private Long createTime;
private Long duration;
private Long idleTime;
private String exception;
public SessionData() {}
@ -41,7 +42,8 @@ public class SessionData {
Map<String, String> conf,
Long createTime,
Long duration,
Long idleTime) {
Long idleTime,
String exception) {
this.identifier = identifier;
this.user = user;
this.ipAddr = ipAddr;
@ -49,6 +51,7 @@ public class SessionData {
this.createTime = createTime;
this.duration = duration;
this.idleTime = idleTime;
this.exception = exception;
}
public String getIdentifier() {
@ -110,6 +113,14 @@ public class SessionData {
this.idleTime = idleTime;
}
public String getException() {
return exception;
}
public void setException(String exception) {
this.exception = exception;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -0,0 +1,56 @@
/*
* 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.kyuubi.server.api
import scala.collection.JavaConverters._
import org.apache.kyuubi.Utils
import org.apache.kyuubi.client.api.v1.dto.{OperationData, SessionData}
import org.apache.kyuubi.events.KyuubiOperationEvent
import org.apache.kyuubi.operation.KyuubiOperation
import org.apache.kyuubi.session.KyuubiSession
object ApiUtils {
def sessionData(session: KyuubiSession): SessionData = {
new SessionData(
session.handle.identifier.toString,
session.user,
session.ipAddress,
session.conf.asJava,
session.createTime,
session.lastAccessTime - session.createTime,
session.getNoOperationTime,
session.getSessionEvent.flatMap(_.exception).map(Utils.prettyPrint).getOrElse(""))
}
def operationData(operation: KyuubiOperation): OperationData = {
val opEvent = KyuubiOperationEvent(operation)
new OperationData(
opEvent.statementId,
opEvent.statement,
opEvent.state,
opEvent.createTime,
opEvent.startTime,
opEvent.completeTime,
opEvent.exception.map(Utils.prettyPrint).getOrElse(""),
opEvent.sessionId,
opEvent.sessionUser,
opEvent.sessionType)
}
}

View File

@ -30,17 +30,16 @@ import io.swagger.v3.oas.annotations.tags.Tag
import org.apache.zookeeper.KeeperException.NoNodeException
import org.apache.kyuubi.{KYUUBI_VERSION, Logging, Utils}
import org.apache.kyuubi.client.api.v1.dto.{Engine, SessionData}
import org.apache.kyuubi.client.api.v1.dto.{Engine, OperationData, SessionData}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiConf._
import org.apache.kyuubi.events.KyuubiOperationEvent
import org.apache.kyuubi.ha.HighAvailabilityConf.HA_NAMESPACE
import org.apache.kyuubi.ha.client.{DiscoveryPaths, ServiceNodeInfo}
import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle}
import org.apache.kyuubi.server.KyuubiServer
import org.apache.kyuubi.server.api.ApiRequestContext
import org.apache.kyuubi.session.SessionHandle
import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils}
import org.apache.kyuubi.session.{KyuubiSession, SessionHandle}
@Tag(name = "Admin")
@Produces(Array(MediaType.APPLICATION_JSON))
@ -122,15 +121,8 @@ private[v1] class AdminResource extends ApiRequestContext with Logging {
throw new NotAllowedException(
s"$userName is not allowed to list all live sessions")
}
fe.be.sessionManager.allSessions().map { session =>
new SessionData(
session.handle.identifier.toString,
session.user,
session.ipAddress,
session.conf.asJava,
session.createTime,
session.lastAccessTime - session.createTime,
session.getNoOperationTime)
fe.be.sessionManager.allSessions().map { case session =>
ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])
}.toSeq
}
@ -157,12 +149,12 @@ private[v1] class AdminResource extends ApiRequestContext with Logging {
content = Array(new Content(
mediaType = MediaType.APPLICATION_JSON,
array = new ArraySchema(schema = new Schema(implementation =
classOf[KyuubiOperationEvent])))),
classOf[OperationData])))),
description =
"get the list of all active operation events")
"get the list of all active operations")
@GET
@Path("operations")
def listOperations(): Seq[KyuubiOperationEvent] = {
def listOperations(): Seq[OperationData] = {
val userName = fe.getSessionUser(Map.empty[String, String])
val ipAddress = fe.getIpAddress
info(s"Received listing all of the active operations request from $userName/$ipAddress")
@ -171,7 +163,7 @@ private[v1] class AdminResource extends ApiRequestContext with Logging {
s"$userName is not allowed to list all the operations")
}
fe.be.sessionManager.operationManager.allOperations()
.map(operation => KyuubiOperationEvent(operation.asInstanceOf[KyuubiOperation])).toSeq
.map(operation => ApiUtils.operationData(operation.asInstanceOf[KyuubiOperation])).toSeq
}
@ApiResponse(

View File

@ -35,7 +35,7 @@ import org.apache.kyuubi.client.api.v1.dto._
import org.apache.kyuubi.config.KyuubiReservedKeys._
import org.apache.kyuubi.events.KyuubiEvent
import org.apache.kyuubi.operation.OperationHandle
import org.apache.kyuubi.server.api.ApiRequestContext
import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils}
import org.apache.kyuubi.session.KyuubiSession
import org.apache.kyuubi.session.SessionHandle
@ -54,15 +54,8 @@ private[v1] class SessionsResource extends ApiRequestContext with Logging {
description = "get the list of all live sessions")
@GET
def sessions(): Seq[SessionData] = {
sessionManager.allSessions().map { session =>
new SessionData(
session.handle.identifier.toString,
session.user,
session.ipAddress,
session.conf.asJava,
session.createTime,
session.lastAccessTime - session.createTime,
session.getNoOperationTime)
sessionManager.allSessions().map { case session =>
ApiUtils.sessionData(session.asInstanceOf[KyuubiSession])
}.toSeq
}

View File

@ -27,13 +27,12 @@ import org.apache.hive.service.rpc.thrift.TProtocolVersion.HIVE_CLI_SERVICE_PROT
import org.scalatest.time.SpanSugar.convertIntToGrainOfTime
import org.apache.kyuubi.{KYUUBI_VERSION, KyuubiFunSuite, RestFrontendTestHelper, Utils}
import org.apache.kyuubi.client.api.v1.dto.{Engine, SessionData, SessionHandle, SessionOpenRequest}
import org.apache.kyuubi.client.api.v1.dto.{Engine, OperationData, SessionData, SessionHandle, SessionOpenRequest}
import org.apache.kyuubi.config.KyuubiConf
import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_CONNECTION_URL_KEY
import org.apache.kyuubi.engine.{ApplicationState, EngineRef, KyuubiApplicationManager}
import org.apache.kyuubi.engine.EngineType.SPARK_SQL
import org.apache.kyuubi.engine.ShareLevel.{CONNECTION, USER}
import org.apache.kyuubi.events.KyuubiOperationEvent
import org.apache.kyuubi.ha.HighAvailabilityConf
import org.apache.kyuubi.ha.client.DiscoveryClientProvider.withDiscoveryClient
import org.apache.kyuubi.ha.client.DiscoveryPaths
@ -189,9 +188,9 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper {
.header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization")
.get()
assert(200 == response.getStatus)
var operations = response.readEntity(new GenericType[Seq[KyuubiOperationEvent]]() {})
var operations = response.readEntity(new GenericType[Seq[OperationData]]() {})
assert(operations.nonEmpty)
assert(operations.map(op => op.statementId).contains(operation.identifier.toString))
assert(operations.map(op => op.getIdentifier).contains(operation.identifier.toString))
// close operation
response = webTarget.path(s"api/v1/admin/operations/${operation.identifier}").request()
@ -203,8 +202,8 @@ class AdminResourceSuite extends KyuubiFunSuite with RestFrontendTestHelper {
response = webTarget.path("api/v1/admin/operations").request()
.header(AUTHORIZATION_HEADER, s"BASIC $encodeAuthorization")
.get()
operations = response.readEntity(new GenericType[Seq[KyuubiOperationEvent]]() {})
assert(!operations.map(op => op.statementId).contains(operation.identifier.toString))
operations = response.readEntity(new GenericType[Seq[OperationData]]() {})
assert(!operations.map(op => op.getIdentifier).contains(operation.identifier.toString))
}
test("delete engine - user share level") {