Merge branch 'master' into websocket
This commit is contained in:
+6
-2
@@ -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}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -31,7 +31,7 @@ type AdminUpdateUserPasswordForm struct {
|
||||
}
|
||||
|
||||
type AdminUpdateUserPermissionsForm struct {
|
||||
IsGrafanaAdmin bool `json:"IsGrafanaAdmin"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin" binding:"Required"`
|
||||
}
|
||||
|
||||
type AdminUserListItem struct {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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) })
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -73,7 +73,6 @@ type Alert struct {
|
||||
Frequency int64
|
||||
|
||||
EvalData *simplejson.Json
|
||||
EvalDate time.Time
|
||||
NewStateDate time.Time
|
||||
StateChanges int
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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/`)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+1
-2
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user