Merge branch 'master' into feature/discord
This commit is contained in:
@@ -5,34 +5,18 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type UpdateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *m.Dashboard
|
||||
}
|
||||
|
||||
type ValidateDashboardAlertsCommand struct {
|
||||
UserId int64
|
||||
OrgId int64
|
||||
Dashboard *m.Dashboard
|
||||
}
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("alerting", updateDashboardAlerts)
|
||||
bus.AddHandler("alerting", validateDashboardAlerts)
|
||||
}
|
||||
|
||||
func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
|
||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if _, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return extractor.ValidateAlerts()
|
||||
}
|
||||
|
||||
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
|
||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
saveAlerts := m.SaveAlertsCommand{
|
||||
OrgId: cmd.OrgId,
|
||||
UserId: cmd.UserId,
|
||||
@@ -41,15 +25,12 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
|
||||
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if alerts, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
saveAlerts.Alerts = alerts
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveAlerts); err != nil {
|
||||
alerts, err := extractor.GetAlerts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
saveAlerts.Alerts = alerts
|
||||
|
||||
return bus.Dispatch(&saveAlerts)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gocontext "context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -104,14 +106,18 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(getDsInfo); err != nil {
|
||||
return nil, fmt.Errorf("Could not find datasource")
|
||||
return nil, fmt.Errorf("Could not find datasource %v", err)
|
||||
}
|
||||
|
||||
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
|
||||
result := make(tsdb.TimeSeriesSlice, 0)
|
||||
|
||||
resp, err := c.HandleRequest(context.Ctx, req)
|
||||
resp, err := c.HandleRequest(context.Ctx, getDsInfo.Result, req)
|
||||
if err != nil {
|
||||
if err == gocontext.DeadlineExceeded {
|
||||
return nil, fmt.Errorf("Alert execution exceeded the timeout")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
|
||||
}
|
||||
|
||||
@@ -133,8 +139,8 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
|
||||
req := &tsdb.Request{
|
||||
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.TsdbQuery {
|
||||
req := &tsdb.TsdbQuery{
|
||||
TimeRange: timeRange,
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
|
||||
@@ -168,7 +168,7 @@ func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error)
|
||||
|
||||
ctx.condition = condition
|
||||
|
||||
condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) {
|
||||
condition.HandleRequest = func(context context.Context, dsInfo *m.DataSource, req *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
return &tsdb.Response{
|
||||
Results: map[string]*tsdb.QueryResult{
|
||||
"A": {Series: ctx.series},
|
||||
|
||||
@@ -94,6 +94,63 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
|
||||
value = (values[(length/2)-1] + values[length/2]) / 2
|
||||
}
|
||||
}
|
||||
case "diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get other points
|
||||
points = points[0:i]
|
||||
for i := len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
value = first - points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
case "percent_diff":
|
||||
var (
|
||||
points = series.Points
|
||||
first float64
|
||||
i int
|
||||
)
|
||||
// get the newest point
|
||||
for i = len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
first = points[i][0].Float64
|
||||
break
|
||||
}
|
||||
}
|
||||
// get other points
|
||||
points = points[0:i]
|
||||
for i := len(points) - 1; i >= 0; i-- {
|
||||
if points[i][0].Valid {
|
||||
allNull = false
|
||||
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
|
||||
value = math.Abs(val)
|
||||
break
|
||||
}
|
||||
}
|
||||
case "count_non_null":
|
||||
for _, v := range series.Points {
|
||||
if v[0].Valid {
|
||||
value++
|
||||
}
|
||||
}
|
||||
|
||||
if value > 0 {
|
||||
allNull = false
|
||||
}
|
||||
}
|
||||
|
||||
if allNull {
|
||||
|
||||
@@ -67,6 +67,35 @@ func TestSimpleReducer(t *testing.T) {
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, false)
|
||||
})
|
||||
|
||||
Convey("count_non_null", func() {
|
||||
Convey("with null values and real values", func() {
|
||||
reducer := NewSimpleReducer("count_non_null")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 3))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
|
||||
|
||||
So(reducer.Reduce(series).Valid, ShouldEqual, true)
|
||||
So(reducer.Reduce(series).Float64, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("with null values", func() {
|
||||
reducer := NewSimpleReducer("count_non_null")
|
||||
series := &tsdb.TimeSeries{
|
||||
Name: "test time serie",
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1))
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
|
||||
|
||||
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{
|
||||
@@ -80,6 +109,17 @@ func TestSimpleReducer(t *testing.T) {
|
||||
|
||||
So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
|
||||
})
|
||||
|
||||
Convey("diff", func() {
|
||||
result := testReducer("diff", 30, 40)
|
||||
So(result, ShouldEqual, float64(10))
|
||||
})
|
||||
|
||||
Convey("percent_diff", func() {
|
||||
result := testReducer("percent_diff", 30, 40)
|
||||
So(result, ShouldEqual, float64(33.33333333333333))
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/opentracing/opentracing-go/ext"
|
||||
tlog "github.com/opentracing/opentracing-go/log"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -81,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
case <-grafanaCtx.Done():
|
||||
return dispatcherGroup.Wait()
|
||||
case job := <-e.execQueue:
|
||||
dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
|
||||
dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
unfinishedWorkTimeout time.Duration = time.Second * 5
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
alertMaxAttempts int = 3
|
||||
)
|
||||
|
||||
func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
}
|
||||
}()
|
||||
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
attemptChan := make(chan int, 1)
|
||||
|
||||
// Initialize with first attemptID=1
|
||||
attemptChan <- 1
|
||||
job.Running = true
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
// In case grafana server context is cancel, let a chance to job processing
|
||||
// to finish gracefully - by waiting a timeout duration - before forcing its end.
|
||||
unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout)
|
||||
select {
|
||||
case <-unfinishedWorkTimer.C:
|
||||
return e.endJob(grafanaCtx.Err(), cancelChan, job)
|
||||
case <-attemptChan:
|
||||
return e.endJob(nil, cancelChan, job)
|
||||
}
|
||||
case attemptID, more := <-attemptChan:
|
||||
if !more {
|
||||
return e.endJob(nil, cancelChan, job)
|
||||
}
|
||||
go e.processJob(attemptID, attemptChan, cancelChan, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
job.Running = false
|
||||
close(cancelChan)
|
||||
for cancelFn := range cancelChan {
|
||||
cancelFn()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
@@ -99,39 +150,53 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
}()
|
||||
|
||||
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
|
||||
cancelChan <- cancelFn
|
||||
span := opentracing.StartSpan("alert execution")
|
||||
alertCtx = opentracing.ContextWithSpan(alertCtx, span)
|
||||
|
||||
job.Running = true
|
||||
evalContext := NewEvalContext(alertCtx, job.Rule)
|
||||
|
||||
done := make(chan struct{})
|
||||
evalContext.Ctx = alertCtx
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
close(done)
|
||||
ext.Error.Set(span, true)
|
||||
span.LogFields(
|
||||
tlog.Error(fmt.Errorf("%v", err)),
|
||||
tlog.String("message", "failed to execute alert rule. panic was recovered."),
|
||||
)
|
||||
span.Finish()
|
||||
close(attemptChan)
|
||||
}
|
||||
}()
|
||||
|
||||
e.evalHandler.Eval(evalContext)
|
||||
e.resultHandler.Handle(evalContext)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
var err error = nil
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
select {
|
||||
case <-time.After(unfinishedWorkTimeout):
|
||||
cancelFn()
|
||||
err = grafanaCtx.Err()
|
||||
case <-done:
|
||||
span.SetTag("alertId", evalContext.Rule.Id)
|
||||
span.SetTag("dashboardId", evalContext.Rule.DashboardId)
|
||||
span.SetTag("firing", evalContext.Firing)
|
||||
span.SetTag("nodatapoints", evalContext.NoDataFound)
|
||||
span.SetTag("attemptID", attemptID)
|
||||
|
||||
if evalContext.Error != nil {
|
||||
ext.Error.Set(span, true)
|
||||
span.LogFields(
|
||||
tlog.Error(evalContext.Error),
|
||||
tlog.String("message", "alerting execution attempt failed"),
|
||||
)
|
||||
if attemptID < alertMaxAttempts {
|
||||
span.Finish()
|
||||
e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
|
||||
attemptChan <- (attemptID + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
}
|
||||
|
||||
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
|
||||
job.Running = false
|
||||
cancelFn()
|
||||
return err
|
||||
evalContext.Rule.State = evalContext.GetNewState()
|
||||
e.resultHandler.Handle(evalContext)
|
||||
span.Finish()
|
||||
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
|
||||
close(attemptChan)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type FakeEvalHandler struct {
|
||||
SuccessCallID int // 0 means never sucess
|
||||
CallNb int
|
||||
}
|
||||
|
||||
func NewFakeEvalHandler(successCallID int) *FakeEvalHandler {
|
||||
return &FakeEvalHandler{
|
||||
SuccessCallID: successCallID,
|
||||
CallNb: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) {
|
||||
handler.CallNb++
|
||||
if handler.CallNb != handler.SuccessCallID {
|
||||
evalContext.Error = errors.New("Fake evaluation failure")
|
||||
}
|
||||
}
|
||||
|
||||
type FakeResultHandler struct{}
|
||||
|
||||
func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestEngineProcessJob(t *testing.T) {
|
||||
Convey("Alerting engine job processing", t, func() {
|
||||
engine := NewEngine()
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{Running: true, Rule: &Rule{}}
|
||||
|
||||
Convey("Should trigger retry if needed", func() {
|
||||
|
||||
Convey("error + not last attempt -> retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
|
||||
for i := 1; i < alertMaxAttempts; i++ {
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(i, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, i+1)
|
||||
So(more, ShouldEqual, true)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("error + last attempt -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("no error -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(1)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(1, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
|
||||
Convey("never sucess -> max retries number", func() {
|
||||
expectedAttempts := alertMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("always sucess -> never retry", func() {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("some errors before sucess -> some retries", func() {
|
||||
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -12,17 +12,19 @@ import (
|
||||
)
|
||||
|
||||
type EvalContext struct {
|
||||
Firing bool
|
||||
IsTestRun bool
|
||||
EvalMatches []*EvalMatch
|
||||
Logs []*ResultLogEntry
|
||||
Error error
|
||||
ConditionEvals string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Rule *Rule
|
||||
log log.Logger
|
||||
dashboardSlug string
|
||||
Firing bool
|
||||
IsTestRun bool
|
||||
EvalMatches []*EvalMatch
|
||||
Logs []*ResultLogEntry
|
||||
Error error
|
||||
ConditionEvals string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Rule *Rule
|
||||
log log.Logger
|
||||
|
||||
dashboardRef *m.DashboardRef
|
||||
|
||||
ImagePublicUrl string
|
||||
ImageOnDiskPath string
|
||||
NoDataFound bool
|
||||
@@ -75,14 +77,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
|
||||
return c.Rule.State != c.PrevAlertState
|
||||
}
|
||||
|
||||
func (c *EvalContext) ShouldSendNotification() bool {
|
||||
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *EvalContext) GetDurationMs() float64 {
|
||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||
}
|
||||
@@ -91,29 +85,61 @@ func (c *EvalContext) GetNotificationTitle() string {
|
||||
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetDashboardSlug() (string, error) {
|
||||
if c.dashboardSlug != "" {
|
||||
return c.dashboardSlug, nil
|
||||
func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) {
|
||||
if c.dashboardRef != nil {
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
slugQuery := &m.GetDashboardSlugByIdQuery{Id: c.Rule.DashboardId}
|
||||
if err := bus.Dispatch(slugQuery); err != nil {
|
||||
return "", err
|
||||
uidQuery := &m.GetDashboardRefByIdQuery{Id: c.Rule.DashboardId}
|
||||
if err := bus.Dispatch(uidQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.dashboardSlug = slugQuery.Result
|
||||
return c.dashboardSlug, nil
|
||||
c.dashboardRef = uidQuery.Result
|
||||
return c.dashboardRef, nil
|
||||
}
|
||||
|
||||
const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d"
|
||||
|
||||
func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
if c.IsTestRun {
|
||||
return setting.AppUrl, nil
|
||||
}
|
||||
|
||||
if slug, err := c.GetDashboardSlug(); err != nil {
|
||||
if ref, err := c.GetDashboardUID(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d", setting.AppUrl, slug, c.Rule.PanelId, c.Rule.OrgId)
|
||||
return ruleUrl, nil
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
if c.Error != nil {
|
||||
c.log.Error("Alert Rule Result Error",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
"error", c.Error,
|
||||
"changing state to", c.Rule.ExecutionErrorState.ToAlertState())
|
||||
|
||||
if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.ExecutionErrorState.ToAlertState()
|
||||
|
||||
} else if c.Firing {
|
||||
return m.AlertStateAlerting
|
||||
|
||||
} else if c.NoDataFound {
|
||||
c.log.Info("Alert Rule returned no data",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
"changing state to", c.Rule.NoDataState.ToAlertState())
|
||||
|
||||
if c.Rule.NoDataState == m.NoDataKeepState {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.NoDataState.ToAlertState()
|
||||
}
|
||||
|
||||
return m.AlertStateOK
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@@ -12,7 +13,7 @@ func TestAlertingEvalContext(t *testing.T) {
|
||||
Convey("Eval context", t, func() {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
|
||||
Convey("Should update alert state", func() {
|
||||
Convey("Should update alert state when needed", func() {
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
@@ -29,19 +30,69 @@ func TestAlertingEvalContext(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should send notifications", func() {
|
||||
Convey("pending -> ok", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.State = models.AlertStateOK
|
||||
|
||||
So(ctx.ShouldSendNotification(), ShouldBeFalse)
|
||||
})
|
||||
Convey("Should compute and replace properly new rule state", func() {
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.State = models.AlertStateAlerting
|
||||
ctx.Firing = true
|
||||
|
||||
So(ctx.ShouldSendNotification(), ShouldBeTrue)
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
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 = ctx.GetNewState()
|
||||
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 = ctx.GetNewState()
|
||||
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 = ctx.GetNewState()
|
||||
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 = ctx.GetNewState()
|
||||
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 = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DefaultEvalHandler struct {
|
||||
@@ -39,6 +38,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
break
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
firing = cr.Firing
|
||||
noDataFound = cr.NoDataFound
|
||||
}
|
||||
|
||||
// calculating Firing based on operator
|
||||
if cr.Operator == "or" {
|
||||
firing = firing || cr.Firing
|
||||
@@ -61,40 +65,7 @@ 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
|
||||
elapsedTime := context.EndTime.Sub(context.StartTime).Nanoseconds() / int64(time.Millisecond)
|
||||
metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime))
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -36,6 +34,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
|
||||
Convey("Show return triggered with single passing condition2", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, true)
|
||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||
})
|
||||
|
||||
Convey("Show return false with not passing asdf", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
@@ -131,6 +139,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
|
||||
})
|
||||
|
||||
Convey("Should return false if no condition is firing using OR operator", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
&conditionStub{firing: false, operator: "or"},
|
||||
},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
|
||||
})
|
||||
|
||||
Convey("Should retuasdfrn no data if one condition has nodata", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
&conditionStub{operator: "or", noData: false},
|
||||
},
|
||||
})
|
||||
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Should return no data if one condition has nodata", func() {
|
||||
context := NewEvalContext(context.TODO(), &Rule{
|
||||
Conditions: []Condition{
|
||||
@@ -166,73 +201,5 @@ func TestAlertingEvaluationHandler(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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,150 +11,217 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// DashAlertExtractor extracts alerts from the dashboard json
|
||||
type DashAlertExtractor struct {
|
||||
Dash *m.Dashboard
|
||||
OrgId int64
|
||||
OrgID int64
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
|
||||
// NewDashAlertExtractor returns a new DashAlertExtractor
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
||||
return &DashAlertExtractor{
|
||||
Dash: dash,
|
||||
OrgId: orgId,
|
||||
OrgID: orgID,
|
||||
log: log.New("alerting.extractor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
|
||||
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
|
||||
if dsName == "" {
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Could not find datasource id for " + dsName)
|
||||
}
|
||||
|
||||
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
|
||||
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
|
||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(targetsObj)
|
||||
|
||||
if target.Get("refId").MustString() == refId {
|
||||
if target.Get("refId").MustString() == refID {
|
||||
return target
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJson, err := in.MarshalJSON()
|
||||
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJSON, err := in.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return simplejson.NewJson(rawJson)
|
||||
return simplejson.NewJson(rawJSON)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("GetAlerts")
|
||||
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
|
||||
alerts := make([]*m.Alert, 0)
|
||||
|
||||
dashboardJson, err := copyJson(e.Dash.Data)
|
||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
|
||||
collapsedJSON, collapsed := panel.CheckGet("collapsed")
|
||||
// check if the panel is collapsed
|
||||
if collapsed && collapsedJSON.MustBool() {
|
||||
|
||||
// extract alerts from sub panels for collapsed panels
|
||||
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts = append(alerts, als...)
|
||||
continue
|
||||
}
|
||||
|
||||
jsonAlert, hasAlert := panel.CheckGet("alert")
|
||||
|
||||
if !hasAlert {
|
||||
continue
|
||||
}
|
||||
|
||||
panelID, err := panel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("panel id is required. err %v", err)
|
||||
}
|
||||
|
||||
// backward compatibility check, can be removed later
|
||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||
if hasEnabled && enabled.MustBool() == false {
|
||||
continue
|
||||
}
|
||||
|
||||
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
||||
}
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgID,
|
||||
PanelId: panelID,
|
||||
Id: jsonAlert.Get("id").MustInt64(),
|
||||
Name: jsonAlert.Get("name").MustString(),
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
Message: jsonAlert.Get("message").MustString(),
|
||||
Frequency: frequency,
|
||||
}
|
||||
|
||||
for _, condition := range jsonAlert.Get("conditions").MustArray() {
|
||||
jsonCondition := simplejson.NewFromAny(condition)
|
||||
|
||||
jsonQuery := jsonCondition.Get("query")
|
||||
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefID(panel, queryRefID)
|
||||
|
||||
if panelQuery == nil {
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
|
||||
return nil, ValidationError{Reason: reason}
|
||||
}
|
||||
|
||||
dsName := ""
|
||||
if panelQuery.Get("datasource").MustString() != "" {
|
||||
dsName = panelQuery.Get("datasource").MustString()
|
||||
} else if panel.Get("datasource").MustString() != "" {
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
datasource, err := e.lookupDatasourceID(dsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
|
||||
if interval, err := panel.Get("interval").String(); err == nil {
|
||||
panelQuery.Set("interval", interval)
|
||||
}
|
||||
|
||||
jsonQuery.Set("model", panelQuery.Interface())
|
||||
}
|
||||
|
||||
alert.Settings = jsonAlert
|
||||
|
||||
// validate
|
||||
_, err = NewRuleFromDBAlert(alert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !validateAlertFunc(alert) {
|
||||
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func validateAlertRule(alert *m.Alert) bool {
|
||||
return alert.ValidToSave()
|
||||
}
|
||||
|
||||
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
return e.extractAlerts(validateAlertRule)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
|
||||
dashboardJSON, err := copyJSON(e.Dash.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts := make([]*m.Alert, 0)
|
||||
for _, rowObj := range dashboardJson.Get("rows").MustArray() {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
|
||||
for _, panelObj := range row.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
jsonAlert, hasAlert := panel.CheckGet("alert")
|
||||
|
||||
if !hasAlert {
|
||||
continue
|
||||
}
|
||||
|
||||
// backward compatability check, can be removed later
|
||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||
if hasEnabled && enabled.MustBool() == false {
|
||||
continue
|
||||
}
|
||||
|
||||
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
|
||||
// We extract alerts from rows to be backwards compatible
|
||||
// with the old dashboard json model.
|
||||
rows := dashboardJSON.Get("rows").MustArray()
|
||||
if len(rows) > 0 {
|
||||
for _, rowObj := range rows {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
a, err := e.getAlertFromPanels(row, validateFunc)
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Could not parse frequency"}
|
||||
}
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgId,
|
||||
PanelId: panel.Get("id").MustInt64(),
|
||||
Id: jsonAlert.Get("id").MustInt64(),
|
||||
Name: jsonAlert.Get("name").MustString(),
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
Message: jsonAlert.Get("message").MustString(),
|
||||
Frequency: frequency,
|
||||
}
|
||||
|
||||
for _, condition := range jsonAlert.Get("conditions").MustArray() {
|
||||
jsonCondition := simplejson.NewFromAny(condition)
|
||||
|
||||
jsonQuery := jsonCondition.Get("query")
|
||||
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefId(panel, queryRefId)
|
||||
|
||||
if panelQuery == nil {
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
|
||||
return nil, ValidationError{Reason: reason}
|
||||
}
|
||||
|
||||
dsName := ""
|
||||
if panelQuery.Get("datasource").MustString() != "" {
|
||||
dsName = panelQuery.Get("datasource").MustString()
|
||||
} else if panel.Get("datasource").MustString() != "" {
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
}
|
||||
|
||||
if interval, err := panel.Get("interval").String(); err == nil {
|
||||
panelQuery.Set("interval", interval)
|
||||
}
|
||||
|
||||
jsonQuery.Set("model", panelQuery.Interface())
|
||||
}
|
||||
|
||||
alert.Settings = jsonAlert
|
||||
|
||||
// validate
|
||||
_, err = NewRuleFromDBAlert(alert)
|
||||
if err == nil && alert.ValidToSave() {
|
||||
alerts = append(alerts, alert)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts = append(alerts, a...)
|
||||
}
|
||||
} else {
|
||||
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts = append(alerts, a...)
|
||||
}
|
||||
|
||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
||||
// in the first validation pass
|
||||
func (e *DashAlertExtractor) ValidateAlerts() error {
|
||||
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -18,14 +18,11 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
return &FakeCondition{}, nil
|
||||
})
|
||||
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
// mock data
|
||||
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
||||
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
||||
influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
|
||||
prom := &m.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
|
||||
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
|
||||
@@ -42,73 +39,15 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
if query.Name == influxDBDs.Name {
|
||||
query.Result = influxDBDs
|
||||
}
|
||||
if query.Name == prom.Name {
|
||||
query.Result = prom
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
json := `
|
||||
{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": ["graphite"],
|
||||
"rows": [
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["A", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{"refId": "A", "target": ""},
|
||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"alert": {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["B", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Extractor should not modify the original json", func() {
|
||||
dashJson, err := simplejson.NewJson([]byte(json))
|
||||
@@ -200,294 +139,63 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Panels missing id should return error", func() {
|
||||
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
_, err = extractor.GetAlerts()
|
||||
|
||||
Convey("panels without Id should return error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Panel with id set to zero should return error", func() {
|
||||
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson([]byte(panelWithIdZero))
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
_, err = extractor.GetAlerts()
|
||||
|
||||
Convey("panel with id 0 should return error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse alerts from dashboard without rows", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
Convey("Get rules without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should have 2 alert rule", func() {
|
||||
So(len(alerts), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
json2 := `{
|
||||
"id": 4,
|
||||
"title": "Influxdb",
|
||||
"tags": [
|
||||
"apa"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "450px",
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
10
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"B",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"frequency": "3s",
|
||||
"handler": 1,
|
||||
"name": "Influxdb",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
{
|
||||
"id": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
"alerting": {},
|
||||
"aliasColors": {
|
||||
"logins.count.count": "#890F02"
|
||||
},
|
||||
"bars": false,
|
||||
"datasource": "InfluxDB",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {},
|
||||
"id": 1,
|
||||
"interval": ">10s",
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"dsType": "influxdb",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"datacenter"
|
||||
],
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"none"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "logins.count",
|
||||
"policy": "default",
|
||||
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
|
||||
"rawQuery": true,
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "count"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"dsType": "influxdb",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": true,
|
||||
"measurement": "cpu",
|
||||
"policy": "default",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "sum"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 10
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"ordering": "alphabetical",
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"limit": 10,
|
||||
"links": [],
|
||||
"show": "current",
|
||||
"span": 2,
|
||||
"stateFilter": [
|
||||
"alerting"
|
||||
],
|
||||
"title": "Alert status",
|
||||
"type": "alertlist"
|
||||
}
|
||||
],
|
||||
"title": "Row"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"now": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 120,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}`
|
||||
|
||||
dashJson, err := simplejson.NewJson([]byte(json2))
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
@@ -511,5 +219,47 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should be able to extract collapsed panels", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJson, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dash := m.NewDashboardFromJson(dashJson)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
alerts, err := extractor.GetAlerts()
|
||||
|
||||
Convey("Get rules without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("should be able to extract collapsed alerts", func() {
|
||||
So(len(alerts), ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJSON)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
err = extractor.ValidateAlerts()
|
||||
|
||||
Convey("Should validate without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should fail on save", func() {
|
||||
_, err := extractor.GetAlerts()
|
||||
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type Notifier interface {
|
||||
Notify(evalContext *EvalContext) error
|
||||
GetType() string
|
||||
NeedsImage() bool
|
||||
PassesFilter(rule *Rule) bool
|
||||
ShouldNotify(evalContext *EvalContext) bool
|
||||
|
||||
GetNotifierId() int64
|
||||
GetIsDefault() bool
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||
"github.com/grafana/grafana/pkg/components/renderer"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
@@ -22,7 +24,7 @@ type NotifierPlugin struct {
|
||||
}
|
||||
|
||||
type NotificationService interface {
|
||||
Send(context *EvalContext) error
|
||||
SendIfNeeded(context *EvalContext) error
|
||||
}
|
||||
|
||||
func NewNotificationService() NotificationService {
|
||||
@@ -39,14 +41,12 @@ func newNotificationService() *notificationService {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *notificationService) Send(context *EvalContext) error {
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||
notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
|
||||
|
||||
if len(notifiers) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -65,7 +65,8 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
not := notifier //avoid updating scope variable in go routine
|
||||
n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||
g.Go(func() error { return not.Notify(context) })
|
||||
}
|
||||
|
||||
@@ -79,16 +80,17 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
}
|
||||
|
||||
renderOpts := &renderer.RenderOpts{
|
||||
Width: "800",
|
||||
Height: "400",
|
||||
Timeout: "30",
|
||||
OrgId: context.Rule.OrgId,
|
||||
Width: "800",
|
||||
Height: "400",
|
||||
Timeout: "30",
|
||||
OrgId: context.Rule.OrgId,
|
||||
IsAlertContext: true,
|
||||
}
|
||||
|
||||
if slug, err := context.GetDashboardSlug(); err != nil {
|
||||
if ref, err := context.GetDashboardUID(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId)
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
}
|
||||
|
||||
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
|
||||
@@ -97,7 +99,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
context.ImageOnDiskPath = imagePath
|
||||
}
|
||||
|
||||
context.ImagePublicUrl, err = uploader.Upload(context.ImageOnDiskPath)
|
||||
context.ImagePublicUrl, err = uploader.Upload(context.Ctx, context.ImageOnDiskPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -106,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@@ -118,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
|
||||
if not, err := n.createNotifierFor(notification); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if shouldUseNotification(not, context) {
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
@@ -136,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
|
||||
return notifierPlugin.Factory(model)
|
||||
}
|
||||
|
||||
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||
if !context.Firing {
|
||||
return true
|
||||
}
|
||||
|
||||
if context.Error != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return notifier.PassesFilter(context.Rule)
|
||||
}
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type FakeNotifier struct {
|
||||
FakeMatchResult bool
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) GetType() string {
|
||||
return "FakeNotifier"
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) NeedsImage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *FakeNotifier) GetNotifierId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (n *FakeNotifier) GetIsDefault() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
|
||||
|
||||
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
|
||||
return fn.FakeMatchResult
|
||||
}
|
||||
|
||||
func TestAlertNotificationExtraction(t *testing.T) {
|
||||
|
||||
Convey("Notifier tests", t, func() {
|
||||
Convey("none firing alerts", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: false,
|
||||
Rule: &Rule{
|
||||
State: m.AlertStateAlerting,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("execution error cannot be ignored", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Error: fmt.Errorf("I used to be a programmer just like you"),
|
||||
Rule: &Rule{
|
||||
State: m.AlertStateOK,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{
|
||||
State: models.AlertStateAlerting,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: true}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that dont match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{State: m.AlertStateOK},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "prometheus-alertmanager",
|
||||
Name: "Prometheus Alertmanager",
|
||||
Description: "Sends alert to Prometheus Alertmanager",
|
||||
Factory: NewAlertmanagerNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Alertmanager 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://localhost:9093"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewAlertmanagerNotifier(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 &AlertmanagerNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.prometheus-alertmanager"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AlertmanagerNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
|
||||
this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState)
|
||||
|
||||
// Do not notify when we become OK for the first time.
|
||||
if (evalContext.PrevAlertState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
// Notify on Alerting -> OK to resolve before alertmanager timeout.
|
||||
if (evalContext.PrevAlertState == m.AlertStateAlerting) && (evalContext.Rule.State == m.AlertStateOK) {
|
||||
return true
|
||||
}
|
||||
return evalContext.Rule.State == m.AlertStateAlerting
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, match *alerting.EvalMatch, ruleUrl string) *simplejson.Json {
|
||||
alertJSON := simplejson.New()
|
||||
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
|
||||
if evalContext.Rule.State == m.AlertStateOK {
|
||||
alertJSON.Set("endsAt", time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
alertJSON.Set("generatorURL", ruleUrl)
|
||||
|
||||
// Annotations (summary and description are very commonly used).
|
||||
alertJSON.SetPath([]string{"annotations", "summary"}, evalContext.Rule.Name)
|
||||
description := ""
|
||||
if evalContext.Rule.Message != "" {
|
||||
description += evalContext.Rule.Message
|
||||
}
|
||||
if evalContext.Error != nil {
|
||||
if description != "" {
|
||||
description += "\n"
|
||||
}
|
||||
description += "Error: " + evalContext.Error.Error()
|
||||
}
|
||||
if description != "" {
|
||||
alertJSON.SetPath([]string{"annotations", "description"}, description)
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
alertJSON.SetPath([]string{"annotations", "image"}, evalContext.ImagePublicUrl)
|
||||
}
|
||||
|
||||
// Labels (from metrics tags + mandatory alertname).
|
||||
tags := make(map[string]string)
|
||||
if match != nil {
|
||||
if len(match.Tags) == 0 {
|
||||
tags["metric"] = match.Metric
|
||||
} else {
|
||||
for k, v := range match.Tags {
|
||||
tags[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
tags["alertname"] = evalContext.Rule.Name
|
||||
alertJSON.Set("labels", tags)
|
||||
return alertJSON
|
||||
}
|
||||
|
||||
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending Alertmanager alert", "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
|
||||
}
|
||||
|
||||
// Send one alert per matching series.
|
||||
alerts := make([]interface{}, 0)
|
||||
for _, match := range evalContext.EvalMatches {
|
||||
alert := this.createAlert(evalContext, match, ruleUrl)
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
// This happens on ExecutionError or NoData
|
||||
if len(alerts) == 0 {
|
||||
alert := this.createAlert(evalContext, nil, ruleUrl)
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.NewFromAny(alerts)
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url + "/api/v1/alerts",
|
||||
HttpMethod: "POST",
|
||||
Body: string(body),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send alertmanager", "error", err, "alertmanager", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestAlertmanagerNotifier(t *testing.T) {
|
||||
Convey("Alertmanager 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: "alertmanager",
|
||||
Type: "alertmanager",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewAlertmanagerNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `{ "url": "http://127.0.0.1:9093/" }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "alertmanager",
|
||||
Type: "alertmanager",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewAlertmanagerNotifier(model)
|
||||
alertmanagerNotifier := not.(*AlertmanagerNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(alertmanagerNotifier.Url, ShouldEqual, "http://127.0.0.1:9093/")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,11 @@ type NotifierBase struct {
|
||||
}
|
||||
|
||||
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
uploadImage := model.Get("uploadImage").MustBool(true)
|
||||
uploadImage := true
|
||||
value, exist := model.CheckGet("uploadImage")
|
||||
if exist {
|
||||
uploadImage = value.MustBool()
|
||||
}
|
||||
|
||||
return NotifierBase{
|
||||
Id: id,
|
||||
@@ -25,10 +30,22 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool {
|
||||
func defaultShouldNotify(context *alerting.EvalContext) bool {
|
||||
// Only notify on state change.
|
||||
if context.PrevAlertState == context.Rule.State {
|
||||
return false
|
||||
}
|
||||
// Do not notify when we become OK for the first time.
|
||||
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool {
|
||||
return defaultShouldNotify(context)
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetType() string {
|
||||
return n.Type
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("Base notifier tests", t, func() {
|
||||
Convey("default constructor for notifiers", func() {
|
||||
bJson := simplejson.New()
|
||||
|
||||
Convey("can parse false value", func() {
|
||||
bJson.Set("uploadImage", false)
|
||||
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("can parse true value", func() {
|
||||
bJson.Set("uploadImage", true)
|
||||
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("default value should be true for backwards compatibility", func() {
|
||||
base := NewNotifierBase(1, false, "name", "email", bJson)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("should notify", func() {
|
||||
Convey("pending -> ok", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStatePending,
|
||||
})
|
||||
context.Rule.State = m.AlertStateOK
|
||||
So(defaultShouldNotify(context), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
context.Rule.State = m.AlertStateAlerting
|
||||
So(defaultShouldNotify(context), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "dingding",
|
||||
Name: "DingDing",
|
||||
Description: "Sends HTTP POST request to DingDing",
|
||||
Factory: NewDingDingNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">DingDing 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="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewDingDingNotifier(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 &DingDingNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.dingding"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DingDingNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending dingding")
|
||||
|
||||
messageUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name)
|
||||
messageUrl = ""
|
||||
}
|
||||
this.log.Info("messageUrl:" + messageUrl)
|
||||
|
||||
message := evalContext.Rule.Message
|
||||
picUrl := evalContext.ImagePublicUrl
|
||||
title := evalContext.GetNotificationTitle()
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(`{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"text": "` + message + `",
|
||||
"title": "` + title + `",
|
||||
"picUrl": "` + picUrl + `",
|
||||
"messageUrl": "` + messageUrl + `"
|
||||
}
|
||||
}`))
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url,
|
||||
Body: string(body),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send DingDing", "error", err, "dingding", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDingDingNotifier(t *testing.T) {
|
||||
Convey("Dingding notifier tests", t, func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "dingding_testing",
|
||||
Type: "dingding",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewDingDingNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
})
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `{ "url": "https://www.google.com" }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "dingding_testing",
|
||||
Type: "dingding",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewDingDingNotifier(model)
|
||||
notifier := not.(*DingDingNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(notifier.Name, ShouldEqual, "dingding_testing")
|
||||
So(notifier.Type, ShouldEqual, "dingding")
|
||||
So(notifier.Url, ShouldEqual, "https://www.google.com")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -21,7 +20,7 @@ func init() {
|
||||
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>
|
||||
<textarea rows="7" class="gf-form-input width-27" required ng-model="ctrl.model.settings.addresses"></textarea>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span>You can enter multiple email addresses using a ";" separator</span>
|
||||
@@ -61,7 +60,6 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
||||
func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "addresses", this.Addresses)
|
||||
metrics.M_Alerting_Notification_Sent_Email.Inc(1)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
@@ -69,6 +67,11 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
error := ""
|
||||
if evalContext.Error != nil {
|
||||
error = evalContext.Error.Error()
|
||||
}
|
||||
|
||||
cmd := &m.SendEmailCommandSync{
|
||||
SendEmailCommand: m.SendEmailCommand{
|
||||
Subject: evalContext.GetNotificationTitle(),
|
||||
@@ -78,6 +81,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
"Name": evalContext.Rule.Name,
|
||||
"StateModel": evalContext.GetStateModel(),
|
||||
"Message": evalContext.Rule.Message,
|
||||
"Error": error,
|
||||
"RuleUrl": ruleUrl,
|
||||
"ImageLink": "",
|
||||
"EmbededImage": "",
|
||||
|
||||
@@ -84,15 +84,17 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
attributes := make([]map[string]interface{}, 0)
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
message += evt.Metric + " :: " + strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64) + "<br>"
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": evt.Metric,
|
||||
"value": evt.Value,
|
||||
"short": true,
|
||||
metricName := evt.Metric
|
||||
if len(metricName) > 50 {
|
||||
metricName = metricName[:50]
|
||||
}
|
||||
attributes = append(attributes, map[string]interface{}{
|
||||
"label": metricName,
|
||||
"value": map[string]interface{}{
|
||||
"label": strconv.FormatFloat(evt.Value.Float64, 'f', -1, 64),
|
||||
},
|
||||
})
|
||||
if index > maxFieldCount {
|
||||
break
|
||||
@@ -100,16 +102,23 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
"short": false,
|
||||
attributes = append(attributes, map[string]interface{}{
|
||||
"label": "Error message",
|
||||
"value": map[string]interface{}{
|
||||
"label": evalContext.Error.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
message := ""
|
||||
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
message = evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text
|
||||
}
|
||||
|
||||
//HipChat has a set list of colors
|
||||
var color string
|
||||
switch evalContext.Rule.State {
|
||||
@@ -123,15 +132,24 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
// Add a card with link to the dashboard
|
||||
card := map[string]interface{}{
|
||||
"style": "link",
|
||||
"style": "application",
|
||||
"url": ruleUrl,
|
||||
"id": "1",
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"description": evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text,
|
||||
"description": message,
|
||||
"icon": map[string]interface{}{
|
||||
"url": "https://grafana.com/assets/img/fav32.png",
|
||||
},
|
||||
"date": evalContext.EndTime.Unix(),
|
||||
"date": evalContext.EndTime.Unix(),
|
||||
"attributes": attributes,
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
card["thumbnail"] = map[string]interface{}{
|
||||
"url": evalContext.ImagePublicUrl,
|
||||
"url@2x": evalContext.ImagePublicUrl,
|
||||
"width": 1193,
|
||||
"height": 564,
|
||||
}
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
@@ -144,6 +162,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
hipUrl := fmt.Sprintf("%s/v2/room/%s/notification?auth_token=%s", this.Url, this.RoomId, this.ApiKey)
|
||||
data, _ := json.Marshal(&body)
|
||||
this.log.Info("Request payload", "json", string(data))
|
||||
cmd := &models.SendWebhookSync{Url: hipUrl, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "kafka",
|
||||
Name: "Kafka REST Proxy",
|
||||
Description: "Sends notifications to Kafka Rest Proxy",
|
||||
Factory: NewKafkaNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Kafka settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Kafka REST Proxy</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaRestProxy" placeholder="http://localhost:8082"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Topic</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.kafkaTopic" placeholder="topic1"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
endpoint := model.Settings.Get("kafkaRestProxy").MustString()
|
||||
if endpoint == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find kafka rest proxy endpoint property in settings"}
|
||||
}
|
||||
topic := model.Settings.Get("kafkaTopic").MustString()
|
||||
if topic == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find kafka topic property in settings"}
|
||||
}
|
||||
|
||||
return &KafkaNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Endpoint: endpoint,
|
||||
Topic: topic,
|
||||
log: log.New("alerting.notifier.kafka"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type KafkaNotifier struct {
|
||||
NotifierBase
|
||||
Endpoint string
|
||||
Topic string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
state := evalContext.Rule.State
|
||||
|
||||
customData := "Triggered metrics:\n\n"
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
||||
this.log.Info("Notifying Kafka", "alert_state", state)
|
||||
|
||||
recordJSON := simplejson.New()
|
||||
records := make([]interface{}, 1)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
|
||||
bodyJSON.Set("client", "Grafana")
|
||||
bodyJSON.Set("details", customData)
|
||||
bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
bodyJSON.Set("client_url", ruleUrl)
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
contexts := make([]interface{}, 1)
|
||||
imageJSON := simplejson.New()
|
||||
imageJSON.Set("type", "image")
|
||||
imageJSON.Set("src", evalContext.ImagePublicUrl)
|
||||
contexts[0] = imageJSON
|
||||
bodyJSON.Set("contexts", contexts)
|
||||
}
|
||||
|
||||
valueJSON := simplejson.New()
|
||||
valueJSON.Set("value", bodyJSON)
|
||||
records[0] = valueJSON
|
||||
recordJSON.Set("records", records)
|
||||
body, _ := recordJSON.MarshalJSON()
|
||||
|
||||
topicUrl := this.Endpoint + "/topics/" + this.Topic
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: topicUrl,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/vnd.kafka.json.v2+json",
|
||||
"Accept": "application/vnd.kafka.v2+json",
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send notification to Kafka", "error", err, "body", string(body))
|
||||
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 TestKafkaNotifier(t *testing.T) {
|
||||
Convey("Kafka 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: "kafka_testing",
|
||||
Type: "kafka",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewKafkaNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("settings should send an event to kafka", func() {
|
||||
json := `
|
||||
{
|
||||
"kafkaRestProxy": "http://localhost:8082",
|
||||
"kafkaTopic": "topic1"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "kafka_testing",
|
||||
Type: "kafka",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewKafkaNotifier(model)
|
||||
kafkaNotifier := not.(*KafkaNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(kafkaNotifier.Name, ShouldEqual, "kafka_testing")
|
||||
So(kafkaNotifier.Type, ShouldEqual, "kafka")
|
||||
So(kafkaNotifier.Endpoint, ShouldEqual, "http://localhost:8082")
|
||||
So(kafkaNotifier.Topic, ShouldEqual, "topic1")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -2,12 +2,12 @@ package notifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"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() {
|
||||
@@ -53,7 +53,6 @@ type LineNotifier struct {
|
||||
|
||||
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 {
|
||||
@@ -75,12 +74,17 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
|
||||
body := fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)
|
||||
form.Add("message", body)
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
form.Add("imageThumbnail", evalContext.ImagePublicUrl)
|
||||
form.Add("imageFullsize", evalContext.ImagePublicUrl)
|
||||
}
|
||||
|
||||
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",
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
Body: form.Encode(),
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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"
|
||||
)
|
||||
@@ -24,6 +23,10 @@ func init() {
|
||||
<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">
|
||||
<span class="gf-form-label width-14">Alert API Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiUrl" placeholder="https://api.opsgenie.com/v2/alerts"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
@@ -38,20 +41,24 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert"
|
||||
opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close"
|
||||
opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts"
|
||||
)
|
||||
|
||||
func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
autoClose := model.Settings.Get("autoClose").MustBool(true)
|
||||
apiKey := model.Settings.Get("apiKey").MustString()
|
||||
apiUrl := model.Settings.Get("apiUrl").MustString()
|
||||
if apiKey == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
|
||||
}
|
||||
if apiUrl == "" {
|
||||
apiUrl = opsgenieAlertURL
|
||||
}
|
||||
|
||||
return &OpsGenieNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
ApiKey: apiKey,
|
||||
ApiUrl: apiUrl,
|
||||
AutoClose: autoClose,
|
||||
log: log.New("alerting.notifier.opsgenie"),
|
||||
}, nil
|
||||
@@ -60,12 +67,12 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
type OpsGenieNotifier struct {
|
||||
NotifierBase
|
||||
ApiKey string
|
||||
ApiUrl string
|
||||
AutoClose bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
metrics.M_Alerting_Notification_Sent_OpsGenie.Inc(1)
|
||||
|
||||
var err error
|
||||
switch evalContext.Rule.State {
|
||||
@@ -88,12 +95,16 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
||||
return err
|
||||
}
|
||||
|
||||
customData := "Triggered metrics:\n\n"
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("apiKey", this.ApiKey)
|
||||
bodyJSON.Set("message", evalContext.Rule.Name)
|
||||
bodyJSON.Set("source", "Grafana")
|
||||
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message))
|
||||
bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message, customData))
|
||||
|
||||
details := simplejson.New()
|
||||
details.Set("url", ruleUrl)
|
||||
@@ -105,9 +116,13 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: opsgenieCreateAlertURL,
|
||||
Url: this.ApiUrl,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
@@ -121,14 +136,17 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro
|
||||
this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("apiKey", this.ApiKey)
|
||||
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
bodyJSON.Set("source", "Grafana")
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: opsgenieCloseAlertURL,
|
||||
Url: fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", this.ApiUrl, evalContext.Rule.Id),
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": fmt.Sprintf("GenieKey %s", this.ApiKey),
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -21,7 +24,7 @@ func init() {
|
||||
<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>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.integrationKey" placeholder="Pagerduty Integration Key"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
@@ -37,11 +40,11 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
pagerdutyEventApiUrl string = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
|
||||
pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue"
|
||||
)
|
||||
|
||||
func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
autoResolve := model.Settings.Get("autoResolve").MustBool(true)
|
||||
autoResolve := model.Settings.Get("autoResolve").MustBool(false)
|
||||
key := model.Settings.Get("integrationKey").MustString()
|
||||
if key == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"}
|
||||
@@ -63,7 +66,6 @@ type PagerdutyNotifier struct {
|
||||
}
|
||||
|
||||
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
metrics.M_Alerting_Notification_Sent_PagerDuty.Inc(1)
|
||||
|
||||
if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {
|
||||
this.log.Info("Not sending a trigger to Pagerduty", "state", evalContext.Rule.State, "auto resolve", this.AutoResolve)
|
||||
@@ -74,30 +76,48 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if evalContext.Rule.State == m.AlertStateOK {
|
||||
eventType = "resolve"
|
||||
}
|
||||
customData := "Triggered metrics:\n\n"
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||
}
|
||||
|
||||
this.log.Info("Notifying Pagerduty", "event_type", eventType)
|
||||
|
||||
payloadJSON := simplejson.New()
|
||||
payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
payloadJSON.Set("source", hostname)
|
||||
}
|
||||
payloadJSON.Set("severity", "critical")
|
||||
payloadJSON.Set("timestamp", time.Now())
|
||||
payloadJSON.Set("component", "Grafana")
|
||||
payloadJSON.Set("custom_details", customData)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("service_key", this.Key)
|
||||
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
|
||||
bodyJSON.Set("client", "Grafana")
|
||||
bodyJSON.Set("event_type", eventType)
|
||||
bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
bodyJSON.Set("routing_key", this.Key)
|
||||
bodyJSON.Set("event_action", eventType)
|
||||
bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
|
||||
bodyJSON.Set("payload", payloadJSON)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
links := make([]interface{}, 1)
|
||||
linkJSON := simplejson.New()
|
||||
linkJSON.Set("href", ruleUrl)
|
||||
bodyJSON.Set("client_url", ruleUrl)
|
||||
bodyJSON.Set("client", "Grafana")
|
||||
links[0] = linkJSON
|
||||
bodyJSON.Set("links", links)
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
contexts := make([]interface{}, 1)
|
||||
imageJSON := simplejson.New()
|
||||
imageJSON.Set("type", "image")
|
||||
imageJSON.Set("src", evalContext.ImagePublicUrl)
|
||||
contexts[0] = imageJSON
|
||||
bodyJSON.Set("contexts", contexts)
|
||||
bodyJSON.Set("images", contexts)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
@@ -106,6 +126,9 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
Url: pagerdutyEventApiUrl,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
func TestPagerdutyNotifier(t *testing.T) {
|
||||
Convey("Pagerduty notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
@@ -26,10 +25,31 @@ func TestPagerdutyNotifier(t *testing.T) {
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("auto resolve should default to false", func() {
|
||||
json := `{ "integrationKey": "abcdefgh0123456789" }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "pagerduty_testing",
|
||||
Type: "pagerduty",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewPagerdutyNotifier(model)
|
||||
pagerdutyNotifier := not.(*PagerdutyNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
|
||||
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
|
||||
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
|
||||
So(pagerdutyNotifier.AutoResolve, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"integrationKey": "abcdefgh0123456789"
|
||||
"integrationKey": "abcdefgh0123456789",
|
||||
"autoResolve": false
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
@@ -46,8 +66,8 @@ func TestPagerdutyNotifier(t *testing.T) {
|
||||
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
|
||||
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
|
||||
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
|
||||
So(pagerdutyNotifier.AutoResolve, ShouldBeFalse)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -125,12 +124,12 @@ type PushoverNotifier struct {
|
||||
}
|
||||
|
||||
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
metrics.M_Alerting_Notification_Sent_Pushover.Inc(1)
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.Rule.Message
|
||||
for idx, evt := range evalContext.EvalMatches {
|
||||
message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
|
||||
@@ -139,8 +138,15 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
}
|
||||
if evalContext.Error != nil {
|
||||
message += fmt.Sprintf("\n<b>Error message</b> %s", evalContext.Error.Error())
|
||||
message += fmt.Sprintf("\n<b>Error message:</b> %s", evalContext.Error.Error())
|
||||
}
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message += fmt.Sprintf("\n<a href=\"%s\">Show graph image</a>", evalContext.ImagePublicUrl)
|
||||
}
|
||||
if message == "" {
|
||||
message = "Notification message missing (Set a notification message to replace this text.)"
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Add("user", this.UserKey)
|
||||
q.Add("token", this.ApiToken)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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() {
|
||||
@@ -23,6 +23,14 @@ func init() {
|
||||
<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">Source</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.source" bs-tooltip="'If empty rule id will be used'" data-placement="right"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Handler</span>
|
||||
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.handler" placeholder="default"></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>
|
||||
@@ -46,7 +54,9 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
User: model.Settings.Get("username").MustString(),
|
||||
Source: model.Settings.Get("source").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
Handler: model.Settings.Get("handler").MustString(),
|
||||
log: log.New("alerting.notifier.sensu"),
|
||||
}, nil
|
||||
}
|
||||
@@ -54,22 +64,27 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
type SensuNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
Source string
|
||||
User string
|
||||
Password string
|
||||
Handler 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))
|
||||
// Sensu alerts require a source. We set it to the user-specified value (optional),
|
||||
// else we fallback and use the grafana ruleID.
|
||||
if this.Source != "" {
|
||||
bodyJSON.Set("source", this.Source)
|
||||
} else {
|
||||
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")
|
||||
@@ -83,6 +98,10 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
bodyJSON.Set("status", 0)
|
||||
}
|
||||
|
||||
if this.Handler != "" {
|
||||
bodyJSON.Set("handler", this.Handler)
|
||||
}
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err == nil {
|
||||
bodyJSON.Set("ruleUrl", ruleUrl)
|
||||
@@ -93,7 +112,7 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
if evalContext.Rule.Message != "" {
|
||||
bodyJSON.Set("message", evalContext.Rule.Message)
|
||||
bodyJSON.Set("output", evalContext.Rule.Message)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
@@ -29,7 +29,9 @@ func TestSensuNotifier(t *testing.T) {
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://sensu-api.example.com:4567/results"
|
||||
"url": "http://sensu-api.example.com:4567/results",
|
||||
"source": "grafana_instance_01",
|
||||
"handler": "myhandler"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
@@ -46,6 +48,8 @@ func TestSensuNotifier(t *testing.T) {
|
||||
So(sensuNotifier.Name, ShouldEqual, "sensu")
|
||||
So(sensuNotifier.Type, ShouldEqual, "sensu")
|
||||
So(sensuNotifier.Url, ShouldEqual, "http://sensu-api.example.com:4567/results")
|
||||
So(sensuNotifier.Source, ShouldEqual, "grafana_instance_01")
|
||||
So(sensuNotifier.Handler, ShouldEqual, "myhandler")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -16,7 +20,7 @@ func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "slack",
|
||||
Name: "Slack",
|
||||
Description: "Sends notifications using Grafana server configured STMP settings",
|
||||
Description: "Sends notifications to Slack via Slack Webhooks",
|
||||
Factory: NewSlackNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Slack settings</h3>
|
||||
@@ -46,6 +50,17 @@ func init() {
|
||||
Mention a user or a group using @ when notifying in a channel
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-6">Token</span>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-30"
|
||||
ng-model="ctrl.model.settings.token"
|
||||
data-placement="right">
|
||||
</input>
|
||||
<info-popover mode="right-absolute">
|
||||
Provide a bot token to use the Slack file.upload API (starts with "xoxb")
|
||||
</info-popover>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
@@ -59,12 +74,16 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
||||
recipient := model.Settings.Get("recipient").MustString()
|
||||
mention := model.Settings.Get("mention").MustString()
|
||||
token := model.Settings.Get("token").MustString()
|
||||
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
|
||||
|
||||
return &SlackNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
Recipient: recipient,
|
||||
Mention: mention,
|
||||
Token: token,
|
||||
Upload: uploadImage,
|
||||
log: log.New("alerting.notifier.slack"),
|
||||
}, nil
|
||||
}
|
||||
@@ -74,12 +93,13 @@ type SlackNotifier struct {
|
||||
Url string
|
||||
Recipient string
|
||||
Mention string
|
||||
Token string
|
||||
Upload bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
metrics.M_Alerting_Notification_Sent_Slack.Inc(1)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
@@ -112,16 +132,22 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
image_url := ""
|
||||
// default to file.upload API method if a token is provided
|
||||
if this.Token == "" {
|
||||
image_url = evalContext.ImagePublicUrl
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
{
|
||||
"fallback": evalContext.GetNotificationTitle(),
|
||||
"color": evalContext.GetStateModel().Color,
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"title_link": ruleUrl,
|
||||
"text": message,
|
||||
"fields": fields,
|
||||
"image_url": evalContext.ImagePublicUrl,
|
||||
"image_url": image_url,
|
||||
"footer": "Grafana v" + setting.BuildVersion,
|
||||
"footer_icon": "https://grafana.com/assets/img/fav32.png",
|
||||
"ts": time.Now().Unix(),
|
||||
@@ -134,14 +160,75 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
if this.Recipient != "" {
|
||||
body["channel"] = this.Recipient
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(&body)
|
||||
cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
if this.Token != "" && this.UploadImage {
|
||||
err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
|
||||
if evalContext.ImageOnDiskPath == "" {
|
||||
evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
|
||||
}
|
||||
log.Info("Uploading to slack via file.upload API")
|
||||
headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := &m.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"}
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload")
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
|
||||
// Slack requires all POSTs to files.upload to present
|
||||
// an "application/x-www-form-urlencoded" encoded querystring
|
||||
// See https://api.slack.com/methods/files.upload
|
||||
var b bytes.Buffer
|
||||
w := multipart.NewWriter(&b)
|
||||
// Add the generated image file
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
defer f.Close()
|
||||
fw, err := w.CreateFormFile("file", file)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
_, err = io.Copy(fw, f)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
// Add the authorization token
|
||||
err = w.WriteField("token", token)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
// Add the channel(s) to POST to
|
||||
err = w.WriteField("channels", recipient)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
w.Close()
|
||||
headers := map[string]string{
|
||||
"Content-Type": w.FormDataContentType(),
|
||||
"Authorization": "auth_token=\"" + token + "\"",
|
||||
}
|
||||
return headers, b, nil
|
||||
}
|
||||
|
||||
@@ -48,14 +48,16 @@ func TestSlackNotifier(t *testing.T) {
|
||||
So(slackNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(slackNotifier.Recipient, ShouldEqual, "")
|
||||
So(slackNotifier.Mention, ShouldEqual, "")
|
||||
So(slackNotifier.Token, ShouldEqual, "")
|
||||
})
|
||||
|
||||
Convey("from settings with Recipient and Mention", func() {
|
||||
Convey("from settings with Recipient, Mention, and Token", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com",
|
||||
"recipient": "#ds-opentsdb",
|
||||
"mention": "@carl"
|
||||
"mention": "@carl",
|
||||
"token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
@@ -74,8 +76,8 @@ func TestSlackNotifier(t *testing.T) {
|
||||
So(slackNotifier.Url, ShouldEqual, "http://google.com")
|
||||
So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb")
|
||||
So(slackNotifier.Mention, ShouldEqual, "@carl")
|
||||
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "teams",
|
||||
Name: "Microsoft Teams",
|
||||
Description: "Sends notifications using Incomming Webhook connector to Microsoft Teams",
|
||||
Factory: NewTeamsNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Teams 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="Teams incoming webhook url"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewTeamsNotifier(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 &TeamsNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.teams"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type TeamsNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
Recipient string
|
||||
Mention string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
fieldLimitCount := 4
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"name": evt.Metric,
|
||||
"value": evt.Value,
|
||||
})
|
||||
if index > fieldLimitCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"name": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
message := this.Mention
|
||||
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
} else {
|
||||
message += " " // summary must not be empty
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"summary": message,
|
||||
"title": evalContext.GetNotificationTitle(),
|
||||
"themeColor": evalContext.GetStateModel().Color,
|
||||
"sections": []map[string]interface{}{
|
||||
{
|
||||
"title": "Details",
|
||||
"facts": fields,
|
||||
"images": []map[string]interface{}{
|
||||
{
|
||||
"image": evalContext.ImagePublicUrl,
|
||||
},
|
||||
},
|
||||
"text": message,
|
||||
"potentialAction": []map[string]interface{}{
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "ViewAction",
|
||||
"name": "View Rule",
|
||||
"target": []string{
|
||||
ruleUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(&body)
|
||||
cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send teams notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestTeamsNotifier(t *testing.T) {
|
||||
Convey("Teams notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "teams",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewTeamsNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("from settings", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "teams",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewTeamsNotifier(model)
|
||||
teamsNotifier := not.(*TeamsNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(teamsNotifier.Name, ShouldEqual, "ops")
|
||||
So(teamsNotifier.Type, ShouldEqual, "teams")
|
||||
So(teamsNotifier.Url, ShouldEqual, "http://google.com")
|
||||
})
|
||||
|
||||
Convey("from settings with Recipient and Mention", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "http://google.com"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "teams",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewTeamsNotifier(model)
|
||||
teamsNotifier := not.(*TeamsNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(teamsNotifier.Name, ShouldEqual, "ops")
|
||||
So(teamsNotifier.Type, ShouldEqual, "teams")
|
||||
So(teamsNotifier.Url, ShouldEqual, "http://google.com")
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
captionLengthLimit = 200
|
||||
)
|
||||
|
||||
var (
|
||||
telegeramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
||||
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -48,9 +53,10 @@ func init() {
|
||||
|
||||
type TelegramNotifier struct {
|
||||
NotifierBase
|
||||
BotToken string
|
||||
ChatID string
|
||||
log log.Logger
|
||||
BotToken string
|
||||
ChatID string
|
||||
UploadImage bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
@@ -60,6 +66,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
|
||||
botToken := model.Settings.Get("bottoken").MustString()
|
||||
chatId := model.Settings.Get("chatid").MustString()
|
||||
uploadImage := model.Settings.Get("uploadImage").MustBool()
|
||||
|
||||
if botToken == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"}
|
||||
@@ -73,38 +80,149 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
BotToken: botToken,
|
||||
ChatID: chatId,
|
||||
UploadImage: uploadImage,
|
||||
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)
|
||||
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
|
||||
if sendImageInline {
|
||||
cmd, err := this.buildMessageInlineImage(evalContext)
|
||||
if err == nil {
|
||||
return cmd
|
||||
} else {
|
||||
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
return this.buildMessageLinkedImage(evalContext)
|
||||
}
|
||||
|
||||
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)
|
||||
func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
|
||||
message := fmt.Sprintf("<b>%s</b>\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)
|
||||
}
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
|
||||
}
|
||||
bodyJSON.Set("text", message)
|
||||
|
||||
url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage")
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
metrics := generateMetricsMessage(evalContext)
|
||||
if metrics != "" {
|
||||
message = message + fmt.Sprintf("\n<i>Metrics:</i>%s", metrics)
|
||||
}
|
||||
|
||||
cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
|
||||
fw, _ := w.CreateFormField("parse_mode")
|
||||
fw.Write([]byte("html"))
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) {
|
||||
var imageFile *os.File
|
||||
var err error
|
||||
|
||||
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
|
||||
defer imageFile.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
|
||||
metrics := generateMetricsMessage(evalContext)
|
||||
message := generateImageCaption(evalContext, ruleUrl, metrics)
|
||||
|
||||
cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
|
||||
fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
|
||||
io.Copy(fw, imageFile)
|
||||
})
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync {
|
||||
var body bytes.Buffer
|
||||
w := multipart.NewWriter(&body)
|
||||
|
||||
fw, _ := w.CreateFormField("chat_id")
|
||||
fw.Write([]byte(this.ChatID))
|
||||
|
||||
fw, _ = w.CreateFormField(messageField)
|
||||
fw.Write([]byte(message))
|
||||
|
||||
extraConf(w)
|
||||
|
||||
w.Close()
|
||||
|
||||
this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
|
||||
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: url,
|
||||
Body: string(body),
|
||||
Body: body.String(),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": w.FormDataContentType(),
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func generateMetricsMessage(evalContext *alerting.EvalContext) string {
|
||||
metrics := ""
|
||||
fieldLimitCount := 4
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
|
||||
if index > fieldLimitCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
|
||||
message := evalContext.GetNotificationTitle()
|
||||
|
||||
if len(evalContext.Rule.Message) > 0 {
|
||||
message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message)
|
||||
}
|
||||
|
||||
if len(message) > captionLengthLimit {
|
||||
message = message[0:captionLengthLimit]
|
||||
|
||||
}
|
||||
|
||||
if len(ruleUrl) > 0 {
|
||||
urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
|
||||
message = appendIfPossible(message, urlLine, captionLengthLimit)
|
||||
}
|
||||
|
||||
if metrics != "" {
|
||||
metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics)
|
||||
message = appendIfPossible(message, metricsLines, captionLengthLimit)
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func appendIfPossible(message string, extra string, sizeLimit int) string {
|
||||
if len(extra)+len(message) <= sizeLimit {
|
||||
return message + extra
|
||||
}
|
||||
log.Debug("Line too long for image caption.", "value", extra)
|
||||
return message
|
||||
}
|
||||
|
||||
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
var cmd *m.SendWebhookSync
|
||||
if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
|
||||
cmd = this.buildMessage(evalContext, true)
|
||||
} else {
|
||||
cmd = this.buildMessage(evalContext, false)
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
|
||||
})
|
||||
|
||||
Convey("generateCaption should generate a message with all pertinent details", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
|
||||
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||
So(caption, ShouldContainSubstring, "Some kind of message.")
|
||||
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||
So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
|
||||
})
|
||||
|
||||
Convey("When generating a message", func() {
|
||||
|
||||
Convey("URL should be skipped if it's too long", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"foo bar")
|
||||
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||
So(caption, ShouldContainSubstring, "Some kind of message.")
|
||||
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||
So(caption, ShouldContainSubstring, "foo bar")
|
||||
So(caption, ShouldNotContainSubstring, "http")
|
||||
})
|
||||
|
||||
Convey("Message should be trimmed if it's too long", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/foo",
|
||||
"")
|
||||
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||
So(caption, ShouldNotContainSubstring, "http")
|
||||
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
|
||||
})
|
||||
|
||||
Convey("Metrics should be skipped if they dont fit", func() {
|
||||
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
|
||||
Name: "This is an alarm",
|
||||
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
|
||||
State: m.AlertStateOK,
|
||||
})
|
||||
|
||||
caption := generateImageCaption(evalContext,
|
||||
"http://grafa.url/foo",
|
||||
"foo bar long song")
|
||||
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
|
||||
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
|
||||
So(caption, ShouldNotContainSubstring, "http")
|
||||
So(caption, ShouldNotContainSubstring, "foo bar")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -118,7 +117,6 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
|
||||
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)
|
||||
metrics.M_Alerting_Notification_Sent_Threema.Inc(1)
|
||||
|
||||
// Set up basic API request data
|
||||
data := url.Values{}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
// AlertStateCritical - Victorops uses "CRITICAL" string to indicate "Alerting" state
|
||||
const AlertStateCritical = "CRITICAL"
|
||||
|
||||
const AlertStateRecovery = "RECOVERY"
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "victorops",
|
||||
@@ -27,6 +28,15 @@ func init() {
|
||||
<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>
|
||||
<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 VictorOps once the alert goes back to ok.">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
@@ -34,6 +44,7 @@ func init() {
|
||||
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
|
||||
// handles posting notifications to Victorops REST API
|
||||
func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
|
||||
autoResolve := model.Settings.Get("autoResolve").MustBool(true)
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find victorops url property in settings"}
|
||||
@@ -42,6 +53,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e
|
||||
return &VictoropsNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
URL: url,
|
||||
AutoResolve: autoResolve,
|
||||
log: log.New("alerting.notifier.victorops"),
|
||||
}, nil
|
||||
}
|
||||
@@ -51,14 +63,14 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e
|
||||
// Victorops specifications (http://victorops.force.com/knowledgebase/articles/Integration/Alert-Ingestion-API-Documentation/)
|
||||
type VictoropsNotifier struct {
|
||||
NotifierBase
|
||||
URL string
|
||||
log log.Logger
|
||||
URL string
|
||||
AutoResolve bool
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// Notify sends notification to Victorops via POST to URL endpoint
|
||||
func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||
metrics.M_Alerting_Notification_Sent_Victorops.Inc(1)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
@@ -66,6 +78,11 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if evalContext.Rule.State == models.AlertStateOK && !this.AutoResolve {
|
||||
this.log.Info("Not alerting VictorOps", "state", evalContext.Rule.State, "auto resolve", this.AutoResolve)
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
fieldLimitCount := 4
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
@@ -92,20 +109,28 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
messageType = AlertStateCritical
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"message_type": messageType,
|
||||
"entity_id": evalContext.Rule.Name,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"state_start_time": evalContext.StartTime.Unix(),
|
||||
"state_message": evalContext.Rule.Message + "\n" + ruleUrl,
|
||||
"monitoring_tool": "Grafana v" + setting.BuildVersion,
|
||||
if evalContext.Rule.State == models.AlertStateOK {
|
||||
messageType = AlertStateRecovery
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(&body)
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("message_type", messageType)
|
||||
bodyJSON.Set("entity_id", evalContext.Rule.Name)
|
||||
bodyJSON.Set("timestamp", time.Now().Unix())
|
||||
bodyJSON.Set("state_start_time", evalContext.StartTime.Unix())
|
||||
bodyJSON.Set("state_message", evalContext.Rule.Message)
|
||||
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
|
||||
bodyJSON.Set("alert_url", ruleUrl)
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
|
||||
}
|
||||
|
||||
data, _ := bodyJSON.MarshalJSON()
|
||||
cmd := &models.SendWebhookSync{Url: this.URL, Body: string(data)}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send victorops notification", "error", err, "webhook", this.Name)
|
||||
this.log.Error("Failed to send Victorops notification", "error", err, "webhook", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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"
|
||||
)
|
||||
@@ -68,7 +67,6 @@ type WebhookNotifier struct {
|
||||
|
||||
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending webhook")
|
||||
metrics.M_Alerting_Notification_Sent_Webhook.Inc(1)
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("title", evalContext.GetNotificationTitle())
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Type: "webhook",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Type: "webhook",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) {
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(webhookNotifier.Name, ShouldEqual, "ops")
|
||||
So(webhookNotifier.Type, ShouldEqual, "email")
|
||||
So(webhookNotifier.Type, ShouldEqual, "webhook")
|
||||
So(webhookNotifier.Url, ShouldEqual, "http://google.com")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,7 +59,7 @@ func (arr *DefaultRuleReader) Fetch() []*Rule {
|
||||
}
|
||||
}
|
||||
|
||||
metrics.M_Alerting_Active_Alerts.Update(int64(len(res)))
|
||||
metrics.M_Alerting_Active_Alerts.Set(float64(len(res)))
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
@@ -31,20 +31,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
executionError := ""
|
||||
annotationData := simplejson.New()
|
||||
|
||||
if evalContext.Firing {
|
||||
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
|
||||
if len(evalContext.EvalMatches) > 0 {
|
||||
annotationData.Set("evalMatches", simplejson.NewFromAny(evalContext.EvalMatches))
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
executionError = evalContext.Error.Error()
|
||||
annotationData.Set("errorMessage", executionError)
|
||||
annotationData.Set("error", executionError)
|
||||
} else if evalContext.NoDataFound {
|
||||
annotationData.Set("noData", true)
|
||||
}
|
||||
|
||||
if evalContext.NoDataFound {
|
||||
annotationData.Set("no_data", true)
|
||||
}
|
||||
|
||||
countStateResult(evalContext.Rule.State)
|
||||
metrics.M_Alerting_Result_State.WithLabelValues(string(evalContext.Rule.State)).Inc()
|
||||
if evalContext.ShouldUpdateAlertState() {
|
||||
handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "prev state", evalContext.PrevAlertState)
|
||||
|
||||
@@ -75,10 +73,8 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
OrgId: evalContext.Rule.OrgId,
|
||||
DashboardId: evalContext.Rule.DashboardId,
|
||||
PanelId: evalContext.Rule.PanelId,
|
||||
Type: annotations.AlertType,
|
||||
AlertId: evalContext.Rule.Id,
|
||||
Title: evalContext.Rule.Name,
|
||||
Text: evalContext.GetStateModel().Text,
|
||||
Text: "",
|
||||
NewState: string(evalContext.Rule.State),
|
||||
PrevState: string(evalContext.PrevAlertState),
|
||||
Epoch: time.Now().Unix(),
|
||||
@@ -89,26 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
if err := annotationRepo.Save(&item); err != nil {
|
||||
handler.log.Error("Failed to save annotation for new alert state", "error", err)
|
||||
}
|
||||
|
||||
if evalContext.ShouldSendNotification() {
|
||||
handler.notifier.Send(evalContext)
|
||||
}
|
||||
}
|
||||
|
||||
handler.notifier.SendIfNeeded(evalContext)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func countStateResult(state m.AlertStateType) {
|
||||
switch state {
|
||||
case m.AlertStatePending:
|
||||
metrics.M_Alerting_Result_State_Pending.Inc(1)
|
||||
case m.AlertStateAlerting:
|
||||
metrics.M_Alerting_Result_State_Alerting.Inc(1)
|
||||
case m.AlertStateOK:
|
||||
metrics.M_Alerting_Result_State_Ok.Inc(1)
|
||||
case m.AlertStatePaused:
|
||||
metrics.M_Alerting_Result_State_Paused.Inc(1)
|
||||
case m.AlertStateNoData:
|
||||
metrics.M_Alerting_Result_State_NoData.Inc(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 127,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 9,
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
200
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"name": "Panel Title alert",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"id": 10,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{job}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 200
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 1
|
||||
},
|
||||
"id": 14,
|
||||
"limit": 10,
|
||||
"links": [],
|
||||
"onlyAlertsOnDashboard": true,
|
||||
"show": "current",
|
||||
"sortOrder": 1,
|
||||
"stateFilter": [],
|
||||
"title": "Panel Title",
|
||||
"type": "alertlist"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 10
|
||||
},
|
||||
"id": 6,
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
200
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"name": "Panel 2 alert",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"id": 11,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{job}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 200
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel 2",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
200
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"name": "Panel 4 alert",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 11
|
||||
},
|
||||
"id": 15,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{job}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 200
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel 4",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"id": 4,
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
200
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"operator": {
|
||||
"type": "and"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"A",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"executionErrorState": "alerting",
|
||||
"frequency": "10s",
|
||||
"handler": 1,
|
||||
"name": "Panel 3 alert",
|
||||
"noDataState": "no_data",
|
||||
"notifications": []
|
||||
},
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 12
|
||||
},
|
||||
"id": 12,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "go_goroutines",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{job}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 200
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel 3",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 16,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "New dashboard Copy",
|
||||
"uid": "6v5pg36zk",
|
||||
"version": 17
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"title": "Influxdb",
|
||||
"tags": [
|
||||
"apa"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "450px",
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
10
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"B",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"frequency": "3s",
|
||||
"handler": 1,
|
||||
"name": "Influxdb",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
{
|
||||
"id": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
"alerting": {},
|
||||
"aliasColors": {
|
||||
"logins.count.count": "#890F02"
|
||||
},
|
||||
"bars": false,
|
||||
"datasource": "InfluxDB",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {},
|
||||
"id": 1,
|
||||
"interval": ">10s",
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"datacenter"
|
||||
],
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"none"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "logins.count",
|
||||
"policy": "default",
|
||||
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
|
||||
"rawQuery": true,
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "count"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": true,
|
||||
"measurement": "cpu",
|
||||
"policy": "default",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "sum"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 10
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"ordering": "alphabetical",
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"limit": 10,
|
||||
"links": [],
|
||||
"show": "current",
|
||||
"span": 2,
|
||||
"stateFilter": [
|
||||
"alerting"
|
||||
],
|
||||
"title": "Alert status",
|
||||
"type": "alertlist"
|
||||
}
|
||||
],
|
||||
"title": "Row"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"now": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 120,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": ["graphite"],
|
||||
"rows": [
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["A", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{"refId": "A", "target": ""},
|
||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"alert": {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["B", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Influxdb",
|
||||
"tags": [
|
||||
"apa"
|
||||
],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"hideControls": false,
|
||||
"sharedCrosshair": false,
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"height": "450px",
|
||||
"panels": [
|
||||
{
|
||||
"alert": {
|
||||
"conditions": [
|
||||
{
|
||||
"evaluator": {
|
||||
"params": [
|
||||
10
|
||||
],
|
||||
"type": "gt"
|
||||
},
|
||||
"query": {
|
||||
"params": [
|
||||
"B",
|
||||
"5m",
|
||||
"now"
|
||||
]
|
||||
},
|
||||
"reducer": {
|
||||
"params": [],
|
||||
"type": "avg"
|
||||
},
|
||||
"type": "query"
|
||||
}
|
||||
],
|
||||
"frequency": "3s",
|
||||
"handler": 1,
|
||||
"name": "Influxdb",
|
||||
"noDataState": "no_data",
|
||||
"notifications": [
|
||||
{
|
||||
"id": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
"alerting": {},
|
||||
"aliasColors": {
|
||||
"logins.count.count": "#890F02"
|
||||
},
|
||||
"bars": false,
|
||||
"datasource": "InfluxDB",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 1,
|
||||
"grid": {},
|
||||
"id": 1,
|
||||
"interval": ">10s",
|
||||
"isNew": true,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"span": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"datacenter"
|
||||
],
|
||||
"type": "tag"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"none"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "logins.count",
|
||||
"policy": "default",
|
||||
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
|
||||
"rawQuery": true,
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "count"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": true,
|
||||
"measurement": "cpu",
|
||||
"policy": "default",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "sum"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 10
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Panel Title",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"ordering": "alphabetical",
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"id": 2,
|
||||
"isNew": true,
|
||||
"limit": 10,
|
||||
"links": [],
|
||||
"show": "current",
|
||||
"span": 2,
|
||||
"stateFilter": [
|
||||
"alerting"
|
||||
],
|
||||
"title": "Alert status",
|
||||
"type": "alertlist"
|
||||
}
|
||||
],
|
||||
"title": "Row"
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"now": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 13,
|
||||
"version": 120,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": ["graphite"],
|
||||
"rows": [
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"id": 0,
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["A", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{"refId": "A", "target": ""},
|
||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"alert": {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["B", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": ["graphite"],
|
||||
"rows": [
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["A", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{"refId": "A", "target": ""},
|
||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"alert": {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["B", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"id": 57,
|
||||
"title": "Graphite 4",
|
||||
"originalTitle": "Graphite 4",
|
||||
"tags": ["graphite"],
|
||||
"panels": [
|
||||
{
|
||||
"title": "Active desktop users",
|
||||
"editable": true,
|
||||
"type": "graph",
|
||||
"id": 3,
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
||||
}
|
||||
],
|
||||
"datasource": null,
|
||||
"alert": {
|
||||
"name": "name1",
|
||||
"message": "desc1",
|
||||
"handler": 1,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["A", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Active mobile users",
|
||||
"id": 4,
|
||||
"targets": [
|
||||
{"refId": "A", "target": ""},
|
||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
||||
],
|
||||
"datasource": "graphite2",
|
||||
"alert": {
|
||||
"name": "name2",
|
||||
"message": "desc2",
|
||||
"handler": 0,
|
||||
"frequency": "60s",
|
||||
"severity": "warning",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"query": {"params": ["B", "5m", "now"]},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
@@ -56,7 +57,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
|
||||
}
|
||||
ctx.IsTestRun = true
|
||||
ctx.Firing = true
|
||||
ctx.Error = nil
|
||||
ctx.Error = fmt.Errorf("This is only a test")
|
||||
ctx.EvalMatches = evalMatchesBasedOnState()
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext {
|
||||
context.IsTestRun = true
|
||||
|
||||
handler.Eval(context)
|
||||
context.Rule.State = context.GetNewState()
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -1,121 +1,121 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
)
|
||||
|
||||
func inspectTick(tick time.Time, last time.Time, offset time.Duration, t *testing.T) {
|
||||
if !tick.Equal(last.Add(time.Duration(1) * time.Second)) {
|
||||
t.Fatalf("expected a tick 1 second more than prev, %s. got: %s", last, tick)
|
||||
}
|
||||
}
|
||||
|
||||
// returns the new last tick seen
|
||||
func assertAdvanceUntil(ticker *Ticker, last, desiredLast time.Time, offset, wait time.Duration, t *testing.T) time.Time {
|
||||
for {
|
||||
select {
|
||||
case tick := <-ticker.C:
|
||||
inspectTick(tick, last, offset, t)
|
||||
last = tick
|
||||
case <-time.NewTimer(wait).C:
|
||||
if last.Before(desiredLast) {
|
||||
t.Fatalf("waited %s for ticker to advance to %s, but only went up to %s", wait, desiredLast, last)
|
||||
}
|
||||
if last.After(desiredLast) {
|
||||
t.Fatalf("timer advanced too far. should only have gone up to %s, but it went up to %s", desiredLast, last)
|
||||
}
|
||||
return last
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoAdvance(ticker *Ticker, desiredLast time.Time, wait time.Duration, t *testing.T) {
|
||||
for {
|
||||
select {
|
||||
case tick := <-ticker.C:
|
||||
t.Fatalf("timer should have stayed at %s, instead it advanced to %s", desiredLast, tick)
|
||||
case <-time.NewTimer(wait).C:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTickerRetro1Hour(t *testing.T) {
|
||||
offset := time.Duration(10) * time.Second
|
||||
last := time.Unix(0, 0)
|
||||
mock := clock.NewMock()
|
||||
mock.Add(time.Duration(1) * time.Hour)
|
||||
desiredLast := mock.Now().Add(-offset)
|
||||
ticker := NewTicker(last, offset, mock)
|
||||
|
||||
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
|
||||
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
|
||||
}
|
||||
|
||||
func TestAdvanceWithUpdateOffset(t *testing.T) {
|
||||
offset := time.Duration(10) * time.Second
|
||||
last := time.Unix(0, 0)
|
||||
mock := clock.NewMock()
|
||||
mock.Add(time.Duration(1) * time.Hour)
|
||||
desiredLast := mock.Now().Add(-offset)
|
||||
ticker := NewTicker(last, offset, mock)
|
||||
|
||||
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
|
||||
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
|
||||
// lowering offset should see a few more ticks
|
||||
offset = time.Duration(5) * time.Second
|
||||
ticker.updateOffset(offset)
|
||||
desiredLast = mock.Now().Add(-offset)
|
||||
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(9)*time.Millisecond, t)
|
||||
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
|
||||
// advancing clock should see even more ticks
|
||||
mock.Add(time.Duration(1) * time.Hour)
|
||||
desiredLast = mock.Now().Add(-offset)
|
||||
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(8)*time.Millisecond, t)
|
||||
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
|
||||
}
|
||||
|
||||
func getCase(lastSeconds, offsetSeconds int) (time.Time, time.Duration) {
|
||||
last := time.Unix(int64(lastSeconds), 0)
|
||||
offset := time.Duration(offsetSeconds) * time.Second
|
||||
return last, offset
|
||||
}
|
||||
|
||||
func TestTickerNoAdvance(t *testing.T) {
|
||||
|
||||
// it's 00:01:00 now. what are some cases where we don't want the ticker to advance?
|
||||
mock := clock.NewMock()
|
||||
mock.Add(time.Duration(60) * time.Second)
|
||||
|
||||
type Case struct {
|
||||
last int
|
||||
offset int
|
||||
}
|
||||
|
||||
// note that some cases add up to now, others go into the future
|
||||
cases := []Case{
|
||||
{50, 10},
|
||||
{50, 30},
|
||||
{59, 1},
|
||||
{59, 10},
|
||||
{59, 30},
|
||||
{60, 1},
|
||||
{60, 10},
|
||||
{60, 30},
|
||||
{90, 1},
|
||||
{90, 10},
|
||||
{90, 30},
|
||||
}
|
||||
for _, c := range cases {
|
||||
last, offset := getCase(c.last, c.offset)
|
||||
ticker := NewTicker(last, offset, mock)
|
||||
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
}
|
||||
}
|
||||
//import (
|
||||
// "testing"
|
||||
// "time"
|
||||
//
|
||||
// "github.com/benbjohnson/clock"
|
||||
//)
|
||||
//
|
||||
//func inspectTick(tick time.Time, last time.Time, offset time.Duration, t *testing.T) {
|
||||
// if !tick.Equal(last.Add(time.Duration(1) * time.Second)) {
|
||||
// t.Fatalf("expected a tick 1 second more than prev, %s. got: %s", last, tick)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// returns the new last tick seen
|
||||
//func assertAdvanceUntil(ticker *Ticker, last, desiredLast time.Time, offset, wait time.Duration, t *testing.T) time.Time {
|
||||
// for {
|
||||
// select {
|
||||
// case tick := <-ticker.C:
|
||||
// inspectTick(tick, last, offset, t)
|
||||
// last = tick
|
||||
// case <-time.NewTimer(wait).C:
|
||||
// if last.Before(desiredLast) {
|
||||
// t.Fatalf("waited %s for ticker to advance to %s, but only went up to %s", wait, desiredLast, last)
|
||||
// }
|
||||
// if last.After(desiredLast) {
|
||||
// t.Fatalf("timer advanced too far. should only have gone up to %s, but it went up to %s", desiredLast, last)
|
||||
// }
|
||||
// return last
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func assertNoAdvance(ticker *Ticker, desiredLast time.Time, wait time.Duration, t *testing.T) {
|
||||
// for {
|
||||
// select {
|
||||
// case tick := <-ticker.C:
|
||||
// t.Fatalf("timer should have stayed at %s, instead it advanced to %s", desiredLast, tick)
|
||||
// case <-time.NewTimer(wait).C:
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func TestTickerRetro1Hour(t *testing.T) {
|
||||
// offset := time.Duration(10) * time.Second
|
||||
// last := time.Unix(0, 0)
|
||||
// mock := clock.NewMock()
|
||||
// mock.Add(time.Duration(1) * time.Hour)
|
||||
// desiredLast := mock.Now().Add(-offset)
|
||||
// ticker := NewTicker(last, offset, mock)
|
||||
//
|
||||
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
|
||||
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func TestAdvanceWithUpdateOffset(t *testing.T) {
|
||||
// offset := time.Duration(10) * time.Second
|
||||
// last := time.Unix(0, 0)
|
||||
// mock := clock.NewMock()
|
||||
// mock.Add(time.Duration(1) * time.Hour)
|
||||
// desiredLast := mock.Now().Add(-offset)
|
||||
// ticker := NewTicker(last, offset, mock)
|
||||
//
|
||||
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
|
||||
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
//
|
||||
// // lowering offset should see a few more ticks
|
||||
// offset = time.Duration(5) * time.Second
|
||||
// ticker.updateOffset(offset)
|
||||
// desiredLast = mock.Now().Add(-offset)
|
||||
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(9)*time.Millisecond, t)
|
||||
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
//
|
||||
// // advancing clock should see even more ticks
|
||||
// mock.Add(time.Duration(1) * time.Hour)
|
||||
// desiredLast = mock.Now().Add(-offset)
|
||||
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(8)*time.Millisecond, t)
|
||||
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func getCase(lastSeconds, offsetSeconds int) (time.Time, time.Duration) {
|
||||
// last := time.Unix(int64(lastSeconds), 0)
|
||||
// offset := time.Duration(offsetSeconds) * time.Second
|
||||
// return last, offset
|
||||
//}
|
||||
//
|
||||
//func TestTickerNoAdvance(t *testing.T) {
|
||||
//
|
||||
// // it's 00:01:00 now. what are some cases where we don't want the ticker to advance?
|
||||
// mock := clock.NewMock()
|
||||
// mock.Add(time.Duration(60) * time.Second)
|
||||
//
|
||||
// type Case struct {
|
||||
// last int
|
||||
// offset int
|
||||
// }
|
||||
//
|
||||
// // note that some cases add up to now, others go into the future
|
||||
// cases := []Case{
|
||||
// {50, 10},
|
||||
// {50, 30},
|
||||
// {59, 1},
|
||||
// {59, 10},
|
||||
// {59, 30},
|
||||
// {60, 1},
|
||||
// {60, 10},
|
||||
// {60, 30},
|
||||
// {90, 1},
|
||||
// {90, 10},
|
||||
// {90, 30},
|
||||
// }
|
||||
// for _, c := range cases {
|
||||
// last, offset := getCase(c.last, c.offset)
|
||||
// ticker := NewTicker(last, offset, mock)
|
||||
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -4,27 +4,41 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
type Repository interface {
|
||||
Save(item *Item) error
|
||||
Find(query *ItemQuery) ([]*Item, error)
|
||||
Update(item *Item) error
|
||||
Find(query *ItemQuery) ([]*ItemDTO, error)
|
||||
Delete(params *DeleteParams) error
|
||||
}
|
||||
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
Type ItemType `json:"type"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
NewState []string `json:"newState"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
AnnotationId int64 `json:"annotationId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Tags []string `json:"tags"`
|
||||
Type string `json:"type"`
|
||||
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type PostParams struct {
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
type DeleteParams struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
@@ -37,27 +51,41 @@ func SetRepository(rep Repository) {
|
||||
repositoryInstance = rep
|
||||
}
|
||||
|
||||
type ItemType string
|
||||
|
||||
const (
|
||||
AlertType ItemType = "alert"
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
CategoryId int64 `json:"categoryId"`
|
||||
Type ItemType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Metric string `json:"metric"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
UserId int64 `json:"userId"`
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
UserId int64 `json:"userId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Text string `json:"text"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
|
||||
Data *simplejson.Json `json:"data"`
|
||||
// needed until we remove it from db
|
||||
Type string
|
||||
Title string
|
||||
}
|
||||
|
||||
type ItemDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
AlertName string `json:"alertName"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
UserId int64 `json:"userId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Time int64 `json:"time"`
|
||||
Text string `json:"text"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
Tags []string `json:"tags"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
@@ -39,12 +39,14 @@ func (service *CleanUpService) Run(ctx context.Context) error {
|
||||
func (service *CleanUpService) start(ctx context.Context) error {
|
||||
service.cleanUpTmpFiles()
|
||||
|
||||
ticker := time.NewTicker(time.Hour * 1)
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
service.cleanUpTmpFiles()
|
||||
service.deleteExpiredSnapshots()
|
||||
service.deleteExpiredDashboardVersions()
|
||||
service.deleteOldLoginAttempts()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -81,5 +83,34 @@ func (service *CleanUpService) cleanUpTmpFiles() {
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteExpiredSnapshots() {
|
||||
bus.Dispatch(&m.DeleteExpiredSnapshotsCommand{})
|
||||
cmd := m.DeleteExpiredSnapshotsCommand{}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Failed to delete expired snapshots", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteExpiredDashboardVersions() {
|
||||
cmd := m.DeleteExpiredVersionsCommand{}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteOldLoginAttempts() {
|
||||
if setting.DisableBruteForceLoginProtection {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := m.DeleteOldLoginAttemptsCommand{
|
||||
OlderThan: time.Now().Add(time.Minute * -10),
|
||||
}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Problem deleting expired login attempts", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// DashboardService service for operating on dashboards
|
||||
type DashboardService interface {
|
||||
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
|
||||
}
|
||||
|
||||
// DashboardProvisioningService service for operating on provisioned dashboards
|
||||
type DashboardProvisioningService interface {
|
||||
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
|
||||
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
|
||||
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
|
||||
}
|
||||
|
||||
// NewService factory for creating a new dashboard service
|
||||
var NewService = func() DashboardService {
|
||||
return &dashboardServiceImpl{}
|
||||
}
|
||||
|
||||
// NewProvisioningService factory for creating a new dashboard provisioning service
|
||||
var NewProvisioningService = func() DashboardProvisioningService {
|
||||
return &dashboardServiceImpl{}
|
||||
}
|
||||
|
||||
type SaveDashboardDTO struct {
|
||||
OrgId int64
|
||||
UpdatedAt time.Time
|
||||
User *models.SignedInUser
|
||||
Message string
|
||||
Overwrite bool
|
||||
Dashboard *models.Dashboard
|
||||
}
|
||||
|
||||
type dashboardServiceImpl struct {
|
||||
orgId int64
|
||||
user *models.SignedInUser
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
|
||||
dash := dto.Dashboard
|
||||
|
||||
dash.Title = strings.TrimSpace(dash.Title)
|
||||
dash.Data.Set("title", dash.Title)
|
||||
dash.SetUid(strings.TrimSpace(dash.Uid))
|
||||
|
||||
if dash.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
if dash.IsFolder && dash.FolderId > 0 {
|
||||
return nil, models.ErrDashboardFolderCannotHaveParent
|
||||
}
|
||||
|
||||
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
|
||||
return nil, models.ErrDashboardFolderNameExists
|
||||
}
|
||||
|
||||
if !util.IsValidShortUid(dash.Uid) {
|
||||
return nil, models.ErrDashboardInvalidUid
|
||||
} else if len(dash.Uid) > 40 {
|
||||
return nil, models.ErrDashboardUidToLong
|
||||
}
|
||||
|
||||
if validateAlerts {
|
||||
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dash,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
}
|
||||
|
||||
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
|
||||
OrgId: dto.OrgId,
|
||||
Dashboard: dash,
|
||||
Overwrite: dto.Overwrite,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
|
||||
if canSave, err := guard.CanSave(); err != nil || !canSave {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, models.ErrDashboardUpdateAccessDenied
|
||||
}
|
||||
|
||||
cmd := &models.SaveDashboardCommand{
|
||||
Dashboard: dash.Data,
|
||||
Message: dto.Message,
|
||||
OrgId: dto.OrgId,
|
||||
Overwrite: dto.Overwrite,
|
||||
UserId: dto.User.UserId,
|
||||
FolderId: dash.FolderId,
|
||||
IsFolder: dash.IsFolder,
|
||||
PluginId: dash.PluginId,
|
||||
}
|
||||
|
||||
if !dto.UpdatedAt.IsZero() {
|
||||
cmd.UpdatedAt = dto.UpdatedAt
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
|
||||
alertCmd := models.UpdateDashboardAlertsCommand{
|
||||
OrgId: dto.OrgId,
|
||||
UserId: dto.User.UserId,
|
||||
Dashboard: cmd.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
||||
return models.ErrDashboardFailedToUpdateAlertData
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||
dto.User = &models.SignedInUser{
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveCmd := &models.SaveProvisionedDashboardCommand{
|
||||
DashboardCmd: cmd,
|
||||
DashboardProvisioning: provisioning,
|
||||
}
|
||||
|
||||
// dashboard
|
||||
err = bus.Dispatch(saveCmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//alerts
|
||||
err = dr.updateAlerting(cmd, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
dto.User = &models.SignedInUser{
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dr.updateAlerting(cmd, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = dr.updateAlerting(cmd, dto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
type FakeDashboardService struct {
|
||||
SaveDashboardResult *models.Dashboard
|
||||
SaveDashboardError error
|
||||
SavedDashboards []*SaveDashboardDTO
|
||||
}
|
||||
|
||||
func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
s.SavedDashboards = append(s.SavedDashboards, dto)
|
||||
|
||||
if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
|
||||
s.SaveDashboardResult = dto.Dashboard
|
||||
}
|
||||
|
||||
return s.SaveDashboardResult, s.SaveDashboardError
|
||||
}
|
||||
|
||||
func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
return s.SaveDashboard(dto)
|
||||
}
|
||||
|
||||
func MockDashboardService(mock *FakeDashboardService) {
|
||||
NewService = func() DashboardService {
|
||||
return mock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardService(t *testing.T) {
|
||||
Convey("Dashboard service tests", t, func() {
|
||||
service := dashboardServiceImpl{}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
|
||||
Convey("Save dashboard validation", func() {
|
||||
dto := &SaveDashboardDTO{}
|
||||
|
||||
Convey("When saving a dashboard with empty title it should return error", func() {
|
||||
titles := []string{"", " ", " \t "}
|
||||
|
||||
for _, title := range titles {
|
||||
dto.Dashboard = models.NewDashboard(title)
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(err, ShouldEqual, models.ErrDashboardTitleEmpty)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should return validation error if it's a folder and have a folder id", func() {
|
||||
dto.Dashboard = models.NewDashboardFolder("Folder")
|
||||
dto.Dashboard.FolderId = 1
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(err, ShouldEqual, models.ErrDashboardFolderCannotHaveParent)
|
||||
})
|
||||
|
||||
Convey("Should return validation error if folder is named General", func() {
|
||||
dto.Dashboard = models.NewDashboardFolder("General")
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(err, ShouldEqual, models.ErrDashboardFolderNameExists)
|
||||
})
|
||||
|
||||
Convey("When saving a dashboard should validate uid", func() {
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
Uid string
|
||||
Error error
|
||||
}{
|
||||
{Uid: "", Error: nil},
|
||||
{Uid: " ", Error: nil},
|
||||
{Uid: " \t ", Error: nil},
|
||||
{Uid: "asdf90_-", Error: nil},
|
||||
{Uid: "asdf/90", Error: models.ErrDashboardInvalidUid},
|
||||
{Uid: " asdfghjklqwertyuiopzxcvbnmasdfghjklqwer ", Error: nil},
|
||||
{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: models.ErrDashboardUidToLong},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
dto.Dashboard = models.NewDashboard("title")
|
||||
dto.Dashboard.SetUid(tc.Uid)
|
||||
dto.User = &models.SignedInUser{}
|
||||
|
||||
_, err := service.buildSaveDashboardCommand(dto, true)
|
||||
So(err, ShouldEqual, tc.Error)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should return validation error if alert data is invalid", func() {
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return errors.New("error")
|
||||
})
|
||||
|
||||
dto.Dashboard = models.NewDashboard("Dash")
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
guardian.New = origNewDashboardGuardian
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
// FolderService service for operating on folders
|
||||
type FolderService interface {
|
||||
GetFolders(limit int) ([]*models.Folder, error)
|
||||
GetFolderByID(id int64) (*models.Folder, error)
|
||||
GetFolderByUID(uid string) (*models.Folder, error)
|
||||
CreateFolder(cmd *models.CreateFolderCommand) error
|
||||
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
|
||||
DeleteFolder(uid string) (*models.Folder, error)
|
||||
}
|
||||
|
||||
// NewFolderService factory for creating a new folder service
|
||||
var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService {
|
||||
return &dashboardServiceImpl{
|
||||
orgId: orgId,
|
||||
user: user,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error) {
|
||||
if limit == 0 {
|
||||
limit = 1000
|
||||
}
|
||||
|
||||
searchQuery := search.Query{
|
||||
SignedInUser: dr.user,
|
||||
DashboardIds: make([]int64, 0),
|
||||
FolderIds: make([]int64, 0),
|
||||
Limit: limit,
|
||||
OrgId: dr.orgId,
|
||||
Type: "dash-folder",
|
||||
Permission: models.PERMISSION_VIEW,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&searchQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
folders := make([]*models.Folder, 0)
|
||||
|
||||
for _, hit := range searchQuery.Result {
|
||||
folders = append(folders, &models.Folder{
|
||||
Id: hit.Id,
|
||||
Uid: hit.Uid,
|
||||
Title: hit.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
|
||||
dashFolder, err := getFolder(query)
|
||||
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||
if canView, err := g.CanView(); err != nil || !canView {
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
|
||||
dashFolder, err := getFolder(query)
|
||||
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||
if canView, err := g.CanView(); err != nil || !canView {
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error {
|
||||
dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId)
|
||||
|
||||
dto := &SaveDashboardDTO{
|
||||
Dashboard: dashFolder,
|
||||
OrgId: dr.orgId,
|
||||
User: dr.user,
|
||||
}
|
||||
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
err = bus.Dispatch(saveDashboardCmd)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
|
||||
dashFolder, err = getFolder(query)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
cmd.Result = dashToFolder(dashFolder)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: existingUid}
|
||||
dashFolder, err := getFolder(query)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
cmd.UpdateDashboardModel(dashFolder, dr.orgId, dr.user.UserId)
|
||||
|
||||
dto := &SaveDashboardDTO{
|
||||
Dashboard: dashFolder,
|
||||
OrgId: dr.orgId,
|
||||
User: dr.user,
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
err = bus.Dispatch(saveDashboardCmd)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
|
||||
dashFolder, err = getFolder(query)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
cmd.Result = dashToFolder(dashFolder)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) DeleteFolder(uid string) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
|
||||
dashFolder, err := getFolder(query)
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
guardian := guardian.New(dashFolder.Id, dr.orgId, dr.user)
|
||||
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||
if err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
return nil, models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: dr.orgId, Id: dashFolder.Id}
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
return dashToFolder(dashFolder), nil
|
||||
}
|
||||
|
||||
func getFolder(query models.GetDashboardQuery) (*models.Dashboard, error) {
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, toFolderError(err)
|
||||
}
|
||||
|
||||
if !query.Result.IsFolder {
|
||||
return nil, models.ErrFolderNotFound
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func dashToFolder(dash *models.Dashboard) *models.Folder {
|
||||
return &models.Folder{
|
||||
Id: dash.Id,
|
||||
Uid: dash.Uid,
|
||||
Title: dash.Title,
|
||||
HasAcl: dash.HasAcl,
|
||||
Url: dash.GetUrl(),
|
||||
Version: dash.Version,
|
||||
Created: dash.Created,
|
||||
CreatedBy: dash.CreatedBy,
|
||||
Updated: dash.Updated,
|
||||
UpdatedBy: dash.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func toFolderError(err error) error {
|
||||
if err == models.ErrDashboardTitleEmpty {
|
||||
return models.ErrFolderTitleEmpty
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardUpdateAccessDenied {
|
||||
return models.ErrFolderAccessDenied
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardWithSameNameInFolderExists {
|
||||
return models.ErrFolderSameNameExists
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardWithSameUIDExists {
|
||||
return models.ErrFolderWithSameUIDExists
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardVersionMismatch {
|
||||
return models.ErrFolderVersionMismatch
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardNotFound {
|
||||
return models.ErrFolderNotFound
|
||||
}
|
||||
|
||||
if err == models.ErrDashboardFailedGenerateUniqueUid {
|
||||
err = models.ErrFolderFailedGenerateUniqueUid
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestFolderService(t *testing.T) {
|
||||
Convey("Folder service tests", t, func() {
|
||||
service := dashboardServiceImpl{
|
||||
orgId: 1,
|
||||
user: &models.SignedInUser{UserId: 1},
|
||||
}
|
||||
|
||||
Convey("Given user has no permissions", func() {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||
query.Result = models.NewDashboardFolder("Folder")
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
return models.ErrDashboardUpdateAccessDenied
|
||||
})
|
||||
|
||||
Convey("When get folder by id should return access denied error", func() {
|
||||
_, err := service.GetFolderByID(1)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Convey("When get folder by uid should return access denied error", func() {
|
||||
_, err := service.GetFolderByUID("uid")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Convey("When creating folder should return access denied error", func() {
|
||||
err := service.CreateFolder(&models.CreateFolderCommand{
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Convey("When updating folder should return access denied error", func() {
|
||||
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Convey("When deleting folder by uid should return access denied error", func() {
|
||||
_, err := service.DeleteFolder("uid")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
guardian.New = origNewGuardian
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given user has permission to save", func() {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
|
||||
dash := models.NewDashboardFolder("Folder")
|
||||
dash.Id = 1
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||
query.Result = dash
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error {
|
||||
cmd.Result = dash
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When creating folder should not return access denied error", func() {
|
||||
err := service.CreateFolder(&models.CreateFolderCommand{
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When updating folder should not return access denied error", func() {
|
||||
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When deleting folder by uid should not return access denied error", func() {
|
||||
_, err := service.DeleteFolder("uid")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
guardian.New = origNewGuardian
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given user has permission to view", func() {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
|
||||
|
||||
dashFolder := models.NewDashboardFolder("Folder")
|
||||
dashFolder.Id = 1
|
||||
dashFolder.Uid = "uid-abc"
|
||||
|
||||
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
|
||||
query.Result = dashFolder
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When get folder by id should return folder", func() {
|
||||
f, _ := service.GetFolderByID(1)
|
||||
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||
})
|
||||
|
||||
Convey("When get folder by uid should return folder", func() {
|
||||
f, _ := service.GetFolderByUID("uid")
|
||||
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
guardian.New = origNewGuardian
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should map errors correct", func() {
|
||||
testCases := []struct {
|
||||
ActualError error
|
||||
ExpectedError error
|
||||
}{
|
||||
{ActualError: models.ErrDashboardTitleEmpty, ExpectedError: models.ErrFolderTitleEmpty},
|
||||
{ActualError: models.ErrDashboardUpdateAccessDenied, ExpectedError: models.ErrFolderAccessDenied},
|
||||
{ActualError: models.ErrDashboardWithSameNameInFolderExists, ExpectedError: models.ErrFolderSameNameExists},
|
||||
{ActualError: models.ErrDashboardWithSameUIDExists, ExpectedError: models.ErrFolderWithSameUIDExists},
|
||||
{ActualError: models.ErrDashboardVersionMismatch, ExpectedError: models.ErrFolderVersionMismatch},
|
||||
{ActualError: models.ErrDashboardNotFound, ExpectedError: models.ErrFolderNotFound},
|
||||
{ActualError: models.ErrDashboardFailedGenerateUniqueUid, ExpectedError: models.ErrFolderFailedGenerateUniqueUid},
|
||||
{ActualError: models.ErrDashboardInvalidUid, ExpectedError: models.ErrDashboardInvalidUid},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
actualError := toFolderError(tc.ActualError)
|
||||
if actualError != tc.ExpectedError {
|
||||
t.Errorf("For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package eventpublisher
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/streadway/amqp"
|
||||
)
|
||||
|
||||
var (
|
||||
url string
|
||||
exchange string
|
||||
conn *amqp.Connection
|
||||
channel *amqp.Channel
|
||||
)
|
||||
|
||||
func getConnection() (*amqp.Connection, error) {
|
||||
c, err := amqp.Dial(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func getChannel() (*amqp.Channel, error) {
|
||||
ch, err := conn.Channel()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ch.ExchangeDeclare(
|
||||
exchange, // name
|
||||
"topic", // type
|
||||
true, // durable
|
||||
false, // auto-deleted
|
||||
false, // internal
|
||||
false, // no-wait
|
||||
nil, // arguments
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ch, err
|
||||
}
|
||||
|
||||
func Init() {
|
||||
sec := setting.Cfg.Section("event_publisher")
|
||||
|
||||
if !sec.Key("enabled").MustBool(false) {
|
||||
return
|
||||
}
|
||||
|
||||
url = sec.Key("rabbitmq_url").String()
|
||||
exchange = sec.Key("exchange").String()
|
||||
bus.AddWildcardListener(eventListener)
|
||||
|
||||
if err := Setup(); err != nil {
|
||||
log.Fatal(4, "Failed to connect to notification queue: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Every connection should declare the topology they expect
|
||||
func Setup() error {
|
||||
c, err := getConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn = c
|
||||
ch, err := getChannel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel = ch
|
||||
|
||||
// listen for close events so we can reconnect.
|
||||
errChan := channel.NotifyClose(make(chan *amqp.Error))
|
||||
go func() {
|
||||
for e := range errChan {
|
||||
fmt.Println("connection to rabbitmq lost.")
|
||||
fmt.Println(e)
|
||||
fmt.Println("attempting to create new rabbitmq channel.")
|
||||
ch, err := getChannel()
|
||||
if err == nil {
|
||||
channel = ch
|
||||
break
|
||||
}
|
||||
|
||||
//could not create channel, so lets close the connection
|
||||
// and re-create.
|
||||
_ = conn.Close()
|
||||
|
||||
for err != nil {
|
||||
time.Sleep(2 * time.Second)
|
||||
fmt.Println("attempting to reconnect to rabbitmq.")
|
||||
err = Setup()
|
||||
}
|
||||
fmt.Println("Connected to rabbitmq again.")
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func publish(routingKey string, msgString []byte) {
|
||||
for {
|
||||
err := channel.Publish(
|
||||
exchange, //exchange
|
||||
routingKey, // routing key
|
||||
false, // mandatory
|
||||
false, // immediate
|
||||
amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
Body: msgString,
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
// failures are most likely because the connection was lost.
|
||||
// the connection will be re-established, so just keep
|
||||
// retrying every 2seconds until we successfully publish.
|
||||
time.Sleep(2 * time.Second)
|
||||
fmt.Println("publish failed, retrying.")
|
||||
}
|
||||
}
|
||||
|
||||
func eventListener(event interface{}) error {
|
||||
wireEvent, err := events.ToOnWriteEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgString, err := json.Marshal(wireEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
routingKey := fmt.Sprintf("%s.%s", wireEvent.Priority, wireEvent.EventType)
|
||||
// this is run in a greenthread and we expect that publish will keep
|
||||
// retrying until the message gets sent.
|
||||
go publish(routingKey, msgString)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrGuardianPermissionExists = errors.New("Permission already exists")
|
||||
ErrGuardianOverride = errors.New("You can only override a permission to be higher")
|
||||
)
|
||||
|
||||
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
|
||||
type DashboardGuardian interface {
|
||||
CanSave() (bool, error)
|
||||
CanEdit() (bool, error)
|
||||
CanView() (bool, error)
|
||||
CanAdmin() (bool, error)
|
||||
HasPermission(permission m.PermissionType) (bool, error)
|
||||
CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error)
|
||||
GetAcl() ([]*m.DashboardAclInfoDTO, error)
|
||||
}
|
||||
|
||||
type dashboardGuardianImpl struct {
|
||||
user *m.SignedInUser
|
||||
dashId int64
|
||||
orgId int64
|
||||
acl []*m.DashboardAclInfoDTO
|
||||
groups []*m.Team
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// New factory for creating a new dashboard guardian instance
|
||||
var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
|
||||
return &dashboardGuardianImpl{
|
||||
user: user,
|
||||
dashId: dashId,
|
||||
orgId: orgId,
|
||||
log: log.New("guardians.dashboard"),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) CanSave() (bool, error) {
|
||||
return g.HasPermission(m.PERMISSION_EDIT)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) CanEdit() (bool, error) {
|
||||
if setting.ViewersCanEdit {
|
||||
return g.HasPermission(m.PERMISSION_VIEW)
|
||||
}
|
||||
|
||||
return g.HasPermission(m.PERMISSION_EDIT)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) CanView() (bool, error) {
|
||||
return g.HasPermission(m.PERMISSION_VIEW)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
|
||||
return g.HasPermission(m.PERMISSION_ADMIN)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
|
||||
if g.user.OrgRole == m.ROLE_ADMIN {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
acl, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return g.checkAcl(permission, acl)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
|
||||
orgRole := g.user.OrgRole
|
||||
teamAclItems := []*m.DashboardAclInfoDTO{}
|
||||
|
||||
for _, p := range acl {
|
||||
// user match
|
||||
if !g.user.IsAnonymous {
|
||||
if p.UserId == g.user.UserId && p.Permission >= permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// role match
|
||||
if p.Role != nil {
|
||||
if *p.Role == orgRole && p.Permission >= permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// remember this rule for later
|
||||
if p.TeamId > 0 {
|
||||
teamAclItems = append(teamAclItems, p)
|
||||
}
|
||||
}
|
||||
|
||||
// do we have team rules?
|
||||
if len(teamAclItems) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// load teams
|
||||
teams, err := g.getTeams()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// evalute team rules
|
||||
for _, p := range acl {
|
||||
for _, ug := range teams {
|
||||
if ug.Id == p.TeamId && p.Permission >= permission {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
|
||||
acl := []*m.DashboardAclInfoDTO{}
|
||||
adminRole := m.ROLE_ADMIN
|
||||
everyoneWithAdminRole := &m.DashboardAclInfoDTO{DashboardId: g.dashId, UserId: 0, TeamId: 0, Role: &adminRole, Permission: m.PERMISSION_ADMIN}
|
||||
|
||||
// validate that duplicate permissions don't exists
|
||||
for _, p := range updatePermissions {
|
||||
aclItem := &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}
|
||||
if aclItem.IsDuplicateOf(everyoneWithAdminRole) {
|
||||
return false, ErrGuardianPermissionExists
|
||||
}
|
||||
|
||||
for _, a := range acl {
|
||||
if a.IsDuplicateOf(aclItem) {
|
||||
return false, ErrGuardianPermissionExists
|
||||
}
|
||||
}
|
||||
|
||||
acl = append(acl, aclItem)
|
||||
}
|
||||
|
||||
existingPermissions, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// validate overridden permissions to be higher
|
||||
for _, a := range acl {
|
||||
for _, existingPerm := range existingPermissions {
|
||||
// handle default permissions
|
||||
if existingPerm.DashboardId == -1 {
|
||||
existingPerm.DashboardId = g.dashId
|
||||
}
|
||||
|
||||
if a.DashboardId == existingPerm.DashboardId {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission {
|
||||
return false, ErrGuardianOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.user.OrgRole == m.ROLE_ADMIN {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return g.checkAcl(permission, acl)
|
||||
}
|
||||
|
||||
// GetAcl returns dashboard acl
|
||||
func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
|
||||
if g.acl != nil {
|
||||
return g.acl, nil
|
||||
}
|
||||
|
||||
query := m.GetDashboardAclInfoListQuery{DashboardId: g.dashId, OrgId: g.orgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, a := range query.Result {
|
||||
// handle default permissions
|
||||
if a.DashboardId == -1 {
|
||||
a.DashboardId = g.dashId
|
||||
}
|
||||
}
|
||||
|
||||
g.acl = query.Result
|
||||
return g.acl, nil
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
|
||||
if g.groups != nil {
|
||||
return g.groups, nil
|
||||
}
|
||||
|
||||
query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId}
|
||||
err := bus.Dispatch(&query)
|
||||
|
||||
g.groups = query.Result
|
||||
return query.Result, err
|
||||
}
|
||||
|
||||
type FakeDashboardGuardian struct {
|
||||
DashId int64
|
||||
OrgId int64
|
||||
User *m.SignedInUser
|
||||
CanSaveValue bool
|
||||
CanEditValue bool
|
||||
CanViewValue bool
|
||||
CanAdminValue bool
|
||||
HasPermissionValue bool
|
||||
CheckPermissionBeforeUpdateValue bool
|
||||
CheckPermissionBeforeUpdateError error
|
||||
GetAclValue []*m.DashboardAclInfoDTO
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
|
||||
return g.CanSaveValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CanEdit() (bool, error) {
|
||||
return g.CanEditValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CanView() (bool, error) {
|
||||
return g.CanViewValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
|
||||
return g.CanAdminValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
|
||||
return g.HasPermissionValue, nil
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
|
||||
return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError
|
||||
}
|
||||
|
||||
func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
|
||||
return g.GetAclValue, nil
|
||||
}
|
||||
|
||||
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
|
||||
New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
|
||||
mock.OrgId = orgId
|
||||
mock.DashId = dashId
|
||||
mock.User = user
|
||||
return mock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestGuardian(t *testing.T) {
|
||||
Convey("Guardian permission tests", t, func() {
|
||||
orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
|
||||
Convey("When trying to update permissions", func() {
|
||||
Convey("With duplicate user permissions should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianPermissionExists)
|
||||
})
|
||||
|
||||
Convey("With duplicate team permissions should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianPermissionExists)
|
||||
})
|
||||
|
||||
Convey("With duplicate everyone with editor role permission should return error", func() {
|
||||
r := m.ROLE_EDITOR
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianPermissionExists)
|
||||
})
|
||||
|
||||
Convey("With duplicate everyone with viewer role permission should return error", func() {
|
||||
r := m.ROLE_VIEWER
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianPermissionExists)
|
||||
})
|
||||
|
||||
Convey("With everyone with admin role permission should return error", func() {
|
||||
r := m.ROLE_ADMIN
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianPermissionExists)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given default permissions", func() {
|
||||
editor := m.ROLE_EDITOR
|
||||
viewer := m.ROLE_VIEWER
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT},
|
||||
{OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() {
|
||||
r := m.ROLE_VIEWER
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() {
|
||||
r := m.ROLE_EDITOR
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has user admin permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has user edit permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has user view permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has team admin permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has team edit permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has team view permission", func() {
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has editor role with edit permission", func() {
|
||||
r := m.ROLE_EDITOR
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given parent folder has editor role with view permission", func() {
|
||||
r := m.ROLE_EDITOR
|
||||
existingPermissions := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = existingPermissions
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
|
||||
}
|
||||
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(err, ShouldEqual, ErrGuardianOverride)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update permissions should return false", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) {
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeFalse)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeTrue)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeTrue)
|
||||
So(canSave, ShouldBeTrue)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
|
||||
canAdmin, _ := sc.g.CanAdmin()
|
||||
canEdit, _ := sc.g.CanEdit()
|
||||
canSave, _ := sc.g.CanSave()
|
||||
canView, _ := sc.g.CanView()
|
||||
So(canAdmin, ShouldBeFalse)
|
||||
So(canEdit, ShouldBeFalse)
|
||||
So(canSave, ShouldBeFalse)
|
||||
So(canView, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("When trying to update permissions should return false", func() {
|
||||
p := []*m.DashboardAcl{
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||
}
|
||||
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
|
||||
So(ok, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
g DashboardGuardian
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
|
||||
func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) {
|
||||
user := &m.SignedInUser{
|
||||
UserId: 1,
|
||||
OrgId: 1,
|
||||
OrgRole: role,
|
||||
}
|
||||
guard := New(1, 1, user)
|
||||
sc := &scenarioContext{
|
||||
g: guard,
|
||||
}
|
||||
|
||||
Convey(desc, func() {
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = permissions
|
||||
return nil
|
||||
})
|
||||
|
||||
teams := []*m.Team{}
|
||||
|
||||
for _, p := range permissions {
|
||||
if p.TeamId > 0 {
|
||||
teams = append(teams, &m.Team{Id: p.TeamId})
|
||||
}
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||
query.Result = teams
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey(desc, func() {
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
|
||||
p := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission},
|
||||
}
|
||||
permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn)
|
||||
}
|
||||
|
||||
func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
|
||||
p := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission},
|
||||
}
|
||||
permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn)
|
||||
}
|
||||
|
||||
func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
|
||||
p := []*m.DashboardAclInfoDTO{
|
||||
{OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission},
|
||||
}
|
||||
permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"gopkg.in/gomail.v2"
|
||||
gomail "gopkg.in/mail.v2"
|
||||
)
|
||||
|
||||
var mailQueue chan *Message
|
||||
@@ -101,13 +101,17 @@ func createDialer() (*gomail.Dialer, error) {
|
||||
|
||||
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
|
||||
d.TLSConfig = tlsconfig
|
||||
d.LocalName = setting.InstanceName
|
||||
if setting.Smtp.EhloIdentity != "" {
|
||||
d.LocalName = setting.Smtp.EhloIdentity
|
||||
} else {
|
||||
d.LocalName = setting.InstanceName
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
if !setting.Smtp.Enabled {
|
||||
return nil, errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin")
|
||||
return nil, m.ErrSmtpNotEnabled
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
@@ -147,7 +147,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
err := sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
To: []string{evt.Email},
|
||||
Template: tmplSignUpStarted,
|
||||
Data: map[string]interface{}{
|
||||
@@ -156,6 +156,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
"SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: evt.Code}
|
||||
return bus.Dispatch(&emailSentCmd)
|
||||
}
|
||||
|
||||
func signUpCompletedHandler(evt *events.SignUpCompleted) error {
|
||||
|
||||
@@ -28,7 +28,8 @@ type Webhook struct {
|
||||
var netTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type configReader struct {
|
||||
path string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, error) {
|
||||
filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
|
||||
yamlFile, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiVersion := &ConfigVersion{ApiVersion: 0}
|
||||
yaml.Unmarshal(yamlFile, &apiVersion)
|
||||
|
||||
if apiVersion.ApiVersion > 0 {
|
||||
|
||||
v1 := &DashboardAsConfigV1{}
|
||||
err := yaml.Unmarshal(yamlFile, &v1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v1 != nil {
|
||||
return v1.mapToDashboardAsConfig(), nil
|
||||
}
|
||||
|
||||
} else {
|
||||
var v0 []*DashboardsAsConfigV0
|
||||
err := yaml.Unmarshal(yamlFile, &v0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v0 != nil {
|
||||
cr.log.Warn("[Deprecated] the dashboard provisioning config is outdated. please upgrade", "filename", filename)
|
||||
return mapV0ToDashboardAsConfig(v0), nil
|
||||
}
|
||||
}
|
||||
|
||||
return []*DashboardsAsConfig{}, nil
|
||||
}
|
||||
|
||||
func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
var dashboards []*DashboardsAsConfig
|
||||
|
||||
files, err := ioutil.ReadDir(cr.path)
|
||||
if err != nil {
|
||||
cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedDashboards, err := cr.parseConfigs(file)
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
if len(parsedDashboards) > 0 {
|
||||
dashboards = append(dashboards, parsedDashboards...)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range dashboards {
|
||||
if dashboards[i].OrgId == 0 {
|
||||
dashboards[i].OrgId = 1
|
||||
}
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
|
||||
oldVersion string = "./test-configs/version-0"
|
||||
brokenConfigs string = "./test-configs/broken-configs"
|
||||
)
|
||||
|
||||
func TestDashboardsAsConfig(t *testing.T) {
|
||||
Convey("Dashboards as configuration", t, func() {
|
||||
logger := log.New("test-logger")
|
||||
|
||||
Convey("Can read config file version 1 format", func() {
|
||||
cfgProvider := configReader{path: simpleDashboardConfig, log: logger}
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
})
|
||||
|
||||
Convey("Can read config file in version 0 format", func() {
|
||||
cfgProvider := configReader{path: oldVersion, log: logger}
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
})
|
||||
|
||||
Convey("Should skip invalid path", func() {
|
||||
|
||||
cfgProvider := configReader{path: "/invalid-directory", log: logger}
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("readConfig return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(cfg), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Should skip broken config files", func() {
|
||||
|
||||
cfgProvider := configReader{path: brokenConfigs, log: logger}
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("readConfig return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(cfg), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
So(len(cfg), ShouldEqual, 2)
|
||||
|
||||
ds := cfg[0]
|
||||
So(ds.Name, ShouldEqual, "general dashboards")
|
||||
So(ds.Type, ShouldEqual, "file")
|
||||
So(ds.OrgId, ShouldEqual, 2)
|
||||
So(ds.Folder, ShouldEqual, "developers")
|
||||
So(ds.Editable, ShouldBeTrue)
|
||||
So(len(ds.Options), ShouldEqual, 1)
|
||||
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds.DisableDeletion, ShouldBeTrue)
|
||||
|
||||
ds2 := cfg[1]
|
||||
So(ds2.Name, ShouldEqual, "default")
|
||||
So(ds2.Type, ShouldEqual, "file")
|
||||
So(ds2.OrgId, ShouldEqual, 1)
|
||||
So(ds2.Folder, ShouldEqual, "")
|
||||
So(ds2.Editable, ShouldBeFalse)
|
||||
So(len(ds2.Options), ShouldEqual, 1)
|
||||
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds2.DisableDeletion, ShouldBeFalse)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
type DashboardProvisioner struct {
|
||||
cfgReader *configReader
|
||||
log log.Logger
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
|
||||
log := log.New("provisioning.dashboard")
|
||||
d := &DashboardProvisioner{
|
||||
cfgReader: &configReader{path: configDirectory, log: log},
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
err := d.Provision(ctx)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
|
||||
cfgs, err := provider.cfgReader.readConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
switch cfg.Type {
|
||||
case "file":
|
||||
fileReader, err := NewDashboardFileReader(cfg, provider.log.New("type", cfg.Type, "name", cfg.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go fileReader.ReadAndListen(ctx)
|
||||
default:
|
||||
return fmt.Errorf("type %s is not supported", cfg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
checkDiskForChangesInterval time.Duration = time.Second * 3
|
||||
|
||||
ErrFolderNameMissing error = errors.New("Folder name missing")
|
||||
)
|
||||
|
||||
type fileReader struct {
|
||||
Cfg *DashboardsAsConfig
|
||||
Path string
|
||||
log log.Logger
|
||||
dashboardService dashboards.DashboardProvisioningService
|
||||
}
|
||||
|
||||
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
|
||||
var path string
|
||||
path, ok := cfg.Options["path"].(string)
|
||||
if !ok {
|
||||
path, ok = cfg.Options["folder"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Failed to load dashboards. path param is not a string")
|
||||
}
|
||||
|
||||
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
return &fileReader{
|
||||
Cfg: cfg,
|
||||
Path: path,
|
||||
log: log,
|
||||
dashboardService: dashboards.NewProvisioningService(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
if err := fr.startWalkingDisk(); err != nil {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(checkDiskForChangesInterval)
|
||||
|
||||
running := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
|
||||
running = true
|
||||
go func() {
|
||||
if err := fr.startWalkingDisk(); err != nil {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
running = false
|
||||
}()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fr *fileReader) startWalkingDisk() error {
|
||||
if _, err := os.Stat(fr.Path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
|
||||
if err != nil && err != ErrFolderNameMissing {
|
||||
return err
|
||||
}
|
||||
|
||||
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, fr.Cfg.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filesFoundOnDisk := map[string]os.FileInfo{}
|
||||
err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk)
|
||||
|
||||
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
|
||||
|
||||
// save dashboards based on json files
|
||||
for path, fileInfo := range filesFoundOnDisk {
|
||||
provisioningMetadata, err := fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
|
||||
sanityChecker.track(provisioningMetadata)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to save dashboard", "error", err)
|
||||
}
|
||||
}
|
||||
sanityChecker.logWarnings(fr.log)
|
||||
|
||||
return nil
|
||||
}
|
||||
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
|
||||
if fr.Cfg.DisableDeletion {
|
||||
return
|
||||
}
|
||||
|
||||
// find dashboards to delete since json file is missing
|
||||
var dashboardToDelete []int64
|
||||
for path, provisioningData := range provisionedDashboardRefs {
|
||||
_, existsOnDisk := filesFoundOnDisk[path]
|
||||
if !existsOnDisk {
|
||||
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
|
||||
}
|
||||
}
|
||||
// delete dashboard that are missing json file
|
||||
for _, dashboardId := range dashboardToDelete {
|
||||
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
|
||||
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to delete dashboard", "id", cmd.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) {
|
||||
provisioningMetadata := provisioningMetadata{}
|
||||
resolvedFileInfo, err := resolveSymlink(fileInfo, path)
|
||||
if err != nil {
|
||||
return provisioningMetadata, err
|
||||
}
|
||||
|
||||
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
|
||||
upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
|
||||
|
||||
dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
||||
return provisioningMetadata, nil
|
||||
}
|
||||
|
||||
// keeps track of what uid's and title's we have already provisioned
|
||||
provisioningMetadata.uid = dash.Dashboard.Uid
|
||||
provisioningMetadata.title = dash.Dashboard.Title
|
||||
|
||||
if upToDate {
|
||||
return provisioningMetadata, nil
|
||||
}
|
||||
|
||||
if dash.Dashboard.Id != 0 {
|
||||
dash.Dashboard.Data.Set("id", nil)
|
||||
dash.Dashboard.Id = 0
|
||||
}
|
||||
|
||||
if alreadyProvisioned {
|
||||
dash.Dashboard.SetId(provisionedData.DashboardId)
|
||||
}
|
||||
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
|
||||
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
|
||||
return provisioningMetadata, err
|
||||
}
|
||||
|
||||
func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningService, name string) (map[string]*models.DashboardProvisioning, error) {
|
||||
arr, err := service.GetProvisionedDashboardData(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
byPath := map[string]*models.DashboardProvisioning{}
|
||||
for _, pd := range arr {
|
||||
byPath[pd.ExternalId] = pd
|
||||
}
|
||||
|
||||
return byPath, nil
|
||||
}
|
||||
|
||||
func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardProvisioningService) (int64, error) {
|
||||
if cfg.Folder == "" {
|
||||
return 0, ErrFolderNameMissing
|
||||
}
|
||||
|
||||
cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId}
|
||||
err := bus.Dispatch(cmd)
|
||||
|
||||
if err != nil && err != models.ErrDashboardNotFound {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// dashboard folder not found. create one.
|
||||
if err == models.ErrDashboardNotFound {
|
||||
dash := &dashboards.SaveDashboardDTO{}
|
||||
dash.Dashboard = models.NewDashboardFolder(cfg.Folder)
|
||||
dash.Dashboard.IsFolder = true
|
||||
dash.Overwrite = true
|
||||
dash.OrgId = cfg.OrgId
|
||||
dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return dbDash.Id, nil
|
||||
}
|
||||
|
||||
if !cmd.Result.IsFolder {
|
||||
return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
|
||||
}
|
||||
|
||||
return cmd.Result.Id, nil
|
||||
}
|
||||
|
||||
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
|
||||
checkFilepath, err := filepath.EvalSymlinks(path)
|
||||
if path != checkFilepath {
|
||||
path = checkFilepath
|
||||
fi, err := os.Lstat(checkFilepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
return fileinfo, err
|
||||
}
|
||||
|
||||
func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
|
||||
return func(path string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isValid, err := validateWalkablePath(fileInfo)
|
||||
if !isValid {
|
||||
return err
|
||||
}
|
||||
|
||||
filesOnDisk[path] = fileInfo
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
|
||||
if fileInfo.IsDir() {
|
||||
if strings.HasPrefix(fileInfo.Name(), ".") {
|
||||
return false, filepath.SkipDir
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(fileInfo.Name(), ".json") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
type provisioningMetadata struct {
|
||||
uid string
|
||||
title string
|
||||
}
|
||||
|
||||
func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker {
|
||||
return provisioningSanityChecker{
|
||||
provisioningProvider: provisioningProvider,
|
||||
uidUsage: map[string]uint8{},
|
||||
titleUsage: map[string]uint8{}}
|
||||
}
|
||||
|
||||
type provisioningSanityChecker struct {
|
||||
provisioningProvider string
|
||||
uidUsage map[string]uint8
|
||||
titleUsage map[string]uint8
|
||||
}
|
||||
|
||||
func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
|
||||
if len(pm.uid) > 0 {
|
||||
checker.uidUsage[pm.uid] += 1
|
||||
}
|
||||
if len(pm.title) > 0 {
|
||||
checker.titleUsage[pm.title] += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
||||
for uid, times := range checker.uidUsage {
|
||||
if times > 1 {
|
||||
log.Error("the same 'uid' is used more than once", "uid", uid, "provider", checker.provisioningProvider)
|
||||
}
|
||||
}
|
||||
|
||||
for title, times := range checker.titleUsage {
|
||||
if times > 1 {
|
||||
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultDashboards = "./test-dashboards/folder-one"
|
||||
brokenDashboards = "./test-dashboards/broken-dashboards"
|
||||
oneDashboard = "./test-dashboards/one-dashboard"
|
||||
containingId = "./test-dashboards/containing-id"
|
||||
|
||||
fakeService *fakeDashboardProvisioningService
|
||||
)
|
||||
|
||||
func TestDashboardFileReader(t *testing.T) {
|
||||
Convey("Dashboard file reader", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
origNewDashboardProvisioningService := dashboards.NewProvisioningService
|
||||
fakeService = mockDashboardProvisioningService()
|
||||
|
||||
bus.AddHandler("test", mockGetDashboardQuery)
|
||||
logger := log.New("test.logger")
|
||||
|
||||
Convey("Reading dashboards from disk", func() {
|
||||
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{},
|
||||
}
|
||||
|
||||
Convey("Can read default dashboard", func() {
|
||||
cfg.Options["path"] = defaultDashboards
|
||||
cfg.Folder = "Team A"
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
folders := 0
|
||||
dashboards := 0
|
||||
|
||||
for _, i := range fakeService.inserted {
|
||||
if i.Dashboard.IsFolder {
|
||||
folders++
|
||||
} else {
|
||||
dashboards++
|
||||
}
|
||||
}
|
||||
|
||||
So(folders, ShouldEqual, 1)
|
||||
So(dashboards, ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("Can read default dashboard and replace old version in database", func() {
|
||||
cfg.Options["path"] = oneDashboard
|
||||
|
||||
stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
|
||||
|
||||
fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{
|
||||
Updated: stat.ModTime().AddDate(0, 0, -1),
|
||||
Slug: "grafana",
|
||||
})
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Overrides id from dashboard.json files", func() {
|
||||
cfg.Options["path"] = containingId
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
err = reader.startWalkingDisk()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("Invalid configuration should return error", func() {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
}
|
||||
|
||||
_, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Broken dashboards should not cause error", func() {
|
||||
cfg.Options["path"] = brokenDashboards
|
||||
|
||||
_, err := NewDashboardFileReader(cfg, logger)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not create new folder if folder name is missing", func() {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{
|
||||
"folder": defaultDashboards,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := getOrCreateFolderId(cfg, fakeService)
|
||||
So(err, ShouldEqual, ErrFolderNameMissing)
|
||||
})
|
||||
|
||||
Convey("can get or Create dashboard folder", func() {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "TEAM A",
|
||||
Options: map[string]interface{}{
|
||||
"folder": defaultDashboards,
|
||||
},
|
||||
}
|
||||
|
||||
folderId, err := getOrCreateFolderId(cfg, fakeService)
|
||||
So(err, ShouldBeNil)
|
||||
inserted := false
|
||||
for _, d := range fakeService.inserted {
|
||||
if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
|
||||
inserted = true
|
||||
}
|
||||
}
|
||||
So(len(fakeService.inserted), ShouldEqual, 1)
|
||||
So(inserted, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Walking the folder with dashboards", func() {
|
||||
noFiles := map[string]os.FileInfo{}
|
||||
|
||||
Convey("should skip dirs that starts with .", func() {
|
||||
shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
|
||||
So(shouldSkip, ShouldEqual, filepath.SkipDir)
|
||||
})
|
||||
|
||||
Convey("should keep walking if file is not .json", func() {
|
||||
shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
|
||||
So(shouldSkip, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can use bpth path and folder as dashboard path", func() {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{},
|
||||
}
|
||||
|
||||
Convey("using path parameter", func() {
|
||||
cfg.Options["path"] = defaultDashboards
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
So(reader.Path, ShouldEqual, defaultDashboards)
|
||||
})
|
||||
|
||||
Convey("using folder as options", func() {
|
||||
cfg.Options["folder"] = defaultDashboards
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
So(reader.Path, ShouldEqual, defaultDashboards)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
dashboards.NewProvisioningService = origNewDashboardProvisioningService
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type FakeFileInfo struct {
|
||||
isDirectory bool
|
||||
name string
|
||||
}
|
||||
|
||||
func (ffi *FakeFileInfo) IsDir() bool {
|
||||
return ffi.isDirectory
|
||||
}
|
||||
|
||||
func (ffi FakeFileInfo) Size() int64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func (ffi FakeFileInfo) Mode() os.FileMode {
|
||||
return 0777
|
||||
}
|
||||
|
||||
func (ffi FakeFileInfo) Name() string {
|
||||
return ffi.name
|
||||
}
|
||||
|
||||
func (ffi FakeFileInfo) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (ffi FakeFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
|
||||
mock := fakeDashboardProvisioningService{}
|
||||
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
|
||||
return &mock
|
||||
}
|
||||
return &mock
|
||||
}
|
||||
|
||||
type fakeDashboardProvisioningService struct {
|
||||
inserted []*dashboards.SaveDashboardDTO
|
||||
provisioned []*models.DashboardProvisioning
|
||||
getDashboard []*models.Dashboard
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
return s.provisioned, nil
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
|
||||
s.inserted = append(s.inserted, dto)
|
||||
s.provisioned = append(s.provisioned, provisioning)
|
||||
return dto.Dashboard, nil
|
||||
}
|
||||
|
||||
func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
s.inserted = append(s.inserted, dto)
|
||||
return dto.Dashboard, nil
|
||||
}
|
||||
|
||||
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
|
||||
for _, d := range fakeService.getDashboard {
|
||||
if d.Slug == cmd.Slug {
|
||||
cmd.Result = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return models.ErrDashboardNotFound
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# - name: 'default'
|
||||
# org_id: 1
|
||||
# folder: ''
|
||||
# type: file
|
||||
# options:
|
||||
# path: /var/lib/grafana/dashboards
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'general dashboards'
|
||||
orgId: 2
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
- name: 'default'
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: 1
|
||||
|
||||
#providers:
|
||||
#- name: 'gasdf'
|
||||
# orgId: 2
|
||||
# folder: 'developers'
|
||||
# editable: true
|
||||
# type: file
|
||||
# options:
|
||||
# path: /var/lib/grafana/dashboards
|
||||
@@ -0,0 +1,13 @@
|
||||
- name: 'general dashboards'
|
||||
org_id: 2
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
- name: 'default'
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
@@ -0,0 +1,6 @@
|
||||
[]
|
||||
{
|
||||
"title": "Grafana",
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"title": "Grafana1",
|
||||
"tags": [],
|
||||
"id": 3,
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"title": "Grafana1",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"title": "Grafana2",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"title": "Grafana",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
"editable": true,
|
||||
"rows": [
|
||||
{
|
||||
"title": "New row",
|
||||
"height": "150px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
|
||||
"style": {},
|
||||
"title": "Welcome to"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Welcome to Grafana",
|
||||
"height": "210px",
|
||||
"collapse": false,
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 2,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
|
||||
"style": {},
|
||||
"title": "Documentation Links"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"span": 6,
|
||||
"type": "text",
|
||||
"mode": "html",
|
||||
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
|
||||
"style": {},
|
||||
"title": "Tips & Shortcuts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "test",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"panels": [
|
||||
{
|
||||
"id": 4,
|
||||
"span": 12,
|
||||
"type": "graph",
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"scale": 1,
|
||||
"y_formats": [
|
||||
"short",
|
||||
"short"
|
||||
],
|
||||
"grid": {
|
||||
"max": null,
|
||||
"min": null,
|
||||
"leftMax": null,
|
||||
"rightMax": null,
|
||||
"leftMin": null,
|
||||
"rightMin": null,
|
||||
"threshold1": null,
|
||||
"threshold2": null,
|
||||
"threshold1Color": "rgba(216, 200, 27, 0.27)",
|
||||
"threshold2Color": "rgba(234, 112, 112, 0.22)"
|
||||
},
|
||||
"resolution": 100,
|
||||
"lines": true,
|
||||
"fill": 1,
|
||||
"linewidth": 2,
|
||||
"dashes": false,
|
||||
"dashLength": 10,
|
||||
"spaceLength": 10,
|
||||
"points": false,
|
||||
"pointradius": 5,
|
||||
"bars": false,
|
||||
"stack": true,
|
||||
"spyable": true,
|
||||
"options": false,
|
||||
"legend": {
|
||||
"show": true,
|
||||
"values": false,
|
||||
"min": false,
|
||||
"max": false,
|
||||
"current": false,
|
||||
"total": false,
|
||||
"avg": false
|
||||
},
|
||||
"interactive": true,
|
||||
"legend_counts": true,
|
||||
"timezone": "browser",
|
||||
"percentage": false,
|
||||
"nullPointMode": "connected",
|
||||
"steppedLine": false,
|
||||
"tooltip": {
|
||||
"value_type": "cumulative",
|
||||
"query_as_alias": true
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target": "randomWalk('random walk')",
|
||||
"function": "mean",
|
||||
"column": "value"
|
||||
}
|
||||
],
|
||||
"aliasColors": {},
|
||||
"aliasYAxis": {},
|
||||
"title": "First Graph (click title to edit)",
|
||||
"datasource": "graphite",
|
||||
"renderer": "flot",
|
||||
"annotate": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"type": "timepicker",
|
||||
"collapse": false,
|
||||
"enable": true,
|
||||
"status": "Stable",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"now": true
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"version": 5
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DashboardsAsConfig struct {
|
||||
Name string
|
||||
Type string
|
||||
OrgId int64
|
||||
Folder string
|
||||
Editable bool
|
||||
Options map[string]interface{}
|
||||
DisableDeletion bool
|
||||
}
|
||||
|
||||
type DashboardsAsConfigV0 struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
}
|
||||
|
||||
type ConfigVersion struct {
|
||||
ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
|
||||
}
|
||||
|
||||
type DashboardAsConfigV1 struct {
|
||||
Providers []*DashboardProviderConfigs `json:"providers" yaml:"providers"`
|
||||
}
|
||||
|
||||
type DashboardProviderConfigs struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
}
|
||||
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
dash := &dashboards.SaveDashboardDTO{}
|
||||
dash.Dashboard = models.NewDashboardFromJson(data)
|
||||
dash.UpdatedAt = lastModified
|
||||
dash.Overwrite = true
|
||||
dash.OrgId = cfg.OrgId
|
||||
dash.Dashboard.OrgId = cfg.OrgId
|
||||
dash.Dashboard.FolderId = folderId
|
||||
if !cfg.Editable {
|
||||
dash.Dashboard.Data.Set("editable", cfg.Editable)
|
||||
}
|
||||
|
||||
if dash.Dashboard.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig {
|
||||
var r []*DashboardsAsConfig
|
||||
|
||||
for _, v := range v0 {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
|
||||
var r []*DashboardsAsConfig
|
||||
|
||||
for _, v := range dc.Providers {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type configReader struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
|
||||
var datasources []*DatasourcesAsConfig
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
cr.log.Error("cant read datasource provisioning files from directory", "path", path)
|
||||
return datasources, nil
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
|
||||
datasource, err := cr.parseDatasourceConfig(path, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if datasource != nil {
|
||||
datasources = append(datasources, datasource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = validateDefaultUniqueness(datasources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return datasources, nil
|
||||
}
|
||||
|
||||
func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*DatasourcesAsConfig, error) {
|
||||
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
|
||||
yamlFile, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiVersion *ConfigVersion
|
||||
err = yaml.Unmarshal(yamlFile, &apiVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apiVersion == nil {
|
||||
apiVersion = &ConfigVersion{ApiVersion: 0}
|
||||
}
|
||||
|
||||
if apiVersion.ApiVersion > 0 {
|
||||
var v1 *DatasourcesAsConfigV1
|
||||
err = yaml.Unmarshal(yamlFile, &v1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
|
||||
}
|
||||
|
||||
var v0 *DatasourcesAsConfigV0
|
||||
err = yaml.Unmarshal(yamlFile, &v0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cr.log.Warn("[Deprecated] the datasource provisioning config is outdated. please upgrade", "filename", filename)
|
||||
|
||||
return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
|
||||
}
|
||||
|
||||
func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
|
||||
defaultCount := 0
|
||||
for i := range datasources {
|
||||
if datasources[i].Datasources == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ds := range datasources[i].Datasources {
|
||||
if ds.OrgId == 0 {
|
||||
ds.OrgId = 1
|
||||
}
|
||||
|
||||
if ds.IsDefault {
|
||||
defaultCount++
|
||||
if defaultCount > 1 {
|
||||
return ErrInvalidConfigToManyDefault
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ds := range datasources[i].DeleteDatasources {
|
||||
if ds.OrgId == 0 {
|
||||
ds.OrgId = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("fake.log")
|
||||
oneDatasourcesConfig string = ""
|
||||
twoDatasourcesConfig string = "./test-configs/two-datasources"
|
||||
twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two"
|
||||
doubleDatasourcesConfig string = "./test-configs/double-default"
|
||||
allProperties string = "./test-configs/all-properties"
|
||||
versionZero string = "./test-configs/version-0"
|
||||
brokenYaml string = "./test-configs/broken-yaml"
|
||||
|
||||
fakeRepo *fakeRepository
|
||||
)
|
||||
|
||||
func TestDatasourceAsConfig(t *testing.T) {
|
||||
Convey("Testing datasource as configuration", t, func() {
|
||||
fakeRepo = &fakeRepository{}
|
||||
bus.ClearBusHandlers()
|
||||
bus.AddHandler("test", mockDelete)
|
||||
bus.AddHandler("test", mockInsert)
|
||||
bus.AddHandler("test", mockUpdate)
|
||||
bus.AddHandler("test", mockGet)
|
||||
bus.AddHandler("test", mockGetAll)
|
||||
|
||||
Convey("One configured datasource", func() {
|
||||
Convey("no datasource in database", func() {
|
||||
dc := newDatasourceProvisioner(logger)
|
||||
err := dc.applyChanges(twoDatasourcesConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("applyChanges return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 2)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("One datasource in database with same name", func() {
|
||||
fakeRepo.loadAll = []*models.DataSource{
|
||||
{Name: "Graphite", OrgId: 1, Id: 1},
|
||||
}
|
||||
|
||||
Convey("should update one datasource", func() {
|
||||
dc := newDatasourceProvisioner(logger)
|
||||
err := dc.applyChanges(twoDatasourcesConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("applyChanges return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 1)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Two datasources with is_default", func() {
|
||||
dc := newDatasourceProvisioner(logger)
|
||||
err := dc.applyChanges(doubleDatasourcesConfig)
|
||||
Convey("should raise error", func() {
|
||||
So(err, ShouldEqual, ErrInvalidConfigToManyDefault)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Two configured datasource and purge others ", func() {
|
||||
Convey("two other datasources in database", func() {
|
||||
fakeRepo.loadAll = []*models.DataSource{
|
||||
{Name: "old-graphite", OrgId: 1, Id: 1},
|
||||
{Name: "old-graphite2", OrgId: 1, Id: 2},
|
||||
}
|
||||
|
||||
Convey("should have two new datasources", func() {
|
||||
dc := newDatasourceProvisioner(logger)
|
||||
err := dc.applyChanges(twoDatasourcesConfigPurgeOthers)
|
||||
if err != nil {
|
||||
t.Fatalf("applyChanges return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 2)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 2)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Two configured datasource and purge others = false", func() {
|
||||
Convey("two other datasources in database", func() {
|
||||
fakeRepo.loadAll = []*models.DataSource{
|
||||
{Name: "Graphite", OrgId: 1, Id: 1},
|
||||
{Name: "old-graphite2", OrgId: 1, Id: 2},
|
||||
}
|
||||
|
||||
Convey("should have two new datasources", func() {
|
||||
dc := newDatasourceProvisioner(logger)
|
||||
err := dc.applyChanges(twoDatasourcesConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("applyChanges return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 1)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("broken yaml should return error", func() {
|
||||
reader := &configReader{}
|
||||
_, err := reader.readConfig(brokenYaml)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("skip invalid directory", func() {
|
||||
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||
cfg, err := cfgProvifer.readConfig("./invalid-directory")
|
||||
if err != nil {
|
||||
t.Fatalf("readConfig return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(cfg), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("can read all properties from version 1", func() {
|
||||
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||
cfg, err := cfgProvifer.readConfig(allProperties)
|
||||
if err != nil {
|
||||
t.Fatalf("readConfig return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(cfg), ShouldEqual, 3)
|
||||
|
||||
dsCfg := cfg[0]
|
||||
|
||||
So(dsCfg.ApiVersion, ShouldEqual, 1)
|
||||
|
||||
validateDatasource(dsCfg)
|
||||
validateDeleteDatasources(dsCfg)
|
||||
|
||||
dsCount := 0
|
||||
delDsCount := 0
|
||||
|
||||
for _, c := range cfg {
|
||||
dsCount += len(c.Datasources)
|
||||
delDsCount += len(c.DeleteDatasources)
|
||||
}
|
||||
|
||||
So(dsCount, ShouldEqual, 2)
|
||||
So(delDsCount, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("can read all properties from version 0", func() {
|
||||
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||
cfg, err := cfgProvifer.readConfig(versionZero)
|
||||
if err != nil {
|
||||
t.Fatalf("readConfig return an error %v", err)
|
||||
}
|
||||
|
||||
So(len(cfg), ShouldEqual, 1)
|
||||
|
||||
dsCfg := cfg[0]
|
||||
|
||||
So(dsCfg.ApiVersion, ShouldEqual, 0)
|
||||
|
||||
validateDatasource(dsCfg)
|
||||
validateDeleteDatasources(dsCfg)
|
||||
})
|
||||
})
|
||||
}
|
||||
func validateDeleteDatasources(dsCfg *DatasourcesAsConfig) {
|
||||
So(len(dsCfg.DeleteDatasources), ShouldEqual, 1)
|
||||
deleteDs := dsCfg.DeleteDatasources[0]
|
||||
So(deleteDs.Name, ShouldEqual, "old-graphite3")
|
||||
So(deleteDs.OrgId, ShouldEqual, 2)
|
||||
}
|
||||
func validateDatasource(dsCfg *DatasourcesAsConfig) {
|
||||
ds := dsCfg.Datasources[0]
|
||||
So(ds.Name, ShouldEqual, "name")
|
||||
So(ds.Type, ShouldEqual, "type")
|
||||
So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
|
||||
So(ds.OrgId, ShouldEqual, 2)
|
||||
So(ds.Url, ShouldEqual, "url")
|
||||
So(ds.User, ShouldEqual, "user")
|
||||
So(ds.Password, ShouldEqual, "password")
|
||||
So(ds.Database, ShouldEqual, "database")
|
||||
So(ds.BasicAuth, ShouldBeTrue)
|
||||
So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user")
|
||||
So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password")
|
||||
So(ds.WithCredentials, ShouldBeTrue)
|
||||
So(ds.IsDefault, ShouldBeTrue)
|
||||
So(ds.Editable, ShouldBeTrue)
|
||||
So(ds.Version, ShouldEqual, 10)
|
||||
|
||||
So(len(ds.JsonData), ShouldBeGreaterThan, 2)
|
||||
So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
|
||||
So(ds.JsonData["tlsAuth"], ShouldEqual, true)
|
||||
So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
|
||||
|
||||
So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
|
||||
So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
|
||||
So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
|
||||
So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
|
||||
}
|
||||
|
||||
type fakeRepository struct {
|
||||
inserted []*models.AddDataSourceCommand
|
||||
deleted []*models.DeleteDataSourceByNameCommand
|
||||
updated []*models.UpdateDataSourceCommand
|
||||
|
||||
loadAll []*models.DataSource
|
||||
}
|
||||
|
||||
func mockDelete(cmd *models.DeleteDataSourceByNameCommand) error {
|
||||
fakeRepo.deleted = append(fakeRepo.deleted, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockUpdate(cmd *models.UpdateDataSourceCommand) error {
|
||||
fakeRepo.updated = append(fakeRepo.updated, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockInsert(cmd *models.AddDataSourceCommand) error {
|
||||
fakeRepo.inserted = append(fakeRepo.inserted, cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockGetAll(cmd *models.GetAllDataSourcesQuery) error {
|
||||
cmd.Result = fakeRepo.loadAll
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockGet(cmd *models.GetDataSourceByNameQuery) error {
|
||||
for _, v := range fakeRepo.loadAll {
|
||||
if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
|
||||
cmd.Result = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return models.ErrDataSourceNotFound
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource can be marked as default")
|
||||
)
|
||||
|
||||
func Provision(configDirectory string) error {
|
||||
dc := newDatasourceProvisioner(log.New("provisioning.datasources"))
|
||||
return dc.applyChanges(configDirectory)
|
||||
}
|
||||
|
||||
type DatasourceProvisioner struct {
|
||||
log log.Logger
|
||||
cfgProvider *configReader
|
||||
}
|
||||
|
||||
func newDatasourceProvisioner(log log.Logger) DatasourceProvisioner {
|
||||
return DatasourceProvisioner{
|
||||
log: log,
|
||||
cfgProvider: &configReader{log: log},
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *DatasourceProvisioner) apply(cfg *DatasourcesAsConfig) error {
|
||||
if err := dc.deleteDatasources(cfg.DeleteDatasources); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ds := range cfg.Datasources {
|
||||
cmd := &models.GetDataSourceByNameQuery{OrgId: ds.OrgId, Name: ds.Name}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil && err != models.ErrDataSourceNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == models.ErrDataSourceNotFound {
|
||||
dc.log.Info("inserting datasource from configuration ", "name", ds.Name)
|
||||
insertCmd := createInsertCommand(ds)
|
||||
if err := bus.Dispatch(insertCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dc.log.Debug("updating datasource from configuration", "name", ds.Name)
|
||||
updateCmd := createUpdateCommand(ds, cmd.Result.Id)
|
||||
if err := bus.Dispatch(updateCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DatasourceProvisioner) applyChanges(configPath string) error {
|
||||
configs, err := dc.cfgProvider.readConfig(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
if err := dc.apply(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourceConfig) error {
|
||||
for _, ds := range dsToDelete {
|
||||
cmd := &models.DeleteDataSourceByNameCommand{OrgId: ds.OrgId, Name: ds.Name}
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.DeletedDatasourcesCount > 0 {
|
||||
dc.log.Info("deleted datasource based on configuration", "name", ds.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: name
|
||||
type: type
|
||||
access: proxy
|
||||
orgId: 2
|
||||
url: url
|
||||
password: password
|
||||
user: user
|
||||
database: database
|
||||
basicAuth: true
|
||||
basicAuthUser: basic_auth_user
|
||||
basicAuthPassword: basic_auth_password
|
||||
withCredentials: true
|
||||
isDefault: true
|
||||
jsonData:
|
||||
graphiteVersion: "1.1"
|
||||
tlsAuth: true
|
||||
tlsAuthWithCACert: true
|
||||
secureJsonData:
|
||||
tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
|
||||
tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
|
||||
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
|
||||
editable: true
|
||||
version: 10
|
||||
|
||||
deleteDatasources:
|
||||
- name: old-graphite3
|
||||
orgId: 2
|
||||
@@ -0,0 +1,32 @@
|
||||
# Should not be included
|
||||
|
||||
|
||||
apiVersion: 1
|
||||
|
||||
#datasources:
|
||||
# - name: name
|
||||
# type: type
|
||||
# access: proxy
|
||||
# orgId: 2
|
||||
# url: url
|
||||
# password: password
|
||||
# user: user
|
||||
# database: database
|
||||
# basicAuth: true
|
||||
# basicAuthUser: basic_auth_user
|
||||
# basicAuthPassword: basic_auth_password
|
||||
# withCredentials: true
|
||||
# jsonData:
|
||||
# graphiteVersion: "1.1"
|
||||
# tlsAuth: true
|
||||
# tlsAuthWithCACert: true
|
||||
# secureJsonData:
|
||||
# tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
|
||||
# tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
|
||||
# tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
|
||||
# editable: true
|
||||
# version: 10
|
||||
#
|
||||
#deleteDatasources:
|
||||
# - name: old-graphite3
|
||||
# orgId: 2
|
||||
@@ -0,0 +1,7 @@
|
||||
purge_other_datasources: true
|
||||
datasources:
|
||||
- name: name2
|
||||
type: type2
|
||||
access: proxy
|
||||
orgId: 2
|
||||
url: url2
|
||||
@@ -0,0 +1,6 @@
|
||||
#sfxzgnsxzcvnbzcvn
|
||||
cvbn
|
||||
cvbn
|
||||
c
|
||||
vbn
|
||||
cvbncvbn
|
||||
@@ -0,0 +1,48 @@
|
||||
# # list of datasources that should be deleted from the database
|
||||
#delete_datasources:
|
||||
# - name: Graphite
|
||||
# org_id: 1
|
||||
|
||||
# # list of datasources to insert/update depending
|
||||
# # whats available in the datbase
|
||||
#datasources:
|
||||
# # <string, required> name of the datasource. Required
|
||||
# - name: Graphite
|
||||
# # <string, required> datasource type. Required
|
||||
# type: graphite
|
||||
# # <string, required> access mode. direct or proxy. Required
|
||||
# access: proxy
|
||||
# # <int> org id. will default to org_id 1 if not specified
|
||||
# org_id: 1
|
||||
# # <string> url
|
||||
# url: http://localhost:8080
|
||||
# # <string> database password, if used
|
||||
# password:
|
||||
# # <string> database user, if used
|
||||
# user:
|
||||
# # <string> database name, if used
|
||||
# database:
|
||||
# # <bool> enable/disable basic auth
|
||||
# basic_auth:
|
||||
# # <string> basic auth username
|
||||
# basic_auth_user:
|
||||
# # <string> basic auth password
|
||||
# basic_auth_password:
|
||||
# # <bool> enable/disable with credentials headers
|
||||
# with_credentials:
|
||||
# # <bool> mark as default datasource. Max one per org
|
||||
# is_default:
|
||||
# # <map> fields that will be converted to json and stored in json_data
|
||||
# json_data:
|
||||
# graphiteVersion: "1.1"
|
||||
# tlsAuth: true
|
||||
# tlsAuthWithCACert: true
|
||||
# # <string> json object of data that will be encrypted.
|
||||
# secure_json_data:
|
||||
# tlsCACert: "..."
|
||||
# tlsClientCert: "..."
|
||||
# tlsClientKey: "..."
|
||||
# version: 1
|
||||
# # <bool> allow users to edit datasources from the UI.
|
||||
# editable: false
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
datasources:
|
||||
- name: Graphite
|
||||
type: graphite
|
||||
access: proxy
|
||||
url: http://localhost:8080
|
||||
is_default: true
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
datasources:
|
||||
- name: Graphite
|
||||
type: graphite
|
||||
access: proxy
|
||||
url: http://localhost:8080
|
||||
is_default: true
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://localhost:9090
|
||||
delete_datasources:
|
||||
- name: old-graphite
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
datasources:
|
||||
- name: Graphite
|
||||
type: graphite
|
||||
access: proxy
|
||||
url: http://localhost:8080
|
||||
delete_datasources:
|
||||
- name: old-graphite3
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
datasources:
|
||||
- name: Graphite
|
||||
type: graphite
|
||||
access: proxy
|
||||
url: http://localhost:8080
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://localhost:9090
|
||||
@@ -0,0 +1,28 @@
|
||||
datasources:
|
||||
- name: name
|
||||
type: type
|
||||
access: proxy
|
||||
org_id: 2
|
||||
url: url
|
||||
password: password
|
||||
user: user
|
||||
database: database
|
||||
basic_auth: true
|
||||
basic_auth_user: basic_auth_user
|
||||
basic_auth_password: basic_auth_password
|
||||
with_credentials: true
|
||||
is_default: true
|
||||
json_data:
|
||||
graphiteVersion: "1.1"
|
||||
tlsAuth: true
|
||||
tlsAuthWithCACert: true
|
||||
secure_json_data:
|
||||
tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
|
||||
tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
|
||||
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
|
||||
editable: true
|
||||
version: 10
|
||||
|
||||
delete_datasources:
|
||||
- name: old-graphite3
|
||||
org_id: 2
|
||||
@@ -0,0 +1,246 @@
|
||||
package datasources
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||
|
||||
type ConfigVersion struct {
|
||||
ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
|
||||
}
|
||||
|
||||
type DatasourcesAsConfig struct {
|
||||
ApiVersion int64
|
||||
|
||||
Datasources []*DataSourceFromConfig
|
||||
DeleteDatasources []*DeleteDatasourceConfig
|
||||
}
|
||||
|
||||
type DeleteDatasourceConfig struct {
|
||||
OrgId int64
|
||||
Name string
|
||||
}
|
||||
|
||||
type DataSourceFromConfig struct {
|
||||
OrgId int64
|
||||
Version int
|
||||
|
||||
Name string
|
||||
Type string
|
||||
Access string
|
||||
Url string
|
||||
Password string
|
||||
User string
|
||||
Database string
|
||||
BasicAuth bool
|
||||
BasicAuthUser string
|
||||
BasicAuthPassword string
|
||||
WithCredentials bool
|
||||
IsDefault bool
|
||||
JsonData map[string]interface{}
|
||||
SecureJsonData map[string]string
|
||||
Editable bool
|
||||
}
|
||||
|
||||
type DatasourcesAsConfigV0 struct {
|
||||
ConfigVersion
|
||||
|
||||
Datasources []*DataSourceFromConfigV0 `json:"datasources" yaml:"datasources"`
|
||||
DeleteDatasources []*DeleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"`
|
||||
}
|
||||
|
||||
type DatasourcesAsConfigV1 struct {
|
||||
ConfigVersion
|
||||
|
||||
Datasources []*DataSourceFromConfigV1 `json:"datasources" yaml:"datasources"`
|
||||
DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
|
||||
}
|
||||
|
||||
type DeleteDatasourceConfigV0 struct {
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
type DeleteDatasourceConfigV1 struct {
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
type DataSourceFromConfigV0 struct {
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Version int `json:"version" yaml:"version"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Access string `json:"access" yaml:"access"`
|
||||
Url string `json:"url" yaml:"url"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
User string `json:"user" yaml:"user"`
|
||||
Database string `json:"database" yaml:"database"`
|
||||
BasicAuth bool `json:"basic_auth" yaml:"basic_auth"`
|
||||
BasicAuthUser string `json:"basic_auth_user" yaml:"basic_auth_user"`
|
||||
BasicAuthPassword string `json:"basic_auth_password" yaml:"basic_auth_password"`
|
||||
WithCredentials bool `json:"with_credentials" yaml:"with_credentials"`
|
||||
IsDefault bool `json:"is_default" yaml:"is_default"`
|
||||
JsonData map[string]interface{} `json:"json_data" yaml:"json_data"`
|
||||
SecureJsonData map[string]string `json:"secure_json_data" yaml:"secure_json_data"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
}
|
||||
|
||||
type DataSourceFromConfigV1 struct {
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Version int `json:"version" yaml:"version"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
Access string `json:"access" yaml:"access"`
|
||||
Url string `json:"url" yaml:"url"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
User string `json:"user" yaml:"user"`
|
||||
Database string `json:"database" yaml:"database"`
|
||||
BasicAuth bool `json:"basicAuth" yaml:"basicAuth"`
|
||||
BasicAuthUser string `json:"basicAuthUser" yaml:"basicAuthUser"`
|
||||
BasicAuthPassword string `json:"basicAuthPassword" yaml:"basicAuthPassword"`
|
||||
WithCredentials bool `json:"withCredentials" yaml:"withCredentials"`
|
||||
IsDefault bool `json:"isDefault" yaml:"isDefault"`
|
||||
JsonData map[string]interface{} `json:"jsonData" yaml:"jsonData"`
|
||||
SecureJsonData map[string]string `json:"secureJsonData" yaml:"secureJsonData"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
}
|
||||
|
||||
func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
|
||||
r := &DatasourcesAsConfig{}
|
||||
|
||||
r.ApiVersion = apiVersion
|
||||
|
||||
if cfg == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
for _, ds := range cfg.Datasources {
|
||||
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
Type: ds.Type,
|
||||
Access: ds.Access,
|
||||
Url: ds.Url,
|
||||
Password: ds.Password,
|
||||
User: ds.User,
|
||||
Database: ds.Database,
|
||||
BasicAuth: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
BasicAuthPassword: ds.BasicAuthPassword,
|
||||
WithCredentials: ds.WithCredentials,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
SecureJsonData: ds.SecureJsonData,
|
||||
Editable: ds.Editable,
|
||||
Version: ds.Version,
|
||||
})
|
||||
}
|
||||
|
||||
for _, ds := range cfg.DeleteDatasources {
|
||||
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
|
||||
r := &DatasourcesAsConfig{}
|
||||
|
||||
r.ApiVersion = apiVersion
|
||||
|
||||
if cfg == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
for _, ds := range cfg.Datasources {
|
||||
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
Type: ds.Type,
|
||||
Access: ds.Access,
|
||||
Url: ds.Url,
|
||||
Password: ds.Password,
|
||||
User: ds.User,
|
||||
Database: ds.Database,
|
||||
BasicAuth: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
BasicAuthPassword: ds.BasicAuthPassword,
|
||||
WithCredentials: ds.WithCredentials,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
SecureJsonData: ds.SecureJsonData,
|
||||
Editable: ds.Editable,
|
||||
Version: ds.Version,
|
||||
})
|
||||
}
|
||||
|
||||
for _, ds := range cfg.DeleteDatasources {
|
||||
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand {
|
||||
jsonData := simplejson.New()
|
||||
if len(ds.JsonData) > 0 {
|
||||
for k, v := range ds.JsonData {
|
||||
jsonData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &models.AddDataSourceCommand{
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
Type: ds.Type,
|
||||
Access: models.DsAccess(ds.Access),
|
||||
Url: ds.Url,
|
||||
Password: ds.Password,
|
||||
User: ds.User,
|
||||
Database: ds.Database,
|
||||
BasicAuth: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
BasicAuthPassword: ds.BasicAuthPassword,
|
||||
WithCredentials: ds.WithCredentials,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: ds.SecureJsonData,
|
||||
ReadOnly: !ds.Editable,
|
||||
}
|
||||
}
|
||||
|
||||
func createUpdateCommand(ds *DataSourceFromConfig, id int64) *models.UpdateDataSourceCommand {
|
||||
jsonData := simplejson.New()
|
||||
if len(ds.JsonData) > 0 {
|
||||
for k, v := range ds.JsonData {
|
||||
jsonData.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return &models.UpdateDataSourceCommand{
|
||||
Id: id,
|
||||
OrgId: ds.OrgId,
|
||||
Name: ds.Name,
|
||||
Type: ds.Type,
|
||||
Access: models.DsAccess(ds.Access),
|
||||
Url: ds.Url,
|
||||
Password: ds.Password,
|
||||
User: ds.User,
|
||||
Database: ds.Database,
|
||||
BasicAuth: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
BasicAuthPassword: ds.BasicAuthPassword,
|
||||
WithCredentials: ds.WithCredentials,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: ds.SecureJsonData,
|
||||
ReadOnly: !ds.Editable,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user