Skip to content

Commit

Permalink
Add query explain distributed plan to UI
Browse files Browse the repository at this point in the history
  • Loading branch information
wendigo committed Jul 29, 2024
1 parent 2eedab9 commit e34705d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@

import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import io.trino.client.NodeVersion;
import io.trino.dispatcher.DispatchManager;
import io.trino.execution.QueryInfo;
import io.trino.execution.QueryState;
import io.trino.metadata.FunctionManager;
import io.trino.metadata.Metadata;
import io.trino.metadata.SessionPropertyManager;
import io.trino.security.AccessControl;
import io.trino.server.BasicQueryInfo;
import io.trino.server.DisableHttpCache;
Expand All @@ -26,6 +30,8 @@
import io.trino.spi.QueryId;
import io.trino.spi.TrinoException;
import io.trino.spi.security.AccessDeniedException;
import io.trino.sql.planner.planprinter.NoOpAnonymizer;
import io.trino.sql.planner.planprinter.ValuePrinter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
Expand All @@ -49,19 +55,28 @@
import static io.trino.security.AccessControlUtil.checkCanViewQueryOwnedBy;
import static io.trino.security.AccessControlUtil.filterQueries;
import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI;
import static io.trino.sql.planner.planprinter.PlanPrinter.textDistributedPlan;
import static java.util.Objects.requireNonNull;

@Path("/ui/api/query")
@DisableHttpCache
public class UiQueryResource
{
private final Metadata metadata;
private final FunctionManager functionManager;
private final NodeVersion nodeVersion;
private final SessionPropertyManager sessionPropertyManager;
private final DispatchManager dispatchManager;
private final AccessControl accessControl;
private final HttpRequestSessionContextFactory sessionContextFactory;

@Inject
public UiQueryResource(DispatchManager dispatchManager, AccessControl accessControl, HttpRequestSessionContextFactory sessionContextFactory)
public UiQueryResource(Metadata metadata, FunctionManager functionManager, NodeVersion nodeVersion, SessionPropertyManager sessionPropertyManager, DispatchManager dispatchManager, AccessControl accessControl, HttpRequestSessionContextFactory sessionContextFactory)
{
this.metadata = requireNonNull(metadata, "metadata is null");
this.functionManager = requireNonNull(functionManager, "functionManager is null");
this.nodeVersion = requireNonNull(nodeVersion, "nodeVersion is null");
this.sessionPropertyManager = requireNonNull(sessionPropertyManager, "sessionPropertyManager is null");
this.dispatchManager = requireNonNull(dispatchManager, "dispatchManager is null");
this.accessControl = requireNonNull(accessControl, "accessControl is null");
this.sessionContextFactory = requireNonNull(sessionContextFactory, "sessionContextFactory is null");
Expand Down Expand Up @@ -105,6 +120,34 @@ public Response getQueryInfo(@PathParam("queryId") QueryId queryId, @Context Htt
return Response.status(Status.GONE).build();
}

@ResourceSecurity(WEB_UI)
@GET
@Path("{queryId}/explain")
public Response explainQuery(@PathParam("queryId") QueryId queryId, @Context HttpServletRequest servletRequest, @Context HttpHeaders httpHeaders)
{
requireNonNull(queryId, "queryId is null");

Optional<QueryInfo> queryInfo = dispatchManager.getFullQueryInfo(queryId);
if (queryInfo.isPresent()) {
try {
checkCanViewQueryOwnedBy(sessionContextFactory.extractAuthorizedIdentity(servletRequest, httpHeaders), queryInfo.get().getSession().toIdentity(), accessControl);

ValuePrinter valuePrinter = new ValuePrinter(metadata, functionManager, queryInfo.get().getSession().toSession(sessionPropertyManager));
return Response.ok(textDistributedPlan(
queryInfo.get().getOutputStage().orElseThrow(),
queryInfo.get().getQueryStats(),
valuePrinter,
true,
new NoOpAnonymizer(),
nodeVersion)).type("text/plain;charset=utf-8").build();
}
catch (AccessDeniedException e) {
throw new ForbiddenException();
}
}
return Response.status(Status.GONE).build();
}

@ResourceSecurity(WEB_UI)
@PUT
@Path("{queryId}/killed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ export class QueryHeader extends React.Component {
return (
<div>
<div className="row">
<div className="col-xs-6">
<div className="col-xs-5">
<h3 className="query-id">
<span id="query-id">{query.queryId}</span>
<a className="btn copy-button" data-clipboard-target="#query-id" data-toggle="tooltip" data-placement="right" title="Copy to clipboard">
<span className="glyphicon glyphicon-copy" aria-hidden="true" alt="Copy to clipboard"/>
</a>
</h3>
</div>
<div className="col-xs-6">
<div className="col-xs-7">
<table className="header-inline-links">
<tbody>
<tr>
Expand All @@ -103,6 +103,8 @@ export class QueryHeader extends React.Component {
&nbsp;
<a href={"/ui/api/query/" + query.queryId + "?pretty"} className="btn btn-info navbar-btn" target="_blank">JSON</a>
&nbsp;
<a href={"/ui/api/query/" + query.queryId + "/explain"} className="btn btn-info navbar-btn" target="_blank">Explain</a>
&nbsp;
{this.renderTab("references.html", "References")}
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ export class QueryListItem extends React.Component {
title="Query JSON">
<span className="glyphicon glyphicon-save-file" style={GLYPHICON_DEFAULT}/>
</a>&nbsp;
<a href={"/ui/api/query/" + query.queryId + "/explain"} target="_blank"
data-toggle="tooltip" data-placement="bottom" data-trigger="hover"
title="Query explain plan">
<span className="glyphicon glyphicon-eye-open" style={GLYPHICON_DEFAULT}/>
</a>&nbsp;
<a href={"stage.html?" + query.queryId}
data-toggle="tooltip" data-placement="bottom" data-trigger="hover"
title="Stage performance">
Expand Down

0 comments on commit e34705d

Please sign in to comment.