Skip to content

Commit

Permalink
Query Explanation (thanos-io#6346)
Browse files Browse the repository at this point in the history
* Return Query Explaination in QueryAPI

A param `explain` is added to QueryAPI, if true then explanation
returned by the `Explain()` method of the query having structure
`ExplainOutputNode` is returned in response.
Query Explanation is added under new field in response that is
`thanosInfo`.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Add explain checkbox in thanos UI

A explain checkbox is added to Thanos Query UI, that requests for
query explanation from thanos query api.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Add ExpandableNode Component

ExpandableNode component renders Query Explanation in the thanos
UI. Requires a new package `react-accessible-treeview`.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Disable Explain checkbox on prometheus engine

Prometheus engine sends out error if toggle explain button. To
provide better experience, the explain checkbox get disbaled on
switching to prometheus engine and enable back on switching to
thanos engine.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Add alert box with horizontal scrolling for Explanation

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Remove ExpandableNode and Add ListTree

Updates the design for query explanation box, removes
`ExpandableNode` and the dependency. Builts a new `ListTree` that
does the same using reactstrap and custom css.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Minor refactor in Query API response

`thanosInfo` is removed from Query reponse and used `explanation`
directly. `disableCheckbox` is also renamed to
`disableExplainCheckbox` in thanos UI.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Update UI tests to passing

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Minor UI changes and test fix

UI improvements and Panel test fix other way around, resetting
the results on panel construction.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Update promql-engine to use Explain method

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Build UI assets

Build UI assets, that runs new thanos UI with explain button.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Revert proxy url change from package.json

`proxy` was accidently changed and committed with package.json
when removed dependency. Hence, reverting it back.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Minor changes in UI

Fix requested changes in UI.
- Rename `state` and `setState` to `mapping` and `setMapping`.
- Rename `NodeTree` to `QueryTree`.
- Use unicode characters instead of `-` and `+`.
- Fix blue box on explain button.

Signed-off-by: Pradyumna Krishna <git@onpy.in>

* Update UI assets

Signed-off-by: Pradyumna Krishna <git@onpy.in>

---------

Signed-off-by: Pradyumna Krishna <git@onpy.in>
  • Loading branch information
PradyumnaKrishna authored and HC Zhu committed Jun 27, 2023
1 parent bacfc5a commit e9e7755
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 94 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ require (
github.com/prometheus/prometheus v0.44.0-rc.0.0.20230508103029-94d9367bbf13
github.com/sony/gobreaker v0.5.0
github.com/stretchr/testify v1.8.2
github.com/thanos-community/promql-engine v0.0.0-20230505104016-5124a98eee24
github.com/thanos-community/promql-engine v0.0.0-20230517140820-97f42f67eaf5
github.com/thanos-io/objstore v0.0.0-20230201072718-11ffbc490204
github.com/uber/jaeger-client-go v2.30.0+incompatible
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
Expand Down
6 changes: 3 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cortexproject/promqlsmith v0.0.0-20230313010502-5c380a3b00b0 h1:NxAuzQ8oCBUgmBTmhC4GyKk9kBl4IoDymGib+tVdqiQ=
github.com/cortexproject/promqlsmith v0.0.0-20230502194647-ed3e43bb7a52 h1:soTzgUC/F+qtsMbh/IWr3uGwPaXXYEUsTT3zYiXP0yM=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand Down Expand Up @@ -893,8 +893,8 @@ github.com/tencentyun/cos-go-sdk-v5 v0.7.40 h1:W6vDGKCHe4wBACI1d2UgE6+50sJFhRWU4
github.com/tencentyun/cos-go-sdk-v5 v0.7.40/go.mod h1:4dCEtLHGh8QPxHEkgq+nFaky7yZxQuYwgSJM87icDaw=
github.com/thanos-community/galaxycache v0.0.0-20211122094458-3a32041a1f1e h1:f1Zsv7OAU9iQhZwigp50Yl38W10g/vd5NC8Rdk1Jzng=
github.com/thanos-community/galaxycache v0.0.0-20211122094458-3a32041a1f1e/go.mod h1:jXcofnrSln/cLI6/dhlBxPQZEEQHVPCcFaH75M+nSzM=
github.com/thanos-community/promql-engine v0.0.0-20230505104016-5124a98eee24 h1:UvZpeeWoiu54gd6ZW8lzJZWhQLekfF/vmccgOMQssYU=
github.com/thanos-community/promql-engine v0.0.0-20230505104016-5124a98eee24/go.mod h1:PbimG7ocz5JmFRLlQ7yMcewnkunNBmvMyVgAoNmyvDw=
github.com/thanos-community/promql-engine v0.0.0-20230517140820-97f42f67eaf5 h1:S+5/eRDfEwDLj7PxcIBLNckB5xGd/vrPLzPyO2LgzAk=
github.com/thanos-community/promql-engine v0.0.0-20230517140820-97f42f67eaf5/go.mod h1:ifUU+eb28dKRho/a4ipv27T4yEaNYaXkd7bing4ovUg=
github.com/thanos-io/objstore v0.0.0-20230201072718-11ffbc490204 h1:W4w5Iph7j32Sf1QFWLJDCqvO0WgZS0jHGID+qnq3wV0=
github.com/thanos-io/objstore v0.0.0-20230201072718-11ffbc490204/go.mod h1:STSgpY8M6EKF2G/raUFdbIMf2U9GgYlEjAEHJxjvpAo=
github.com/themihai/gomemcache v0.0.0-20180902122335-24332e2d58ab h1:7ZR3hmisBWw77ZpO1/o86g+JV3VKlk3d48jopJxzTjU=
Expand Down
49 changes: 42 additions & 7 deletions pkg/api/query/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const (
ShardInfoParam = "shard_info"
LookbackDeltaParam = "lookback_delta"
EngineParam = "engine"
QueryExplainParam = "explain"
)

type PromqlEngineType string
Expand Down Expand Up @@ -272,7 +273,8 @@ type queryData struct {
Result parser.Value `json:"result"`
Stats stats.QueryStats `json:"stats,omitempty"`
// Additional Thanos Response field.
Warnings []error `json:"warnings,omitempty"`
QueryExplanation *engine.ExplainOutputNode `json:"explanation,omitempty"`
Warnings []error `json:"warnings,omitempty"`
}

func (qapi *QueryAPI) parseEnableDedupParam(r *http.Request) (enableDeduplication bool, _ *api.ApiError) {
Expand Down Expand Up @@ -419,6 +421,27 @@ func (qapi *QueryAPI) parseShardInfo(r *http.Request) (*storepb.ShardInfo, *api.
return &info, nil
}

func (qapi *QueryAPI) parseQueryExplainParam(r *http.Request, query promql.Query) (*engine.ExplainOutputNode, *api.ApiError) {
var explanation *engine.ExplainOutputNode

if val := r.FormValue(QueryExplainParam); val != "" {
var err error
enableExplanation, err := strconv.ParseBool(val)
if err != nil {
return explanation, &api.ApiError{Typ: api.ErrorBadData, Err: errors.Wrapf(err, "'%s' parameter", QueryExplainParam)}
}
if enableExplanation {
if eq, ok := query.(engine.ExplainableQuery); ok {
explanation = eq.Explain()
} else {
return explanation, &api.ApiError{Typ: api.ErrorBadData, Err: errors.Errorf("Query not explainable")}
}
}
}

return explanation, nil
}

func (qapi *QueryAPI) query(r *http.Request) (interface{}, []error, *api.ApiError, func()) {
ts, err := parseTimeParam(r, "time", qapi.baseAPI.Now())
if err != nil {
Expand Down Expand Up @@ -509,6 +532,11 @@ func (qapi *QueryAPI) query(r *http.Request) (interface{}, []error, *api.ApiErro
return nil, nil, &api.ApiError{Typ: api.ErrorBadData, Err: err}, func() {}
}

explanation, apiErr := qapi.parseQueryExplainParam(r, qry)
if apiErr != nil {
return nil, nil, apiErr, func() {}
}

tracing.DoInSpan(ctx, "query_gate_ismyturn", func(ctx context.Context) {
err = qapi.gate.Start(ctx)
})
Expand Down Expand Up @@ -541,9 +569,10 @@ func (qapi *QueryAPI) query(r *http.Request) (interface{}, []error, *api.ApiErro
qs = stats.NewQueryStats(qry.Stats())
}
return &queryData{
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
QueryExplanation: explanation,
}, res.Warnings, nil, qry.Close
}

Expand Down Expand Up @@ -667,6 +696,11 @@ func (qapi *QueryAPI) queryRange(r *http.Request) (interface{}, []error, *api.Ap
return nil, nil, &api.ApiError{Typ: api.ErrorBadData, Err: err}, func() {}
}

explanation, apiErr := qapi.parseQueryExplainParam(r, qry)
if apiErr != nil {
return nil, nil, apiErr, func() {}
}

tracing.DoInSpan(ctx, "query_gate_ismyturn", func(ctx context.Context) {
err = qapi.gate.Start(ctx)
})
Expand Down Expand Up @@ -697,9 +731,10 @@ func (qapi *QueryAPI) queryRange(r *http.Request) (interface{}, []error, *api.Ap
qs = stats.NewQueryStats(qry.Stats())
}
return &queryData{
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
ResultType: res.Value.Type(),
Result: res.Value,
Stats: qs,
QueryExplanation: explanation,
}, res.Warnings, nil, qry.Close
}

Expand Down
80 changes: 40 additions & 40 deletions pkg/ui/bindata.go

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions pkg/ui/react-app/src/components/ListTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { InputProps, Collapse, ListGroupItem, ListGroup } from 'reactstrap';

export interface QueryTree {
name: string;
children?: QueryTree[];
}

interface NodeProps extends InputProps {
node: QueryTree | null;
}

const ListTree: React.FC<NodeProps> = ({ id, node }) => {
type idMapping = {
[key: string]: boolean;
};

const [mapping, setMapping] = useState<idMapping>({});
const toggle = (e: React.MouseEvent<HTMLDivElement>) => {
const el = e.target as HTMLDivElement;
const id = el.getAttribute('id');
if (id) {
setMapping({ ...mapping, [id]: !mapping[id] });
}
};

// Constructing List Items recursively.
const mapper = (nodes: QueryTree[], parentId?: any, lvl?: any) => {
return nodes.map((node: QueryTree, index: number) => {
const id = `${index}-${parentId ? parentId : 'top'}`.replace(/[^a-zA-Z0-9-_]/g, '');
const item = (
<React.Fragment>
<ListGroupItem
className={`bg-transparent p-0 border-0 ${parentId ? `rounded-0 ${lvl ? 'border-bottom-0' : ''}` : ''}`}
>
{
<div className={`d-flex align-items-center`} style={{ paddingLeft: `${25 * lvl}px` }}>
{node.children && (
<div className="pl-0 btn text-primary" style={{ cursor: 'inherit' }} color="link">
{mapping[id] ? '\u002B' : '\u002D'}
</div>
)}
<div id={id} style={{ cursor: `${node.children ? 'pointer' : 'inherit'}` }} onClick={toggle}>
{node.name}
</div>
</div>
}
</ListGroupItem>
{node.children && <Collapse isOpen={mapping[id]}>{mapper(node.children, id, (lvl || 0) + 1)}</Collapse>}
</React.Fragment>
);

return item;
});
};

if (node) {
return <ListGroup>{mapper([node], id)}</ListGroup>;
} else {
return null;
}
};

export default ListTree;
7 changes: 7 additions & 0 deletions pkg/ui/react-app/src/pages/graph/Panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const defaultProps: PanelProps = {
usePartialResponse: false,
storeMatches: [],
engine: 'prometheus',
explain: false,
disableExplainCheckbox: false,
},
onOptionsChanged: (): void => {
// Do nothing.
Expand Down Expand Up @@ -55,6 +57,9 @@ describe('Panel', () => {
results.push(opts);
};
const panel = shallow(<Panel {...defaultProps} onOptionsChanged={onOptionsChanged} />);
// Panel construction updates Explain checkbox prop to disbale.
// Hence, a result is added and dropping it.
results.length = 0;
const links = panel.find(NavLink);
[
{ panelType: 'Table', active: true },
Expand Down Expand Up @@ -92,6 +97,8 @@ describe('Panel', () => {
usePartialResponse: false,
storeMatches: [],
engine: 'prometheus',
explain: false,
disableExplainCheckbox: false,
};
const graphPanel = mount(<Panel {...defaultProps} options={options} />);
const controls = graphPanel.find(GraphControls);
Expand Down
Loading

0 comments on commit e9e7755

Please sign in to comment.