Merge branch 'master' into websocket

This commit is contained in:
Torkel Ödegaard
2017-01-31 08:56:49 +01:00
283 changed files with 29756 additions and 7727 deletions
+6 -2
View File
@@ -73,9 +73,9 @@ func GetAlerts(c *middleware.Context) Response {
Name: alert.Name,
Message: alert.Message,
State: alert.State,
EvalDate: alert.EvalDate,
NewStateDate: alert.NewStateDate,
ExecutionError: alert.ExecutionError,
EvalData: alert.EvalData,
})
}
@@ -121,10 +121,10 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
}
res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{
Firing: res.Firing,
ConditionEvals: res.ConditionEvals,
State: res.Rule.State,
}
if res.Error != nil {
@@ -173,6 +173,10 @@ func DelAlert(c *middleware.Context) Response {
return Json(200, resp)
}
func GetAlertNotifiers(c *middleware.Context) Response {
return Json(200, alerting.GetNotifiers())
}
func GetAlertNotifications(c *middleware.Context) Response {
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
+3
View File
@@ -125,6 +125,8 @@ func (hs *HttpServer) registerRoutes() {
r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
r.Get("/lookup", wrap(GetUserByLoginOrEmail))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin)
@@ -261,6 +263,7 @@ func (hs *HttpServer) registerRoutes() {
})
r.Get("/alert-notifications", wrap(GetAlertNotifications))
r.Get("/alert-notifiers", wrap(GetAlertNotifiers))
r.Group("/alert-notifications", func() {
r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
+44 -17
View File
@@ -17,7 +17,6 @@ import (
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -90,7 +89,7 @@ type cache struct {
var awsCredentialCache map[string]cache = make(map[string]cache)
var credentialCacheLock sync.RWMutex
func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
credentialCacheLock.RLock()
if _, ok := awsCredentialCache[cacheKey]; ok {
@@ -98,7 +97,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
result := awsCredentialCache[cacheKey].credential
credentialCacheLock.RUnlock()
return result
return result, nil
}
}
credentialCacheLock.RUnlock()
@@ -130,8 +129,7 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
svc := sts.New(session.New(stsConfig), stsConfig)
resp, err := svc.AssumeRole(params)
if err != nil {
// ignore
log.Error(3, "CloudWatch: Failed to assume role", err)
return nil, err
}
if resp.Credentials != nil {
accessKeyId = *resp.Credentials.AccessKeyId
@@ -165,19 +163,28 @@ func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
}
credentialCacheLock.Unlock()
return creds
return creds, nil
}
func getAwsConfig(req *cwRequest) *aws.Config {
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
creds, err := getCredentials(req.GetDatasourceInfo())
if err != nil {
return nil, err
}
cfg := &aws.Config{
Region: aws.String(req.Region),
Credentials: getCredentials(req.GetDatasourceInfo()),
Credentials: creds,
}
return cfg
return cfg, nil
}
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -220,7 +227,11 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
}
func handleListMetrics(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -239,7 +250,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
}
var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params,
err = svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
@@ -257,7 +268,11 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
}
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -296,7 +311,11 @@ func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
}
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -336,7 +355,11 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
}
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -368,7 +391,11 @@ func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
}
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
cfg := getAwsConfig(req)
cfg, err := getAwsConfig(req)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
svc := ec2.New(session.New(cfg), cfg)
reqParam := &struct {
@@ -388,7 +415,7 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
}
var resp ec2.DescribeInstancesOutput
err := svc.DescribeInstancesPages(params,
err = svc.DescribeInstancesPages(params,
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
for _, reservation := range reservations {
+9 -5
View File
@@ -111,7 +111,7 @@ func init() {
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {"ClientId", "DomainName"},
"AWS/Events": {"RuleName"},
"AWS/Firehose": {},
"AWS/Firehose": {"DeliveryStreamName"},
"AWS/IoT": {"Protocol"},
"AWS/Kinesis": {"StreamName", "ShardID"},
"AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
@@ -140,8 +140,8 @@ func init() {
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
func handleGetRegions(req *cwRequest, c *middleware.Context) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
}
result := []interface{}{}
@@ -248,9 +248,13 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
}
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
creds, err := getCredentials(cwData)
if err != nil {
return cloudwatch.ListMetricsOutput{}, err
}
cfg := &aws.Config{
Region: aws.String(cwData.Region),
Credentials: getCredentials(cwData),
Credentials: creds,
}
svc := cloudwatch.New(session.New(cfg), cfg)
@@ -260,7 +264,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
}
var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params,
err = svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
+33
View File
@@ -1,6 +1,8 @@
package api
import (
"bytes"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
@@ -8,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/api/cloudwatch"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -15,6 +18,10 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var (
dataproxyLogger log.Logger = log.New("data-proxy-log")
)
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
@@ -121,6 +128,32 @@ func ProxyDataSourceRequest(c *middleware.Context) {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
logProxyRequest(ds.Type, c)
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
func logProxyRequest(dataSourceType string, c *middleware.Context) {
if !setting.DataProxyLogging {
return
}
var body string
if c.Req.Request.Body != nil {
buffer, err := ioutil.ReadAll(c.Req.Request.Body)
if err == nil {
c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
body = string(buffer)
}
}
dataproxyLogger.Info("Proxying incoming request",
"userid", c.UserId,
"orgid", c.OrgId,
"username", c.Login,
"datasource", dataSourceType,
"uri", c.Req.RequestURI,
"method", c.Req.Request.Method,
"body", body)
}
+2 -2
View File
@@ -100,7 +100,7 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
return
}
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id, "name": cmd.Result.Name})
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
@@ -117,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp
return ApiError(500, "Failed to update datasource", err)
}
return Json(200, util.DynMap{"message": "Datasource updated"})
return Json(200, util.DynMap{"message": "Datasource updated", "id": cmd.Id, "name": cmd.Name})
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
+4 -1
View File
@@ -3,6 +3,7 @@ package dtos
import (
"time"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
)
@@ -16,6 +17,7 @@ type AlertRule struct {
State m.AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
EvalDate time.Time `json:"evalDate"`
EvalData *simplejson.Json `json:"evalData"`
ExecutionError string `json:"executionError"`
DashbboardUri string `json:"dashboardUri"`
}
@@ -36,6 +38,7 @@ type AlertTestCommand struct {
type AlertTestResult struct {
Firing bool `json:"firing"`
State m.AlertStateType `json:"state"`
ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
@@ -51,7 +54,7 @@ type AlertTestResultLog struct {
type EvalMatch struct {
Tags map[string]string `json:"tags,omitempty"`
Metric string `json:"metric"`
Value float64 `json:"value"`
Value null.Float `json:"value"`
}
type NotificationTestCommand struct {
+1 -1
View File
@@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
}
type AdminUpdateUserPermissionsForm struct {
IsGrafanaAdmin bool `json:"IsGrafanaAdmin"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
}
type AdminUserListItem struct {
+1
View File
@@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
"allowOrgCreate": (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
"authProxyEnabled": setting.AuthProxyEnabled,
"ldapEnabled": setting.LdapEnabled,
"alertingEnabled": setting.AlertingEnabled,
"buildInfo": map[string]interface{}{
"version": setting.BuildVersion,
"commit": setting.BuildCommit,
+2 -2
View File
@@ -103,10 +103,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
{Text: "Notifications", Url: setting.AppSubUrl + "/alerting/notifications"},
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
+6 -5
View File
@@ -14,11 +14,12 @@ func RenderToPng(c *middleware.Context) {
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
renderOpts := &renderer.RenderOpts{
Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"),
Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"),
Timezone: queryReader.Get("tz", ""),
}
pngPath, err := renderer.RenderToPng(renderOpts)
+30 -6
View File
@@ -13,7 +13,7 @@ func GetSignedInUser(c *middleware.Context) Response {
return getUserUserProfile(c.UserId)
}
// GET /api/user/:id
// GET /api/users/:id
func GetUserById(c *middleware.Context) Response {
return getUserUserProfile(c.ParamsInt64(":id"))
}
@@ -22,12 +22,36 @@ func getUserUserProfile(userId int64) Response {
query := m.GetUserProfileQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserNotFound {
return ApiError(404, m.ErrUserNotFound.Error(), nil)
}
return ApiError(500, "Failed to get user", err)
}
return Json(200, query.Result)
}
// GET /api/users/lookup
func GetUserByLoginOrEmail(c *middleware.Context) Response {
query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
if err := bus.Dispatch(&query); err != nil {
if err == m.ErrUserNotFound {
return ApiError(404, m.ErrUserNotFound.Error(), nil)
}
return ApiError(500, "Failed to get user", err)
}
user := query.Result
result := m.UserProfileDTO{
Name: user.Name,
Email: user.Email,
Login: user.Login,
Theme: user.Theme,
IsGrafanaAdmin: user.IsAdmin,
OrgId: user.OrgId,
}
return Json(200, &result)
}
// POST /api/user
func UpdateSignedInUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
if setting.AuthProxyEnabled {
@@ -60,7 +84,7 @@ func UpdateUserActiveOrg(c *middleware.Context) Response {
cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed change active organization", err)
return ApiError(500, "Failed to change active organization", err)
}
return ApiSuccess("Active organization changed")
@@ -70,12 +94,12 @@ func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return ApiError(400, "Validation error, need specify either username or email", nil)
return ApiError(400, "Validation error, need to specify either username or email", nil)
}
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "failed to update user", err)
return ApiError(500, "Failed to update user", err)
}
return ApiSuccess("User updated")
@@ -95,7 +119,7 @@ func getUserOrgList(userId int64) Response {
query := m.GetUserOrgListQuery{UserId: userId}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Faile to get user organziations", err)
return ApiError(500, "Failed to get user organizations", err)
}
return Json(200, query.Result)
@@ -130,7 +154,7 @@ func UserSetUsingOrg(c *middleware.Context) Response {
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed change active organization", err)
return ApiError(500, "Failed to change active organization", err)
}
return ApiSuccess("Active organization changed")
+1 -1
View File
@@ -55,7 +55,7 @@ func (g *GrafanaServerImpl) Start() {
plugins.Init()
// init alerting
if setting.ExecuteAlerts {
if setting.AlertingEnabled && setting.ExecuteAlerts {
engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) })
}
+13 -10
View File
@@ -2,6 +2,7 @@ package imguploader
import (
"fmt"
"regexp"
"github.com/grafana/grafana/pkg/setting"
)
@@ -30,19 +31,21 @@ func NewImageUploader() (ImageUploader, error) {
accessKey := s3sec.Key("access_key").MustString("")
secretKey := s3sec.Key("secret_key").MustString("")
if bucket == "" {
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"
}
}
if accessKey == "" {
return nil, fmt.Errorf("Could not find accessKey setting for image.uploader.s3")
}
if secretKey == "" {
return nil, fmt.Errorf("Could not find secretKey setting for image.uploader.s3")
}
return NewS3Uploader(bucket, accessKey, secretKey), nil
return NewS3Uploader(region, bucket, "public-read", accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
if err != nil {
@@ -19,7 +19,7 @@ func TestImageUploaderFactory(t *testing.T) {
setting.ImageUploadProvider = "s3"
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "bucket_url")
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")
@@ -29,9 +29,10 @@ func TestImageUploaderFactory(t *testing.T) {
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(original.bucket, ShouldEqual, "bucket_url")
})
Convey("Webdav uploader", func() {
+39 -33
View File
@@ -1,26 +1,33 @@
package imguploader
import (
"io/ioutil"
"net/http"
"net/url"
"path"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"github.com/kr/s3/s3util"
)
type S3Uploader struct {
region string
bucket string
acl string
secretKey string
accessKey string
log log.Logger
}
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
func NewS3Uploader(region, bucket, acl, accessKey, secretKey string) *S3Uploader {
return &S3Uploader{
region: region,
bucket: bucket,
acl: acl,
accessKey: accessKey,
secretKey: secretKey,
log: log.New("s3uploader"),
@@ -28,42 +35,41 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
}
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
s3util.DefaultConfig.AccessKey = u.accessKey
s3util.DefaultConfig.SecretKey = u.secretKey
header := make(http.Header)
header.Add("x-amz-acl", "public-read")
header.Add("Content-Type", "image/png")
var imageUrl *url.URL
var err error
if imageUrl, err = url.Parse(u.bucket); err != nil {
return "", err
sess := session.New()
creds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: u.accessKey,
SecretAccessKey: u.secretKey,
}},
&credentials.EnvProvider{},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
cfg := &aws.Config{
Region: aws.String(u.region),
Credentials: creds,
}
// add image to url
imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
imageUrlString := imageUrl.String()
log.Debug("Uploading image to s3", "url", imageUrlString)
key := util.GetRandomString(20) + ".png"
log.Debug("Uploading image to s3", "bucket = ", u.bucket, ", key = ", key)
writer, err := s3util.Create(imageUrlString, header, nil)
file, err := os.Open(imageDiskPath)
if err != nil {
return "", err
}
defer writer.Close()
imgData, err := ioutil.ReadFile(imageDiskPath)
svc := s3.New(session.New(cfg), cfg)
params := &s3.PutObjectInput{
Bucket: aws.String(u.bucket),
Key: aws.String(key),
ACL: aws.String(u.acl),
Body: file,
ContentType: aws.String("image/png"),
}
_, err = svc.PutObject(params)
if err != nil {
return "", err
}
_, err = writer.Write(imgData)
if err != nil {
return "", err
}
return imageUrlString, nil
return "https://" + u.bucket + ".s3.amazonaws.com/" + key, nil
}
+127
View File
@@ -0,0 +1,127 @@
package null
import (
"database/sql"
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// Float is a nullable float64.
// It does not consider zero values to be null.
// It will decode to null, not zero, if null.
type Float struct {
sql.NullFloat64
}
// NewFloat creates a new Float
func NewFloat(f float64, valid bool) Float {
return Float{
NullFloat64: sql.NullFloat64{
Float64: f,
Valid: valid,
},
}
}
// FloatFrom creates a new Float that will always be valid.
func FloatFrom(f float64) Float {
return NewFloat(f, true)
}
// FloatFromPtr creates a new Float that be null if f is nil.
func FloatFromPtr(f *float64) Float {
if f == nil {
return NewFloat(0, false)
}
return NewFloat(*f, true)
}
// UnmarshalJSON implements json.Unmarshaler.
// It supports number and null input.
// 0 will not be considered a null Float.
// It also supports unmarshalling a sql.NullFloat64.
func (f *Float) UnmarshalJSON(data []byte) error {
var err error
var v interface{}
if err = json.Unmarshal(data, &v); err != nil {
return err
}
switch x := v.(type) {
case float64:
f.Float64 = float64(x)
case map[string]interface{}:
err = json.Unmarshal(data, &f.NullFloat64)
case nil:
f.Valid = false
return nil
default:
err = fmt.Errorf("json: cannot unmarshal %v into Go value of type null.Float", reflect.TypeOf(v).Name())
}
f.Valid = err == nil
return err
}
// UnmarshalText implements encoding.TextUnmarshaler.
// It will unmarshal to a null Float if the input is a blank or not an integer.
// It will return an error if the input is not an integer, blank, or "null".
func (f *Float) UnmarshalText(text []byte) error {
str := string(text)
if str == "" || str == "null" {
f.Valid = false
return nil
}
var err error
f.Float64, err = strconv.ParseFloat(string(text), 64)
f.Valid = err == nil
return err
}
// MarshalJSON implements json.Marshaler.
// It will encode null if this Float is null.
func (f Float) MarshalJSON() ([]byte, error) {
if !f.Valid {
return []byte("null"), nil
}
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
}
// MarshalText implements encoding.TextMarshaler.
// It will encode a blank string if this Float is null.
func (f Float) MarshalText() ([]byte, error) {
if !f.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
}
// MarshalText implements encoding.TextMarshaler.
// It will encode a blank string if this Float is null.
func (f Float) String() string {
if !f.Valid {
return "null"
}
return fmt.Sprintf("%1.3f", f.Float64)
}
// SetValid changes this Float's value and also sets it to be non-null.
func (f *Float) SetValid(n float64) {
f.Float64 = n
f.Valid = true
}
// Ptr returns a pointer to this Float's value, or a nil pointer if this Float is null.
func (f Float) Ptr() *float64 {
if !f.Valid {
return nil
}
return &f.Float64
}
// IsZero returns true for invalid Floats, for future omitempty support (Go 1.4?)
// A non-null Float with a 0 value will not be considered zero.
func (f Float) IsZero() bool {
return !f.Valid
}
+35 -5
View File
@@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
@@ -18,15 +20,38 @@ import (
)
type RenderOpts struct {
Path string
Width string
Height string
Timeout string
OrgId int64
Path string
Width string
Height string
Timeout string
OrgId int64
Timezone string
}
var rendererLog log.Logger = log.New("png-renderer")
func isoTimeOffsetToPosixTz(isoOffset string) string {
// invert offset
if strings.HasPrefix(isoOffset, "UTC+") {
return strings.Replace(isoOffset, "UTC+", "UTC-", 1)
}
if strings.HasPrefix(isoOffset, "UTC-") {
return strings.Replace(isoOffset, "UTC-", "UTC+", 1)
}
return isoOffset
}
func appendEnviron(baseEnviron []string, name string, value string) []string {
results := make([]string, 0)
prefix := fmt.Sprintf("%s=", name)
for _, v := range baseEnviron {
if !strings.HasPrefix(v, prefix) {
results = append(results, v)
}
}
return append(results, fmt.Sprintf("%s=%s", name, value))
}
func RenderToPng(params *RenderOpts) (string, error) {
rendererLog.Info("Rendering", "path", params.Path)
@@ -73,6 +98,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
return "", err
}
if params.Timezone != "" {
baseEnviron := os.Environ()
cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(params.Timezone))
}
err = cmd.Start()
if err != nil {
return "", err
+6
View File
@@ -45,8 +45,11 @@ var (
M_Alerting_Notification_Sent_Email Counter
M_Alerting_Notification_Sent_Webhook Counter
M_Alerting_Notification_Sent_PagerDuty Counter
M_Alerting_Notification_Sent_LINE Counter
M_Alerting_Notification_Sent_Victorops Counter
M_Alerting_Notification_Sent_OpsGenie Counter
M_Alerting_Notification_Sent_Telegram Counter
M_Alerting_Notification_Sent_Sensu Counter
M_Aws_CloudWatch_GetMetricStatistics Counter
M_Aws_CloudWatch_ListMetrics Counter
@@ -114,6 +117,9 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
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_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
-1
View File
@@ -73,7 +73,6 @@ type Alert struct {
Frequency int64
EvalData *simplejson.Json
EvalDate time.Time
NewStateDate time.Time
StateChanges int
+1
View File
@@ -23,6 +23,7 @@ type SendWebhookSync struct {
Password string
Body string
HttpMethod string
HttpHeader map[string]string
}
type SendResetPasswordEmailCommand struct {
@@ -3,9 +3,9 @@ package conditions
import (
"encoding/json"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting"
"gopkg.in/guregu/null.v3"
)
var (
@@ -3,10 +3,10 @@ package conditions
import (
"testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
)
func evalutorScenario(json string, reducedValue float64, datapoints ...float64) bool {
+21 -3
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
@@ -45,18 +46,18 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
emptySerieCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue.Valid == false {
emptySerieCount++
continue
}
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue.Float64),
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %s", c.Index, evalMatch, series.Name, reducedValue),
})
}
@@ -65,11 +66,28 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name,
Value: reducedValue.Float64,
Value: reducedValue,
})
}
}
// handle no series special case
if len(seriesList) == 0 {
// eval condition for null value
evalMatch := c.Evaluator.Eval(null.FloatFromPtr(nil))
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),
})
}
if evalMatch {
evalMatchCount++
matches = append(matches, &alerting.EvalMatch{Metric: "NoData", Value: null.FloatFromPtr(nil)})
}
}
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
+32 -2
View File
@@ -4,9 +4,8 @@ import (
"context"
"testing"
null "gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
@@ -72,7 +71,38 @@ func TestQueryCondition(t *testing.T) {
So(cr.Firing, ShouldBeTrue)
})
Convey("No series", func() {
Convey("Should set NoDataFound when condition is gt", func() {
ctx.series = tsdb.TimeSeriesSlice{}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeFalse)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should be firing when condition is no_value", func() {
ctx.evaluator = `{"type": "no_value", "params": []}`
ctx.series = tsdb.TimeSeriesSlice{}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
})
Convey("Empty series", func() {
Convey("Should set Firing if eval match", func() {
ctx.evaluator = `{"type": "no_value", "params": []}`
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Should set NoDataFound both series are empty", func() {
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
+1 -1
View File
@@ -5,8 +5,8 @@ import (
"sort"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/tsdb"
"gopkg.in/guregu/null.v3"
)
type QueryReducer interface {
@@ -3,10 +3,10 @@ package conditions
import (
"testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/tsdb"
)
func TestSimpleReducer(t *testing.T) {
@@ -57,6 +57,16 @@ func TestSimpleReducer(t *testing.T) {
So(result, ShouldEqual, float64(2))
})
Convey("avg with only nulls", func() {
reducer := NewSimpleReducer("avg")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
So(reducer.Reduce(series).Valid, ShouldEqual, false)
})
Convey("avg of number values and null values should ignore nulls", func() {
reducer := NewSimpleReducer("avg")
series := &tsdb.TimeSeries{
+35
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
)
type DefaultEvalHandler struct {
@@ -60,6 +61,40 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
context.Firing = firing
context.NoDataFound = noDataFound
context.EndTime = time.Now()
context.Rule.State = e.getNewState(context)
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Execution_Time.Update(elapsedTime)
}
// This should be move into evalContext once its been refactored.
func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"error", evalContext.Error,
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.ExecutionErrorState.ToAlertState()
}
} else if evalContext.Firing {
return models.AlertStateAlerting
} else if evalContext.NoDataFound {
handler.log.Info("Alert Rule returned no data",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
if evalContext.Rule.NoDataState == models.NoDataKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.NoDataState.ToAlertState()
}
}
return models.AlertStateOK
}
+73 -3
View File
@@ -2,8 +2,10 @@ package alerting
import (
"context"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -18,8 +20,8 @@ func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
}
func TestAlertingExecutor(t *testing.T) {
Convey("Test alert execution", t, func() {
func TestAlertingEvaluationHandler(t *testing.T) {
Convey("Test alert evaluation handler", t, func() {
handler := NewEvalHandler()
Convey("Show return triggered with single passing condition", func() {
@@ -37,7 +39,7 @@ func TestAlertingExecutor(t *testing.T) {
Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{{}, {}}},
&conditionStub{firing: false, operator: "and"},
},
})
@@ -164,5 +166,73 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context)
So(context.NoDataFound, ShouldBeTrue)
})
Convey("EvalHandler can replace alert state based for errors and no_data", func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
dummieError := fmt.Errorf("dummie error")
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Firing = true
So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
Convey("ok -> no_data(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataSetAlerting
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
})
})
})
}
+15 -2
View File
@@ -60,12 +60,25 @@ func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Jso
return nil
}
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
rawJson, err := in.MarshalJSON()
if err != nil {
return nil, err
}
return simplejson.NewJson(rawJson)
}
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("GetAlerts")
alerts := make([]*m.Alert, 0)
dashboardJson, err := copyJson(e.Dash.Data)
if err != nil {
return nil, err
}
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
alerts := make([]*m.Alert, 0)
for _, rowObj := range dashboardJson.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
+28
View File
@@ -110,6 +110,34 @@ func TestAlertRuleExtraction(t *testing.T) {
]
}`
Convey("Extractor should not modify the original json", func() {
dashJson, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
getTarget := func(j *simplejson.Json) string {
rowObj := j.Get("rows").MustArray()[0]
row := simplejson.NewFromAny(rowObj)
panelObj := row.Get("panels").MustArray()[0]
panel := simplejson.NewFromAny(panelObj)
conditionObj := panel.Get("alert").Get("conditions").MustArray()[0]
condition := simplejson.NewFromAny(conditionObj)
return condition.Get("query").Get("model").Get("target").MustString()
}
Convey("Dashboard json rows.panels.alert.query.model.target should be empty", func() {
So(getTarget(dashJson), ShouldEqual, "")
})
extractor := NewDashAlertExtractor(dash, 1)
_, _ = extractor.GetAlerts()
Convey("Dashboard json should not be updated after extracting rules", func() {
So(getTarget(dashJson), ShouldEqual, "")
})
})
Convey("Parsing and validating dashboard containing graphite alerts", func() {
dashJson, err := simplejson.NewJson([]byte(json))
+3 -1
View File
@@ -1,5 +1,7 @@
package alerting
import "github.com/grafana/grafana/pkg/components/null"
type Job struct {
Offset int64
OffsetWait bool
@@ -14,7 +16,7 @@ type ResultLogEntry struct {
}
type EvalMatch struct {
Value float64 `json:"value"`
Value null.Float `json:"value"`
Metric string `json:"metric"`
Tags map[string]string `json:"tags"`
}
+23 -5
View File
@@ -13,6 +13,14 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
type NotifierPlugin struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
OptionsTemplate string `json:"optionsTemplate"`
Factory NotifierFactory `json:"-"`
}
type RootNotifier struct {
log log.Logger
}
@@ -130,12 +138,12 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex
}
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
factory, found := notifierFactories[model.Type]
notifierPlugin, found := notifierFactories[model.Type]
if !found {
return nil, errors.New("Unsupported notification type")
}
return factory(model)
return notifierPlugin.Factory(model)
}
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
@@ -152,8 +160,18 @@ func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
func RegisterNotifier(typeName string, factory NotifierFactory) {
notifierFactories[typeName] = factory
func RegisterNotifier(plugin *NotifierPlugin) {
notifierFactories[plugin.Type] = plugin
}
func GetNotifiers() []*NotifierPlugin {
list := make([]*NotifierPlugin, 0)
for _, value := range notifierFactories {
list = append(list, value)
}
return list
}
+15 -1
View File
@@ -13,7 +13,21 @@ import (
)
func init() {
alerting.RegisterNotifier("email", NewEmailNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "email",
Name: "Email",
Description: "Sends notifications using Grafana server configured STMP settings",
Factory: NewEmailNotifier,
OptionsTemplate: `
<h3 class="page-heading">Email addresses</h3>
<div class="gf-form">
<textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
</div>
<div class="gf-form">
<span>You can enter multiple email addresses using a ";" separator</span>
</div>
`,
})
}
type EmailNotifier struct {
+94
View File
@@ -0,0 +1,94 @@
package notifiers
import (
"fmt"
"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"
"net/url"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "LINE",
Name: "LINE",
Description: "Send notifications to LINE notify",
Factory: NewLINENotifier,
OptionsTemplate: `
<div class="gf-form-group">
<h3 class="page-heading">LINE notify settings</h3>
<div class="gf-form">
<span class="gf-form-label width-14">Token</span>
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.token" placeholder="LINE notify token key"></input>
</div>
</div>
`,
})
}
const (
lineNotifyUrl string = "https://notify-api.line.me/api/notify"
)
func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) {
token := model.Settings.Get("token").MustString()
if token == "" {
return nil, alerting.ValidationError{Reason: "Could not find token in settings"}
}
return &LineNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Token: token,
log: log.New("alerting.notifier.line"),
}, nil
}
type LineNotifier struct {
NotifierBase
Token string
log log.Logger
}
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
metrics.M_Alerting_Notification_Sent_LINE.Inc(1)
var err error
switch evalContext.Rule.State {
case m.AlertStateAlerting:
err = this.createAlert(evalContext)
}
return err
}
func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
this.log.Info("Creating Line notify", "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
}
form := url.Values{}
body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)
form.Add("message", body)
cmd := &m.SendWebhookSync{
Url: lineNotifyUrl,
HttpMethod: "POST",
HttpHeader: map[string]string{
"Authorization": fmt.Sprintf("Bearer %s", this.Token),
"Content-Type": "application/x-www-form-urlencoded",
},
Body: form.Encode(),
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
return err
}
return nil
}
@@ -0,0 +1,49 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestLineNotifier(t *testing.T) {
Convey("Line notifier tests", t, func() {
Convey("empty settings should return error", func() {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
_, err := NewLINENotifier(model)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
{
"token": "abcdefgh0123456789"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
not, err := NewLINENotifier(model)
lineNotifier := not.(*LineNotifier)
So(err, ShouldBeNil)
So(lineNotifier.Name, ShouldEqual, "line_testing")
So(lineNotifier.Type, ShouldEqual, "line")
So(lineNotifier.Token, ShouldEqual, "abcdefgh0123456789")
})
})
}
+22 -1
View File
@@ -13,7 +13,28 @@ import (
)
func init() {
alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "opsgenie",
Name: "OpsGenie",
Description: "Sends notifications to OpsGenie",
Factory: NewOpsGenieNotifier,
OptionsTemplate: `
<h3 class="page-heading">OpsGenie settings</h3>
<div class="gf-form">
<span class="gf-form-label width-14">API Key</span>
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
</div>
<div class="gf-form">
<gf-form-switch
class="gf-form"
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.">
</gf-form-switch>
</div>
`,
})
}
var (
+22 -1
View File
@@ -12,7 +12,28 @@ import (
)
func init() {
alerting.RegisterNotifier("pagerduty", NewPagerdutyNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "pagerduty",
Name: "PagerDuty",
Description: "Sends notifications to PagerDuty",
Factory: NewPagerdutyNotifier,
OptionsTemplate: `
<h3 class="page-heading">PagerDuty settings</h3>
<div class="gf-form">
<span class="gf-form-label width-14">Integration Key</span>
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.integrationKey" placeholder="Pagerduty integeration Key"></input>
</div>
<div class="gf-form">
<gf-form-switch
class="gf-form"
label="Auto resolve incidents"
label-class="width-14"
checked="ctrl.model.settings.autoResolve"
tooltip="Resolve incidents in pagerduty once the alert goes back to ok.">
</gf-form-switch>
</div>
`,
})
}
var (
+115
View File
@@ -0,0 +1,115 @@
package notifiers
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"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"
"strconv"
"strings"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "sensu",
Name: "Sensu",
Description: "Sends HTTP POST request to a Sensu API",
Factory: NewSensuNotifier,
OptionsTemplate: `
<h3 class="page-heading">Sensu settings</h3>
<div class="gf-form">
<span class="gf-form-label width-10">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://sensu-api.local:4567/results"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Username</span>
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Password</span>
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
</div>
`,
})
}
func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
url := model.Settings.Get("url").MustString()
if url == "" {
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
}
return &SensuNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(),
log: log.New("alerting.notifier.sensu"),
}, nil
}
type SensuNotifier struct {
NotifierBase
Url string
User string
Password string
log log.Logger
}
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending sensu result")
metrics.M_Alerting_Notification_Sent_Sensu.Inc(1)
bodyJSON := simplejson.New()
bodyJSON.Set("ruleId", evalContext.Rule.Id)
// Sensu alerts cannot have spaces in them
bodyJSON.Set("name", strings.Replace(evalContext.Rule.Name, " ", "_", -1))
// Sensu alerts require a command
// We set it to the grafana ruleID
bodyJSON.Set("source", "grafana_rule_"+strconv.FormatInt(evalContext.Rule.Id, 10))
// Finally, sensu expects an output
// We set it to a default output
bodyJSON.Set("output", "Grafana Metric Condition Met")
bodyJSON.Set("evalMatches", evalContext.EvalMatches)
if evalContext.Rule.State == "alerting" {
bodyJSON.Set("status", 2)
} else if evalContext.Rule.State == "no_data" {
bodyJSON.Set("status", 1)
} else {
bodyJSON.Set("status", 0)
}
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
bodyJSON.Set("ruleUrl", ruleUrl)
}
if evalContext.ImagePublicUrl != "" {
bodyJSON.Set("imageUrl", evalContext.ImagePublicUrl)
}
if evalContext.Rule.Message != "" {
bodyJSON.Set("message", evalContext.Rule.Message)
}
body, _ := bodyJSON.MarshalJSON()
cmd := &m.SendWebhookSync{
Url: this.Url,
User: this.User,
Password: this.Password,
Body: string(body),
HttpMethod: "POST",
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send sensu event", "error", err, "sensu", this.Name)
return err
}
return nil
}
@@ -0,0 +1,52 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestSensuNotifier(t *testing.T) {
Convey("Sensu 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: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
_, err := NewSensuNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("from settings", func() {
json := `
{
"url": "http://sensu-api.example.com:4567/results"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
not, err := NewSensuNotifier(model)
sensuNotifier := not.(*SensuNotifier)
So(err, ShouldBeNil)
So(sensuNotifier.Name, ShouldEqual, "sensu")
So(sensuNotifier.Type, ShouldEqual, "sensu")
So(sensuNotifier.Url, ShouldEqual, "http://sensu-api.example.com:4567/results")
})
})
})
}
+36 -1
View File
@@ -13,7 +13,42 @@ import (
)
func init() {
alerting.RegisterNotifier("slack", NewSlackNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "slack",
Name: "Slack",
Description: "Sends notifications using Grafana server configured STMP settings",
Factory: NewSlackNotifier,
OptionsTemplate: `
<h3 class="page-heading">Slack settings</h3>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Recipient</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.recipient"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Override default channel or user, use #channel-name or @username
</info-popover>
</div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Mention</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.mention"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Mention a user or a group using @ when notifying in a channel
</info-popover>
</div>
`,
})
}
func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+113
View File
@@ -0,0 +1,113 @@
package notifiers
import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"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 (
telegeramApiUrl string = "https://api.telegram.org/bot%s/%s"
)
func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "telegram",
Name: "Telegram",
Description: "Sends notifications to Telegram",
Factory: NewTelegramNotifier,
OptionsTemplate: `
<h3 class="page-heading">Telegram API settings</h3>
<div class="gf-form">
<span class="gf-form-label width-9">BOT API Token</span>
<input type="text" required
class="gf-form-input"
ng-model="ctrl.model.settings.bottoken"
placeholder="Telegram BOT API Token"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Chat ID</span>
<input type="text" required
class="gf-form-input"
ng-model="ctrl.model.settings.chatid"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Integer Telegram Chat Identifier
</info-popover>
</div>
`,
})
}
type TelegramNotifier struct {
NotifierBase
BotToken string
ChatID string
log log.Logger
}
func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
if model.Settings == nil {
return nil, alerting.ValidationError{Reason: "No Settings Supplied"}
}
botToken := model.Settings.Get("bottoken").MustString()
chatId := model.Settings.Get("chatid").MustString()
if botToken == "" {
return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"}
}
if chatId == "" {
return nil, alerting.ValidationError{Reason: "Could not find Chat Id in settings"}
}
return &TelegramNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
BotToken: botToken,
ChatID: chatId,
log: log.New("alerting.notifier.telegram"),
}, nil
}
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
this.log.Info("Sending alert notification to", "chat_id", this.ChatID)
metrics.M_Alerting_Notification_Sent_Telegram.Inc(1)
bodyJSON := simplejson.New()
bodyJSON.Set("chat_id", this.ChatID)
bodyJSON.Set("parse_mode", "html")
message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
}
bodyJSON.Set("text", message)
url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
body, _ := bodyJSON.MarshalJSON()
cmd := &m.SendWebhookSync{
Url: url,
Body: string(body),
HttpMethod: "POST",
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
return err
}
return nil
}
@@ -0,0 +1,55 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestTelegramNotifier(t *testing.T) {
Convey("Telegram 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: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
_, err := NewTelegramNotifier(model)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
{
"bottoken": "abcdefgh0123456789",
"chatid": "-1234567890"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
not, err := NewTelegramNotifier(model)
telegramNotifier := not.(*TelegramNotifier)
So(err, ShouldBeNil)
So(telegramNotifier.Name, ShouldEqual, "telegram_testing")
So(telegramNotifier.Type, ShouldEqual, "telegram")
So(telegramNotifier.BotToken, ShouldEqual, "abcdefgh0123456789")
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
})
})
})
}
+13 -1
View File
@@ -16,7 +16,19 @@ import (
const AlertStateCritical = "CRITICAL"
func init() {
alerting.RegisterNotifier("victorops", NewVictoropsNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "victorops",
Name: "VictorOps",
Description: "Sends notifications to VictorOps",
Factory: NewVictoropsNotifier,
OptionsTemplate: `
<h3 class="page-heading">VictorOps settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="VictorOps url"></input>
</div>
`,
})
}
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
+30 -2
View File
@@ -10,7 +10,35 @@ import (
)
func init() {
alerting.RegisterNotifier("webhook", NewWebHookNotifier)
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "webhook",
Name: "webhook",
Description: "Sends HTTP POST request to a URL",
Factory: NewWebHookNotifier,
OptionsTemplate: `
<h3 class="page-heading">Webhook settings</h3>
<div class="gf-form">
<span class="gf-form-label width-10">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Http Method</span>
<div class="gf-form-select-wrapper width-14">
<select class="gf-form-input" ng-model="ctrl.model.settings.httpMethod" ng-options="t for t in ['POST', 'PUT']">
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Username</span>
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Password</span>
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
</div>
`,
})
}
func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
@@ -22,7 +50,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
return &WebhookNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url,
User: model.Settings.Get("user").MustString(),
User: model.Settings.Get("username").MustString(),
Password: model.Settings.Get("password").MustString(),
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
log: log.New("alerting.notifier.webhook"),
+5 -34
View File
@@ -27,50 +27,21 @@ func NewResultHandler() *DefaultResultHandler {
}
}
func (handler *DefaultResultHandler) GetStateFromEvaluation(evalContext *EvalContext) m.AlertStateType {
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"error", evalContext.Error,
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
if evalContext.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.ExecutionErrorState.ToAlertState()
}
} else if evalContext.Firing {
return m.AlertStateAlerting
} else if evalContext.NoDataFound {
handler.log.Info("Alert Rule returned no data",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
if evalContext.Rule.NoDataState == m.NoDataKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.NoDataState.ToAlertState()
}
}
return m.AlertStateOK
}
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
executionError := ""
annotationData := simplejson.New()
evalContext.Rule.State = handler.GetStateFromEvaluation(evalContext)
if evalContext.Firing {
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
}
if evalContext.Error != nil {
executionError = evalContext.Error.Error()
annotationData.Set("errorMessage", executionError)
}
if evalContext.Firing {
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
if evalContext.NoDataFound {
annotationData.Set("no_data", true)
}
countStateResult(evalContext.Rule.State)
@@ -1,90 +0,0 @@
package alerting
import (
"context"
"testing"
"fmt"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingResultHandler(t *testing.T) {
Convey("Result handler", t, func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
dummieError := fmt.Errorf("dummie")
handler := NewResultHandler()
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Firing = true
So(handler.GetStateFromEvaluation(ctx), ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> error(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("pending -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("ok -> no_data(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataSetAlerting
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
Convey("pending -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.GetStateFromEvaluation(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
})
})
}
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
@@ -63,12 +64,12 @@ func evalMatchesBasedOnState() []*EvalMatch {
matches := make([]*EvalMatch, 0)
matches = append(matches, &EvalMatch{
Metric: "High value",
Value: 100,
Value: null.FloatFrom(100),
})
matches = append(matches, &EvalMatch{
Metric: "Higher Value",
Value: 200,
Value: null.FloatFrom(200),
})
return matches
+1
View File
@@ -101,6 +101,7 @@ func createDialer() (*gomail.Dialer, error) {
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
d.TLSConfig = tlsconfig
d.LocalName = setting.InstanceName
return d, nil
}
@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
Password: cmd.Password,
Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
HttpHeader: cmd.HttpHeader,
})
}
+5
View File
@@ -19,6 +19,7 @@ type Webhook struct {
Password string
Body string
HttpMethod string
HttpHeader map[string]string
}
var (
@@ -63,6 +64,10 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
}
for k, v := range webhook.HttpHeader {
request.Header.Set(k, v)
}
resp, err := ctxhttp.Do(ctx, http.DefaultClient, request)
if err != nil {
return err
+6
View File
@@ -113,6 +113,12 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
return err
}
for i := range alerts {
if alerts[i].ExecutionError == " " {
alerts[i].ExecutionError = ""
}
}
query.Result = alerts
return nil
}
+7 -1
View File
@@ -133,10 +133,16 @@ func UpdateOrg(cmd *m.UpdateOrgCommand) error {
Updated: time.Now(),
}
if _, err := sess.Id(cmd.OrgId).Update(&org); err != nil {
affectedRows, err := sess.Id(cmd.OrgId).Update(&org)
if err != nil {
return err
}
if affectedRows == 0 {
return m.ErrOrgNotFound
}
sess.publishAfterCommit(&events.OrgUpdated{
Timestamp: org.Updated,
Id: org.Id,
+11 -2
View File
@@ -14,9 +14,10 @@ import (
"runtime"
"strings"
"github.com/go-macaron/session"
"gopkg.in/ini.v1"
"github.com/go-macaron/session"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
)
@@ -65,6 +66,7 @@ var (
SshPort int
CertFile, KeyFile string
RouterLogging bool
DataProxyLogging bool
StaticRootPath string
EnableGzip bool
EnforceDomain bool
@@ -148,7 +150,8 @@ var (
Quota QuotaSettings
// Alerting
ExecuteAlerts bool
AlertingEnabled bool
ExecuteAlerts bool
// logger
logger log.Logger
@@ -490,6 +493,7 @@ func NewConfigContext(args *CommandLineArgs) error {
HttpAddr = server.Key("http_addr").MustString(DEFAULT_HTTP_ADDR)
HttpPort = server.Key("http_port").MustString("3000")
RouterLogging = server.Key("router_logging").MustBool(false)
EnableGzip = server.Key("enable_gzip").MustBool(false)
EnforceDomain = server.Key("enforce_domain").MustBool(false)
StaticRootPath = makeAbsolute(server.Key("static_root_path").String(), HomePath)
@@ -498,6 +502,10 @@ func NewConfigContext(args *CommandLineArgs) error {
return err
}
// read data proxy settings
dataproxy := Cfg.Section("dataproxy")
DataProxyLogging = dataproxy.Key("logging").MustBool(false)
// read security settings
security := Cfg.Section("security")
SecretKey = security.Key("secret_key").String()
@@ -571,6 +579,7 @@ func NewConfigContext(args *CommandLineArgs) error {
LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true)
alerting := Cfg.Section("alerting")
AlertingEnabled = alerting.Key("enabled").MustBool(true)
ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true)
readSessionConfig()
+42 -11
View File
@@ -2,7 +2,9 @@ package influxdb
import (
"fmt"
"strconv"
"strings"
"time"
"regexp"
@@ -15,24 +17,53 @@ var (
)
func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) {
var res string
if query.UseRawQuery && query.RawQuery != "" {
q := query.RawQuery
q = strings.Replace(q, "$timeFilter", query.renderTimeFilter(queryContext), 1)
q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1)
return q, nil
res = query.RawQuery
} else {
res = query.renderSelectors(queryContext)
res += query.renderMeasurement()
res += query.renderWhereClause()
res += query.renderTimeFilter(queryContext)
res += query.renderGroupBy(queryContext)
}
res := query.renderSelectors(queryContext)
res += query.renderMeasurement()
res += query.renderWhereClause()
res += query.renderTimeFilter(queryContext)
res += query.renderGroupBy(queryContext)
interval, err := getDefinedInterval(query, queryContext)
if err != nil {
return "", err
}
res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), 1)
res = strings.Replace(res, "$interval", interval.Text, 1)
res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), 1)
res = strings.Replace(res, "$__interval", interval.Text, 1)
return res, nil
}
func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) (*tsdb.Interval, error) {
defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange)
if query.Interval == "" {
return &defaultInterval, nil
}
setInterval := strings.Replace(strings.Replace(query.Interval, "<", "", 1), ">", "", 1)
parsedSetInterval, err := time.ParseDuration(setInterval)
if err != nil {
return nil, err
}
if strings.Contains(query.Interval, ">") {
if defaultInterval.Value > parsedSetInterval {
return &defaultInterval, nil
}
}
return &tsdb.Interval{Value: parsedSetInterval, Text: setInterval}, nil
}
func (query *Query) renderTags() []string {
var res []string
for i, tag := range query.Tags {
+2 -23
View File
@@ -3,7 +3,6 @@ package influxdb
import (
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb"
)
@@ -93,30 +92,10 @@ func fieldRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPar
return fmt.Sprintf(`"%s"`, part.Params[0])
}
func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) string {
setInterval := strings.Replace(strings.Replace(query.Interval, "<", "", 1), ">", "", 1)
defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange)
if strings.Contains(query.Interval, ">") {
parsedDefaultInterval, err := time.ParseDuration(defaultInterval)
parsedSetInterval, err2 := time.ParseDuration(setInterval)
if err == nil && err2 == nil && parsedDefaultInterval > parsedSetInterval {
return defaultInterval
}
}
return setInterval
}
func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string {
for i, param := range part.Params {
if param == "$interval" {
if query.Interval != "" {
part.Params[i] = getDefinedInterval(query, queryContext)
} else {
part.Params[i] = tsdb.CalculateInterval(queryContext.TimeRange)
}
if part.Type == "time" && param == "auto" {
part.Params[i] = "$__interval"
}
}
+5 -18
View File
@@ -37,33 +37,20 @@ func TestInfluxdbQueryPart(t *testing.T) {
So(res, ShouldEqual, "bottom(value, 3)")
})
Convey("render time", func() {
Convey("render time with $interval", func() {
part, err := NewQueryPart("time", []string{"$interval"})
So(err, ShouldBeNil)
res := part.Render(query, queryContext, "")
So(res, ShouldEqual, "time(200ms)")
So(res, ShouldEqual, "time($interval)")
})
Convey("render time interval >10s", func() {
part, err := NewQueryPart("time", []string{"$interval"})
Convey("render time with auto", func() {
part, err := NewQueryPart("time", []string{"auto"})
So(err, ShouldBeNil)
query.Interval = ">10s"
res := part.Render(query, queryContext, "")
So(res, ShouldEqual, "time(10s)")
})
Convey("render time interval >1s and higher interval calculation", func() {
part, err := NewQueryPart("time", []string{"$interval"})
queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("1y", "now")}
So(err, ShouldBeNil)
query.Interval = ">1s"
res := part.Render(query, queryContext, "")
So(res, ShouldEqual, "time(168h)")
So(res, ShouldEqual, "time($__interval)")
})
Convey("render spread", func() {
+51 -8
View File
@@ -16,10 +16,15 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
qp1, _ := NewQueryPart("field", []string{"value"})
qp2, _ := NewQueryPart("mean", []string{})
groupBy1, _ := NewQueryPart("time", []string{"$interval"})
mathPartDivideBy100, _ := NewQueryPart("math", []string{"/ 100"})
mathPartDivideByIntervalMs, _ := NewQueryPart("math", []string{"/ $__interval_ms"})
groupBy1, _ := NewQueryPart("time", []string{"$__interval"})
groupBy2, _ := NewQueryPart("tag", []string{"datacenter"})
groupBy3, _ := NewQueryPart("fill", []string{"null"})
groupByOldInterval, _ := NewQueryPart("time", []string{"$interval"})
tag1 := &Tag{Key: "hostname", Value: "server1", Operator: "="}
tag2 := &Tag{Key: "hostname", Value: "server2", Operator: "=", Condition: "OR"}
@@ -55,6 +60,43 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
})
Convey("can build query with math part", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2, *mathPartDivideBy100}},
Measurement: "cpu",
Interval: "5s",
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") / 100 FROM "cpu" WHERE time > now() - 5m`)
})
Convey("can build query with math part using $__interval_ms variable", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2, *mathPartDivideByIntervalMs}},
Measurement: "cpu",
Interval: "5s",
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") / 5000 FROM "cpu" WHERE time > now() - 5m`)
})
Convey("can build query with old $interval variable", func() {
query := &Query{
Selects: []*Select{{*qp1, *qp2}},
Measurement: "cpu",
Policy: "",
GroupBy: []*QueryPart{groupByOldInterval},
}
rawQuery, err := query.Build(queryContext)
So(err, ShouldBeNil)
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE time > now() - 5m GROUP BY time(200ms)`)
})
Convey("can render time range", func() {
query := Query{}
Convey("render from: 2h to now-1h", func() {
@@ -86,43 +128,43 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
})
Convey("can render normal tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "", Value: `value`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
})
Convey("can render regex tags without operator", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "", Value: `/value/`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render regex tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "=~", Value: `/value/`, Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`)
})
Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`)
})
Convey("can render numbers less then condition tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "<", Value: "10001", Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "<", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`)
})
Convey("can render number greather then condition tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: ">", Value: "10001", Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: ">", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`)
})
Convey("can render string tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}}
query := &Query{Tags: []*Tag{{Operator: "=", Value: "value", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`)
})
@@ -139,4 +181,5 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`)
})
})
}
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/tsdb"
"gopkg.in/guregu/null.v3"
)
type ResponseParser struct{}
+2 -2
View File
@@ -19,7 +19,7 @@ func TestInfluxdbResponseParser(t *testing.T) {
response := &Response{
Results: []Result{
Result{
{
Series: []Row{
{
Name: "cpu",
@@ -69,7 +69,7 @@ func TestInfluxdbResponseParser(t *testing.T) {
response := &Response{
Results: []Result{
Result{
{
Series: []Row{
{
Name: "cpu.upc",
+8 -3
View File
@@ -12,14 +12,19 @@ var (
day time.Duration = time.Hour * 24 * 365
)
func CalculateInterval(timerange *TimeRange) string {
type Interval struct {
Text string
Value time.Duration
}
func CalculateInterval(timerange *TimeRange) Interval {
interval := time.Duration((timerange.MustGetTo().UnixNano() - timerange.MustGetFrom().UnixNano()) / defaultRes)
if interval < minInterval {
return formatDuration(minInterval)
return Interval{Text: formatDuration(minInterval), Value: interval}
}
return formatDuration(roundInterval(interval))
return Interval{Text: formatDuration(roundInterval(interval)), Value: interval}
}
func formatDuration(inter time.Duration) string {
+4 -4
View File
@@ -18,28 +18,28 @@ func TestInterval(t *testing.T) {
tr := NewTimeRange("5m", "now")
interval := CalculateInterval(tr)
So(interval, ShouldEqual, "200ms")
So(interval.Text, ShouldEqual, "200ms")
})
Convey("for 15min", func() {
tr := NewTimeRange("15m", "now")
interval := CalculateInterval(tr)
So(interval, ShouldEqual, "500ms")
So(interval.Text, ShouldEqual, "500ms")
})
Convey("for 30min", func() {
tr := NewTimeRange("30m", "now")
interval := CalculateInterval(tr)
So(interval, ShouldEqual, "1s")
So(interval.Text, ShouldEqual, "1s")
})
Convey("for 1h", func() {
tr := NewTimeRange("1h", "now")
interval := CalculateInterval(tr)
So(interval, ShouldEqual, "2s")
So(interval.Text, ShouldEqual, "2s")
})
Convey("Round interval", func() {
+1 -1
View File
@@ -1,9 +1,9 @@
package tsdb
import (
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"gopkg.in/guregu/null.v3"
)
type Query struct {
+1 -2
View File
@@ -5,10 +5,9 @@ import (
"io/ioutil"
"net/http"
null "gopkg.in/guregu/null.v3"
"fmt"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
)
+8 -8
View File
@@ -27,16 +27,16 @@ func TestWildcardExpansion(t *testing.T) {
Convey("Without wildcard series", func() {
query := &Query{
Metrics: []Metric{
Metric{Metric: "os.cpu.3.idle", Alias: ""},
Metric{Metric: "os.cpu.2.idle", Alias: ""},
Metric{Metric: "os.cpu.1.idle", Alias: "cpu"},
{Metric: "os.cpu.3.idle", Alias: ""},
{Metric: "os.cpu.2.idle", Alias: ""},
{Metric: "os.cpu.1.idle", Alias: "cpu"},
},
Hosts: []string{"staples-lab-1", "staples-lab-2"},
Cluster: []string{"demoapp-1", "demoapp-2"},
AddClusterToAlias: false,
AddHostToAlias: false,
FunctionList: []Function{
Function{Func: "aggregate.min"},
{Func: "aggregate.min"},
},
TimeRange: &tsdb.TimeRange{Now: now, From: "5m", To: "now"},
}
@@ -52,15 +52,15 @@ func TestWildcardExpansion(t *testing.T) {
Convey("With two aggregate functions", func() {
query := &Query{
Metrics: []Metric{
Metric{Metric: "os.cpu.3.idle", Alias: ""},
{Metric: "os.cpu.3.idle", Alias: ""},
},
Hosts: []string{"staples-lab-1", "staples-lab-2"},
Cluster: []string{"demoapp-1", "demoapp-2"},
AddClusterToAlias: false,
AddHostToAlias: false,
FunctionList: []Function{
Function{Func: "aggregate.min"},
Function{Func: "aggregate.max"},
{Func: "aggregate.min"},
{Func: "aggregate.max"},
},
TimeRange: &tsdb.TimeRange{Now: now, From: "5m", To: "now"},
}
@@ -74,7 +74,7 @@ func TestWildcardExpansion(t *testing.T) {
Convey("Containg wildcard series", func() {
query := &Query{
Metrics: []Metric{
Metric{Metric: "os.cpu*", Alias: ""},
{Metric: "os.cpu*", Alias: ""},
},
Hosts: []string{"staples-lab-1"},
AddClusterToAlias: false,
+1 -2
View File
@@ -14,8 +14,7 @@ import (
"net/http"
"net/url"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
+1 -2
View File
@@ -7,10 +7,9 @@ import (
"strings"
"time"
"gopkg.in/guregu/null.v3"
"net/http"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
+1 -2
View File
@@ -6,8 +6,7 @@ import (
"strings"
"time"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
)