Commit 3866839b by Stephen SORRIAUX Committed by Andrej Ocenas

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.
parent 2596ce50
...@@ -32,6 +32,7 @@ Name | Description ...@@ -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: ...@@ -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 ...@@ -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 ( ...@@ -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 ...@@ -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())
......
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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 @@ ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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');
});
}); });
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment