1package expr
2
3import (
4	"encoding/json"
5	"fmt"
6	"time"
7
8	"github.com/grafana/grafana-plugin-sdk-go/backend"
9	"github.com/grafana/grafana/pkg/bus"
10	"github.com/grafana/grafana/pkg/models"
11	"github.com/grafana/grafana/pkg/plugins/adapters"
12	"github.com/grafana/grafana/pkg/tsdb/legacydata"
13	"github.com/grafana/grafana/pkg/util/errutil"
14	"github.com/prometheus/client_golang/prometheus"
15	"golang.org/x/net/context"
16)
17
18var (
19	expressionsQuerySummary *prometheus.SummaryVec
20)
21
22func init() {
23	expressionsQuerySummary = prometheus.NewSummaryVec(
24		prometheus.SummaryOpts{
25			Name:       "expressions_queries_duration_milliseconds",
26			Help:       "Expressions query summary",
27			Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
28		},
29		[]string{"status"},
30	)
31
32	prometheus.MustRegister(expressionsQuerySummary)
33}
34
35// WrapTransformData creates and executes transform requests
36func (s *Service) WrapTransformData(ctx context.Context, query legacydata.DataQuery) (*backend.QueryDataResponse, error) {
37	req := Request{
38		OrgId:   query.User.OrgId,
39		Queries: []Query{},
40	}
41
42	for _, q := range query.Queries {
43		if q.DataSource == nil {
44			return nil, fmt.Errorf("mising datasource info: " + q.RefID)
45		}
46		modelJSON, err := q.Model.MarshalJSON()
47		if err != nil {
48			return nil, err
49		}
50		req.Queries = append(req.Queries, Query{
51			JSON:          modelJSON,
52			Interval:      time.Duration(q.IntervalMS) * time.Millisecond,
53			RefID:         q.RefID,
54			MaxDataPoints: q.MaxDataPoints,
55			QueryType:     q.QueryType,
56			Datasource: DataSourceRef{
57				Type: q.DataSource.Type,
58				UID:  q.DataSource.Uid,
59			},
60			TimeRange: TimeRange{
61				From: query.TimeRange.GetFromAsTimeUTC(),
62				To:   query.TimeRange.GetToAsTimeUTC(),
63			},
64		})
65	}
66	return s.TransformData(ctx, &req)
67}
68
69// Request is similar to plugins.DataQuery but with the Time Ranges is per Query.
70type Request struct {
71	Headers map[string]string
72	Debug   bool
73	OrgId   int64
74	Queries []Query
75}
76
77// Query is like plugins.DataSubQuery, but with a a time range, and only the UID
78// for the data source. Also interval is a time.Duration.
79type Query struct {
80	RefID         string
81	TimeRange     TimeRange
82	DatasourceUID string        // deprecated, value -100 when expressions
83	Datasource    DataSourceRef `json:"datasource"`
84	JSON          json.RawMessage
85	Interval      time.Duration
86	QueryType     string
87	MaxDataPoints int64
88}
89
90type DataSourceRef struct {
91	Type string `json:"type"` // value should be __expr__
92	UID  string `json:"uid"`  // value should be __expr__
93}
94
95func (q *Query) GetDatasourceUID() string {
96	if q.DatasourceUID != "" {
97		return q.DatasourceUID // backwards compatibility gets precedence
98	}
99
100	if q.Datasource.UID != "" {
101		return q.Datasource.UID
102	}
103	return ""
104}
105
106// TimeRange is a time.Time based TimeRange.
107type TimeRange struct {
108	From time.Time
109	To   time.Time
110}
111
112// TransformData takes Queries which are either expressions nodes
113// or are datasource requests.
114func (s *Service) TransformData(ctx context.Context, req *Request) (r *backend.QueryDataResponse, err error) {
115	if s.isDisabled() {
116		return nil, fmt.Errorf("server side expressions are disabled")
117	}
118
119	start := time.Now()
120	defer func() {
121		var respStatus string
122		switch {
123		case err == nil:
124			respStatus = "success"
125		default:
126			respStatus = "failure"
127		}
128		duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond)
129		expressionsQuerySummary.WithLabelValues(respStatus).Observe(duration)
130	}()
131
132	// Build the pipeline from the request, checking for ordering issues (e.g. loops)
133	// and parsing graph nodes from the queries.
134	pipeline, err := s.BuildPipeline(req)
135	if err != nil {
136		return nil, err
137	}
138
139	// Execute the pipeline
140	responses, err := s.ExecutePipeline(ctx, pipeline)
141	if err != nil {
142		return nil, err
143	}
144
145	// Get which queries have the Hide property so they those queries' results
146	// can be excluded from the response.
147	hidden, err := hiddenRefIDs(req.Queries)
148	if err != nil {
149		return nil, err
150	}
151
152	if len(hidden) != 0 {
153		filteredRes := backend.NewQueryDataResponse()
154		for refID, res := range responses.Responses {
155			if _, ok := hidden[refID]; !ok {
156				filteredRes.Responses[refID] = res
157			}
158		}
159		responses = filteredRes
160	}
161
162	return responses, nil
163}
164
165func hiddenRefIDs(queries []Query) (map[string]struct{}, error) {
166	hidden := make(map[string]struct{})
167
168	for _, query := range queries {
169		hide := struct {
170			Hide bool `json:"hide"`
171		}{}
172
173		if err := json.Unmarshal(query.JSON, &hide); err != nil {
174			return nil, err
175		}
176
177		if hide.Hide {
178			hidden[query.RefID] = struct{}{}
179		}
180	}
181	return hidden, nil
182}
183
184// queryData is called used to query datasources that are not expression commands, but are used
185// alongside expressions and/or are the input of an expression command.
186func (s *Service) queryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
187	if len(req.Queries) == 0 {
188		return nil, fmt.Errorf("zero queries found in datasource request")
189	}
190
191	datasourceID := int64(0)
192	var datasourceUID string
193
194	if req.PluginContext.DataSourceInstanceSettings != nil {
195		datasourceID = req.PluginContext.DataSourceInstanceSettings.ID
196		datasourceUID = req.PluginContext.DataSourceInstanceSettings.UID
197	}
198
199	getDsInfo := &models.GetDataSourceQuery{
200		OrgId: req.PluginContext.OrgID,
201		Id:    datasourceID,
202		Uid:   datasourceUID,
203	}
204
205	if err := bus.DispatchCtx(ctx, getDsInfo); err != nil {
206		return nil, fmt.Errorf("could not find datasource: %w", err)
207	}
208
209	dsInstanceSettings, err := adapters.ModelToInstanceSettings(getDsInfo.Result, s.decryptSecureJsonDataFn(ctx))
210	if err != nil {
211		return nil, errutil.Wrap("failed to convert datasource instance settings", err)
212	}
213
214	req.PluginContext.DataSourceInstanceSettings = dsInstanceSettings
215	req.PluginContext.PluginID = getDsInfo.Result.Type
216
217	return s.dataService.QueryData(ctx, req)
218}
219
220func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(map[string][]byte) map[string]string {
221	return func(m map[string][]byte) map[string]string {
222		decryptedJsonData, err := s.secretsService.DecryptJsonData(ctx, m)
223		if err != nil {
224			logger.Error("Failed to decrypt secure json data", "error", err)
225		}
226		return decryptedJsonData
227	}
228}
229