Merge branch 'master' into sql-proxy

This commit is contained in:
Torkel Ödegaard
2017-03-29 16:40:14 +02:00
326 changed files with 6477 additions and 1825 deletions
+4 -2
View File
@@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() {
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/search", wrap(SearchUsersWithPaging))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
@@ -195,10 +196,11 @@ func (hs *HttpServer) registerRoutes() {
// Data sources
r.Group("/datasources", func() {
r.Get("/", GetDataSources)
r.Get("/", wrap(GetDataSources))
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSource)
r.Delete("/:id", DeleteDataSourceById)
r.Delete("/name/:name", DeleteDataSourceByName)
r.Get("/:id", wrap(GetDataSourceById))
r.Get("/name/:name", wrap(GetDataSourceByName))
}, reqOrgAdmin)
-35
View File
@@ -1,35 +0,0 @@
package api
import (
"testing"
)
func TestHttpApi(t *testing.T) {
// Convey("Given the grafana api", t, func() {
// ConveyApiScenario("Can sign up", func(c apiTestContext) {
// c.PostJson()
// So(c.Resp, ShouldEqualJsonApiResponse, "User created and logged in")
// })
//
// m := macaron.New()
// m.Use(middleware.GetContextHandler())
// m.Use(middleware.Sessioner(&session.Options{}))
// Register(m)
//
// var context *middleware.Context
// m.Get("/", func(c *middleware.Context) {
// context = c
// })
//
// resp := httptest.NewRecorder()
// req, err := http.NewRequest("GET", "/", nil)
// So(err, ShouldBeNil)
//
// m.ServeHTTP(resp, req)
//
// Convey("should red 200", func() {
// So(resp.Code, ShouldEqual, 200)
// })
// })
}
+77 -10
View File
@@ -3,7 +3,9 @@ package cloudwatch
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
"time"
@@ -12,6 +14,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
@@ -114,19 +117,26 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
DurationSeconds: aws.Int64(900),
}
stsSess := session.New()
stsSess, err := session.NewSession()
if err != nil {
return nil, err
}
stsCreds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(stsSess), ExpiryWindow: 5 * time.Minute},
remoteCredProvider(stsSess),
})
stsConfig := &aws.Config{
Region: aws.String(dsInfo.Region),
Credentials: stsCreds,
}
svc := sts.New(session.New(stsConfig), stsConfig)
sess, err := session.NewSession(stsConfig)
if err != nil {
return nil, err
}
svc := sts.New(sess, stsConfig)
resp, err := svc.AssumeRole(params)
if err != nil {
return nil, err
@@ -139,7 +149,10 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
}
}
sess := session.New()
sess, err := session.NewSession()
if err != nil {
return nil, err
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
@@ -166,6 +179,30 @@ func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
return creds, nil
}
func remoteCredProvider(sess *session.Session) credentials.Provider {
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
if len(ecsCredURI) > 0 {
return ecsCredProvider(sess, ecsCredURI)
}
return ec2RoleProvider(sess)
}
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
const host = `169.254.170.2`
c := ec2metadata.New(sess)
return endpointcreds.NewProviderClient(
c.Client.Config,
c.Client.Handlers,
fmt.Sprintf("http://%s%s", host, uri),
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
}
func ec2RoleProvider(sess *session.Session) credentials.Provider {
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
}
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
creds, err := getCredentials(req.GetDatasourceInfo())
if err != nil {
@@ -185,7 +222,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
@@ -232,7 +274,12 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
@@ -273,7 +320,12 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
@@ -316,7 +368,12 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
@@ -360,7 +417,12 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(sess, cfg)
reqParam := &struct {
Parameters struct {
@@ -396,7 +458,12 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := ec2.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := ec2.New(sess, cfg)
reqParam := &struct {
Parameters struct {
+41
View File
@@ -0,0 +1,41 @@
package cloudwatch
import (
"fmt"
"os"
"testing"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
"github.com/aws/aws-sdk-go/aws/session"
. "github.com/smartystreets/goconvey/convey"
)
func TestECSCredProvider(t *testing.T) {
Convey("Running in an ECS container task", t, func() {
defer os.Clearenv()
os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/abc/123")
provider := remoteCredProvider(&session.Session{})
So(provider, ShouldNotBeNil)
ecsProvider, ok := provider.(*endpointcreds.Provider)
So(ecsProvider, ShouldNotBeNil)
So(ok, ShouldBeTrue)
So(ecsProvider.Client.Endpoint, ShouldEqual, fmt.Sprintf("http://169.254.170.2/abc/123"))
})
}
func TestDefaultEC2RoleProvider(t *testing.T) {
Convey("Running outside an ECS container task", t, func() {
provider := remoteCredProvider(&session.Session{})
So(provider, ShouldNotBeNil)
ec2Provider, ok := provider.(*ec2rolecreds.EC2RoleProvider)
So(ec2Provider, ShouldNotBeNil)
So(ok, ShouldBeTrue)
})
}
+9 -4
View File
@@ -81,7 +81,8 @@ func init() {
"AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "FailedSqlStatements", "FreeableMemory", "FreeStorageSpace", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
"AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
"AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"},
"AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
"AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
@@ -122,7 +123,8 @@ func init() {
"AWS/Redshift": {"NodeID", "ClusterIdentifier"},
"AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName"},
"AWS/Route53": {"HealthCheckId"},
"AWS/S3": {"BucketName", "StorageType"},
"AWS/S3": {"BucketName", "StorageType", "FilterId"},
"AWS/SES": {},
"AWS/SNS": {"Application", "Platform", "TopicName"},
"AWS/SQS": {"QueueName"},
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
@@ -256,8 +258,11 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
Region: aws.String(cwData.Region),
Credentials: creds,
}
svc := cloudwatch.New(session.New(cfg), cfg)
sess, err := session.NewSession(cfg)
if err != nil {
return cloudwatch.ListMetricsOutput{}, err
}
svc := cloudwatch.New(sess, cfg)
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(cwData.Namespace),
+8
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
"github.com/grafana/grafana/pkg/api/cloudwatch"
@@ -115,6 +116,13 @@ func ProxyDataSourceRequest(c *middleware.Context) {
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)
+25 -6
View File
@@ -11,12 +11,11 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) {
func GetDataSources(c *middleware.Context) Response {
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to query datasources", err)
return
return ApiError(500, "Failed to query datasources", err)
}
result := make(dtos.DataSourceList, 0)
@@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
}
sort.Sort(result)
c.JSON(200, result)
return Json(200, &result)
}
func GetDataSourceById(c *middleware.Context) Response {
@@ -68,7 +68,7 @@ func GetDataSourceById(c *middleware.Context) Response {
return Json(200, &dtos)
}
func DeleteDataSource(c *middleware.Context) {
func DeleteDataSourceById(c *middleware.Context) {
id := c.ParamsInt64(":id")
if id <= 0 {
@@ -76,7 +76,26 @@ func DeleteDataSource(c *middleware.Context) {
return
}
cmd := &m.DeleteDataSourceCommand{Id: id, OrgId: c.OrgId}
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
err := bus.Dispatch(cmd)
if err != nil {
c.JsonApiErr(500, "Failed to delete datasource", err)
return
}
c.JsonOK("Data source deleted")
}
func DeleteDataSourceByName(c *middleware.Context) {
name := c.Params(":name")
if name == "" {
c.JsonApiErr(400, "Missing valid datasource name", nil)
return
}
cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
err := bus.Dispatch(cmd)
if err != nil {
+132
View File
@@ -0,0 +1,132 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/grafana/grafana/pkg/models"
macaron "gopkg.in/macaron.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
. "github.com/smartystreets/goconvey/convey"
)
const (
TestOrgID = 1
TestUserID = 1
)
func TestDataSourcesProxy(t *testing.T) {
Convey("Given a user is logged in", t, func() {
loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) {
// Stubs the database query
bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error {
So(query.OrgId, ShouldEqual, TestOrgID)
query.Result = []*models.DataSource{
{Name: "mmm"},
{Name: "ZZZ"},
{Name: "BBB"},
{Name: "aaa"},
}
return nil
})
// handler func being tested
sc.handlerFunc = GetDataSources
sc.fakeReq("GET", "/api/datasources").exec()
respJSON := []map[string]interface{}{}
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
So(err, ShouldBeNil)
Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() {
So(respJSON[0]["name"], ShouldEqual, "aaa")
So(respJSON[1]["name"], ShouldEqual, "BBB")
So(respJSON[2]["name"], ShouldEqual, "mmm")
So(respJSON[3]["name"], ShouldEqual, "ZZZ")
})
})
})
}
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
Directory: viewsPath,
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR
if sc.handlerFunc != nil {
return sc.handlerFunc(sc.context)
}
return nil
})
sc.m.Get(url, sc.defaultHandler)
fn(sc)
})
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
So(err, ShouldBeNil)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *middleware.Context
resp *httptest.ResponseRecorder
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
sc.m.ServeHTTP(sc.resp, sc.req)
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *middleware.Context) Response
+1 -1
View File
@@ -91,7 +91,7 @@ func (slice DataSourceList) Len() int {
}
func (slice DataSourceList) Less(i, j int) bool {
return slice[i].Name < slice[j].Name
return strings.ToLower(slice[i].Name) < strings.ToLower(slice[j].Name)
}
func (slice DataSourceList) Swap(i, j int) {
+11 -8
View File
@@ -133,14 +133,17 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
}
jsonObj := map[string]interface{}{
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"panels": panels,
"appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"defaultDatasource": defaultDatasource,
"datasources": datasources,
"panels": panels,
"appSubUrl": setting.AppSubUrl,
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm,
"disableSignoutMenu": setting.DisableSignoutMenu,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
+47 -3
View File
@@ -2,6 +2,7 @@ package api
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
@@ -24,6 +25,8 @@ type HttpServer struct {
macaron *macaron.Macaron
context context.Context
streamManager *live.StreamManager
httpSrv *http.Server
}
func NewHttpServer() *HttpServer {
@@ -45,11 +48,20 @@ func (hs *HttpServer) Start(ctx context.Context) error {
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
switch setting.Protocol {
case setting.HTTP:
err = http.ListenAndServe(listenAddr, hs.macaron)
err = hs.httpSrv.ListenAndServe()
if err == http.ErrServerClosed {
hs.log.Debug("server was shutdown gracefully")
return nil
}
case setting.HTTPS:
err = hs.listenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile)
err = hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
if err == http.ErrServerClosed {
hs.log.Debug("server was shutdown gracefully")
return nil
}
default:
hs.log.Error("Invalid protocol", "protocol", setting.Protocol)
err = errors.New("Invalid Protocol")
@@ -58,6 +70,12 @@ func (hs *HttpServer) Start(ctx context.Context) error {
return err
}
func (hs *HttpServer) Shutdown(ctx context.Context) error {
err := hs.httpSrv.Shutdown(ctx)
hs.log.Info("stopped http server")
return err
}
func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) error {
if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
@@ -75,7 +93,32 @@ func (hs *HttpServer) listenAndServeTLS(listenAddr, certfile, keyfile string) er
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
}
return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, hs.macaron)
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
},
}
srv := &http.Server{
Addr: listenAddr,
Handler: hs.macaron,
TLSConfig: tlsCfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
return srv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
}
func (hs *HttpServer) newMacaron() *macaron.Macaron {
@@ -107,6 +150,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
m.Use(middleware.RequestMetrics())
m.Use(middleware.OrgRedirect())
// needs to be after context handler
if setting.EnforceDomain {
+9
View File
@@ -35,6 +35,11 @@ func LoginView(c *middleware.Context) {
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
if loginError, ok := c.Session.Get("loginError").(string); ok {
c.Session.Delete("loginError")
viewData.Settings["loginError"] = loginError
}
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, VIEW_INDEX, viewData)
return
@@ -94,6 +99,10 @@ func LoginApiPing(c *middleware.Context) {
}
func LoginPost(c *middleware.Context, cmd dtos.LoginCommand) Response {
if setting.DisableLoginForm {
return ApiError(401, "Login is disabled", nil)
}
authQuery := login.LoginUserQuery{
Username: cmd.User,
Password: cmd.Password,
+27 -10
View File
@@ -10,6 +10,7 @@ import (
"io/ioutil"
"log"
"net/http"
"net/url"
"golang.org/x/net/context"
"golang.org/x/oauth2"
@@ -22,6 +23,13 @@ import (
"github.com/grafana/grafana/pkg/social"
)
var (
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
ErrUsersQuotaReached = errors.New("Users quota reached")
)
func GenStateString() string {
rnd := make([]byte, 32)
rand.Read(rnd)
@@ -44,8 +52,7 @@ func OAuthLogin(ctx *middleware.Context) {
error := ctx.Query("error")
if error != "" {
errorDesc := ctx.Query("error_description")
ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
redirectWithError(ctx, ErrProviderDeniedRequest, "error", error, "errorDesc", errorDesc)
return
}
@@ -117,10 +124,8 @@ func OAuthLogin(ctx *middleware.Context) {
// get user info
userInfo, err := connect.UserInfo(client)
if err != nil {
if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
} else if err == social.ErrMissingOrganizationMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
if sErr, ok := err.(*social.Error); ok {
redirectWithError(ctx, sErr)
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
@@ -131,8 +136,7 @@ func OAuthLogin(ctx *middleware.Context) {
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
redirectWithError(ctx, ErrEmailNotAllowed)
return
}
@@ -142,7 +146,7 @@ func OAuthLogin(ctx *middleware.Context) {
// create account if missing
if err == m.ErrUserNotFound {
if !connect.IsSignupAllowed() {
ctx.Redirect(setting.AppSubUrl + "/login")
redirectWithError(ctx, ErrSignUpNotAllowed)
return
}
limitReached, err := middleware.QuotaReached(ctx, "user")
@@ -151,7 +155,7 @@ func OAuthLogin(ctx *middleware.Context) {
return
}
if limitReached {
ctx.Redirect(setting.AppSubUrl + "/login")
redirectWithError(ctx, ErrUsersQuotaReached)
return
}
cmd := m.CreateUserCommand{
@@ -177,5 +181,18 @@ func OAuthLogin(ctx *middleware.Context) {
metrics.M_Api_Login_OAuth.Inc(1)
if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 {
ctx.SetCookie("redirect_to", "", -1, setting.AppSubUrl+"/")
ctx.Redirect(redirectTo)
return
}
ctx.Redirect(setting.AppSubUrl + "/")
}
func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
ctx.Logger.Info(err.Error(), v...)
// TODO: we can use the flash storage here once it's implemented
ctx.Session.Set("loginError", err.Error())
ctx.Redirect(setting.AppSubUrl + "/login")
}
+1 -1
View File
@@ -33,7 +33,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
})
}
resp, err := tsdb.HandleRequest(context.TODO(), request)
resp, err := tsdb.HandleRequest(context.Background(), request)
if err != nil {
return ApiError(500, "Metric request error", err)
}
+4
View File
@@ -38,6 +38,10 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
if err != m.ErrUserNotFound {
return ApiError(500, "Failed to query db for existing user check", err)
}
if setting.DisableLoginForm {
return ApiError(401, "User could not be found", nil)
}
} else {
return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
}
+36 -2
View File
@@ -210,14 +210,48 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result.Users)
}
// GET /api/search
func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
searchQuery := c.Query("query")
query := &m.SearchUsersQuery{Query: searchQuery, Page: page, Limit: perPage}
if err := bus.Dispatch(query); err != nil {
return nil, err
}
query.Result.Page = page
query.Result.PerPage = perPage
return query, nil
}
func SetHelpFlag(c *middleware.Context) Response {
flag := c.ParamsInt64(":id")
+109
View File
@@ -0,0 +1,109 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserApiEndpoint(t *testing.T) {
Convey("Given a user is logged in", t, func() {
mockResult := models.SearchUserQueryResult{
Users: []*models.UserSearchHitDTO{
{Name: "user1"},
{Name: "user2"},
},
TotalCount: 2,
}
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
}
+1 -1
View File
@@ -35,7 +35,7 @@ func main() {
cli.StringFlag{
Name: "repo",
Usage: "url to the plugin repository",
Value: "https://grafana.net/api/plugins",
Value: "https://grafana.com/api/plugins",
EnvVar: "GF_PLUGIN_REPO",
},
cli.BoolFlag{
@@ -40,6 +40,8 @@ func returnOsDefault(currentOs string) string {
return "../data/plugins"
case "darwin":
return "/usr/local/var/lib/grafana/plugins"
case "freebsd":
return "/var/db/grafana/plugins"
default: //"linux"
return "/var/lib/grafana/plugins"
}
+10 -18
View File
@@ -3,7 +3,6 @@ package main
import (
"context"
"os"
"time"
"golang.org/x/sync/errgroup"
@@ -39,6 +38,8 @@ type GrafanaServerImpl struct {
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
httpServer *api.HttpServer
}
func (g *GrafanaServerImpl) Start() {
@@ -74,9 +75,9 @@ func (g *GrafanaServerImpl) Start() {
}
func (g *GrafanaServerImpl) startHttpServer() {
httpServer := api.NewHttpServer()
g.httpServer = api.NewHttpServer()
err := httpServer.Start(g.context)
err := g.httpServer.Start(g.context)
if err != nil {
g.log.Error("Fail to start server", "error", err)
@@ -88,24 +89,15 @@ func (g *GrafanaServerImpl) startHttpServer() {
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.log.Info("Shutdown started", "code", code, "reason", reason)
err := g.httpServer.Shutdown(g.context)
if err != nil {
g.log.Error("Failed to shutdown server", "error", err)
}
g.shutdownFn()
err := g.childRoutines.Wait()
err = g.childRoutines.Wait()
g.log.Info("Shutdown completed", "reason", err)
log.Close()
os.Exit(code)
}
// implement context.Context
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
return g.context.Deadline()
}
func (g *GrafanaServerImpl) Done() <-chan struct{} {
return g.context.Done()
}
func (g *GrafanaServerImpl) Err() error {
return g.context.Err()
}
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
return g.context.Value(key)
}
+39 -15
View File
@@ -27,25 +27,15 @@ func NewImageUploader() (ImageUploader, error) {
return nil, err
}
bucket := s3sec.Key("bucket_url").MustString("")
bucketUrl := s3sec.Key("bucket_url").MustString("")
accessKey := s3sec.Key("access_key").MustString("")
secretKey := s3sec.Key("secret_key").MustString("")
region := ""
rBucket := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
matches := rBucket.FindStringSubmatch(bucket)
if len(matches) == 0 {
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
} else {
bucket = matches[1]
if matches[3] != "" {
region = matches[3]
} else {
region = "us-east-1"
}
info, err := getRegionAndBucketFromUrl(bucketUrl)
if err != nil {
return nil, err
}
return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
return NewS3Uploader(info.region, info.bucket, "public-read", accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
if err != nil {
@@ -65,3 +55,37 @@ func NewImageUploader() (ImageUploader, error) {
return NopImageUploader{}, nil
}
type s3Info struct {
region string
bucket string
}
func getRegionAndBucketFromUrl(url string) (*s3Info, error) {
info := &s3Info{}
urlRegex := regexp.MustCompile(`https?:\/\/(.*)\.s3(-([^.]+))?\.amazonaws\.com\/?`)
matches := urlRegex.FindStringSubmatch(url)
if len(matches) > 0 {
info.bucket = matches[1]
if matches[3] != "" {
info.region = matches[3]
} else {
info.region = "us-east-1"
}
return info, nil
}
urlRegex2 := regexp.MustCompile(`https?:\/\/s3(-([^.]+))?\.amazonaws\.com\/(.*)?`)
matches2 := urlRegex2.FindStringSubmatch(url)
if len(matches2) > 0 {
info.bucket = matches2[3]
if matches2[2] != "" {
info.region = matches2[2]
} else {
info.region = "us-east-1"
}
return info, nil
}
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
}
+51 -14
View File
@@ -10,29 +10,66 @@ import (
func TestImageUploaderFactory(t *testing.T) {
Convey("Can create image uploader for ", t, func() {
Convey("S3ImageUploader", func() {
var err error
Convey("S3ImageUploader config", func() {
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "s3"
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-east-2")
So(original.bucket, ShouldEqual, "foo.bar.baz")
So(original.accessKey, ShouldEqual, "access_key")
So(original.secretKey, ShouldEqual, "secret_key")
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-east-2")
So(original.bucket, ShouldEqual, "foo.bar.baz")
So(original.accessKey, ShouldEqual, "access_key")
So(original.secretKey, ShouldEqual, "secret_key")
})
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-east-1")
So(original.bucket, ShouldEqual, "my.bucket.com")
So(original.accessKey, ShouldEqual, "access_key")
So(original.secretKey, ShouldEqual, "secret_key")
})
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*S3Uploader)
So(ok, ShouldBeTrue)
So(original.region, ShouldEqual, "us-west-2")
So(original.bucket, ShouldEqual, "my.bucket.com")
So(original.accessKey, ShouldEqual, "access_key")
So(original.secretKey, ShouldEqual, "secret_key")
})
})
Convey("Webdav uploader", func() {
+9 -2
View File
@@ -35,7 +35,10 @@ func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader
}
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
sess := session.New()
sess, err := session.NewSession()
if err != nil {
return "", err
}
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
@@ -58,7 +61,11 @@ func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
return "", err
}
svc := s3.New(session.New(cfg), cfg)
sess, err = session.NewSession(cfg)
if err != nil {
return "", err
}
svc := s3.New(sess, cfg)
params := &s3.PutObjectInput{
Bucket: aws.String(u.bucket),
Key: aws.String(key),
+15 -1
View File
@@ -4,9 +4,11 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"path"
"time"
"github.com/grafana/grafana/pkg/util"
)
@@ -17,6 +19,18 @@ type WebdavUploader struct {
password string
}
var netTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 60 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 60,
Transport: netTransport,
}
func (u *WebdavUploader) Upload(pa string) (string, error) {
url, _ := url.Parse(u.url)
url.Path = path.Join(url.Path, util.GetRandomString(20)+".png")
@@ -28,7 +42,7 @@ func (u *WebdavUploader) Upload(pa string) (string, error) {
req.SetBasicAuth(u.username, u.password)
}
res, err := http.DefaultClient.Do(req)
res, err := netClient.Do(req)
if err != nil {
return "", err
+4
View File
@@ -49,7 +49,9 @@ var (
M_Alerting_Notification_Sent_Victorops Counter
M_Alerting_Notification_Sent_OpsGenie Counter
M_Alerting_Notification_Sent_Telegram Counter
M_Alerting_Notification_Sent_Threema Counter
M_Alerting_Notification_Sent_Sensu Counter
M_Alerting_Notification_Sent_Pushover Counter
M_Aws_CloudWatch_GetMetricStatistics Counter
M_Aws_CloudWatch_ListMetrics Counter
@@ -118,8 +120,10 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram")
M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema")
M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover")
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
+4
View File
@@ -90,6 +90,10 @@ func (s *mockSession) Get(k interface{}) interface{} {
return s.value
}
func (s *mockSession) Delete(k interface{}) interface{} {
return nil
}
func (s *mockSession) ID() string {
return ""
}
+1
View File
@@ -326,6 +326,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
// mock out gc goroutine
startSessionGC = func() {}
sc.m.Use(Sessioner(&session.Options{}))
sc.m.Use(OrgRedirect())
sc.defaultHandler = func(c *Context) {
sc.context = c
+44
View File
@@ -0,0 +1,44 @@
package middleware
import (
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"gopkg.in/macaron.v1"
)
func OrgRedirect() macaron.Handler {
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
orgIdValue := req.URL.Query().Get("orgId")
orgId, err := strconv.ParseInt(orgIdValue, 10, 32)
if err != nil || orgId == 0 {
return
}
ctx, ok := c.Data["ctx"].(*Context)
if !ok || !ctx.IsSignedIn {
return
}
if orgId == ctx.OrgId {
return
}
cmd := models.SetUsingOrgCommand{UserId: ctx.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
if ctx.IsApiRequest() {
ctx.JsonApiErr(404, "Not found", nil)
} else {
ctx.Error(404, "Not found")
}
return
}
c.Redirect(c.Req.URL.String(), 302)
}
}
+60
View File
@@ -0,0 +1,60 @@
package middleware
import (
"testing"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() {
middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return nil
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
Convey("change org and redirect", func() {
So(sc.resp.Code, ShouldEqual, 302)
})
})
middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
sc.fakeReq("GET", "/").handler(func(c *Context) {
c.Session.Set(SESS_KEY_USERID, int64(12))
}).exec()
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
Convey("not allowed to change org", func() {
So(sc.resp.Code, ShouldEqual, 404)
})
})
})
}
+16 -1
View File
@@ -1,6 +1,7 @@
package middleware
import (
"math/rand"
"time"
"github.com/go-macaron/session"
@@ -8,6 +9,7 @@ import (
_ "github.com/go-macaron/session/mysql"
_ "github.com/go-macaron/session/postgres"
_ "github.com/go-macaron/session/redis"
"github.com/grafana/grafana/pkg/log"
"gopkg.in/macaron.v1"
)
@@ -22,10 +24,12 @@ var sessionManager *session.Manager
var sessionOptions *session.Options
var startSessionGC func()
var getSessionCount func() int
var sessionLogger = log.New("session")
func init() {
startSessionGC = func() {
sessionManager.GC()
sessionLogger.Debug("Session GC")
time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, startSessionGC)
}
getSessionCount = func() int {
@@ -67,7 +71,9 @@ func Sessioner(options *session.Options) macaron.Handler {
panic(err)
}
go startSessionGC()
// start GC threads after some random seconds
rndSeconds := 10 + rand.Int63n(180)
time.AfterFunc(time.Duration(rndSeconds)*time.Second, startSessionGC)
return func(ctx *Context) {
ctx.Next()
@@ -87,6 +93,8 @@ type SessionStore interface {
Set(interface{}, interface{}) error
// Get gets value by given key in session.
Get(interface{}) interface{}
// Delete deletes a key from session.
Delete(interface{}) interface{}
// ID returns current session ID.
ID() string
// Release releases session resource and save data to provider.
@@ -122,6 +130,13 @@ func (s *SessionWrapper) Get(k interface{}) interface{} {
return nil
}
func (s *SessionWrapper) Delete(k interface{}) interface{} {
if s.session != nil {
return s.session.Delete(k)
}
return nil
}
func (s *SessionWrapper) ID() string {
if s.session != nil {
return s.session.ID()
+1
View File
@@ -35,6 +35,7 @@ const (
var (
ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert")
ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.")
)
func (s AlertStateType) IsValid() bool {
+6 -1
View File
@@ -122,11 +122,16 @@ type UpdateDataSourceCommand struct {
Id int64 `json:"-"`
}
type DeleteDataSourceCommand struct {
type DeleteDataSourceByIdCommand struct {
Id int64
OrgId int64
}
type DeleteDataSourceByNameCommand struct {
Name string
OrgId int64
}
// ---------------------
// QUERIES
-4
View File
@@ -1,10 +1,6 @@
package models
import "context"
type GrafanaServer interface {
context.Context
Start()
Shutdown(code int, reason string)
}
+8 -1
View File
@@ -130,7 +130,14 @@ type SearchUsersQuery struct {
Page int
Limit int
Result []*UserSearchHitDTO
Result SearchUserQueryResult
}
type SearchUserQueryResult struct {
TotalCount int64 `json:"totalCount"`
Users []*UserSearchHitDTO `json:"users"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
type GetUserOrgListQuery struct {
+3 -3
View File
@@ -60,7 +60,7 @@ func checkForUpdates() {
log.Trace("Checking for updates")
pluginSlugs := getAllExternalPluginSlugs()
resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
if err != nil {
log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
@@ -71,14 +71,14 @@ func checkForUpdates() {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Trace("Update check failed, reading response from grafana.net, %v", err.Error())
log.Trace("Update check failed, reading response from grafana.com, %v", err.Error())
return
}
gNetPlugins := []GrafanaNetPlugin{}
err = json.Unmarshal(body, &gNetPlugins)
if err != nil {
log.Trace("Failed to unmarshal plugin repo, reading response from grafana.net, %v", err.Error())
log.Trace("Failed to unmarshal plugin repo, reading response from grafana.com, %v", err.Error())
return
}
+2 -1
View File
@@ -67,6 +67,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name,
Value: reducedValue,
Tags: series.Tags,
})
}
}
@@ -78,7 +79,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
Message: fmt.Sprintf("Condition: Eval: %v, Query Returned No Series (reduced to null/no value)", evalMatch),
})
}
+1 -1
View File
@@ -98,7 +98,7 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
}
}()
alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
job.Running = true
evalContext := NewEvalContext(alertCtx, job.Rule)
+1 -1
View File
@@ -113,7 +113,7 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
if slug, err := c.GetDashboardSlug(); err != nil {
return "", err
} else {
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId)
return ruleUrl, nil
}
}
+12
View File
@@ -21,6 +21,18 @@ type Notifier interface {
GetIsDefault() bool
}
type NotifierSlice []Notifier
func (notifiers NotifierSlice) ShouldUploadImage() bool {
for _, notifier := range notifiers {
if notifier.NeedsImage() {
return true
}
}
return false
}
type ConditionResult struct {
Firing bool
NoDataFound bool
+20 -30
View File
@@ -21,37 +21,25 @@ type NotifierPlugin struct {
Factory NotifierFactory `json:"-"`
}
type RootNotifier struct {
type NotificationService interface {
Send(context *EvalContext) error
}
func NewNotificationService() NotificationService {
return newNotificationService()
}
type notificationService struct {
log log.Logger
}
func NewRootNotifier() *RootNotifier {
return &RootNotifier{
func newNotificationService() *notificationService {
return &notificationService{
log: log.New("alerting.notifier"),
}
}
func (n *RootNotifier) GetType() string {
return "root"
}
func (n *RootNotifier) NeedsImage() bool {
return false
}
func (n *RootNotifier) PassesFilter(rule *Rule) bool {
return false
}
func (n *RootNotifier) GetNotifierId() int64 {
return 0
}
func (n *RootNotifier) GetIsDefault() bool {
return false
}
func (n *RootNotifier) Notify(context *EvalContext) error {
func (n *notificationService) Send(context *EvalContext) error {
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil {
return err
@@ -63,14 +51,16 @@ func (n *RootNotifier) Notify(context *EvalContext) error {
return nil
}
if err = n.uploadImage(context); err != nil {
n.log.Error("Failed to upload alert panel image.", "error", err)
if notifiers.ShouldUploadImage() {
if err = n.uploadImage(context); err != nil {
n.log.Error("Failed to upload alert panel image.", "error", err)
}
}
return n.sendNotifications(context, notifiers)
}
func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error {
g, _ := errgroup.WithContext(context.Ctx)
for _, notifier := range notifiers {
@@ -82,7 +72,7 @@ func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notif
return g.Wait()
}
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
func (n *notificationService) uploadImage(context *EvalContext) (err error) {
uploader, err := imguploader.NewImageUploader()
if err != nil {
return err
@@ -116,7 +106,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
return nil
}
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil {
@@ -137,7 +127,7 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
return result, nil
}
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
notifierPlugin, found := notifierFactories[model.Type]
if !found {
return nil, errors.New("Unsupported notification type")
+13 -9
View File
@@ -6,18 +6,22 @@ import (
)
type NotifierBase struct {
Name string
Type string
Id int64
IsDeault bool
Name string
Type string
Id int64
IsDeault bool
UploadImage bool
}
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
uploadImage := model.Get("uploadImage").MustBool(true)
return NotifierBase{
Id: id,
Name: name,
IsDeault: isDefault,
Type: notifierType,
Id: id,
Name: name,
IsDeault: isDefault,
Type: notifierType,
UploadImage: uploadImage,
}
}
@@ -30,7 +34,7 @@ func (n *NotifierBase) GetType() string {
}
func (n *NotifierBase) NeedsImage() bool {
return true
return n.UploadImage
}
func (n *NotifierBase) GetNotifierId() int64 {
+1 -1
View File
@@ -16,7 +16,7 @@ func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "email",
Name: "Email",
Description: "Sends notifications using Grafana server configured STMP settings",
Description: "Sends notifications using Grafana server configured SMTP settings",
Factory: NewEmailNotifier,
OptionsTemplate: `
<h3 class="page-heading">Email addresses</h3>
+155
View File
@@ -0,0 +1,155 @@
package notifiers
import (
"encoding/json"
"strconv"
"strings"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "hipchat",
Name: "HipChat",
Description: "Sends notifications uto a HipChat Room",
Factory: NewHipChatNotifier,
OptionsTemplate: `
<h3 class="page-heading">HipChat settings</h3>
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Hip Chat Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="HipChat URL (ex https://grafana.hipchat.com)"></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">API Key</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.apikey" placeholder="HipChat API Key"></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-8">Room ID</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.roomid"
data-placement="right">
</input>
</div>
`,
})
}
const (
maxFieldCount int = 4
)
func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
apikey := model.Settings.Get("apikey").MustString()
roomId := model.Settings.Get("roomid").MustString()
return &HipChatNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
ApiKey: apikey,
RoomId: roomId,
log: log.New("alerting.notifier.hipchat"),
}, nil
}
type HipChatNotifier struct {
NotifierBase
Url string
ApiKey string
RoomId string
log log.Logger
}
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return err
}
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
fields := make([]map[string]interface{}, 0)
message += "<br>"
for index, evt := range evalContext.EvalMatches {
message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "<br>"
fields = append(fields, map[string]interface{}{
"title": evt.Metric,
"value": evt.Value,
"short": true,
})
if index > maxFieldCount {
break
}
}
if evalContext.Error != nil {
fields = append(fields, map[string]interface{}{
"title": "Error message",
"value": evalContext.Error.Error(),
"short": false,
})
}
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
}
//HipChat has a set list of colors
var color string
switch evalContext.Rule.State {
case models.AlertStateOK:
color = "green"
case models.AlertStateNoData:
color = "grey"
case models.AlertStateAlerting:
color = "red"
}
// Add a card with link to the dashboard
card := map[string]interface{}{
"style": "link",
"url": ruleUrl,
"id": "1",
"title": evalContext.GetNotificationTitle(),
"description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text,
"icon": map[string]interface{}{
"url": "https://grafana.com/assets/img/fav32.png",
},
"date": evalContext.EndTime.Unix(),
}
body := map[string]interface{}{
"message": message,
"notify": "true",
"message_format": "html",
"color": color,
"card": card,
}
hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
data, _ := json.Marshal(&body)
cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send hipchat notification", "error", err, "webhook", this.Name)
return err
}
return nil
}
@@ -0,0 +1,81 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestHipChatNotifier(t *testing.T) {
Convey("HipChat notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
_, err := NewHipChatNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("from settings", func() {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model)
hipchatNotifier := not.(*HipChatNotifier)
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
So(hipchatNotifier.Url, ShouldEqual, "http://google.com")
So(hipchatNotifier.ApiKey, ShouldEqual, "")
So(hipchatNotifier.RoomId, ShouldEqual, "")
})
Convey("from settings with Recipient and Mention", func() {
json := `
{
"url": "http://www.hipchat.com",
"apikey": "1234",
"roomid": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model)
hipchatNotifier := not.(*HipChatNotifier)
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
So(hipchatNotifier.Url, ShouldEqual, "http://www.hipchat.com")
So(hipchatNotifier.ApiKey, ShouldEqual, "1234")
So(hipchatNotifier.RoomId, ShouldEqual, "1234")
})
})
})
}
+1 -1
View File
@@ -30,7 +30,7 @@ func init() {
label="Auto close incidents"
label-class="width-14"
checked="ctrl.model.settings.autoClose"
tooltip="Automatically close alerts in OpseGenie once the alert goes back to ok.">
tooltip="Automatically close alerts in OpsGenie once the alert goes back to ok.">
</gf-form-switch>
</div>
`,
+177
View File
@@ -0,0 +1,177 @@
package notifiers
import (
"fmt"
"net/url"
"strconv"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json"
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "pushover",
Name: "Pushover",
Description: "Sends HTTP POST request to the Pushover API",
Factory: NewPushoverNotifier,
OptionsTemplate: `
<h3 class="page-heading">Pushover settings</h3>
<div class="gf-form">
<span class="gf-form-label width-10">API Token</span>
<input type="text" class="gf-form-input" required placeholder="Application token" ng-model="ctrl.model.settings.apiToken"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">User key(s)</span>
<input type="text" class="gf-form-input" required placeholder="comma-separated list" ng-model="ctrl.model.settings.userKey"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Device(s) (optional)</span>
<input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Priority</span>
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
Emergency: '2',
High: '1',
Normal: '0',
Low: '-1',
Lowest: '-2'
}" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
</div>
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
<span class="gf-form-label width-10">Retry</span>
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
</div>
<div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
<span class="gf-form-label width-10">Expire</span>
<input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Sound</span>
<select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
'default',
'pushover',
'bike',
'bugle',
'cashregister',
'classical',
'cosmic',
'falling',
'gamelan',
'incoming',
'intermission',
'magic',
'mechanical',
'pianobar',
'siren',
'spacealarm',
'tugboat',
'alien',
'climb',
'persistent',
'echo',
'updown',
'none'
]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
</div>
`,
})
}
func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
userKey := model.Settings.Get("userKey").MustString()
apiToken := model.Settings.Get("apiToken").MustString()
device := model.Settings.Get("device").MustString()
priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
sound := model.Settings.Get("sound").MustString()
if userKey == "" {
return nil, alerting.ValidationError{Reason: "User key not given"}
}
if apiToken == "" {
return nil, alerting.ValidationError{Reason: "API token not given"}
}
return &PushoverNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
UserKey: userKey,
ApiToken: apiToken,
Priority: priority,
Retry: retry,
Expire: expire,
Device: device,
Sound: sound,
log: log.New("alerting.notifier.pushover"),
}, nil
}
type PushoverNotifier struct {
NotifierBase
UserKey string
ApiToken string
Priority int
Retry int
Expire int
Device string
Sound string
log log.Logger
}
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
metrics.M_Alerting_Notification_Sent_Pushover.Inc(1)
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return err
}
message := evalContext.Rule.Message
for idx, evt := range evalContext.EvalMatches {
message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
if idx > 4 {
break
}
}
if evalContext.Error != nil {
message += fmt.Sprintf("\n<b>Error message</b> %s", evalContext.Error.Error())
}
q := url.Values{}
q.Add("user", this.UserKey)
q.Add("token", this.ApiToken)
q.Add("priority", strconv.Itoa(this.Priority))
if this.Priority == 2 {
q.Add("retry", strconv.Itoa(this.Retry))
q.Add("expire", strconv.Itoa(this.Expire))
}
if this.Device != "" {
q.Add("device", this.Device)
}
if this.Sound != "default" {
q.Add("sound", this.Sound)
}
q.Add("title", evalContext.GetNotificationTitle())
q.Add("url", ruleUrl)
q.Add("url_title", "Show dashboard with alert")
q.Add("message", message)
q.Add("html", "1")
cmd := &m.SendWebhookSync{
Url: PUSHOVER_ENDPOINT,
HttpMethod: "POST",
HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
Body: q.Encode(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
return err
}
return nil
}
@@ -0,0 +1,58 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestPushoverNotifier(t *testing.T) {
Convey("Pushover notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
_, err := NewPushoverNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("from settings", func() {
json := `
{
"apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve",
"userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o",
"priority": "1",
"sound": "pushover"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
not, err := NewPushoverNotifier(model)
pushoverNotifier := not.(*PushoverNotifier)
So(err, ShouldBeNil)
So(pushoverNotifier.Name, ShouldEqual, "Pushover")
So(pushoverNotifier.Type, ShouldEqual, "pushover")
So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
So(pushoverNotifier.Priority, ShouldEqual, 1)
So(pushoverNotifier.Sound, ShouldEqual, "pushover")
})
})
})
}
+2 -1
View File
@@ -116,6 +116,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
body := map[string]interface{}{
"attachments": []map[string]interface{}{
{
"fallback": evalContext.GetNotificationTitle(),
"color": evalContext.GetStateModel().Color,
"title": evalContext.GetNotificationTitle(),
"title_link": ruleUrl,
@@ -123,7 +124,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
"fields": fields,
"image_url": evalContext.ImagePublicUrl,
"footer": "Grafana v" + setting.BuildVersion,
"footer_icon": "http://grafana.org/assets/img/fav32.png",
"footer_icon": "https://grafana.com/assets/img/fav32.png",
"ts": time.Now().Unix(),
},
},
@@ -93,6 +93,9 @@ func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
if err == nil {
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
}
if evalContext.ImagePublicUrl != "" {
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
}
bodyJSON.Set("text", message)
url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
+171
View File
@@ -0,0 +1,171 @@
package notifiers
import (
"fmt"
"net/url"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
var (
threemaGwBaseURL = "https://msgapi.threema.ch/%s"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "threema",
Name: "Threema Gateway",
Description: "Sends notifications to Threema using the Threema Gateway",
Factory: NewThreemaNotifier,
OptionsTemplate: `
<h3 class="page-heading">Threema Gateway settings</h3>
<p>
Notifications can be configured for any Threema Gateway ID of type
"Basic". End-to-End IDs are not currently supported.
</p>
<p>
The Threema Gateway ID can be set up at
<a href="https://gateway.threema.ch/" target="_blank" rel="noopener noreferrer">https://gateway.threema.ch/</a>.
</p>
<div class="gf-form">
<span class="gf-form-label width-14">Gateway ID</span>
<input type="text" required maxlength="8" pattern="\*[0-9A-Z]{7}"
class="gf-form-input max-width-14"
ng-model="ctrl.model.settings.gateway_id"
placeholder="*3MAGWID">
</input>
<info-popover mode="right-normal">
Your 8 character Threema Gateway ID (starting with a *)
</info-popover>
</div>
<div class="gf-form">
<span class="gf-form-label width-14">Recipient ID</span>
<input type="text" required maxlength="8" pattern="[0-9A-Z]{8}"
class="gf-form-input max-width-14"
ng-model="ctrl.model.settings.recipient_id"
placeholder="YOUR3MID">
</input>
<info-popover mode="right-normal">
The 8 character Threema ID that should receive the alerts
</info-popover>
</div>
<div class="gf-form">
<span class="gf-form-label width-14">API Secret</span>
<input type="text" required
class="gf-form-input max-width-24"
ng-model="ctrl.model.settings.api_secret">
</input>
<info-popover mode="right-normal">
Your Threema Gateway API secret
</info-popover>
</div>
`,
})
}
type ThreemaNotifier struct {
NotifierBase
GatewayID string
RecipientID string
APISecret string
log log.Logger
}
func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
}
gatewayID := model.Settings.Get("gateway_id").MustString()
recipientID := model.Settings.Get("recipient_id").MustString()
apiSecret := model.Settings.Get("api_secret").MustString()
// Validation
if gatewayID == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"}
}
if !strings.HasPrefix(gatewayID, "*") {
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"}
}
if len(gatewayID) != 8 {
return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"}
}
if recipientID == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"}
}
if len(recipientID) != 8 {
return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"}
}
if apiSecret == "" {
return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"}
}
return &ThreemaNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
GatewayID: gatewayID,
RecipientID: recipientID,
APISecret: apiSecret,
log: log.New("alerting.notifier.threema"),
}, nil
}
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
metrics.M_Alerting_Notification_Sent_Threema.Inc(1)
// Set up basic API request data
data := url.Values{}
data.Set("from", notifier.GatewayID)
data.Set("to", notifier.RecipientID)
data.Set("secret", notifier.APISecret)
// Determine emoji
stateEmoji := ""
switch evalContext.Rule.State {
case m.AlertStateOK:
stateEmoji = "\u2705 " // White Heavy Check Mark
case m.AlertStateNoData:
stateEmoji = "\u2753 " // Black Question Mark Ornament
case m.AlertStateAlerting:
stateEmoji = "\u26A0 " // Warning sign
}
// Build message
message := fmt.Sprintf("%s%s\n\n*State:* %s\n*Message:* %s\n",
stateEmoji, evalContext.GetNotificationTitle(),
evalContext.Rule.Name, evalContext.Rule.Message)
ruleURL, err := evalContext.GetRuleUrl()
if err == nil {
message = message + fmt.Sprintf("*URL:* %s\n", ruleURL)
}
if evalContext.ImagePublicUrl != "" {
message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl)
}
data.Set("text", message)
// Prepare and send request
url := fmt.Sprintf(threemaGwBaseURL, "send_simple")
body := data.Encode()
headers := map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
}
cmd := &m.SendWebhookSync{
Url: url,
Body: body,
HttpMethod: "POST",
HttpHeader: headers,
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name)
return err
}
return nil
}
@@ -0,0 +1,119 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestThreemaNotifier(t *testing.T) {
Convey("Threema notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
_, err := NewThreemaNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("valid settings should be parsed successfully", func() {
json := `
{
"gateway_id": "*3MAGWID",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model)
So(err, ShouldBeNil)
threemaNotifier := not.(*ThreemaNotifier)
So(err, ShouldBeNil)
So(threemaNotifier.Name, ShouldEqual, "threema_testing")
So(threemaNotifier.Type, ShouldEqual, "threema")
So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID")
So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO")
So(threemaNotifier.APISecret, ShouldEqual, "1234")
})
Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() {
json := `
{
"gateway_id": "ECHOECHO",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model)
So(not, ShouldBeNil)
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *")
})
Convey("invalid Threema Gateway IDs should be rejected (length)", func() {
json := `
{
"gateway_id": "*ECHOECHO",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model)
So(not, ShouldBeNil)
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long")
})
Convey("invalid Threema Recipient IDs should be rejected (length)", func() {
json := `
{
"gateway_id": "*3MAGWID",
"recipient_id": "ECHOECH",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model)
So(not, ShouldBeNil)
So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long")
})
})
})
}
+9 -3
View File
@@ -16,14 +16,14 @@ type ResultHandler interface {
}
type DefaultResultHandler struct {
notifier Notifier
notifier NotificationService
log log.Logger
}
func NewResultHandler() *DefaultResultHandler {
return &DefaultResultHandler{
log: log.New("alerting.resultHandler"),
notifier: NewRootNotifier(),
notifier: NewNotificationService(),
}
}
@@ -61,6 +61,12 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
handler.log.Error("Cannot change state on alert thats pause", "error", err)
return err
}
if err == m.ErrRequiresNewState {
handler.log.Info("Alert already updated")
return nil
}
handler.log.Error("Failed to save state", "error", err)
}
@@ -85,7 +91,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
}
if evalContext.ShouldSendNotification() {
handler.notifier.Notify(evalContext)
handler.notifier.Send(evalContext)
}
}
+7 -5
View File
@@ -23,7 +23,7 @@ func init() {
}
func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
notifier := NewRootNotifier()
notifier := newNotificationService()
model := &m.AlertNotification{
Name: cmd.Name,
@@ -38,10 +38,10 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
return err
}
return notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
return notifier.sendNotifications(createTestEvalContext(cmd), []Notifier{notifiers})
}
func createTestEvalContext() *EvalContext {
func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
testRule := &Rule{
DashboardId: 1,
PanelId: 1,
@@ -50,8 +50,10 @@ func createTestEvalContext() *EvalContext {
State: m.AlertStateAlerting,
}
ctx := NewEvalContext(context.TODO(), testRule)
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
ctx := NewEvalContext(context.Background(), testRule)
if cmd.Settings.Get("uploadImage").MustBool(true) {
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
}
ctx.IsTestRun = true
ctx.Firing = true
ctx.Error = nil
+1 -1
View File
@@ -49,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
func testAlertRule(rule *Rule) *EvalContext {
handler := NewEvalHandler()
context := NewEvalContext(context.TODO(), rule)
context := NewEvalContext(context.Background(), rule)
context.IsTestRun = true
handler.Eval(context)
+3 -3
View File
@@ -11,14 +11,14 @@ type Repository interface {
type ItemQuery struct {
OrgId int64 `json:"orgId"`
From int64 `json:"from"`
To int64 `json:"from"`
To int64 `json:"to"`
Type ItemType `json:"type"`
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
NewState []string `json:"newState"`
Limit int64 `json:"alertId"`
Limit int64 `json:"limit"`
}
type DeleteParams struct {
@@ -48,7 +48,7 @@ type Item struct {
OrgId int64 `json:"orgId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
CategoryId int64 `json:"panelId"`
CategoryId int64 `json:"categoryId"`
Type ItemType `json:"type"`
Title string `json:"title"`
Text string `json:"text"`
+1 -1
View File
@@ -150,7 +150,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
return &Message{
To: cmd.To,
From: setting.Smtp.FromAddress,
From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress),
Subject: subject,
Body: buffer.String(),
EmbededFiles: cmd.EmbededFiles,
+1 -1
View File
@@ -48,7 +48,7 @@ func Init() error {
}
if !util.IsEmail(setting.Smtp.FromAddress) {
return errors.New("Invalid email address for smpt from_adress config")
return errors.New("Invalid email address for SMTP from_address config")
}
if setting.EmailCodeValidMinutes == 0 {
@@ -23,6 +23,7 @@ func TestNotifications(t *testing.T) {
setting.Smtp.Enabled = true
setting.Smtp.TemplatesPattern = "emails/*.html"
setting.Smtp.FromAddress = "from@address.com"
setting.Smtp.FromName = "Grafana Admin"
err := Init()
So(err, ShouldBeNil)
@@ -18,12 +18,15 @@ func TestEmailIntegrationTest(t *testing.T) {
setting.Smtp.Enabled = true
setting.Smtp.TemplatesPattern = "emails/*.html"
setting.Smtp.FromAddress = "from@address.com"
setting.Smtp.FromName = "Grafana Admin"
setting.BuildVersion = "4.0.0"
err := Init()
So(err, ShouldBeNil)
addToMailQueue = func(msg *Message) {
So(msg.From, ShouldEqual, "Grafana Admin <from@address.com>")
So(msg.To[0], ShouldEqual, "asdf@asdf.com")
ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777)
}
@@ -40,6 +43,7 @@ func TestEmailIntegrationTest(t *testing.T) {
"RuleUrl": "http://localhost:3000/dashboard/db/graphite-dashboard",
"ImageLink": "http://localhost:3000/render/dashboard-solo/db/graphite-dashboard?panelId=1&from=1471008499616&to=1471012099617&width=1000&height=500",
"AlertPageUrl": "http://localhost:3000/alerting",
"EmbededImage": "test.png",
"EvalMatches": []map[string]string{
{
"Metric": "desktop",
@@ -51,7 +55,7 @@ func TestEmailIntegrationTest(t *testing.T) {
},
},
},
To: []string{"asdf@asdf.com "},
To: []string{"asdf@asdf.com"},
Template: "alert_notification.html",
}
+16 -2
View File
@@ -5,7 +5,9 @@ import (
"context"
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
"golang.org/x/net/context/ctxhttp"
@@ -22,6 +24,18 @@ type Webhook struct {
HttpHeader map[string]string
}
var netTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
Timeout: time.Second * 30,
Transport: netTransport,
}
var (
webhookQueue chan *Webhook
webhookLog log.Logger
@@ -37,7 +51,7 @@ func processWebhookQueue() {
for {
select {
case webhook := <-webhookQueue:
err := sendWebRequestSync(context.TODO(), webhook)
err := sendWebRequestSync(context.Background(), webhook)
if err != nil {
webhookLog.Error("Failed to send webrequest ", "error", err)
@@ -68,7 +82,7 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
request.Header.Set(k, v)
}
resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
resp, err := ctxhttp.Do(ctx, netClient, request)
if err != nil {
return err
}
+4
View File
@@ -240,6 +240,10 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
return m.ErrCannotChangeStateOnPausedAlert
}
if alert.State == cmd.State {
return m.ErrRequiresNewState
}
alert.State = cmd.State
alert.StateChanges += 1
alert.NewStateDate = time.Now()
+11 -2
View File
@@ -13,7 +13,8 @@ import (
func init() {
bus.AddHandler("sql", GetDataSources)
bus.AddHandler("sql", AddDataSource)
bus.AddHandler("sql", DeleteDataSource)
bus.AddHandler("sql", DeleteDataSourceById)
bus.AddHandler("sql", DeleteDataSourceByName)
bus.AddHandler("sql", UpdateDataSource)
bus.AddHandler("sql", GetDataSourceById)
bus.AddHandler("sql", GetDataSourceByName)
@@ -50,7 +51,7 @@ func GetDataSources(query *m.GetDataSourcesQuery) error {
return sess.Find(&query.Result)
}
func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
func DeleteDataSourceById(cmd *m.DeleteDataSourceByIdCommand) error {
return inTransaction(func(sess *xorm.Session) error {
var rawSql = "DELETE FROM data_source WHERE id=? and org_id=?"
_, err := sess.Exec(rawSql, cmd.Id, cmd.OrgId)
@@ -58,6 +59,14 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
})
}
func DeleteDataSourceByName(cmd *m.DeleteDataSourceByNameCommand) error {
return inTransaction(func(sess *xorm.Session) error {
var rawSql = "DELETE FROM data_source WHERE name=? and org_id=?"
_, err := sess.Exec(rawSql, cmd.Name, cmd.OrgId)
return err
})
}
func AddDataSource(cmd *m.AddDataSourceCommand) error {
return inTransaction(func(sess *xorm.Session) error {
+11 -3
View File
@@ -79,8 +79,16 @@ func TestDataAccess(t *testing.T) {
ds := query.Result[0]
Convey("Can delete datasource", func() {
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: ds.OrgId})
Convey("Can delete datasource by id", func() {
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: ds.OrgId})
So(err, ShouldBeNil)
GetDataSources(&query)
So(len(query.Result), ShouldEqual, 0)
})
Convey("Can delete datasource by name", func() {
err := DeleteDataSourceByName(&m.DeleteDataSourceByNameCommand{Name: ds.Name, OrgId: ds.OrgId})
So(err, ShouldBeNil)
GetDataSources(&query)
@@ -88,7 +96,7 @@ func TestDataAccess(t *testing.T) {
})
Convey("Can not delete datasource with wrong orgId", func() {
err := DeleteDataSource(&m.DeleteDataSourceCommand{Id: ds.Id, OrgId: 123123})
err := DeleteDataSourceById(&m.DeleteDataSourceByIdCommand{Id: ds.Id, OrgId: 123123})
So(err, ShouldBeNil)
GetDataSources(&query)
+123
View File
@@ -0,0 +1,123 @@
package sqlstore
import (
"fmt"
glog "github.com/grafana/grafana/pkg/log"
"github.com/go-xorm/core"
)
type XormLogger struct {
grafanaLog glog.Logger
level glog.Lvl
showSQL bool
}
func NewXormLogger(level glog.Lvl, grafanaLog glog.Logger) *XormLogger {
return &XormLogger{
grafanaLog: grafanaLog,
level: level,
showSQL: true,
}
}
// Error implement core.ILogger
func (s *XormLogger) Err(v ...interface{}) error {
if s.level <= glog.LvlError {
s.grafanaLog.Error(fmt.Sprint(v...))
}
return nil
}
// Errorf implement core.ILogger
func (s *XormLogger) Errf(format string, v ...interface{}) error {
if s.level <= glog.LvlError {
s.grafanaLog.Error(fmt.Sprintf(format, v...))
}
return nil
}
// Debug implement core.ILogger
func (s *XormLogger) Debug(v ...interface{}) error {
if s.level <= glog.LvlDebug {
s.grafanaLog.Debug(fmt.Sprint(v...))
}
return nil
}
// Debugf implement core.ILogger
func (s *XormLogger) Debugf(format string, v ...interface{}) error {
if s.level <= glog.LvlDebug {
s.grafanaLog.Debug(fmt.Sprintf(format, v...))
}
return nil
}
// Info implement core.ILogger
func (s *XormLogger) Info(v ...interface{}) error {
if s.level <= glog.LvlInfo {
s.grafanaLog.Info(fmt.Sprint(v...))
}
return nil
}
// Infof implement core.ILogger
func (s *XormLogger) Infof(format string, v ...interface{}) error {
if s.level <= glog.LvlInfo {
s.grafanaLog.Info(fmt.Sprintf(format, v...))
}
return nil
}
// Warn implement core.ILogger
func (s *XormLogger) Warning(v ...interface{}) error {
if s.level <= glog.LvlWarn {
s.grafanaLog.Warn(fmt.Sprint(v...))
}
return nil
}
// Warnf implement core.ILogger
func (s *XormLogger) Warningf(format string, v ...interface{}) error {
if s.level <= glog.LvlWarn {
s.grafanaLog.Warn(fmt.Sprintf(format, v...))
}
return nil
}
// Level implement core.ILogger
func (s *XormLogger) Level() core.LogLevel {
switch s.level {
case glog.LvlError:
return core.LOG_ERR
case glog.LvlWarn:
return core.LOG_WARNING
case glog.LvlInfo:
return core.LOG_INFO
case glog.LvlDebug:
return core.LOG_DEBUG
default:
return core.LOG_ERR
}
}
// SetLevel implement core.ILogger
func (s *XormLogger) SetLevel(l core.LogLevel) error {
return nil
}
// ShowSQL implement core.ILogger
func (s *XormLogger) ShowSQL(show ...bool) {
s.grafanaLog.Error("ShowSQL", "show", "show")
if len(show) == 0 {
s.showSQL = true
return
}
s.showSQL = show[0]
}
// IsShowSQL implement core.ILogger
func (s *XormLogger) IsShowSQL() bool {
return s.showSQL
}
+16 -1
View File
@@ -50,7 +50,7 @@ func addAlertMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "settings", Type: DB_Text, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
@@ -67,4 +67,19 @@ func addAlertMigrations(mg *Migrator) {
}))
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "message", Type: DB_Text, Nullable: false},
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "settings", Type: DB_Text, Nullable: false},
{Name: "severity", Type: DB_Text, Nullable: false},
{Name: "execution_error", Type: DB_Text, Nullable: false},
{Name: "eval_data", Type: DB_Text, Nullable: true},
}))
mg.AddMigration("Update alert_notification table charset", NewTableCharsetMigration("alert_notification", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "settings", Type: DB_Text, Nullable: false},
}))
}
@@ -44,4 +44,14 @@ func addAnnotationMig(mg *Migrator) {
mg.AddMigration("add index annotation 2 v3", NewAddIndexMigration(table, table.Indices[2]))
mg.AddMigration("add index annotation 3 v3", NewAddIndexMigration(table, table.Indices[3]))
mg.AddMigration("add index annotation 4 v3", NewAddIndexMigration(table, table.Indices[4]))
mg.AddMigration("Update annotation table charset", NewTableCharsetMigration("annotation", []*Column{
{Name: "type", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "title", Type: DB_Text, Nullable: false},
{Name: "text", Type: DB_Text, Nullable: false},
{Name: "metric", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "data", Type: DB_Text, Nullable: false},
}))
}
@@ -8,7 +8,7 @@ func addApiKeyMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "account_id", Type: DB_BigInt, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false},
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
@@ -41,8 +41,8 @@ func addApiKeyMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_Varchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "key", Type: DB_Varchar, Length: 190, Nullable: false},
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
@@ -72,4 +72,10 @@ func addApiKeyMigrations(mg *Migrator) {
}))
mg.AddMigration("Drop old table api_key_v1", NewDropTableMigration("api_key_v1"))
mg.AddMigration("Update api_key table charset", NewTableCharsetMigration("api_key", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "key", Type: DB_Varchar, Length: 190, Nullable: false},
{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
}))
}
@@ -8,7 +8,7 @@ func addDashboardMigration(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "data", Type: DB_Text, Nullable: false},
{Name: "account_id", Type: DB_BigInt, Nullable: false},
@@ -56,7 +56,7 @@ func addDashboardMigration(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "slug", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "data", Type: DB_Text, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
@@ -125,4 +125,15 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
Cols: []string{"dashboard_id"}, Type: IndexType,
}))
mg.AddMigration("Update dashboard table charset", NewTableCharsetMigration("dashboard", []*Column{
{Name: "slug", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "title", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "plugin_id", Type: DB_NVarchar, Nullable: true, Length: 255},
{Name: "data", Type: DB_MediumText, Nullable: false},
}))
mg.AddMigration("Update dashboard_tag table charset", NewTableCharsetMigration("dashboard_tag", []*Column{
{Name: "term", Type: DB_NVarchar, Length: 50, Nullable: false},
}))
}
@@ -8,7 +8,7 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "dashboard", Type: DB_Text, Nullable: false},
{Name: "expires", Type: DB_DateTime, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
@@ -28,8 +28,8 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "delete_key", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "delete_key", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "user_id", Type: DB_BigInt, Nullable: false},
{Name: "external", Type: DB_Bool, Nullable: false},
@@ -54,4 +54,12 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
Sqlite("SELECT 0 WHERE 0;").
Postgres("SELECT 0;").
Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
mg.AddMigration("Update dashboard_snapshot table charset", NewTableCharsetMigration("dashboard_snapshot", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "key", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "delete_key", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "dashboard", Type: DB_MediumText, Nullable: false},
}))
}
@@ -10,7 +10,7 @@ func addDataSourceMigration(mg *Migrator) {
{Name: "account_id", Type: DB_BigInt, Nullable: false},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
@@ -49,7 +49,7 @@ func addDataSourceMigration(mg *Migrator) {
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
@@ -106,4 +106,18 @@ func addDataSourceMigration(mg *Migrator) {
mg.AddMigration("Add secure json data column", NewAddColumnMigration(tableV2, &Column{
Name: "secure_json_data", Type: DB_Text, Nullable: true,
}))
mg.AddMigration("Update data_source table charset", NewTableCharsetMigration(tableV2.Name, []*Column{
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "access", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "url", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "user", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "database", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "basic_auth_user", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "basic_auth_password", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "json_data", Type: DB_Text, Nullable: true},
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
}))
}
@@ -5,7 +5,7 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
// --- Migration Guide line ---
// 1. Never change a migration that is committed and pushed to master
// 2. Always add new migrations (to change or undo previous migrations)
// 3. Some migraitons are not yet written (rename column, table, drop table, index etc)
// 3. Some migrations are not yet written (rename column, table, drop table, index etc)
func AddMigrations(mg *Migrator) {
addMigrationLogMigrations(mg)
+16 -1
View File
@@ -8,7 +8,7 @@ func addOrgMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "address2", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "city", Type: DB_NVarchar, Length: 255, Nullable: true},
@@ -68,4 +68,19 @@ func addOrgMigrations(mg *Migrator) {
mg.AddMigration("Drop old table account", NewDropTableMigration("account"))
mg.AddMigration("Drop old table account_user", NewDropTableMigration("account_user"))
mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "address2", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "city", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "zip_code", Type: DB_NVarchar, Length: 50, Nullable: true},
{Name: "country", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "billing_email", Type: DB_NVarchar, Length: 255, Nullable: true},
}))
mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
{Name: "role", Type: DB_NVarchar, Length: 20},
}))
}
@@ -32,4 +32,15 @@ func addPlaylistMigrations(mg *Migrator) {
}
mg.AddMigration("create playlist item table v2", NewAddTableMigration(playlistItemV2))
mg.AddMigration("Update playlist table charset", NewTableCharsetMigration("playlist", []*Column{
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "interval", Type: DB_NVarchar, Length: 255, Nullable: false},
}))
mg.AddMigration("Update playlist_item table charset", NewTableCharsetMigration("playlist_item", []*Column{
{Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "value", Type: DB_Text, Nullable: false},
{Name: "title", Type: DB_Text, Nullable: false},
}))
}
@@ -9,7 +9,7 @@ func addAppSettingsMigration(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt, Nullable: true},
{Name: "plugin_id", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "plugin_id", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "enabled", Type: DB_Bool, Nullable: false},
{Name: "pinned", Type: DB_Bool, Nullable: false},
{Name: "json_data", Type: DB_Text, Nullable: true},
@@ -32,4 +32,10 @@ func addAppSettingsMigration(mg *Migrator) {
Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50,
}))
mg.AddMigration("Update plugin_setting table charset", NewTableCharsetMigration("plugin_setting", []*Column{
{Name: "plugin_id", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "json_data", Type: DB_Text, Nullable: true},
{Name: "secure_json_data", Type: DB_Text, Nullable: true},
{Name: "plugin_version", Type: DB_NVarchar, Nullable: true, Length: 50},
}))
}
@@ -29,4 +29,9 @@ func addPreferencesMigrations(mg *Migrator) {
// create table
mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2))
mg.AddMigration("Update preferences table charset", NewTableCharsetMigration("preferences", []*Column{
{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
}))
}
@@ -12,7 +12,7 @@ func addQuotaMigration(mg *Migrator) {
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "org_id", Type: DB_BigInt, Nullable: true},
{Name: "user_id", Type: DB_BigInt, Nullable: true},
{Name: "target", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "target", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "limit", Type: DB_BigInt, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
@@ -25,4 +25,8 @@ func addQuotaMigration(mg *Migrator) {
//------- indexes ------------------
addTableIndicesMigrations(mg, "v1", quotaV1)
mg.AddMigration("Update quota table charset", NewTableCharsetMigration("quota", []*Column{
{Name: "target", Type: DB_NVarchar, Length: 190, Nullable: false},
}))
}
@@ -35,4 +35,13 @@ func addTempUserMigrations(mg *Migrator) {
// create table
mg.AddMigration("create temp user table v1-7", NewAddTableMigration(tempUserV1))
addTableIndicesMigrations(mg, "v1-7", tempUserV1)
mg.AddMigration("Update temp_user table charset", NewTableCharsetMigration("temp_user", []*Column{
{Name: "email", Type: DB_NVarchar, Length: 255},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "role", Type: DB_NVarchar, Length: 20, Nullable: true},
{Name: "code", Type: DB_NVarchar, Length: 255},
{Name: "status", Type: DB_Varchar, Length: 20},
{Name: "remote_addr", Type: DB_Varchar, Length: 255, Nullable: true},
}))
}
+15 -4
View File
@@ -8,8 +8,8 @@ func addUserMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
@@ -47,8 +47,8 @@ func addUserMigrations(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "version", Type: DB_Int, Nullable: false},
{Name: "login", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "email", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
@@ -92,4 +92,15 @@ func addUserMigrations(mg *Migrator) {
mg.AddMigration("Add column help_flags1 to user table", NewAddColumnMigration(userV2, &Column{
Name: "help_flags1", Type: DB_BigInt, Nullable: false, Default: "0",
}))
mg.AddMigration("Update user table charset", NewTableCharsetMigration("user", []*Column{
{Name: "login", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "email", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "password", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "salt", Type: DB_NVarchar, Length: 50, Nullable: true},
{Name: "rands", Type: DB_NVarchar, Length: 50, Nullable: true},
{Name: "company", Type: DB_NVarchar, Length: 255, Nullable: true},
{Name: "theme", Type: DB_NVarchar, Length: 255, Nullable: true},
}))
}
+6 -1
View File
@@ -29,6 +29,7 @@ type Dialect interface {
TableCheckSql(tableName string) (string, []interface{})
RenameTable(oldName string, newName string) string
UpdateTableSql(tableName string, columns []*Column) string
}
func NewDialect(name string) Dialect {
@@ -102,7 +103,7 @@ func (b *BaseDialect) CreateTableSql(table *Table) string {
sql = sql[:len(sql)-2] + ")"
if b.dialect.SupportEngine() {
sql += " ENGINE=InnoDB DEFAULT CHARSET UTF8 "
sql += " ENGINE=InnoDB DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci"
}
sql += ";"
@@ -160,3 +161,7 @@ func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
name = index.XName(tableName)
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
}
func (db *BaseDialect) UpdateTableSql(tableName string, columns []*Column) string {
return "-- NOT REQUIRED"
}
@@ -200,3 +200,17 @@ func (m *CopyTableDataMigration) IfTableExists(tableName string) *CopyTableDataM
func (m *CopyTableDataMigration) Sql(d Dialect) string {
return d.CopyTableData(m.sourceTable, m.targetTable, m.sourceCols, m.targetCols)
}
type TableCharsetMigration struct {
MigrationBase
tableName string
columns []*Column
}
func NewTableCharsetMigration(tableName string, columns []*Column) *TableCharsetMigration {
return &TableCharsetMigration{tableName: tableName, columns: columns}
}
func (m *TableCharsetMigration) Sql(d Dialect) string {
return d.UpdateTableSql(m.tableName, m.columns)
}
@@ -1,6 +1,9 @@
package migrator
import "strconv"
import (
"strconv"
"strings"
)
type Mysql struct {
BaseDialect
@@ -76,6 +79,12 @@ func (db *Mysql) SqlType(c *Column) string {
} else if hasLen1 {
res += "(" + strconv.Itoa(c.Length) + ")"
}
switch c.Type {
case DB_Char, DB_Varchar, DB_NVarchar, DB_TinyText, DB_Text, DB_MediumText, DB_LongText:
res += " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
}
return res
}
@@ -84,3 +93,15 @@ func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
return sql, args
}
func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
var statements = []string{}
statements = append(statements, "DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
for _, col := range columns {
statements = append(statements, "MODIFY "+col.StringNoPk(db))
}
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
}
@@ -3,6 +3,7 @@ package migrator
import (
"fmt"
"strconv"
"strings"
)
type Postgres struct {
@@ -112,3 +113,13 @@ func (db *Postgres) DropIndexSql(tableName string, index *Index) string {
idxName := index.XName(tableName)
return fmt.Sprintf("DROP INDEX %v", quote(idxName))
}
func (db *Postgres) UpdateTableSql(tableName string, columns []*Column) string {
var statements = []string{}
for _, col := range columns {
statements = append(statements, "ALTER "+db.QuoteStr()+col.Name+db.QuoteStr()+" TYPE "+db.SqlType(col))
}
return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
}
+2 -2
View File
@@ -63,8 +63,8 @@ func TestAccountDataAccess(t *testing.T) {
err := SearchUsers(&query)
So(err, ShouldBeNil)
So(query.Result[0].Email, ShouldEqual, "ac1@test.com")
So(query.Result[1].Email, ShouldEqual, "ac2@test.com")
So(query.Result.Users[0].Email, ShouldEqual, "ac1@test.com")
So(query.Result.Users[1].Email, ShouldEqual, "ac2@test.com")
})
Convey("Given an added org user", func() {
+16 -2
View File
@@ -29,6 +29,8 @@ type DatabaseConfig struct {
ClientKeyPath string
ClientCertPath string
ServerCertName string
MaxOpenConn int
MaxIdleConn int
}
var (
@@ -112,7 +114,7 @@ func getEngine() (*xorm.Engine, error) {
protocol = "unix"
}
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8",
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4",
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
@@ -150,7 +152,17 @@ func getEngine() (*xorm.Engine, error) {
}
sqlog.Info("Initializing DB", "dbtype", DbCfg.Type)
return xorm.NewEngine(DbCfg.Type, cnnstr)
engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
if err != nil {
return nil, err
} else {
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
// engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
// engine.ShowSQL = true
// engine.ShowInfo = true
}
return engine, nil
}
func LoadConfig() {
@@ -177,6 +189,8 @@ func LoadConfig() {
DbCfg.Host = sec.Key("host").String()
DbCfg.Name = sec.Key("name").String()
DbCfg.User = sec.Key("user").String()
DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
if len(DbCfg.Pwd) == 0 {
DbCfg.Pwd = sec.Key("password").String()
}
+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=utf8"}
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8mb4"}
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
func CleanDB(x *xorm.Engine) {
+38 -4
View File
@@ -6,6 +6,8 @@ import (
"github.com/go-xorm/xorm"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/events"
m "github.com/grafana/grafana/pkg/models"
@@ -260,6 +262,20 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
}
func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
getOrgsForUserCmd := &m.GetUserOrgListQuery{UserId: cmd.UserId}
GetUserOrgList(getOrgsForUserCmd)
valid := false
for _, other := range getOrgsForUserCmd.Result {
if other.OrgId == cmd.OrgId {
valid = true
}
}
if !valid {
return fmt.Errorf("user does not belong ot org")
}
return inTransaction(func(sess *xorm.Session) error {
user := m.User{}
sess.Id(cmd.UserId).Get(&user)
@@ -344,12 +360,30 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
}
func SearchUsers(query *m.SearchUsersQuery) error {
query.Result = make([]*m.UserSearchHitDTO, 0)
query.Result = m.SearchUserQueryResult{
Users: make([]*m.UserSearchHitDTO, 0),
}
queryWithWildcards := "%" + query.Query + "%"
sess := x.Table("user")
sess.Where("email LIKE ?", query.Query+"%")
sess.Limit(query.Limit, query.Limit*query.Page)
if query.Query != "" {
sess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
sess.Cols("id", "email", "name", "login", "is_admin")
err := sess.Find(&query.Result)
if err := sess.Find(&query.Result.Users); err != nil {
return err
}
user := m.User{}
countSess := x.Table("user")
if query.Query != "" {
countSess.Where("email LIKE ? OR name LIKE ? OR login like ?", queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
count, err := countSess.Count(&user)
query.Result.TotalCount = count
return err
}
+93
View File
@@ -0,0 +1,93 @@
package sqlstore
import (
"fmt"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models"
)
func TestUserDataAccess(t *testing.T) {
Convey("Testing DB", t, func() {
InitTestDB(t)
var err error
for i := 0; i < 5; i++ {
err = CreateUser(&models.CreateUserCommand{
Email: fmt.Sprint("user", i, "@test.com"),
Name: fmt.Sprint("user", i),
Login: fmt.Sprint("loginuser", i),
})
So(err, ShouldBeNil)
}
Convey("Can return the first page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
})
Convey("Can return the second page of users and a total count", func() {
query := models.SearchUsersQuery{Query: "", Page: 2, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 2)
So(query.Result.TotalCount, ShouldEqual, 5)
})
Convey("Can return list of users matching query on user name", func() {
query := models.SearchUsersQuery{Query: "use", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 3)
So(query.Result.TotalCount, ShouldEqual, 5)
query = models.SearchUsersQuery{Query: "ser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "USER1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
query = models.SearchUsersQuery{Query: "idontexist", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 0)
So(query.Result.TotalCount, ShouldEqual, 0)
})
Convey("Can return list of users matching query on email", func() {
query := models.SearchUsersQuery{Query: "ser1@test.com", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
Convey("Can return list of users matching query on login name", func() {
query := models.SearchUsersQuery{Query: "loginuser1", Page: 1, Limit: 3}
err = SearchUsers(&query)
So(err, ShouldBeNil)
So(len(query.Result.Users), ShouldEqual, 1)
So(query.Result.TotalCount, ShouldEqual, 1)
})
})
}
+3 -17
View File
@@ -96,6 +96,7 @@ var (
LoginHint string
DefaultTheme string
DisableLoginForm bool
DisableSignoutMenu bool
// Http auth
AdminUser string
@@ -455,22 +456,6 @@ func validateStaticRootPath() error {
return fmt.Errorf("Failed to detect generated css or javascript files in static root (%s), have you executed default grunt task?", StaticRootPath)
}
// func readInstanceName() string {
// hostname, _ := os.Hostname()
// if hostname == "" {
// hostname = "hostname_unknown"
// }
//
// instanceName := Cfg.Section("").Key("instance_name").MustString("")
// if instanceName = "" {
// // set value as it might be used in other places
// Cfg.Section("").Key("instance_name").SetValue(hostname)
// instanceName = hostname
// }
//
// return
// }
func NewConfigContext(args *CommandLineArgs) error {
setHomePath(args)
loadConfiguration(args)
@@ -544,6 +529,7 @@ func NewConfigContext(args *CommandLineArgs) error {
// auth
auth := Cfg.Section("auth")
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
// anonymous access
AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false)
@@ -590,7 +576,7 @@ func NewConfigContext(args *CommandLineArgs) error {
log.Warn("require_email_validation is enabled but smpt is disabled")
}
GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.net")
GrafanaNetUrl = Cfg.Section("grafana_net").Key("url").MustString("https://grafana.com")
imageUploadingSection := Cfg.Section("external_image_storage")
ImageUploadProvider = imageUploadingSection.Key("provider").MustString("internal")
+2
View File
@@ -8,6 +8,7 @@ type SmtpSettings struct {
CertFile string
KeyFile string
FromAddress string
FromName string
SkipVerify bool
SendWelcomeEmailOnSignUp bool
@@ -23,6 +24,7 @@ func readSmtpSettings() {
Smtp.CertFile = sec.Key("cert_file").String()
Smtp.KeyFile = sec.Key("key_file").String()
Smtp.FromAddress = sec.Key("from_address").String()
Smtp.FromName = sec.Key("from_name").String()
Smtp.SkipVerify = sec.Key("skip_verify").MustBool(false)
emails := Cfg.Section("emails")
+2 -6
View File
@@ -2,7 +2,6 @@ package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -21,11 +20,8 @@ type SocialGithub struct {
}
var (
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
)
var (
ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
ErrMissingTeamMembership = &Error{"User not a member of one of the required teams"}
ErrMissingOrganizationMembership = &Error{"User not a member of one of the required organizations"}
)
func (s *SocialGithub) Type() int {
+8
View File
@@ -29,6 +29,14 @@ type SocialConnector interface {
Client(ctx context.Context, t *oauth2.Token) *http.Client
}
type Error struct {
s string
}
func (e *Error) Error() string {
return e.s
}
var (
SocialBaseUrl = "/login/"
SocialMap = make(map[string]SocialConnector)
+3 -2
View File
@@ -51,8 +51,9 @@ type QueryResult struct {
}
type TimeSeries struct {
Name string `json:"name"`
Points TimeSeriesPoints `json:"points"`
Name string `json:"name"`
Points TimeSeriesPoints `json:"points"`
Tags map[string]string `json:"tags"`
}
type TimePoint [2]null.Float
+1 -1
View File
@@ -86,7 +86,7 @@ func (e *apiClient) spawnWorker(ctx context.Context, id int, jobs chan QueryToSe
return
}
series, err := e.responseParser.Parse(resp, query.QueryRef)
series, err := e.responseParser.Parse(resp, query)
if err != nil {
errors <- err
return
+1
View File
@@ -40,6 +40,7 @@ func init() {
type QueryToSend struct {
RawQuery string
Metric Metric
QueryRef *Query
}
+88 -13
View File
@@ -4,9 +4,13 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"strings"
"fmt"
"regexp"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
@@ -18,6 +22,16 @@ func NewResponseParser() *ResponseParser {
}
}
var (
indexAliasPattern *regexp.Regexp
wildcardAliasPattern *regexp.Regexp
)
func init() {
indexAliasPattern = regexp.MustCompile(`\$(\d)`)
wildcardAliasPattern = regexp.MustCompile(`[*!]`)
}
type MQEResponse struct {
Success bool `json:"success"`
Name string `json:"name"`
@@ -47,7 +61,7 @@ type ResponseParser struct {
log log.Logger
}
func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsdb.TimeSeries, error) {
func (parser *ResponseParser) Parse(res *http.Response, queryRef QueryToSend) ([]*tsdb.TimeSeries, error) {
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
@@ -73,22 +87,14 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd
var series []*tsdb.TimeSeries
for _, body := range data.Body {
for _, mqeSerie := range body.Series {
namePrefix := ""
//append predefined tags to seriename
for key, value := range mqeSerie.Tagset {
if key == "cluster" && queryRef.AddClusterToAlias {
namePrefix += value + " "
}
serie := &tsdb.TimeSeries{
Tags: map[string]string{},
Name: parser.formatLegend(body, mqeSerie, queryRef),
}
for key, value := range mqeSerie.Tagset {
if key == "host" && queryRef.AddHostToAlias {
namePrefix += value + " "
}
serie.Tags[key] = value
}
serie := &tsdb.TimeSeries{Name: namePrefix + body.Name}
for i, value := range mqeSerie.Values {
timestamp := body.TimeRange.Start + int64(i)*body.TimeRange.Resolution
serie.Points = append(serie.Points, tsdb.NewTimePoint(value, float64(timestamp)))
@@ -100,3 +106,72 @@ func (parser *ResponseParser) Parse(res *http.Response, queryRef *Query) ([]*tsd
return series, nil
}
func (parser *ResponseParser) formatLegend(body MQEResponseSerie, mqeSerie MQESerie, queryToSend QueryToSend) string {
namePrefix := ""
//append predefined tags to seriename
for key, value := range mqeSerie.Tagset {
if key == "cluster" && queryToSend.QueryRef.AddClusterToAlias {
namePrefix += value + " "
}
}
for key, value := range mqeSerie.Tagset {
if key == "host" && queryToSend.QueryRef.AddHostToAlias {
namePrefix += value + " "
}
}
return namePrefix + parser.formatName(body, queryToSend)
}
func (parser *ResponseParser) formatName(body MQEResponseSerie, queryToSend QueryToSend) string {
if indexAliasPattern.MatchString(queryToSend.Metric.Alias) {
return parser.indexAlias(body, queryToSend)
}
if wildcardAliasPattern.MatchString(queryToSend.Metric.Metric) && wildcardAliasPattern.MatchString(queryToSend.Metric.Alias) {
return parser.wildcardAlias(body, queryToSend)
}
return body.Name
}
func (parser *ResponseParser) wildcardAlias(body MQEResponseSerie, queryToSend QueryToSend) string {
regString := strings.Replace(queryToSend.Metric.Metric, `*`, `(.*)`, 1)
reg, err := regexp.Compile(regString)
if err != nil {
return queryToSend.Metric.Alias
}
matches := reg.FindAllStringSubmatch(queryToSend.RawQuery, -1)
if len(matches) == 0 || len(matches[0]) < 2 {
return queryToSend.Metric.Alias
}
return matches[0][1]
}
func (parser *ResponseParser) indexAlias(body MQEResponseSerie, queryToSend QueryToSend) string {
queryNameParts := strings.Split(queryToSend.Metric.Metric, `.`)
name := indexAliasPattern.ReplaceAllStringFunc(queryToSend.Metric.Alias, func(in string) string {
positionName := strings.TrimSpace(strings.Replace(in, "$", "", 1))
pos, err := strconv.Atoi(positionName)
if err != nil {
return ""
}
for i, part := range queryNameParts {
if i == pos-1 {
return strings.TrimSpace(part)
}
}
return ""
})
return strings.Replace(name, " ", ".", -1)
}
+62 -6
View File
@@ -8,11 +8,12 @@ import (
"io/ioutil"
"github.com/grafana/grafana/pkg/components/null"
. "github.com/smartystreets/goconvey/convey"
)
var (
dummieJson string
testJson string
)
func TestMQEResponseParser(t *testing.T) {
@@ -20,14 +21,17 @@ func TestMQEResponseParser(t *testing.T) {
parser := NewResponseParser()
Convey("Can parse response", func() {
queryRef := &Query{
AddClusterToAlias: true,
AddHostToAlias: true,
queryRef := QueryToSend{
QueryRef: &Query{
AddClusterToAlias: true,
AddHostToAlias: true,
},
Metric: Metric{Alias: ""},
}
response := &http.Response{
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader(dummieJson)),
Body: ioutil.NopCloser(strings.NewReader(testJson)),
}
res, err := parser.Parse(response, queryRef)
So(err, ShouldBeNil)
@@ -39,12 +43,64 @@ func TestMQEResponseParser(t *testing.T) {
So(res[0].Points[i][0].Float64, ShouldEqual, i+1)
So(res[0].Points[i][1].Float64, ShouldEqual, startTime+(i*30000))
}
})
Convey("Can format legend", func() {
mqeSerie := MQESerie{
Tagset: map[string]string{
"cluster": "demoapp",
"host": "staples-lab-1",
},
Values: []null.Float{null.NewFloat(3, true)},
}
Convey("with empty alias", func() {
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"}
queryRef := QueryToSend{
QueryRef: &Query{
AddClusterToAlias: true,
AddHostToAlias: true,
},
Metric: Metric{Alias: ""},
}
legend := parser.formatLegend(serie, mqeSerie, queryRef)
So(legend, ShouldEqual, "demoapp staples-lab-1 os.disk.sda3.weighted_io_time")
})
Convey("with index alias (ex $2 $3)", func() {
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"}
queryRef := QueryToSend{
QueryRef: &Query{
AddClusterToAlias: true,
AddHostToAlias: true,
},
Metric: Metric{Alias: "$2 $3", Metric: "os.disk.sda3.weighted_io_time"},
}
legend := parser.formatLegend(serie, mqeSerie, queryRef)
So(legend, ShouldEqual, "demoapp staples-lab-1 disk.sda3")
})
Convey("with wildcard alias", func() {
serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time", Query: "os.disk.*"}
queryRef := QueryToSend{
QueryRef: &Query{
AddClusterToAlias: true,
AddHostToAlias: true,
},
RawQuery: "os.disk.sda3.weighted_io_time",
Metric: Metric{Alias: "*", Metric: "os.disk.*.weighted_io_time"},
}
legend := parser.formatLegend(serie, mqeSerie, queryRef)
So(legend, ShouldEqual, "demoapp staples-lab-1 sda3")
})
})
})
}
func init() {
dummieJson = `{
testJson = `{
"success": true,
"name": "select",
"body": [
+2
View File
@@ -53,6 +53,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
queriesToSend = append(queriesToSend, QueryToSend{
RawQuery: rawQuery,
QueryRef: q,
Metric: metric,
})
} else {
m := strings.Replace(metric.Metric, "*", ".*", -1)
@@ -70,6 +71,7 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
queriesToSend = append(queriesToSend, QueryToSend{
RawQuery: rawQuery,
QueryRef: q,
Metric: metric,
})
}
}
+4
View File
@@ -190,6 +190,10 @@ func (e *OpenTsdbExecutor) buildMetric(query *tsdb.Query) map[string]interface{}
rateOptions["resetValue"] = resetValue.MustFloat64()
}
if !counterMaxCheck && (!resetValueCheck || resetValue.MustFloat64() == 0) {
rateOptions["dropcounter"] = true
}
metric["rateOptions"] = rateOptions
}
+25
View File
@@ -22,6 +22,18 @@ type PrometheusExecutor struct {
Transport *http.Transport
}
type basicAuthTransport struct {
*http.Transport
username string
password string
}
func (bat basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.SetBasicAuth(bat.username, bat.password)
return bat.Transport.RoundTrip(req)
}
func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
transport, err := dsInfo.GetHttpTransport()
if err != nil {
@@ -51,6 +63,14 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
Transport: e.Transport,
}
if e.BasicAuth {
cfg.Transport = basicAuthTransport{
Transport: e.Transport,
username: e.BasicAuthUser,
password: e.BasicAuthPassword,
}
}
client, err := prometheus.New(cfg)
if err != nil {
return nil, err
@@ -157,6 +177,11 @@ func parseResponse(value pmodel.Value, query *PrometheusQuery) (map[string]*tsdb
for _, v := range data {
series := tsdb.TimeSeries{
Name: formatLegend(v.Metric, query),
Tags: map[string]string{},
}
for k, v := range v.Metric {
series.Tags[string(k)] = string(v)
}
for _, k := range v.Values {