Skip to content

Commit e6b5890

Browse files
yesoreyeramivanahuckovawardbekker
authored
3.0.0-beta.1 release (#1134)
Signed-off-by: Sriram <[email protected]> Co-authored-by: Ivana Huckova <[email protected]> Co-authored-by: Ward Bekker <[email protected]>
1 parent 6d58f49 commit e6b5890

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2949
-2723
lines changed

CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# Change Log
22

3-
## 2.12.2
3+
## 3.0.0-beta.1
4+
5+
🚀 **New Feature**: Support for passing grafana meta data such as user id, datasource uid to the underlying API as headers / query params via datasource settings
6+
🚀 **Improvements**: Added support for gzip compression for outgoing requests by default. Fixes [#1003](https://github.com/grafana/grafana-infinity-datasource/issues/1003)
7+
🚀 **Improvements**: Added frame type to dataplane compliant numeric data frames. This will help us to handle the results correctly in alerts, recorded queries, SSE etc.
8+
🎉 **Chore**: BREAKING: Plugin now requires Grafana 10.4.8 or newer
49

5-
### Patch Changes
10+
## 2.12.2
611

712
🐛 Build and publish pipelines uses latest go lang version `1.23.5` which includes security fixes to the `crypto/x509` and `net/http` packages ( CVE-2024-45341 and CVE-2024-45336 ). More details can be found [here](https://groups.google.com/g/golang-announce/c/sSaUhLA-2SI)
813

cspell.config.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"countif",
4343
"csvframer",
4444
"dataframe",
45+
"dataplane",
4546
"datapoints",
4647
"dataproxy",
4748
"datasource",

docs/sources/references/url.md

+18
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ You can configure the headers required for the URL in the datasource config and
5858

5959
Note: We suggest adding secure headers only via configuration and not in query.
6060

61+
## Forwarding Grafana meta data as headers / query params
62+
63+
From Infinity plugin version 3.0.0, You will be able to forward grafana meta data such as user id, datasource uid to the outgoing requests via **Custom HTTP Headers** / **URL Query parameters\*** from the datasource settings page. In the datasource **URL** section, you can add any number of custom headers / query parameters with their own values. The values can include following macros which will be interpolated into actual value from the request context.
64+
65+
| Macro name | Description |
66+
| --------------------- | ------------------------------------------------------------------- |
67+
| `${__org.id}` | This will be replaced by grafana org id where the request came from |
68+
| `${__plugin.id}` | This will be replaced by the plugin id |
69+
| `${__plugin.version}` | This will be replaced by the plugin version |
70+
| `${__ds.uid}` | This will be replaced by the datasource uid |
71+
| `${__ds.name}` | This will be replaced by the datasource name |
72+
| `${__ds.id}` | This will be replaced by the datasource id (deprecated) |
73+
| `${__user.login}` | This will be replaced by the user login id |
74+
| `${__user.email}` | This will be replaced by the user login email |
75+
| `${__user.name}` | This will be replaced by the user name |
76+
77+
> Note: Certain macros such as `${__user.login}` won't be available in the context of alerts, recorded queries, public dashboards etc.
78+
6179
## Allowed Hosts
6280

6381
Leaving blank will allow all the hosts. This is by default.

package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "grafana-infinity-datasource",
3-
"version": "2.12.2",
3+
"version": "3.0.0-beta.1",
44
"description": "JSON, CSV, XML, GraphQL, HTML and REST API datasource for Grafana. Do infinite things with Grafana. Transform data with UQL/GROQ. Visualize data from many apis, RSS/ATOM feeds directly",
55
"keywords": [
66
"grafana",
@@ -56,10 +56,10 @@
5656
},
5757
"dependencies": {
5858
"@emotion/css": "11.10.6",
59-
"@grafana/data": "10.3.3",
60-
"@grafana/runtime": "10.3.3",
61-
"@grafana/schema": "10.3.3",
62-
"@grafana/ui": "10.3.3",
59+
"@grafana/data": "10.4.8",
60+
"@grafana/runtime": "10.4.8",
61+
"@grafana/schema": "10.4.8",
62+
"@grafana/ui": "10.4.8",
6363
"cheerio": "^1.0.0-rc.10",
6464
"csv-parse": "^4.12.0",
6565
"groq-js": "1.1.8",
@@ -94,6 +94,7 @@
9494
"@types/lodash": "^4.14.194",
9595
"@types/mathjs": "^6.0.5",
9696
"@types/node": "^20.8.7",
97+
"@types/react": "18.2.0",
9798
"@types/react-router-dom": "^5.2.0",
9899
"@types/testing-library__jest-dom": "5.14.8",
99100
"@types/xml2js": "^0.4.6",

pkg/dataplane/dataplane.go

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package dataplane
2+
3+
import "github.com/grafana/grafana-plugin-sdk-go/data"
4+
5+
type fieldTypeCount struct {
6+
nullableFields int
7+
nonNullableFields int
8+
unknownFields int
9+
numericFields int
10+
boolFields int
11+
stringFields int
12+
timeFields int
13+
jsonFields int
14+
enumFields int
15+
}
16+
17+
func getFieldTypesCount(frame *data.Frame) fieldTypeCount {
18+
res := fieldTypeCount{
19+
nullableFields: 0,
20+
nonNullableFields: 0,
21+
unknownFields: 0,
22+
numericFields: 0,
23+
boolFields: 0,
24+
stringFields: 0,
25+
timeFields: 0,
26+
jsonFields: 0,
27+
}
28+
for _, field := range frame.Fields {
29+
if field == nil {
30+
continue
31+
}
32+
if field.Nullable() {
33+
res.nullableFields++
34+
}
35+
if !field.Nullable() {
36+
res.nonNullableFields++
37+
}
38+
if field.Type().Numeric() {
39+
res.numericFields++
40+
continue
41+
}
42+
if field.Type().Time() {
43+
res.timeFields++
44+
continue
45+
}
46+
if field.Type().JSON() {
47+
res.jsonFields++
48+
continue
49+
}
50+
switch field.Type() {
51+
case data.FieldTypeBool,
52+
data.FieldTypeNullableBool:
53+
res.boolFields++
54+
case data.FieldTypeString,
55+
data.FieldTypeNullableString:
56+
res.stringFields++
57+
case data.FieldTypeEnum,
58+
data.FieldTypeNullableEnum:
59+
res.enumFields++
60+
default:
61+
res.unknownFields++
62+
}
63+
}
64+
return res
65+
}
66+
67+
// CanBeNumericWide asserts if the data frame comply with numeric wide type
68+
// https://grafana.com/developers/dataplane/numeric#numeric-wide-format-numericwide
69+
func CanBeNumericWide(frame *data.Frame) bool {
70+
if frame == nil {
71+
return false
72+
}
73+
ftCount := getFieldTypesCount(frame)
74+
rowLen, err := frame.RowLen()
75+
if err != nil {
76+
return false
77+
}
78+
if rowLen <= 1 && (ftCount.numericFields+ftCount.boolFields) > 0 {
79+
return true
80+
}
81+
return false
82+
}
83+
84+
// CanBeNumericLong asserts if the data frame comply with numeric long type
85+
// https://grafana.com/developers/dataplane/numeric#numeric-long-format-numericlong-sql-table-like
86+
func CanBeNumericLong(frame *data.Frame) bool {
87+
if frame == nil {
88+
return false
89+
}
90+
ftCount := getFieldTypesCount(frame)
91+
rowLen, err := frame.RowLen()
92+
if err != nil {
93+
return false
94+
}
95+
if rowLen == 1 && ftCount.numericFields > 0 && ftCount.stringFields == 0 {
96+
return true
97+
}
98+
if rowLen > 1 && ftCount.numericFields > 0 && ftCount.stringFields > 0 {
99+
return true
100+
}
101+
return false
102+
}

pkg/infinity/client.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package infinity
22

33
import (
44
"bytes"
5+
"compress/gzip"
56
"context"
67
"encoding/json"
78
"errors"
@@ -98,11 +99,11 @@ func replaceSect(input string, settings models.InfinitySettings, includeSect boo
9899
return input
99100
}
100101

101-
func (client *Client) req(ctx context.Context, url string, body io.Reader, settings models.InfinitySettings, query models.Query, requestHeaders map[string]string) (obj any, statusCode int, duration time.Duration, err error) {
102+
func (client *Client) req(ctx context.Context, pCtx *backend.PluginContext, url string, body io.Reader, settings models.InfinitySettings, query models.Query, requestHeaders map[string]string) (obj any, statusCode int, duration time.Duration, err error) {
102103
ctx, span := tracing.DefaultTracer().Start(ctx, "client.req")
103104
logger := backend.Logger.FromContext(ctx)
104105
defer span.End()
105-
req, err := GetRequest(ctx, settings, body, query, requestHeaders, true)
106+
req, err := GetRequest(ctx, pCtx, settings, body, query, requestHeaders, true)
106107
if err != nil {
107108
return nil, http.StatusInternalServerError, 0, backend.DownstreamError(fmt.Errorf("error preparing request. %w", err))
108109
}
@@ -145,7 +146,7 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti
145146
// therefore any incoming error is considered downstream
146147
return nil, res.StatusCode, duration, backend.DownstreamError(err)
147148
}
148-
bodyBytes, err := io.ReadAll(res.Body)
149+
bodyBytes, err := getBodyBytes(res)
149150
if err != nil {
150151
logger.Debug("error reading response body", "url", url, "error", err.Error())
151152
return nil, res.StatusCode, duration, backend.DownstreamError(err)
@@ -164,12 +165,24 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti
164165
return string(bodyBytes), res.StatusCode, duration, err
165166
}
166167

168+
func getBodyBytes(res *http.Response) ([]byte, error) {
169+
if strings.EqualFold(res.Header.Get("Content-Encoding"), "gzip") {
170+
reader, err := gzip.NewReader(res.Body)
171+
if err != nil {
172+
return nil, err
173+
}
174+
defer reader.Close()
175+
return io.ReadAll(reader)
176+
}
177+
return io.ReadAll(res.Body)
178+
}
179+
167180
// https://stackoverflow.com/questions/31398044/got-error-invalid-character-%C3%AF-looking-for-beginning-of-value-from-json-unmar
168181
func removeBOMContent(input []byte) []byte {
169182
return bytes.TrimPrefix(input, []byte("\xef\xbb\xbf"))
170183
}
171184

172-
func (client *Client) GetResults(ctx context.Context, query models.Query, requestHeaders map[string]string) (o any, statusCode int, duration time.Duration, err error) {
185+
func (client *Client) GetResults(ctx context.Context, pCtx *backend.PluginContext, query models.Query, requestHeaders map[string]string) (o any, statusCode int, duration time.Duration, err error) {
173186
logger := backend.Logger.FromContext(ctx)
174187
if query.Source == "azure-blob" {
175188
if strings.TrimSpace(query.AzBlobContainerName) == "" || strings.TrimSpace(query.AzBlobName) == "" {
@@ -202,9 +215,9 @@ func (client *Client) GetResults(ctx context.Context, query models.Query, reques
202215
switch strings.ToUpper(query.URLOptions.Method) {
203216
case http.MethodPost:
204217
body := GetQueryBody(ctx, query)
205-
return client.req(ctx, query.URL, body, client.Settings, query, requestHeaders)
218+
return client.req(ctx, pCtx, query.URL, body, client.Settings, query, requestHeaders)
206219
default:
207-
return client.req(ctx, query.URL, nil, client.Settings, query, requestHeaders)
220+
return client.req(ctx, pCtx, query.URL, nil, client.Settings, query, requestHeaders)
208221
}
209222
}
210223

pkg/infinity/client_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/grafana/grafana-infinity-datasource/pkg/infinity"
1010
"github.com/grafana/grafana-infinity-datasource/pkg/models"
11+
"github.com/grafana/grafana-plugin-sdk-go/backend"
1112
"github.com/stretchr/testify/assert"
1213
)
1314

@@ -65,7 +66,8 @@ func TestInfinityClient_GetResults(t *testing.T) {
6566
Settings: tt.settings,
6667
HttpClient: &http.Client{},
6768
}
68-
gotO, statusCode, duration, err := client.GetResults(context.Background(), tt.query, tt.requestHeaders)
69+
pluginContext := &backend.PluginContext{}
70+
gotO, statusCode, duration, err := client.GetResults(context.Background(), pluginContext, tt.query, tt.requestHeaders)
6971
if (err != nil) != tt.wantErr {
7072
t.Errorf("GetResults() error = %v, wantErr %v", err, tt.wantErr)
7173
return

pkg/infinity/headers.go

+16-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
"github.com/grafana/grafana-infinity-datasource/pkg/models"
13+
"github.com/grafana/grafana-plugin-sdk-go/backend"
1314
"go.opentelemetry.io/otel"
1415
"go.opentelemetry.io/otel/propagation"
1516
)
@@ -22,10 +23,11 @@ const (
2223
)
2324

2425
const (
25-
headerKeyAccept = "Accept"
26-
headerKeyContentType = "Content-Type"
27-
HeaderKeyAuthorization = "Authorization"
28-
HeaderKeyIdToken = "X-Id-Token"
26+
headerKeyAccept = "Accept"
27+
headerKeyContentType = "Content-Type"
28+
headerKeyAcceptEncoding = "Accept-Encoding"
29+
HeaderKeyAuthorization = "Authorization"
30+
HeaderKeyIdToken = "X-Id-Token"
2931
)
3032

3133
func ApplyAcceptHeader(_ context.Context, query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request {
@@ -68,17 +70,20 @@ func ApplyContentTypeHeader(_ context.Context, query models.Query, settings mode
6870
return req
6971
}
7072

71-
func ApplyHeadersFromSettings(_ context.Context, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request {
73+
func ApplyAcceptEncodingHeader(_ context.Context, query models.Query, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request {
74+
req.Header.Set(headerKeyAcceptEncoding, "gzip")
75+
return req
76+
}
77+
78+
func ApplyHeadersFromSettings(_ context.Context, pCtx *backend.PluginContext, requestHeaders map[string]string, settings models.InfinitySettings, req *http.Request, includeSect bool) *http.Request {
7279
for key, value := range settings.CustomHeaders {
73-
val := dummyHeader
80+
headerValue := dummyHeader
7481
if includeSect {
75-
val = value
82+
headerValue = value
7683
}
84+
headerValue = interpolateGrafanaMetaDataMacros(headerValue, pCtx)
7785
if key != "" {
78-
req.Header.Add(key, val)
79-
if strings.EqualFold(key, headerKeyAccept) || strings.EqualFold(key, headerKeyContentType) {
80-
req.Header.Set(key, val)
81-
}
86+
req.Header.Set(key, headerValue)
8287
}
8388
}
8489
return req

0 commit comments

Comments
 (0)