diff --git a/docs/sources/features/datasources/influxdb.md b/docs/sources/features/datasources/influxdb.md index 94ae448f16d..e75fb2c5511 100644 --- a/docs/sources/features/datasources/influxdb.md +++ b/docs/sources/features/datasources/influxdb.md @@ -32,6 +32,7 @@ Name | Description *Database* | Name of your influxdb database *User* | Name of your database user *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. @@ -212,4 +213,6 @@ datasources: user: grafana password: grafana url: http://localhost:8086 + jsonData: + httpMode: GET ``` diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index d199211575a..6bff961f981 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -3,10 +3,12 @@ package influxdb import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" "path" + "strings" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" @@ -33,6 +35,8 @@ var ( glog log.Logger ) +var ErrInvalidHttpMode error = errors.New("'httpMode' should be either 'GET' or 'POST'") + func init() { glog = log.New("tsdb.influxdb") 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) { + u, _ := url.Parse(dsInfo.Url) 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 { return nil, err } + req.Header.Set("User-Agent", "Grafana") + params := req.URL.Query() - params.Set("q", query) params.Set("db", dsInfo.Database) 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 { req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword()) diff --git a/pkg/tsdb/influxdb/influxdb_test.go b/pkg/tsdb/influxdb/influxdb_test.go new file mode 100644 index 00000000000..335335477b3 --- /dev/null +++ b/pkg/tsdb/influxdb/influxdb_test.go @@ -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) + }) + + }) + + }) +} diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 494bda2dcd0..71ed01776e8 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -17,6 +17,7 @@ export default class InfluxDatasource { withCredentials: any; interval: any; responseParser: any; + httpMode: string; /** @ngInject */ constructor(instanceSettings, private $q, private backendSrv, private templateSrv) { @@ -33,6 +34,7 @@ export default class InfluxDatasource { this.withCredentials = instanceSettings.withCredentials; this.interval = (instanceSettings.jsonData || {}).timeInterval; this.responseParser = new ResponseParser(); + this.httpMode = instanceSettings.jsonData.httpMode; } query(options) { @@ -190,7 +192,7 @@ export default class InfluxDatasource { 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) { @@ -245,7 +247,12 @@ export default class InfluxDatasource { 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); data = null; } @@ -268,6 +275,10 @@ export default class InfluxDatasource { req.headers.Authorization = this.basicAuth; } + if (method === 'POST') { + req.headers['Content-type'] = 'application/x-www-form-urlencoded'; + } + return this.backendSrv.datasourceRequest(req).then( result => { return result.data; diff --git a/public/app/plugins/datasource/influxdb/module.ts b/public/app/plugins/datasource/influxdb/module.ts index b25c15101c1..a3da6ea57d0 100644 --- a/public/app/plugins/datasource/influxdb/module.ts +++ b/public/app/plugins/datasource/influxdb/module.ts @@ -15,7 +15,10 @@ class InfluxConfigCtrl { constructor() { this.onPasswordReset = createResetHandler(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 { diff --git a/public/app/plugins/datasource/influxdb/partials/config.html b/public/app/plugins/datasource/influxdb/partials/config.html index 01431639690..977b0a0fddc 100644 --- a/public/app/plugins/datasource/influxdb/partials/config.html +++ b/public/app/plugins/datasource/influxdb/partials/config.html @@ -26,6 +26,19 @@ /> + +