mirror of https://github.com/grafana/grafana
Live: support query execution with live RPC (#43118)
Co-authored-by: Alexander Emelin <frvzmb@gmail.com>pull/43127/head
parent
f4cc353225
commit
c0ff685d3b
@ -0,0 +1,17 @@ |
|||||||
|
package query |
||||||
|
|
||||||
|
import "fmt" |
||||||
|
|
||||||
|
// ErrBadQuery returned whenever request is malformed and must contain a message
|
||||||
|
// suitable to return in API response.
|
||||||
|
type ErrBadQuery struct { |
||||||
|
Message string |
||||||
|
} |
||||||
|
|
||||||
|
func NewErrBadQuery(msg string) *ErrBadQuery { |
||||||
|
return &ErrBadQuery{Message: msg} |
||||||
|
} |
||||||
|
|
||||||
|
func (e ErrBadQuery) Error() string { |
||||||
|
return fmt.Sprintf("bad query: %s", e.Message) |
||||||
|
} |
@ -0,0 +1,265 @@ |
|||||||
|
package query |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos" |
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson" |
||||||
|
"github.com/grafana/grafana/pkg/expr" |
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
"github.com/grafana/grafana/pkg/models" |
||||||
|
"github.com/grafana/grafana/pkg/plugins" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/adapters" |
||||||
|
"github.com/grafana/grafana/pkg/services/datasources" |
||||||
|
"github.com/grafana/grafana/pkg/services/oauthtoken" |
||||||
|
"github.com/grafana/grafana/pkg/services/secrets" |
||||||
|
"github.com/grafana/grafana/pkg/setting" |
||||||
|
"github.com/grafana/grafana/pkg/tsdb/grafanads" |
||||||
|
"github.com/grafana/grafana/pkg/tsdb/legacydata" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||||
|
) |
||||||
|
|
||||||
|
func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, expressionService *expr.Service, |
||||||
|
pluginRequestValidator models.PluginRequestValidator, SecretsService secrets.Service, |
||||||
|
pluginClient plugins.Client, OAuthTokenService oauthtoken.OAuthTokenService) *Service { |
||||||
|
g := &Service{ |
||||||
|
cfg: cfg, |
||||||
|
dataSourceCache: dataSourceCache, |
||||||
|
expressionService: expressionService, |
||||||
|
pluginRequestValidator: pluginRequestValidator, |
||||||
|
secretsService: SecretsService, |
||||||
|
pluginClient: pluginClient, |
||||||
|
oAuthTokenService: OAuthTokenService, |
||||||
|
log: log.New("query_data"), |
||||||
|
} |
||||||
|
g.log.Info("Query Service initialization") |
||||||
|
return g |
||||||
|
} |
||||||
|
|
||||||
|
// Gateway receives data and translates it to Grafana Live publications.
|
||||||
|
type Service struct { |
||||||
|
cfg *setting.Cfg |
||||||
|
dataSourceCache datasources.CacheService |
||||||
|
expressionService *expr.Service |
||||||
|
pluginRequestValidator models.PluginRequestValidator |
||||||
|
secretsService secrets.Service |
||||||
|
pluginClient plugins.Client |
||||||
|
oAuthTokenService oauthtoken.OAuthTokenService |
||||||
|
log log.Logger |
||||||
|
} |
||||||
|
|
||||||
|
// Run Service.
|
||||||
|
func (s *Service) Run(ctx context.Context) error { |
||||||
|
<-ctx.Done() |
||||||
|
return ctx.Err() |
||||||
|
} |
||||||
|
|
||||||
|
// QueryData can process queries and return query responses.
|
||||||
|
func (s *Service) QueryData(ctx context.Context, user *models.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest, handleExpressions bool) (*backend.QueryDataResponse, error) { |
||||||
|
parsedReq, err := s.parseMetricRequest(user, skipCache, reqDTO) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if handleExpressions && parsedReq.hasExpression { |
||||||
|
return s.handleExpressions(ctx, user, parsedReq) |
||||||
|
} |
||||||
|
return s.handleQueryData(ctx, user, parsedReq) |
||||||
|
} |
||||||
|
|
||||||
|
// handleExpressions handles POST /api/ds/query when there is an expression.
|
||||||
|
func (s *Service) handleExpressions(ctx context.Context, user *models.SignedInUser, parsedReq *parsedRequest) (*backend.QueryDataResponse, error) { |
||||||
|
exprReq := expr.Request{ |
||||||
|
OrgId: user.OrgId, |
||||||
|
Queries: []expr.Query{}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, pq := range parsedReq.parsedQueries { |
||||||
|
if pq.datasource == nil { |
||||||
|
return nil, NewErrBadQuery(fmt.Sprintf("query mising datasource info: %s", pq.query.RefID)) |
||||||
|
} |
||||||
|
|
||||||
|
exprReq.Queries = append(exprReq.Queries, expr.Query{ |
||||||
|
JSON: pq.query.JSON, |
||||||
|
Interval: pq.query.Interval, |
||||||
|
RefID: pq.query.RefID, |
||||||
|
MaxDataPoints: pq.query.MaxDataPoints, |
||||||
|
QueryType: pq.query.QueryType, |
||||||
|
Datasource: expr.DataSourceRef{ |
||||||
|
Type: pq.datasource.Type, |
||||||
|
UID: pq.datasource.Uid, |
||||||
|
}, |
||||||
|
TimeRange: expr.TimeRange{ |
||||||
|
From: pq.query.TimeRange.From, |
||||||
|
To: pq.query.TimeRange.To, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
qdr, err := s.expressionService.TransformData(ctx, &exprReq) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("expression request error: %w", err) |
||||||
|
} |
||||||
|
return qdr, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser, parsedReq *parsedRequest) (*backend.QueryDataResponse, error) { |
||||||
|
ds := parsedReq.parsedQueries[0].datasource |
||||||
|
if err := s.pluginRequestValidator.Validate(ds.Url, nil); err != nil { |
||||||
|
return nil, models.ErrDataSourceAccessDenied |
||||||
|
} |
||||||
|
|
||||||
|
instanceSettings, err := adapters.ModelToInstanceSettings(ds, s.decryptSecureJsonDataFn(ctx)) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to convert data source to instance settings: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
req := &backend.QueryDataRequest{ |
||||||
|
PluginContext: backend.PluginContext{ |
||||||
|
OrgID: ds.OrgId, |
||||||
|
PluginID: ds.Type, |
||||||
|
User: adapters.BackendUserFromSignedInUser(user), |
||||||
|
DataSourceInstanceSettings: instanceSettings, |
||||||
|
}, |
||||||
|
Headers: map[string]string{}, |
||||||
|
Queries: []backend.DataQuery{}, |
||||||
|
} |
||||||
|
|
||||||
|
if s.oAuthTokenService.IsOAuthPassThruEnabled(ds) { |
||||||
|
if token := s.oAuthTokenService.GetCurrentOAuthToken(ctx, user); token != nil { |
||||||
|
req.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for _, q := range parsedReq.parsedQueries { |
||||||
|
req.Queries = append(req.Queries, q.query) |
||||||
|
} |
||||||
|
|
||||||
|
return s.pluginClient.QueryData(ctx, req) |
||||||
|
} |
||||||
|
|
||||||
|
type parsedQuery struct { |
||||||
|
datasource *models.DataSource |
||||||
|
query backend.DataQuery |
||||||
|
} |
||||||
|
|
||||||
|
type parsedRequest struct { |
||||||
|
hasExpression bool |
||||||
|
parsedQueries []parsedQuery |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) parseMetricRequest(user *models.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) { |
||||||
|
if len(reqDTO.Queries) == 0 { |
||||||
|
return nil, NewErrBadQuery("no queries found") |
||||||
|
} |
||||||
|
|
||||||
|
timeRange := legacydata.NewDataTimeRange(reqDTO.From, reqDTO.To) |
||||||
|
req := &parsedRequest{ |
||||||
|
hasExpression: false, |
||||||
|
parsedQueries: []parsedQuery{}, |
||||||
|
} |
||||||
|
|
||||||
|
// Parse the queries
|
||||||
|
datasourcesByUid := map[string]*models.DataSource{} |
||||||
|
for _, query := range reqDTO.Queries { |
||||||
|
ds, err := s.getDataSourceFromQuery(user, skipCache, query, datasourcesByUid) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if ds == nil { |
||||||
|
return nil, NewErrBadQuery("invalid data source ID") |
||||||
|
} |
||||||
|
|
||||||
|
datasourcesByUid[ds.Uid] = ds |
||||||
|
if expr.IsDataSource(ds.Uid) { |
||||||
|
req.hasExpression = true |
||||||
|
} |
||||||
|
|
||||||
|
s.log.Debug("Processing metrics query", "query", query) |
||||||
|
|
||||||
|
modelJSON, err := query.MarshalJSON() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
req.parsedQueries = append(req.parsedQueries, parsedQuery{ |
||||||
|
datasource: ds, |
||||||
|
query: backend.DataQuery{ |
||||||
|
TimeRange: backend.TimeRange{ |
||||||
|
From: timeRange.GetFromAsTimeUTC(), |
||||||
|
To: timeRange.GetToAsTimeUTC(), |
||||||
|
}, |
||||||
|
RefID: query.Get("refId").MustString("A"), |
||||||
|
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), |
||||||
|
Interval: time.Duration(query.Get("intervalMs").MustInt64(1000)) * time.Millisecond, |
||||||
|
QueryType: query.Get("queryType").MustString(""), |
||||||
|
JSON: modelJSON, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if !req.hasExpression { |
||||||
|
if len(datasourcesByUid) > 1 { |
||||||
|
// We do not (yet) support mixed query type
|
||||||
|
return nil, NewErrBadQuery("all queries must use the same datasource") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return req, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) getDataSourceFromQuery(user *models.SignedInUser, skipCache bool, query *simplejson.Json, history map[string]*models.DataSource) (*models.DataSource, error) { |
||||||
|
var err error |
||||||
|
uid := query.Get("datasource").Get("uid").MustString() |
||||||
|
|
||||||
|
// before 8.3 special types could be sent as datasource (expr)
|
||||||
|
if uid == "" { |
||||||
|
uid = query.Get("datasource").MustString() |
||||||
|
} |
||||||
|
|
||||||
|
// check cache value
|
||||||
|
ds, ok := history[uid] |
||||||
|
if ok { |
||||||
|
return ds, nil |
||||||
|
} |
||||||
|
|
||||||
|
if expr.IsDataSource(uid) { |
||||||
|
return expr.DataSourceModel(), nil |
||||||
|
} |
||||||
|
|
||||||
|
if uid == grafanads.DatasourceUID { |
||||||
|
return grafanads.DataSourceModel(user.OrgId), nil |
||||||
|
} |
||||||
|
|
||||||
|
// use datasourceId if it exists
|
||||||
|
id := query.Get("datasourceId").MustInt64(0) |
||||||
|
if id > 0 { |
||||||
|
ds, err = s.dataSourceCache.GetDatasource(id, user, skipCache) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return ds, nil |
||||||
|
} |
||||||
|
|
||||||
|
if uid != "" { |
||||||
|
ds, err = s.dataSourceCache.GetDatasourceByUID(uid, user, skipCache) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return ds, nil |
||||||
|
} |
||||||
|
|
||||||
|
return nil, NewErrBadQuery("missing data source ID/UID") |
||||||
|
} |
||||||
|
|
||||||
|
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(map[string][]byte) map[string]string { |
||||||
|
return func(m map[string][]byte) map[string]string { |
||||||
|
decryptedJsonData, err := s.secretsService.DecryptJsonData(ctx, m) |
||||||
|
if err != nil { |
||||||
|
s.log.Error("Failed to decrypt secure json data", "error", err) |
||||||
|
} |
||||||
|
return decryptedJsonData |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue