mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Datasources: add support for POST HTTP verb for InfluxDB (#16690)
A new parameter `queryMode` is added to the InfluxDB datasource to provide a way to use POST instead of GET when querying the database. This prevents to get any error when querying the database with a heavy request. Default configuration is kept to GET for backward compatibility. Tests and documentation have been added for this new behaviour.
This commit is contained in:
parent
2596ce5076
commit
3866839b19
@ -32,6 +32,7 @@ Name | Description
|
|||||||
*Database* | Name of your influxdb database
|
*Database* | Name of your influxdb database
|
||||||
*User* | Name of your database user
|
*User* | Name of your database user
|
||||||
*Password* | Database user's password
|
*Password* | Database user's password
|
||||||
|
*HTTP mode* | How to query the database (`GET` or `POST` HTTP verb). The `POST` verb allows heavy queries that would return an error using the `GET` verb. Default is `GET`.
|
||||||
|
|
||||||
Access mode controls how requests to the data source will be handled. Server should be the preferred way if nothing else stated.
|
Access mode controls how requests to the data source will be handled. Server should be the preferred way if nothing else stated.
|
||||||
|
|
||||||
@ -212,4 +213,6 @@ datasources:
|
|||||||
user: grafana
|
user: grafana
|
||||||
password: grafana
|
password: grafana
|
||||||
url: http://localhost:8086
|
url: http://localhost:8086
|
||||||
|
jsonData:
|
||||||
|
httpMode: GET
|
||||||
```
|
```
|
||||||
|
@ -3,10 +3,12 @@ package influxdb
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@ -33,6 +35,8 @@ var (
|
|||||||
glog log.Logger
|
glog log.Logger
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrInvalidHttpMode error = errors.New("'httpMode' should be either 'GET' or 'POST'")
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
glog = log.New("tsdb.influxdb")
|
glog = log.New("tsdb.influxdb")
|
||||||
tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxDBExecutor)
|
tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxDBExecutor)
|
||||||
@ -108,21 +112,42 @@ func (e *InfluxDBExecutor) getQuery(dsInfo *models.DataSource, queries []*tsdb.Q
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *InfluxDBExecutor) createRequest(dsInfo *models.DataSource, query string) (*http.Request, error) {
|
func (e *InfluxDBExecutor) createRequest(dsInfo *models.DataSource, query string) (*http.Request, error) {
|
||||||
|
|
||||||
u, _ := url.Parse(dsInfo.Url)
|
u, _ := url.Parse(dsInfo.Url)
|
||||||
u.Path = path.Join(u.Path, "query")
|
u.Path = path.Join(u.Path, "query")
|
||||||
|
httpMode := dsInfo.JsonData.Get("httpMode").MustString("GET")
|
||||||
|
|
||||||
|
req, err := func() (*http.Request, error) {
|
||||||
|
switch httpMode {
|
||||||
|
case "GET":
|
||||||
|
return http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
case "POST":
|
||||||
|
bodyValues := url.Values{}
|
||||||
|
bodyValues.Add("q", query)
|
||||||
|
body := bodyValues.Encode()
|
||||||
|
return http.NewRequest(http.MethodPost, u.String(), strings.NewReader(body))
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidHttpMode
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "Grafana")
|
||||||
|
|
||||||
params := req.URL.Query()
|
params := req.URL.Query()
|
||||||
params.Set("q", query)
|
|
||||||
params.Set("db", dsInfo.Database)
|
params.Set("db", dsInfo.Database)
|
||||||
params.Set("epoch", "s")
|
params.Set("epoch", "s")
|
||||||
req.URL.RawQuery = params.Encode()
|
|
||||||
|
|
||||||
req.Header.Set("User-Agent", "Grafana")
|
if httpMode == "GET" {
|
||||||
|
params.Set("q", query)
|
||||||
|
} else if httpMode == "POST" {
|
||||||
|
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = params.Encode()
|
||||||
|
|
||||||
if dsInfo.BasicAuth {
|
if dsInfo.BasicAuth {
|
||||||
req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
|
req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
|
||||||
|
77
pkg/tsdb/influxdb/influxdb_test.go
Normal file
77
pkg/tsdb/influxdb/influxdb_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package influxdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInfluxDB(t *testing.T) {
|
||||||
|
Convey("InfluxDB", t, func() {
|
||||||
|
datasource := &models.DataSource{
|
||||||
|
Url: "http://awesome-influxdb:1337",
|
||||||
|
Database: "awesome-db",
|
||||||
|
JsonData: simplejson.New(),
|
||||||
|
}
|
||||||
|
query := "SELECT awesomeness FROM somewhere"
|
||||||
|
e := &InfluxDBExecutor{
|
||||||
|
QueryParser: &InfluxdbQueryParser{},
|
||||||
|
ResponseParser: &ResponseParser{},
|
||||||
|
}
|
||||||
|
Convey("createRequest with GET httpMode", func() {
|
||||||
|
req, _ := e.createRequest(datasource, query)
|
||||||
|
|
||||||
|
Convey("as default", func() {
|
||||||
|
So(req.Method, ShouldEqual, "GET")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("has a 'q' GET param that equals to query", func() {
|
||||||
|
q := req.URL.Query().Get("q")
|
||||||
|
So(q, ShouldEqual, query)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("has an empty body", func() {
|
||||||
|
So(req.Body, ShouldEqual, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("createRequest with POST httpMode", func() {
|
||||||
|
datasource.JsonData.Set("httpMode", "POST")
|
||||||
|
req, _ := e.createRequest(datasource, query)
|
||||||
|
|
||||||
|
Convey("method should be POST", func() {
|
||||||
|
So(req.Method, ShouldEqual, "POST")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("has no 'q' GET param", func() {
|
||||||
|
q := req.URL.Query().Get("q")
|
||||||
|
So(q, ShouldEqual, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("has the request as GET param in body", func() {
|
||||||
|
body, _ := ioutil.ReadAll(req.Body)
|
||||||
|
testBodyValues := url.Values{}
|
||||||
|
testBodyValues.Add("q", query)
|
||||||
|
testBody := testBodyValues.Encode()
|
||||||
|
So(string(body[:]), ShouldEqual, testBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("createRequest with PUT httpMode", func() {
|
||||||
|
datasource.JsonData.Set("httpMode", "PUT")
|
||||||
|
_, err := e.createRequest(datasource, query)
|
||||||
|
|
||||||
|
Convey("should miserably fail", func() {
|
||||||
|
So(err, ShouldEqual, ErrInvalidHttpMode)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
@ -17,6 +17,7 @@ export default class InfluxDatasource {
|
|||||||
withCredentials: any;
|
withCredentials: any;
|
||||||
interval: any;
|
interval: any;
|
||||||
responseParser: any;
|
responseParser: any;
|
||||||
|
httpMode: string;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(instanceSettings, private $q, private backendSrv, private templateSrv) {
|
constructor(instanceSettings, private $q, private backendSrv, private templateSrv) {
|
||||||
@ -33,6 +34,7 @@ export default class InfluxDatasource {
|
|||||||
this.withCredentials = instanceSettings.withCredentials;
|
this.withCredentials = instanceSettings.withCredentials;
|
||||||
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
||||||
this.responseParser = new ResponseParser();
|
this.responseParser = new ResponseParser();
|
||||||
|
this.httpMode = instanceSettings.jsonData.httpMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
query(options) {
|
query(options) {
|
||||||
@ -190,7 +192,7 @@ export default class InfluxDatasource {
|
|||||||
query = query.replace('$timeFilter', timeFilter);
|
query = query.replace('$timeFilter', timeFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options);
|
return this._influxRequest(this.httpMode, '/query', { q: query, epoch: 'ms' }, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
serializeParams(params) {
|
serializeParams(params) {
|
||||||
@ -245,7 +247,12 @@ export default class InfluxDatasource {
|
|||||||
params.db = this.database;
|
params.db = this.database;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === 'GET') {
|
if (method === 'POST' && _.has(data, 'q')) {
|
||||||
|
// verb is POST and 'q' param is defined
|
||||||
|
_.extend(params, _.omit(data, ['q']));
|
||||||
|
data = this.serializeParams(_.pick(data, ['q']));
|
||||||
|
} else if (method === 'GET' || method === 'POST') {
|
||||||
|
// verb is GET, or POST without 'q' param
|
||||||
_.extend(params, data);
|
_.extend(params, data);
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
@ -268,6 +275,10 @@ export default class InfluxDatasource {
|
|||||||
req.headers.Authorization = this.basicAuth;
|
req.headers.Authorization = this.basicAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'POST') {
|
||||||
|
req.headers['Content-type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
|
|
||||||
return this.backendSrv.datasourceRequest(req).then(
|
return this.backendSrv.datasourceRequest(req).then(
|
||||||
result => {
|
result => {
|
||||||
return result.data;
|
return result.data;
|
||||||
|
@ -15,7 +15,10 @@ class InfluxConfigCtrl {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
|
this.onPasswordReset = createResetHandler(this, PasswordFieldEnum.Password);
|
||||||
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
|
this.onPasswordChange = createChangeHandler(this, PasswordFieldEnum.Password);
|
||||||
|
this.current.jsonData.httpMode = this.current.jsonData.httpMode || 'GET';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpMode = [{ name: 'GET', value: 'GET' }, { name: 'POST', value: 'POST' }];
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfluxAnnotationsQueryCtrl {
|
class InfluxAnnotationsQueryCtrl {
|
||||||
|
@ -26,6 +26,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-8">HTTP Method</label>
|
||||||
|
<div class="gf-form-select-wrapper width-8 gf-form-select-wrapper--has-help-icon">
|
||||||
|
<select class="gf-form-input" ng-model="ctrl.current.jsonData.httpMode" ng-options="f.value as f.name for f in ctrl.httpMode"></select>
|
||||||
|
<info-popover mode="right-absolute">
|
||||||
|
You can use either <code>GET</code> or <code>POST</code> HTTP method to query your InfluxDB database. The <code>POST</code>
|
||||||
|
method allows you to perform heavy requests (with a lots of <code>WHERE</code> clause) while the <code>GET</code> method
|
||||||
|
will restrict you and return an error if the query is too large.
|
||||||
|
</info-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ describe('InfluxDataSource', () => {
|
|||||||
backendSrv: {},
|
backendSrv: {},
|
||||||
$q: $q,
|
$q: $q,
|
||||||
templateSrv: new TemplateSrvStub(),
|
templateSrv: new TemplateSrvStub(),
|
||||||
instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} },
|
instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'GET' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -23,11 +23,13 @@ describe('InfluxDataSource', () => {
|
|||||||
to: '2018-01-02T00:00:00Z',
|
to: '2018-01-02T00:00:00Z',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let requestQuery;
|
let requestQuery, requestMethod, requestData;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
ctx.backendSrv.datasourceRequest = req => {
|
ctx.backendSrv.datasourceRequest = req => {
|
||||||
|
requestMethod = req.method;
|
||||||
requestQuery = req.params.q;
|
requestQuery = req.params.q;
|
||||||
|
requestData = req.data;
|
||||||
return ctx.$q.when({
|
return ctx.$q.when({
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
@ -49,5 +51,69 @@ describe('InfluxDataSource', () => {
|
|||||||
it('should replace $timefilter', () => {
|
it('should replace $timefilter', () => {
|
||||||
expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms');
|
expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use the HTTP GET method', () => {
|
||||||
|
expect(requestMethod).toBe('GET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have any data in request body', () => {
|
||||||
|
expect(requestData).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('InfluxDataSource in POST query mode', () => {
|
||||||
|
const ctx: any = {
|
||||||
|
backendSrv: {},
|
||||||
|
$q: $q,
|
||||||
|
templateSrv: new TemplateSrvStub(),
|
||||||
|
instanceSettings: { url: 'url', name: 'influxDb', jsonData: { httpMode: 'POST' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ctx.instanceSettings.url = '/api/datasources/proxy/1';
|
||||||
|
ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When issuing metricFindQuery', () => {
|
||||||
|
const query = 'SELECT max(value) FROM measurement';
|
||||||
|
const queryOptions: any = {};
|
||||||
|
let requestMethod, requestQueryParameter, queryEncoded, requestQuery;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ctx.backendSrv.datasourceRequest = req => {
|
||||||
|
requestMethod = req.method;
|
||||||
|
requestQueryParameter = req.params;
|
||||||
|
requestQuery = req.data;
|
||||||
|
return ctx.$q.when({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'measurement',
|
||||||
|
columns: ['max'],
|
||||||
|
values: [[1]],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
queryEncoded = await ctx.ds.serializeParams({ q: query });
|
||||||
|
await ctx.ds.metricFindQuery(query, queryOptions).then(_ => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the query form urlencoded', () => {
|
||||||
|
expect(requestQuery).toBe(queryEncoded);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the HTTP POST method', () => {
|
||||||
|
expect(requestMethod).toBe('POST');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have q as a query parameter', () => {
|
||||||
|
expect(requestQueryParameter).not.toHaveProperty('q');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user