Commit 2a6ac88a by Ryan McKinley Committed by GitHub

QueryInspector: add common way to show the raw query (#25204)

parent c5636338
......@@ -34,10 +34,15 @@ export interface QueryResultMeta {
preferredVisualisationType?: PreferredVisualisationType;
/**
* This is the raw query sent to the underlying system. All macros and templating
* as been applied. When metadata contains this value, it will be shown in the query inspector
*/
executedQueryString?: string;
/**
* Legacy data source specific, should be moved to custom
* */
gmdMeta?: any[]; // used by cloudwatch
rawQuery?: string; // used by stackdriver
alignmentPeriod?: string; // used by stackdriver
query?: string; // used by azure log
searchWords?: string[]; // used by log models and loki
......
......@@ -333,7 +333,7 @@ func TestMSSQL(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 ORDER BY 1")
})
})
......@@ -698,7 +698,7 @@ func TestMSSQL(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
})
......
......@@ -336,7 +336,7 @@ func TestMySQL(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT UNIX_TIMESTAMP(time) DIV 60 * 60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT UNIX_TIMESTAMP(time) DIV 60 * 60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
})
})
......@@ -778,7 +778,7 @@ func TestMySQL(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > FROM_UNIXTIME(1521118500) OR time < FROM_UNIXTIME(1521118800) OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > FROM_UNIXTIME(1521118500) OR time < FROM_UNIXTIME(1521118800) OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
})
......
......@@ -261,7 +261,7 @@ func TestPostgres(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1")
})
})
......@@ -708,7 +708,7 @@ func TestPostgres(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
So(queryResult.Meta.Get(sqleng.MetaKeyExecutedQueryString).MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
})
})
......
......@@ -25,6 +25,9 @@ import (
"xorm.io/xorm"
)
// MetaKeyExecutedQueryString is the key where the executed query should get stored
const MetaKeyExecutedQueryString = "executedQueryString"
// SqlMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and
// timeRange to be able to generate queries that use from and to.
type SqlMacroEngine interface {
......@@ -153,7 +156,7 @@ func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource,
continue
}
queryResult.Meta.Set("sql", rawSQL)
queryResult.Meta.Set(MetaKeyExecutedQueryString, rawSQL)
wg.Add(1)
......
......@@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/opentracing/opentracing-go"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/oauth2/google"
......@@ -328,7 +329,7 @@ func (e *StackdriverExecutor) executeQuery(ctx context.Context, query *stackdriv
}
req.URL.RawQuery = query.Params.Encode()
queryResult.Meta.Set("rawQuery", req.URL.RawQuery)
queryResult.Meta.Set(sqleng.MetaKeyExecutedQueryString, req.URL.RawQuery)
alignmentPeriod, ok := req.URL.Query()["aggregation.alignmentPeriod"]
if ok {
......
......@@ -354,7 +354,7 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
)}
{activeTab === InspectTab.Error && this.renderErrorTab(error)}
{activeTab === InspectTab.Stats && this.renderStatsTab()}
{activeTab === InspectTab.Query && <QueryInspector panel={panel} />}
{activeTab === InspectTab.Query && <QueryInspector panel={panel} data={last.series} />}
</TabContent>
</CustomScrollbar>
</Drawer>
......
import React, { PureComponent } from 'react';
import { Button, JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { AppEvents, PanelEvents } from '@grafana/data';
import { AppEvents, PanelEvents, DataFrame } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
......@@ -9,14 +9,24 @@ import { CoreEvents } from 'app/types';
import { PanelModel } from 'app/features/dashboard/state';
import { getPanelInspectorStyles } from './styles';
import { supportsDataQuery } from '../PanelEditor/utils';
import { config } from '@grafana/runtime';
import { css } from 'emotion';
interface DsQuery {
isLoading: boolean;
response: {};
}
interface ExecutedQueryInfo {
refId: string;
query: string;
frames: number;
rows: number;
}
interface Props {
panel: PanelModel;
data: DataFrame[];
}
interface State {
......@@ -24,6 +34,7 @@ interface State {
isMocking: boolean;
mockedResponse: string;
dsQuery: DsQuery;
executedQueries: ExecutedQueryInfo[];
}
export class QueryInspector extends PureComponent<Props, State> {
......@@ -33,6 +44,7 @@ export class QueryInspector extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
executedQueries: [],
allNodesExpanded: null,
isMocking: false,
mockedResponse: '',
......@@ -47,6 +59,43 @@ export class QueryInspector extends PureComponent<Props, State> {
appEvents.on(CoreEvents.dsRequestResponse, this.onDataSourceResponse);
appEvents.on(CoreEvents.dsRequestError, this.onRequestError);
this.props.panel.events.on(PanelEvents.refresh, this.onPanelRefresh);
this.updateQueryList();
}
componentDidUpdate(oldProps: Props) {
if (this.props.data !== oldProps.data) {
this.updateQueryList();
}
}
/**
* Find the list of executed queries
*/
updateQueryList() {
const { data } = this.props;
const executedQueries: ExecutedQueryInfo[] = [];
if (data?.length) {
let last: ExecutedQueryInfo | undefined = undefined;
data.forEach((frame, idx) => {
const query = frame.meta?.executedQueryString;
if (query) {
const refId = frame.refId || '?';
if (last?.refId === refId) {
last.frames++;
last.rows += frame.length;
} else {
last = {
refId,
frames: 0,
rows: frame.length,
query,
};
executedQueries.push(last);
}
}
});
}
this.setState({ executedQueries });
}
onIssueNewQuery = () => {
......@@ -182,8 +231,39 @@ export class QueryInspector extends PureComponent<Props, State> {
}));
};
renderExecutedQueries(executedQueries: ExecutedQueryInfo[]) {
if (!executedQueries.length) {
return null;
}
const styles = {
refId: css`
font-weight: ${config.theme.typography.weight.semibold};
color: ${config.theme.colors.textBlue};
margin-right: 8px;
`,
};
return (
<div>
{executedQueries.map(info => {
return (
<div key={info.refId}>
<div>
<span className={styles.refId}>{info.refId}:</span>
{info.frames > 1 && <span>{info.frames} frames, </span>}
<span>{info.rows} rows</span>
</div>
<pre>{info.query}</pre>
</div>
);
})}
</div>
);
}
render() {
const { allNodesExpanded } = this.state;
const { allNodesExpanded, executedQueries } = this.state;
const { response, isLoading } = this.state.dsQuery;
const openNodes = this.getNrOfOpenNodes();
const styles = getPanelInspectorStyles();
......@@ -202,6 +282,7 @@ export class QueryInspector extends PureComponent<Props, State> {
new query. Hit refresh button below to trigger a new query.
</p>
</div>
{this.renderExecutedQueries(executedQueries)}
<div className={styles.toolbar}>
<Button
icon="sync"
......
......@@ -20,11 +20,10 @@
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
<div class="gf-form">
<label class="gf-form-label query-keyword" ng-click="ctrl.showQueryInspector()">
Generated SQL
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form gf-form--grow">
......
......@@ -2,6 +2,7 @@ import _ from 'lodash';
import { QueryCtrl } from 'app/plugins/sdk';
import { auto } from 'angular';
import { PanelEvents } from '@grafana/data';
import { getLocationSrv } from '@grafana/runtime';
export interface MssqlQuery {
refId: string;
......@@ -28,10 +29,8 @@ ORDER BY
export class MssqlQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
showLastQuerySQL: boolean;
formats: any[];
target: MssqlQuery;
lastQueryMeta: QueryMeta;
lastQueryError: string;
showHelp: boolean;
......@@ -60,21 +59,21 @@ export class MssqlQueryCtrl extends QueryCtrl {
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
}
showQueryInspector() {
getLocationSrv().update({
query: { inspect: this.panel.id, inspectTab: 'query' },
partial: true,
});
}
onDataReceived(dataList: any) {
this.lastQueryMeta = null;
this.lastQueryError = null;
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
if (anySeriesFromQuery) {
this.lastQueryMeta = anySeriesFromQuery.meta;
}
}
onDataError(err: any) {
if (err.data && err.data.results) {
const queryRes = err.data.results[this.target.refId];
if (queryRes) {
this.lastQueryMeta = queryRes.meta;
this.lastQueryError = queryRes.error;
}
}
......
......@@ -120,11 +120,10 @@
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showQueryInspector()">
Generated SQL
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form gf-form--grow">
......@@ -132,10 +131,6 @@
</div>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
- return column named time or time_sec (in UTC), as a unix time stamp or any sql native date data type. You can use the macros below.
......
......@@ -10,6 +10,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { CoreEvents } from 'app/types';
import { PanelEvents } from '@grafana/data';
import { VariableWithMultiSupport } from 'app/features/templating/types';
import { getLocationSrv } from '@grafana/runtime';
export interface QueryMeta {
sql: string;
......@@ -27,9 +28,7 @@ ORDER BY <time_column> ASC
export class MysqlQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
showLastQuerySQL: boolean;
formats: any[];
lastQueryMeta: QueryMeta;
lastQueryError: string;
showHelp: boolean;
......@@ -110,6 +109,13 @@ export class MysqlQueryCtrl extends QueryCtrl {
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
}
showQueryInspector() {
getLocationSrv().update({
query: { inspect: this.panel.id, inspectTab: 'query' },
partial: true,
});
}
updateRawSqlAndRefresh() {
if (!this.target.rawQuery) {
this.target.rawSql = this.queryModel.buildQuery();
......@@ -273,20 +279,13 @@ export class MysqlQueryCtrl extends QueryCtrl {
}
onDataReceived(dataList: any) {
this.lastQueryMeta = null;
this.lastQueryError = null;
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
if (anySeriesFromQuery) {
this.lastQueryMeta = anySeriesFromQuery.meta;
}
}
onDataError(err: any) {
if (err.data && err.data.results) {
const queryRes = err.data.results[this.target.refId];
if (queryRes) {
this.lastQueryMeta = queryRes.meta;
this.lastQueryError = queryRes.error;
}
}
......
......@@ -120,11 +120,10 @@
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form" ng-show="ctrl.lastQueryMeta">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showLastQuerySQL = !ctrl.showLastQuerySQL">
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showQueryInspector()">
Generated SQL
<icon name="'angle-down'" ng-show="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" ng-hide="ctrl.showLastQuerySQL" style="margin-top: 3px;"></icon>
<icon name="'angle-right'" style="margin-top: 3px;"></icon>
</label>
</div>
<div class="gf-form gf-form--grow">
......@@ -132,9 +131,6 @@
</div>
</div>
<div class="gf-form" ng-show="ctrl.showLastQuerySQL">
<pre class="gf-form-pre">{{ctrl.lastQueryMeta.sql}}</pre>
</div>
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info">Time series:
......
......@@ -10,6 +10,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv';
import { CoreEvents } from 'app/types';
import { PanelEvents } from '@grafana/data';
import { VariableWithMultiSupport } from 'app/features/templating/types';
import { getLocationSrv } from '@grafana/runtime';
export interface QueryMeta {
sql: string;
......@@ -31,7 +32,6 @@ export class PostgresQueryCtrl extends QueryCtrl {
formats: any[];
queryModel: PostgresQuery;
metaBuilder: PostgresMetaQuery;
lastQueryMeta: QueryMeta;
lastQueryError: string;
showHelp: boolean;
tableSegment: any;
......@@ -108,6 +108,13 @@ export class PostgresQueryCtrl extends QueryCtrl {
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope);
}
showQueryInspector() {
getLocationSrv().update({
query: { inspect: this.panel.id, inspectTab: 'query' },
partial: true,
});
}
updateRawSqlAndRefresh() {
if (!this.target.rawQuery) {
this.target.rawSql = this.queryModel.buildQuery();
......@@ -306,21 +313,13 @@ export class PostgresQueryCtrl extends QueryCtrl {
}
onDataReceived(dataList: any) {
this.lastQueryMeta = null;
this.lastQueryError = null;
console.log('postgres query data received', dataList);
const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
if (anySeriesFromQuery) {
this.lastQueryMeta = anySeriesFromQuery.meta;
}
}
onDataError(err: any) {
if (err.data && err.data.results) {
const queryRes = err.data.results[this.target.refId];
if (queryRes) {
this.lastQueryMeta = queryRes.meta;
this.lastQueryError = queryRes.error;
}
}
......
......@@ -107,7 +107,10 @@ export class QueryEditor extends PureComponent<Props, State> {
query={sloQuery}
></SLOQueryEditor>
)}
<Help rawQuery={decodeURIComponent(meta?.rawQuery ?? '')} lastQueryError={this.state.lastQueryError} />
<Help
rawQuery={decodeURIComponent(meta?.executedQueryString ?? '')}
lastQueryError={this.state.lastQueryError}
/>
</>
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment