diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 3927be404e0..04af0024e30 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -33,11 +33,16 @@ export interface QueryResultMeta { /** Currently used to show results in Explore only in preferred visualisation option */ 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 diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index a36d2d8fb75..1819137129c 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -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") }) diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 720aca8cc36..19e75cd351f 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -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") }) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index edf87eff1dc..92d0481d631 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -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") }) }) diff --git a/pkg/tsdb/sqleng/sql_engine.go b/pkg/tsdb/sqleng/sql_engine.go index 730d366be09..0d66fc06b32 100644 --- a/pkg/tsdb/sqleng/sql_engine.go +++ b/pkg/tsdb/sqleng/sql_engine.go @@ -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) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 9fccdc5dc51..db7fc94c588 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -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 { diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index bed7fe01f3e..974eb0b9f49 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -354,7 +354,7 @@ export class PanelInspectorUnconnected extends PureComponent { )} {activeTab === InspectTab.Error && this.renderErrorTab(error)} {activeTab === InspectTab.Stats && this.renderStatsTab()} - {activeTab === InspectTab.Query && } + {activeTab === InspectTab.Query && } diff --git a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx index 0f71b6767c6..a5df84432d3 100644 --- a/public/app/features/dashboard/components/Inspector/QueryInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/QueryInspector.tsx @@ -1,7 +1,7 @@ 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 { @@ -33,6 +44,7 @@ export class QueryInspector extends PureComponent { constructor(props: Props) { super(props); this.state = { + executedQueries: [], allNodesExpanded: null, isMocking: false, mockedResponse: '', @@ -47,6 +59,43 @@ export class QueryInspector extends PureComponent { 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 { })); }; + 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 ( +
+ {executedQueries.map(info => { + return ( +
+
+ {info.refId}: + {info.frames > 1 && {info.frames} frames, } + {info.rows} rows +
+
{info.query}
+
+ ); + })} +
+ ); + } + 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 { new query. Hit refresh button below to trigger a new query.

+ {this.renderExecutedQueries(executedQueries)}
-
-