@@ -7,12 +7,15 @@ import (
7
7
"errors"
8
8
"fmt"
9
9
"net/http"
10
+ "strings"
10
11
"time"
11
12
12
13
"github.com/grafana/dataplane/sdata/timeseries"
13
14
"github.com/grafana/grafana-plugin-sdk-go/backend"
14
15
"github.com/grafana/grafana-plugin-sdk-go/data"
15
16
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
17
+ "github.com/prometheus/client_golang/prometheus"
18
+ "github.com/prometheus/client_golang/prometheus/promauto"
16
19
)
17
20
18
21
// FormatQueryOption defines how the user has chosen to represent the data
@@ -36,6 +39,12 @@ const (
36
39
// Deprecated: use sqlutil.Query directly instead
37
40
type Query = sqlutil.Query
38
41
42
+ var duration = promauto .NewHistogramVec (prometheus.HistogramOpts {
43
+ Namespace : "plugins" ,
44
+ Name : "plugin_external_requests_duration_seconds" ,
45
+ Help : "Duration of requests to external services" ,
46
+ }, []string {"datasource_name" , "datasource_type" , "error_source" })
47
+
39
48
// GetQuery wraps sqlutil's GetQuery to add headers if needed
40
49
func GetQuery (query backend.DataQuery , headers http.Header , setHeaders bool ) (* Query , error ) {
41
50
model , err := sqlutil .GetQuery (query )
@@ -51,29 +60,63 @@ func GetQuery(query backend.DataQuery, headers http.Header, setHeaders bool) (*Q
51
60
return model , nil
52
61
}
53
62
54
- // QueryDB sends the query to the connection and converts the rows to a dataframe.
55
- func QueryDB (ctx context.Context , db Connection , converters []sqlutil.Converter , fillMode * data.FillMissing , query * Query , args ... interface {}) (data.Frames , error ) {
56
- // Query the rows from the database
57
- rows , err := db .QueryContext (ctx , query .RawSQL , args ... )
63
+ type DBQuery struct {
64
+ DSName string
65
+ DB Connection
66
+ Settings backend.DataSourceInstanceSettings
67
+ converters []sqlutil.Converter
68
+ fillMode * data.FillMissing
69
+ }
70
+
71
+ func NewQuery (db Connection , settings backend.DataSourceInstanceSettings , converters []sqlutil.Converter , fillMode * data.FillMissing ) * DBQuery {
72
+ dsName , ok := sanitizeLabelName (settings .Name )
73
+ if ! ok {
74
+ backend .Logger .Warn ("Failed to sanitize datasource name for prometheus label" , dsName )
75
+ }
76
+ return & DBQuery {
77
+ DB : db ,
78
+ DSName : dsName ,
79
+ converters : converters ,
80
+ fillMode : fillMode ,
81
+ }
82
+ }
83
+
84
+ // Run sends the query to the connection and converts the rows to a dataframe.
85
+ func (q * DBQuery ) Run (ctx context.Context , query * Query , args ... interface {}) (data.Frames , error ) {
86
+ start := time .Now ()
87
+ var errWithSource error
88
+
89
+ defer func () {
90
+ if q .DSName != "" {
91
+ errorSource := "none"
92
+ if errWithSource != nil {
93
+ errorSource = string (ErrorSource (errWithSource ))
94
+ }
95
+
96
+ duration .WithLabelValues (q .DSName , q .Settings .Type , errorSource ).Observe (time .Since (start ).Seconds ())
97
+ }
98
+ }()
99
+
100
+ rows , err := q .DB .QueryContext (ctx , query .RawSQL , args ... )
58
101
if err != nil {
59
102
errType := ErrorQuery
60
103
if errors .Is (err , context .Canceled ) {
61
104
errType = context .Canceled
62
105
}
63
- err := DownstreamError (fmt .Errorf ("%w: %s" , errType , err .Error ()))
64
- return sqlutil .ErrorFrameFromQuery (query ), err
106
+ errWithSource := DownstreamError (fmt .Errorf ("%w: %s" , errType , err .Error ()))
107
+ return sqlutil .ErrorFrameFromQuery (query ), errWithSource
65
108
}
66
109
67
110
// Check for an error response
68
111
if err := rows .Err (); err != nil {
69
112
if errors .Is (err , sql .ErrNoRows ) {
70
113
// Should we even response with an error here?
71
114
// The panel will simply show "no data"
72
- err := DownstreamError (fmt .Errorf ("%s: %w" , "No results from query" , err ))
73
- return sqlutil .ErrorFrameFromQuery (query ), err
115
+ errWithSource := DownstreamError (fmt .Errorf ("%s: %w" , "No results from query" , err ))
116
+ return sqlutil .ErrorFrameFromQuery (query ), errWithSource
74
117
}
75
- err := DownstreamError (fmt .Errorf ("%s: %w" , "Error response from database" , err ))
76
- return sqlutil .ErrorFrameFromQuery (query ), err
118
+ errWithSource := DownstreamError (fmt .Errorf ("%s: %w" , "Error response from database" , err ))
119
+ return sqlutil .ErrorFrameFromQuery (query ), errWithSource
77
120
}
78
121
79
122
defer func () {
@@ -83,10 +126,10 @@ func QueryDB(ctx context.Context, db Connection, converters []sqlutil.Converter,
83
126
}()
84
127
85
128
// Convert the response to frames
86
- res , err := getFrames (rows , - 1 , converters , fillMode , query )
129
+ res , err := getFrames (rows , - 1 , q . converters , q . fillMode , query )
87
130
if err != nil {
88
- err := PluginError (fmt .Errorf ("%w: %s" , err , "Could not process SQL results" ))
89
- return sqlutil .ErrorFrameFromQuery (query ), err
131
+ errWithSource := PluginError (fmt .Errorf ("%w: %s" , err , "Could not process SQL results" ))
132
+ return sqlutil .ErrorFrameFromQuery (query ), errWithSource
90
133
}
91
134
92
135
return res , nil
@@ -203,3 +246,29 @@ func applyHeaders(query *Query, headers http.Header) *Query {
203
246
204
247
return query
205
248
}
249
+
250
+ // sanitizeLabelName removes all invalid chars from the label name.
251
+ // If the label name is empty or contains only invalid chars, it will return false indicating it was not sanitized.
252
+ // copied from https://github.com/grafana/grafana/blob/main/pkg/infra/metrics/metricutil/utils.go#L14
253
+ func sanitizeLabelName (name string ) (string , bool ) {
254
+ if len (name ) == 0 {
255
+ backend .Logger .Warn (fmt .Sprintf ("label name cannot be empty: %s" , name ))
256
+ return "" , false
257
+ }
258
+
259
+ out := strings.Builder {}
260
+ for i , b := range name {
261
+ if (b >= 'a' && b <= 'z' ) || (b >= 'A' && b <= 'Z' ) || b == '_' || (b >= '0' && b <= '9' && i > 0 ) {
262
+ out .WriteRune (b )
263
+ } else if b == ' ' {
264
+ out .WriteRune ('_' )
265
+ }
266
+ }
267
+
268
+ if out .Len () == 0 {
269
+ backend .Logger .Warn (fmt .Sprintf ("label name only contains invalid chars: %q" , name ))
270
+ return "" , false
271
+ }
272
+
273
+ return out .String (), true
274
+ }
0 commit comments