Merge branch 'master' into develop-newgrid-row-design2

This commit is contained in:
Torkel Ödegaard
2017-09-07 15:05:41 +02:00
131 changed files with 3458 additions and 1632 deletions
+3 -3
View File
@@ -222,7 +222,7 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/plugins", wrap(GetPluginList))
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
r.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
r.Group("/plugins", func() {
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
@@ -230,8 +230,8 @@ func (hs *HttpServer) registerRoutes() {
}, reqOrgAdmin)
r.Get("/frontend/settings/", GetFrontendSettings)
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
r.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
// Dashboard
r.Group("/dashboards", func() {
+1 -2
View File
@@ -32,8 +32,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
handlers := make([]macaron.Handler, 0)
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
ReqSignedIn: true,
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
ReqSignedIn: true,
}))
if route.ReqRole != "" {
+4 -1
View File
@@ -217,7 +217,10 @@ func (this *thunderTask) Fetch() {
this.Done()
}
var client = &http.Client{}
var client *http.Client = &http.Client{
Timeout: time.Second * 2,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
func (this *thunderTask) fetch() error {
this.Avatar.timestamp = time.Now()
+11 -15
View File
@@ -91,7 +91,7 @@ func init() {
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
"AWS/VPN": {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
"WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
"KMS": {"SecondsUntilKeyMaterialExpiration"},
}
@@ -133,7 +133,7 @@ func init() {
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
"AWS/VPN": {"VpnId", "TunnelIpAddress"},
"AWS/WAF": {"Rule", "WebACL"},
"WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
"KMS": {"KeyId"},
}
@@ -166,9 +166,7 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
if customNamespaces != "" {
for _, key := range strings.Split(customNamespaces, ",") {
keys = append(keys, key)
}
keys = append(keys, strings.Split(customNamespaces, ",")...)
}
sort.Sort(sort.StringSlice(keys))
@@ -292,11 +290,6 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
var metricsCacheLock sync.Mutex
func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
metricsCacheLock.Lock()
defer metricsCacheLock.Unlock()
@@ -314,6 +307,10 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
if customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
@@ -330,11 +327,6 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
var dimensionsCacheLock sync.Mutex
func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
dimensionsCacheLock.Lock()
defer dimensionsCacheLock.Unlock()
@@ -352,6 +344,10 @@ func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*d
if customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
+22 -159
View File
@@ -1,197 +1,60 @@
package api
import (
"bytes"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"fmt"
"time"
"github.com/grafana/grafana/pkg/api/cloudwatch"
"github.com/grafana/grafana/pkg/api/pluginproxy"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/plugins"
)
var (
dataproxyLogger log.Logger = log.New("data-proxy-log")
)
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
req.URL.Host = targetUrl.Host
req.Host = targetUrl.Host
func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) {
cacheKey := fmt.Sprintf("ds-%d", id)
reqQueryVals := req.URL.Query()
if ds.Type == m.DS_INFLUXDB_08 {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, "db/"+ds.Database+"/"+proxyPath)
reqQueryVals.Add("u", ds.User)
reqQueryVals.Add("p", ds.Password)
req.URL.RawQuery = reqQueryVals.Encode()
} else if ds.Type == m.DS_INFLUXDB {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.User, ds.Password))
}
} else {
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
}
if ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword))
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
req.Header.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
// clear X-Forwarded Host/Port/Proto headers
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteAddr = req.RemoteAddr
}
if req.Header.Get("X-Forwarded-For") != "" {
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
} else {
req.Header.Set("X-Forwarded-For", remoteAddr)
if !nocache {
if cached, found := hs.cache.Get(cacheKey); found {
ds := cached.(*m.DataSource)
if ds.OrgId == orgId {
return ds, nil
}
}
// reqBytes, _ := httputil.DumpRequestOut(req, true);
// log.Trace("Proxying datasource request: %s", string(reqBytes))
}
return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200}
}
func getDatasource(id int64, orgId int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
hs.cache.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
func ProxyDataSourceRequest(c *middleware.Context) {
func (hs *HttpServer) ProxyDataSourceRequest(c *middleware.Context) {
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
ds, err := getDatasource(c.ParamsInt64(":id"), c.OrgId)
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache)
if err != nil {
c.JsonApiErr(500, "Unable to load datasource meta data", err)
return
}
if ds.Type == m.DS_INFLUXDB {
if c.Query("db") != ds.Database {
c.JsonApiErr(403, "Datasource is not configured to allow this database", nil)
return
}
}
if ds.Type == m.DS_CLOUDWATCH {
cloudwatch.HandleRequest(c, ds)
return
}
targetUrl, _ := url.Parse(ds.Url)
if !checkWhiteList(c, targetUrl.Host) {
// find plugin
plugin, ok := plugins.DataSources[ds.Type]
if !ok {
c.JsonApiErr(500, "Unable to find datasource plugin", err)
return
}
proxyPath := c.Params("*")
if ds.Type == m.DS_PROMETHEUS {
if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") {
c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil)
return
}
}
if ds.Type == m.DS_ES {
if c.Req.Request.Method == "DELETE" {
c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
return
}
if c.Req.Request.Method == "PUT" {
c.JsonApiErr(403, "Puts not allowed on proxied Elasticsearch datasource", nil)
return
}
if c.Req.Request.Method == "POST" && proxyPath != "_msearch" {
c.JsonApiErr(403, "Posts not allowed on proxied Elasticsearch datasource except on /_msearch", nil)
return
}
}
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
proxy.Transport, err = ds.GetHttpTransport()
if err != nil {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
logProxyRequest(ds.Type, c)
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
func logProxyRequest(dataSourceType string, c *middleware.Context) {
if !setting.DataProxyLogging {
return
}
var body string
if c.Req.Request.Body != nil {
buffer, err := ioutil.ReadAll(c.Req.Request.Body)
if err == nil {
c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
body = string(buffer)
}
}
dataproxyLogger.Info("Proxying incoming request",
"userid", c.UserId,
"orgid", c.OrgId,
"username", c.Login,
"datasource", dataSourceType,
"uri", c.Req.RequestURI,
"method", c.Req.Request.Method,
"body", body)
}
func checkWhiteList(c *middleware.Context, host string) bool {
if host != "" && len(setting.DataProxyWhiteList) > 0 {
if _, exists := setting.DataProxyWhiteList[host]; !exists {
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)
return false
}
}
return true
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
proxy.HandleRequest()
}
-63
View File
@@ -1,63 +0,0 @@
package api
import (
"net/http"
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
)
func TestDataSourceProxy(t *testing.T) {
Convey("When getting graphite datasource proxy", t, func() {
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "/render", targetUrl)
proxy.Transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
So(ok, ShouldBeTrue)
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.Director(&req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
})
})
Convey("When getting influxdb datasource proxy", t, func() {
ds := m.DataSource{
Type: m.DS_INFLUXDB_08,
Url: "http://influxdb:8083",
Database: "site",
User: "user",
Password: "password",
}
targetUrl, _ := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "", targetUrl)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.Director(&req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
})
Convey("Should add username and password", func() {
queryVals := req.URL.Query()
So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password")
})
})
}
+5 -1
View File
@@ -9,7 +9,9 @@ import (
"net/http"
"os"
"path"
"time"
gocache "github.com/patrickmn/go-cache"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/live"
@@ -29,13 +31,15 @@ type HttpServer struct {
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
cache *gocache.Cache
httpSrv *http.Server
}
func NewHttpServer() *HttpServer {
return &HttpServer{
log: log.New("http.server"),
log: log.New("http.server"),
cache: gocache.New(5*time.Minute, 10*time.Minute),
}
}
+349
View File
@@ -0,0 +1,349 @@
package pluginproxy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"text/template"
"time"
"github.com/grafana/grafana/pkg/api/cloudwatch"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
logger log.Logger = log.New("data-proxy-log")
client *http.Client = &http.Client{
Timeout: time.Second * 30,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
tokenCache = map[int64]*jwtToken{}
)
type jwtToken struct {
ExpiresOn time.Time `json:"-"`
ExpiresOnString string `json:"expires_on"`
AccessToken string `json:"access_token"`
}
type DataSourceProxy struct {
ds *m.DataSource
ctx *middleware.Context
targetUrl *url.URL
proxyPath string
route *plugins.AppPluginRoute
plugin *plugins.DataSourcePlugin
}
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *middleware.Context, proxyPath string) *DataSourceProxy {
targetUrl, _ := url.Parse(ds.Url)
return &DataSourceProxy{
ds: ds,
plugin: plugin,
ctx: ctx,
proxyPath: proxyPath,
targetUrl: targetUrl,
}
}
func (proxy *DataSourceProxy) HandleRequest() {
if proxy.ds.Type == m.DS_CLOUDWATCH {
cloudwatch.HandleRequest(proxy.ctx, proxy.ds)
return
}
if err := proxy.validateRequest(); err != nil {
proxy.ctx.JsonApiErr(403, err.Error(), nil)
return
}
reverseProxy := &httputil.ReverseProxy{
Director: proxy.getDirector(),
FlushInterval: time.Millisecond * 200,
}
var err error
reverseProxy.Transport, err = proxy.ds.GetHttpTransport()
if err != nil {
proxy.ctx.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
proxy.logRequest()
reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request)
proxy.ctx.Resp.Header().Del("Set-Cookie")
}
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
return func(req *http.Request) {
req.URL.Scheme = proxy.targetUrl.Scheme
req.URL.Host = proxy.targetUrl.Host
req.Host = proxy.targetUrl.Host
reqQueryVals := req.URL.Query()
if proxy.ds.Type == m.DS_INFLUXDB_08 {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
reqQueryVals.Add("u", proxy.ds.User)
reqQueryVals.Add("p", proxy.ds.Password)
req.URL.RawQuery = reqQueryVals.Encode()
} else if proxy.ds.Type == m.DS_INFLUXDB {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
req.URL.RawQuery = reqQueryVals.Encode()
if !proxy.ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.Password))
}
} else {
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
}
if proxy.ds.BasicAuth {
req.Header.Del("Authorization")
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
req.Header.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
// clear X-Forwarded Host/Port/Proto headers
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-Port")
req.Header.Del("X-Forwarded-Proto")
// set X-Forwarded-For header
if req.RemoteAddr != "" {
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
remoteAddr = req.RemoteAddr
}
if req.Header.Get("X-Forwarded-For") != "" {
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
} else {
req.Header.Set("X-Forwarded-For", remoteAddr)
}
}
if proxy.route != nil {
proxy.applyRoute(req)
}
}
}
func (proxy *DataSourceProxy) validateRequest() error {
if proxy.ds.Type == m.DS_INFLUXDB {
if proxy.ctx.Query("db") != proxy.ds.Database {
return errors.New("Datasource is not configured to allow this database")
}
}
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
return errors.New("Target url is not a valid target")
}
if proxy.ds.Type == m.DS_PROMETHEUS {
if proxy.ctx.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxy.proxyPath, "api/") {
return errors.New("GET is only allowed on proxied Prometheus datasource")
}
}
if proxy.ds.Type == m.DS_ES {
if proxy.ctx.Req.Request.Method == "DELETE" {
return errors.New("Deletes not allowed on proxied Elasticsearch datasource")
}
if proxy.ctx.Req.Request.Method == "PUT" {
return errors.New("Puts not allowed on proxied Elasticsearch datasource")
}
if proxy.ctx.Req.Request.Method == "POST" && proxy.proxyPath != "_msearch" {
return errors.New("Posts not allowed on proxied Elasticsearch datasource except on /_msearch")
}
}
// found route if there are any
if len(proxy.plugin.Routes) > 0 {
for _, route := range proxy.plugin.Routes {
// method match
if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
continue
}
if route.ReqRole.IsValid() {
if !proxy.ctx.HasUserRole(route.ReqRole) {
return errors.New("Plugin proxy route access denied")
}
}
if strings.HasPrefix(proxy.proxyPath, route.Path) {
proxy.route = route
break
}
}
}
return nil
}
func (proxy *DataSourceProxy) logRequest() {
if !setting.DataProxyLogging {
return
}
var body string
if proxy.ctx.Req.Request.Body != nil {
buffer, err := ioutil.ReadAll(proxy.ctx.Req.Request.Body)
if err == nil {
proxy.ctx.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
body = string(buffer)
}
}
logger.Info("Proxying incoming request",
"userid", proxy.ctx.UserId,
"orgid", proxy.ctx.OrgId,
"username", proxy.ctx.Login,
"datasource", proxy.ds.Type,
"uri", proxy.ctx.Req.RequestURI,
"method", proxy.ctx.Req.Request.Method,
"body", body)
}
func checkWhiteList(c *middleware.Context, host string) bool {
if host != "" && len(setting.DataProxyWhiteList) > 0 {
if _, exists := setting.DataProxyWhiteList[host]; !exists {
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)
return false
}
}
return true
}
func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
data := templateData{
JsonData: proxy.ds.JsonData.Interface().(map[string]interface{}),
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
}
routeUrl, err := url.Parse(proxy.route.Url)
if err != nil {
logger.Error("Error parsing plugin route url")
return
}
req.URL.Scheme = routeUrl.Scheme
req.URL.Host = routeUrl.Host
req.Host = routeUrl.Host
req.URL.Path = util.JoinUrlFragments(routeUrl.Path, proxy.proxyPath)
if err := addHeaders(&req.Header, proxy.route, data); err != nil {
logger.Error("Failed to render plugin headers", "error", err)
}
if proxy.route.TokenAuth != nil {
if token, err := proxy.getAccessToken(data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
logger.Info("Requesting", "url", req.URL.String())
}
func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
if cachedToken, found := tokenCache[proxy.ds.Id]; found {
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
logger.Info("Using token from cache")
return cachedToken.AccessToken, nil
}
}
urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data)
if err != nil {
return "", err
}
params := make(url.Values)
for key, value := range proxy.route.TokenAuth.Params {
if interpolatedParam, err := interpolateString(value, data); err != nil {
return "", err
} else {
params.Add(key, interpolatedParam)
}
}
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
resp, err := client.Do(getTokenReq)
if err != nil {
return "", err
}
defer resp.Body.Close()
var token jwtToken
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return "", err
}
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
tokenCache[proxy.ds.Id] = &token
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
return token.AccessToken, nil
}
func interpolateString(text string, data templateData) (string, error) {
t, err := template.New("content").Parse(text)
if err != nil {
return "", errors.New(fmt.Sprintf("Could not parse template %s.", text))
}
var contentBuf bytes.Buffer
err = t.Execute(&contentBuf, data)
if err != nil {
return "", errors.New(fmt.Sprintf("Failed to execute template %s.", text))
}
return contentBuf.String(), nil
}
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
for _, header := range route.Headers {
interpolated, err := interpolateString(header.Content, data)
if err != nil {
return err
}
reqHeaders.Add(header.Name, interpolated)
}
return nil
}
+165
View File
@@ -0,0 +1,165 @@
package pluginproxy
import (
"net/http"
"net/url"
"testing"
macaron "gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
. "github.com/smartystreets/goconvey/convey"
)
func TestDSRouteRule(t *testing.T) {
Convey("DataSourceProxy", t, func() {
Convey("Plugin with routes", func() {
plugin := &plugins.DataSourcePlugin{
Routes: []*plugins.AppPluginRoute{
{
Path: "api/v4/",
Url: "https://www.google.com",
ReqRole: m.ROLE_EDITOR,
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
{
Path: "api/admin",
Url: "https://www.google.com",
ReqRole: m.ROLE_ADMIN,
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
{
Path: "api/anon",
Url: "https://www.google.com",
Headers: []plugins.AppPluginRouteHeader{
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
},
},
},
}
setting.SecretKey = "password"
key, _ := util.Encrypt([]byte("123"), "password")
ds := &m.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"clientId": "asd",
}),
SecureJsonData: map[string][]byte{
"key": key,
},
}
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
ctx := &middleware.Context{
Context: &macaron.Context{
Req: macaron.Request{Request: req},
},
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
}
Convey("When matching route path", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
proxy.route = plugin.Routes[0]
proxy.applyRoute(req)
Convey("should add headers and update url", func() {
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
})
})
Convey("Validating request", func() {
Convey("plugin route with valid role", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
Convey("plugin route with admin role and user is editor", func() {
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
err := proxy.validateRequest()
So(err, ShouldNotBeNil)
})
Convey("plugin route with admin role and user is admin", func() {
ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
err := proxy.validateRequest()
So(err, ShouldBeNil)
})
})
})
Convey("When proxying graphite", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
ctx := &middleware.Context{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.getDirector()(&req)
Convey("Can translate request url and path", func() {
So(req.URL.Host, ShouldEqual, "graphite:8080")
So(req.URL.Path, ShouldEqual, "/render")
})
})
Convey("When proxying InfluxDB", func() {
plugin := &plugins.DataSourcePlugin{}
ds := &m.DataSource{
Type: m.DS_INFLUXDB_08,
Url: "http://influxdb:8083",
Database: "site",
User: "user",
Password: "password",
}
ctx := &middleware.Context{}
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
proxy.getDirector()(&req)
Convey("Should add db to url", func() {
So(req.URL.Path, ShouldEqual, "/db/site/")
})
Convey("Should add username and password", func() {
queryVals := req.URL.Query()
So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password")
})
})
Convey("When interpolating string", func() {
data := templateData{
SecureJsonData: map[string]string{
"Test": "0asd+asd",
},
}
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
So(err, ShouldBeNil)
So(interpolated, ShouldEqual, "0asd+asd")
})
})
}
+2 -21
View File
@@ -1,15 +1,11 @@
package pluginproxy
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"text/template"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
@@ -38,23 +34,8 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
}
for _, header := range route.Headers {
var contentBuf bytes.Buffer
t, err := template.New("content").Parse(header.Content)
if err != nil {
return nil, errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name))
}
err = t.Execute(&contentBuf, data)
if err != nil {
return nil, errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name))
}
log.Trace("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
result.Add(header.Name, contentBuf.String())
}
return result, nil
err := addHeaders(&result, route, data)
return result, err
}
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
+4 -3
View File
@@ -147,15 +147,16 @@ func GetPluginDashboards(c *middleware.Context) Response {
}
}
func GetPluginReadme(c *middleware.Context) Response {
func GetPluginMarkdown(c *middleware.Context) Response {
pluginId := c.Params(":pluginId")
name := c.Params(":name")
if content, err := plugins.GetPluginReadme(pluginId); err != nil {
if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
return ApiError(404, notfound.Error(), nil)
}
return ApiError(500, "Could not get readme", err)
return ApiError(500, "Could not get markdown file", err)
} else {
return Respond(200, content)
}
-38
View File
@@ -3,10 +3,8 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/trace"
"strconv"
@@ -16,7 +14,6 @@ import (
"net/http"
_ "net/http/pprof"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@@ -87,46 +84,11 @@ func main() {
server.Start()
}
func initRuntime() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
log.Fatal(3, err.Error())
}
logger := log.New("main")
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
}
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
func writePIDFile() {
if *pidFile == "" {
return
}
// Ensure the required directory structure exists.
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
if err != nil {
log.Fatal(3, "Failed to verify pid directory", err)
}
// Retrieve the PID and write it.
pid := strconv.Itoa(os.Getpid())
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
log.Fatal(3, "Failed to write pidfile", err)
}
}
func listenToSystemSignals(server models.GrafanaServer) {
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
+46 -2
View File
@@ -2,7 +2,12 @@ package main
import (
"context"
"flag"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"time"
"golang.org/x/sync/errgroup"
@@ -45,8 +50,9 @@ type GrafanaServerImpl struct {
func (g *GrafanaServerImpl) Start() {
go listenToSystemSignals(g)
writePIDFile()
initRuntime()
g.initLogging()
g.writePIDFile()
initSql()
metrics.Init()
search.Init()
@@ -74,6 +80,22 @@ func (g *GrafanaServerImpl) Start() {
g.startHttpServer()
}
func (g *GrafanaServerImpl) initLogging() {
err := setting.NewConfigContext(&setting.CommandLineArgs{
Config: *configFile,
HomePath: *homePath,
Args: flag.Args(),
})
if err != nil {
g.log.Error(err.Error())
os.Exit(1)
}
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
}
func (g *GrafanaServerImpl) startHttpServer() {
g.httpServer = api.NewHttpServer()
@@ -101,3 +123,25 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
log.Close()
os.Exit(code)
}
func (g *GrafanaServerImpl) writePIDFile() {
if *pidFile == "" {
return
}
// Ensure the required directory structure exists.
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
if err != nil {
g.log.Error("Failed to verify pid directory", "error", err)
os.Exit(1)
}
// Retrieve the PID and write it.
pid := strconv.Itoa(os.Getpid())
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
g.log.Error("Failed to write pidfile", "error", err)
os.Exit(1)
}
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
}
+1 -1
View File
@@ -231,7 +231,7 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
// in the JSON document to a top level key.
//
// In order to produce distinct "blocks" when rendering the basic diff,
// we need a way to distinguish between differnt sections of data.
// we need a way to distinguish between different sections of data.
// To do this, we consider the value(s) of each top-level JSON key to
// represent a distinct block for Grafana's JSON data structure, so
// we perform this check to see if we've entered a new "block". If we
+3 -3
View File
@@ -302,16 +302,16 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
return nil
}
func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, postion diff.Position) (results []diff.Delta) {
func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
results = make([]diff.Delta, 0)
for _, delta := range deltas {
switch delta.(type) {
case diff.PostDelta:
if delta.(diff.PostDelta).PostPosition() == postion {
if delta.(diff.PostDelta).PostPosition() == position {
results = append(results, delta)
}
case diff.PreDelta:
if delta.(diff.PreDelta).PrePosition() == postion {
if delta.(diff.PreDelta).PrePosition() == position {
results = append(results, delta)
}
default:
+2 -2
View File
@@ -33,8 +33,8 @@ func AuthenticateUser(query *LoginUserQuery) error {
if setting.LdapEnabled {
for _, server := range LdapCfg.Servers {
auther := NewLdapAuthenticator(server)
err = auther.Login(query)
author := NewLdapAuthenticator(server)
err = author.Login(query)
if err == nil || err != ErrInvalidCredentials {
return err
}
+3
View File
@@ -57,6 +57,7 @@ var (
M_Alerting_Notification_Sent_Pushover Counter
M_Aws_CloudWatch_GetMetricStatistics Counter
M_Aws_CloudWatch_ListMetrics Counter
M_DB_DataSource_QueryById Counter
// Timers
M_DataSource_ProxyReq_Timer Timer
@@ -135,6 +136,8 @@ func initMetricVars(settings *MetricSettings) {
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
M_DB_DataSource_QueryById = RegCounter("db.datasource.query_by_id")
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
M_Alerting_Execution_Time = RegTimer("alerting.execution_time")
+3 -3
View File
@@ -67,7 +67,7 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
// remove session
if err := ctx.Session.Destory(ctx); err != nil {
log.Error(3, "Failed to destory session, err")
log.Error(3, "Failed to destroy session, err")
}
// initialize a new session
@@ -107,8 +107,8 @@ var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQue
ldapCfg := login.LdapCfg
for _, server := range ldapCfg.Servers {
auther := login.NewLdapAuthenticator(server)
if err := auther.SyncSignedInUser(query.Result); err != nil {
author := login.NewLdapAuthenticator(server)
if err := author.SyncSignedInUser(query.Result); err != nil {
return err
}
}
+11 -6
View File
@@ -23,12 +23,12 @@ type AppPlugin struct {
}
type AppPluginRoute struct {
Path string `json:"path"`
Method string `json:"method"`
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
ReqRole models.RoleType `json:"reqRole"`
Url string `json:"url"`
Headers []AppPluginRouteHeader `json:"headers"`
Path string `json:"path"`
Method string `json:"method"`
ReqRole models.RoleType `json:"reqRole"`
Url string `json:"url"`
Headers []AppPluginRouteHeader `json:"headers"`
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
}
type AppPluginRouteHeader struct {
@@ -36,6 +36,11 @@ type AppPluginRouteHeader struct {
Content string `json:"content"`
}
type JwtTokenAuth struct {
Url string `json:"url"`
Params map[string]string `json:"params"`
}
func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&app); err != nil {
return err
+22 -7
View File
@@ -1,15 +1,21 @@
package plugins
import "encoding/json"
import (
"encoding/json"
"os"
"path/filepath"
)
type DataSourcePlugin struct {
FrontendPluginBase
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
BuiltIn bool `json:"builtIn"`
Mixed bool `json:"mixed"`
App string `json:"app"`
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
HasQueryHelp bool `json:"hasQueryHelp,omitempty"`
Routes []*AppPluginRoute `json:"routes"`
}
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
@@ -21,6 +27,15 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
return err
}
// look for help markdown
helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
if _, err := os.Stat(helpPath); os.IsNotExist(err) {
helpPath = filepath.Join(p.PluginDir, "query_help.md")
}
if _, err := os.Stat(helpPath); err == nil {
p.HasQueryHelp = true
}
DataSources[p.Id] = p
return nil
}
+2 -5
View File
@@ -38,8 +38,8 @@ type PluginBase struct {
Includes []*PluginInclude `json:"includes"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
HideFromList bool `json:"hideFromList"`
State string `json:"state"`
HideFromList bool `json:"hideFromList,omitempty"`
State string `json:"state,omitempty"`
IncludedInAppId string `json:"-"`
PluginDir string `json:"-"`
@@ -48,9 +48,6 @@ type PluginBase struct {
GrafanaNetVersion string `json:"-"`
GrafanaNetHasUpdate bool `json:"-"`
// cache for readme file contents
Readme []byte `json:"-"`
}
func (pb *PluginBase) registerPlugin(pluginDir string) error {
+9 -14
View File
@@ -3,6 +3,7 @@ package plugins
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
@@ -166,30 +167,24 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
return loader.Load(jsonParser, currentDir)
}
func GetPluginReadme(pluginId string) ([]byte, error) {
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
plug, exists := Plugins[pluginId]
if !exists {
return nil, PluginNotFoundError{pluginId}
}
if plug.Readme != nil {
return plug.Readme, nil
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
if _, err := os.Stat(path); os.IsNotExist(err) {
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
}
readmePath := filepath.Join(plug.PluginDir, "README.md")
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
readmePath = filepath.Join(plug.PluginDir, "readme.md")
if _, err := os.Stat(path); os.IsNotExist(err) {
return make([]byte, 0), nil
}
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
plug.Readme = make([]byte, 0)
return plug.Readme, nil
}
if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
if data, err := ioutil.ReadFile(path); err != nil {
return nil, err
} else {
plug.Readme = readmeBytes
return plug.Readme, nil
return data, nil
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
}
if err := bus.Dispatch(getDsInfo); err != nil {
return nil, fmt.Errorf("Could not find datasource")
return nil, fmt.Errorf("Could not find datasource %v", err)
}
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
+1 -1
View File
@@ -84,7 +84,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
return err
}
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dashboard</a>"
fields := make([]map[string]interface{}, 0)
message += "<br>"
for index, evt := range evalContext.EvalMatches {
+3
View File
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
)
@@ -19,6 +20,8 @@ func init() {
}
func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
metrics.M_DB_DataSource_QueryById.Inc(1)
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&datasource)
+1 -1
View File
@@ -114,7 +114,7 @@ func getEngine() (*xorm.Engine, error) {
protocol = "unix"
}
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4",
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
+1 -1
View File
@@ -12,7 +12,7 @@ type TestDB struct {
}
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8mb4"}
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
func CleanDB(x *xorm.Engine) {
+5 -1
View File
@@ -151,8 +151,12 @@ func (query *Query) renderMeasurement() string {
func (query *Query) renderWhereClause() string {
res := " WHERE "
conditions := query.renderTags()
res += strings.Join(conditions, " ")
if len(conditions) > 0 {
if len(conditions) > 1 {
res += "(" + strings.Join(conditions, " ") + ")"
} else {
res += conditions[0]
}
res += " AND "
}
+1 -1
View File
@@ -57,7 +57,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
})
Convey("can build query with math part", func() {
+1 -1
View File
@@ -71,7 +71,7 @@ func TestWildcardExpansion(t *testing.T) {
So(expandeQueries[0].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.3.idle`|aggregate.min|aggregate.max where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to))
})
Convey("Containg wildcard series", func() {
Convey("Containing wildcard series", func() {
query := &Query{
Metrics: []Metric{
{Metric: "os.cpu*", Alias: ""},
+7 -3
View File
@@ -1,6 +1,7 @@
package mysql
import (
"container/list"
"context"
"database/sql"
"fmt"
@@ -65,7 +66,7 @@ func (e *MysqlExecutor) initEngine() error {
}
}
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
e.log.Debug("getEngine", "connection", cnnstr)
engine, err := xorm.NewEngine("mysql", cnnstr)
@@ -245,6 +246,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows)
func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
pointsBySeries := make(map[string]*tsdb.TimeSeries)
seriesByQueryOrder := list.New()
columnNames, err := rows.Columns()
if err != nil {
@@ -282,11 +284,13 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows,
series := &tsdb.TimeSeries{Name: rowData.metric}
series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
pointsBySeries[rowData.metric] = series
seriesByQueryOrder.PushBack(rowData.metric)
}
}
for _, value := range pointsBySeries {
result.Series = append(result.Series, value)
for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
key := elem.Value.(string)
result.Series = append(result.Series, pointsBySeries[key])
}
result.Meta.Set("rowCount", rowCount)