Merge branch 'master' into alerting_opentsdb

This commit is contained in:
bergquist 2016-10-03 10:36:33 +02:00
commit 8368a4a88a
70 changed files with 965 additions and 522 deletions

View File

@ -3,7 +3,7 @@
[Website](http://grafana.org) |
[Twitter](https://twitter.com/grafana) |
[IRC](https://webchat.freenode.net/?channels=grafana) |
![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)
[![Slack](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)](http://slack.raintank.io)
[Slack](http://slack.raintank.io) |
[Email](mailto:contact@grafana.org)

View File

@ -12,7 +12,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an
### ** Data Source **
Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes.
The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), and [KairosDB](/datasources/kairosdb)
The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), [Prometheus](/datasources/prometheus/), [Elasticsearch](/datasources/elasticsearch/), [CloudWatch](/datasources/cloudwatch/), and [KairosDB](/datasources/kairosdb)
The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization.
@ -58,7 +58,7 @@ There are a wide variety of styling and formatting options that each Panel expos
Panels can be dragged and dropped and rearranged on the Dashboard. They can also be resized.
There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), and [Text](/reference/text/).
There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), [Table](/reference/table_panel/),and [Text](/reference/text/).
Panels like the [Graph](/reference/graph/) panel allow you to graph as many metrics and series as you want. Other panels like [Singlestat](/reference/singlestat/) require a reduction of a single query into a single number. [Dashlist](/reference/dashlist/) and [Text](/reference/text/) are special panels that do not connect to any Data Source.
@ -66,7 +66,7 @@ Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/t
Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected.
The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-override).
The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift).
Panels (or an entire Dashboard) can be [Shared](/reference/sharing/) easily in a variety of ways. You can send a link to someone who has a login to your Grafana. You can use the [Snapshot](/reference/sharing/#snapshots) feature to encode all the data currently being viewed into a static and interactive JSON document; it's so much better than emailing a screenshot!

View File

@ -29,7 +29,7 @@ The image above shows you the top header for a Dashboard.
6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
## Dashboards, Panels, Rows, the building blocks of Grafana...
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Core Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail.
## Adding & Editing Graphs and Panels

View File

@ -492,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul
### path
The full path to a directory containing your json dashboards.
## [smtp]
Email server settings.
### enabled
defaults to false
### host
defaults to localhost:25
### user
In case of SMTP auth, defaults to `empty`
### password
In case of SMTP auth, defaults to `empty`
### cert_file
File path to a cert file, defaults to `empty`
### key_file
File path to a key file, defaults to `empty`
### skip_verify
Verify SSL for smtp server? defaults to `false`
### from_address
Address used when sending out emails, defaults to `admin@grafana.localhost`
## [log]
### mode

View File

@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
}
}
func GetAlertStatesForDashboard(c *middleware.Context) Response {
dashboardId := c.QueryInt64("dashboardId")
if dashboardId == 0 {
return ApiError(400, "Missing query parameter dashboardId", nil)
}
query := models.GetAlertStatesForDashboardQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch alert states", err)
}
return Json(200, query.Result)
}
// GET /api/alerts
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{

View File

@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
r.Get("/", wrap(GetAlerts))
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
})
r.Get("/alert-notifications", wrap(GetAlertNotifications))

View File

@ -79,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse {
func ApiSuccess(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Respond(200, resp)
return Json(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {

View File

@ -92,6 +92,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDataSourceNameExists {
c.JsonApiErr(409, err.Error(), err)
return
}
c.JsonApiErr(500, "Failed to add datasource", err)
return
}

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"net/http"
@ -31,7 +32,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
})
}
resp, err := tsdb.HandleRequest(request)
resp, err := tsdb.HandleRequest(context.TODO(), request)
if err != nil {
return ApiError(500, "Metric request error", err)
}

View File

@ -1,18 +1,22 @@
package bus
import (
"context"
"fmt"
"reflect"
)
type HandlerFunc interface{}
type CtxHandlerFunc func()
type Msg interface{}
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
Publish(msg Msg) error
AddHandler(handler HandlerFunc)
AddCtxHandler(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
AddWildcardListener(handler HandlerFunc)
}
@ -34,6 +38,27 @@ func New() Bus {
return bus
}
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlers[msgName]
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
}
var params = make([]reflect.Value, 2)
params[0] = reflect.ValueOf(ctx)
params[1] = reflect.ValueOf(msg)
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
}
func (b *InProcBus) Dispatch(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
@ -90,6 +115,12 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) {
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddCtxHandler(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(0).Elem().Name()
@ -105,6 +136,11 @@ func AddHandler(implName string, handler HandlerFunc) {
globalBus.AddHandler(handler)
}
// Package level functions
func AddCtxHandler(implName string, handler HandlerFunc) {
globalBus.AddCtxHandler(handler)
}
// Package level functions
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
@ -118,6 +154,10 @@ func Dispatch(msg Msg) error {
return globalBus.Dispatch(msg)
}
func DispatchCtx(ctx context.Context, msg Msg) error {
return globalBus.DispatchCtx(ctx, msg)
}
func Publish(msg Msg) error {
return globalBus.Publish(msg)
}

View File

@ -1,7 +1,6 @@
package main
import (
"context"
"flag"
"fmt"
"io/ioutil"
@ -13,21 +12,11 @@ import (
"syscall"
"time"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
"github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
@ -66,41 +55,8 @@ func main() {
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
appContext, shutdownFn := context.WithCancel(context.Background())
grafanaGroup, appContext := errgroup.WithContext(appContext)
go listenToSystemSignals(shutdownFn, grafanaGroup)
flag.Parse()
writePIDFile()
initRuntime()
initSql()
metrics.Init()
search.Init()
login.Init()
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
// init alerting
if setting.AlertingEnabled {
engine := alerting.NewEngine()
grafanaGroup.Go(func() error { return engine.Run(appContext) })
}
// cleanup service
cleanUpService := cleanup.NewCleanUpService()
grafanaGroup.Go(func() error { return cleanUpService.Run(appContext) })
if err := notifications.Init(); err != nil {
log.Fatal(3, "Notification service failed to initialize", err)
}
exitCode := StartServer()
grafanaGroup.Wait()
exitChan <- exitCode
server := NewGrafanaServer()
server.Start()
}
func initRuntime() {
@ -143,7 +99,7 @@ func writePIDFile() {
}
}
func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Group) {
func listenToSystemSignals(server models.GrafanaServer) {
signalChan := make(chan os.Signal, 1)
code := 0
@ -151,18 +107,8 @@ func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Gro
select {
case sig := <-signalChan:
log.Info2("Received system signal. Shutting down", "signal", sig)
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
case code = <-exitChan:
switch code {
case 0:
log.Info("Shutting down")
default:
log.Warn("Shutting down")
}
server.Shutdown(code, "startup error")
}
cancel()
grafanaGroup.Wait()
log.Close()
os.Exit(code)
}

View File

@ -0,0 +1,150 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"gopkg.in/macaron.v1"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
func NewGrafanaServer() models.GrafanaServer {
rootCtx, shutdownFn := context.WithCancel(context.Background())
childRoutines, childCtx := errgroup.WithContext(rootCtx)
return &GrafanaServerImpl{
context: childCtx,
shutdownFn: shutdownFn,
childRoutines: childRoutines,
log: log.New("server"),
}
}
type GrafanaServerImpl struct {
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
}
func (g *GrafanaServerImpl) Start() {
go listenToSystemSignals(g)
writePIDFile()
initRuntime()
initSql()
metrics.Init()
search.Init()
login.Init()
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
// init alerting
if setting.AlertingEnabled {
engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) })
}
// cleanup service
cleanUpService := cleanup.NewCleanUpService()
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
if err := notifications.Init(); err != nil {
g.log.Error("Notification service failed to initialize", "erro", err)
g.Shutdown(1, "Startup failed")
return
}
g.startHttpServer()
}
func (g *GrafanaServerImpl) startHttpServer() {
logger = log.New("http.server")
var err error
m := newMacaron()
api.Register(m)
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
switch setting.Protocol {
case setting.HTTP:
err = http.ListenAndServe(listenAddr, m)
case setting.HTTPS:
err = ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
default:
g.log.Error("Invalid protocol", "protocol", setting.Protocol)
g.Shutdown(1, "Startup failed")
}
if err != nil {
g.log.Error("Fail to start server", "error", err)
g.Shutdown(1, "Startup failed")
return
}
}
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.log.Info("Shutdown started", "code", code, "reason", reason)
g.shutdownFn()
err := g.childRoutines.Wait()
g.log.Info("Shutdown completed", "reason", err)
log.Close()
os.Exit(code)
}
func ListenAndServeTLS(listenAddr, certfile, keyfile string, m *macaron.Macaron) error {
if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
if keyfile == "" {
return fmt.Errorf("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(setting.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`Cannot find SSL cert_file at %v`, setting.CertFile)
}
if _, err := os.Stat(setting.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
}
return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
}
// implement context.Context
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
return g.context.Deadline()
}
func (g *GrafanaServerImpl) Done() <-chan struct{} {
return g.context.Done()
}
func (g *GrafanaServerImpl) Err() error {
return g.context.Err()
}
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
return g.context.Value(key)
}

View File

@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
Result *Alert
}
type GetAlertStatesForDashboardQuery struct {
OrgId int64
DashboardId int64
Result []*AlertStateInfoDTO
}
type AlertStateInfoDTO struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
}

View File

@ -22,7 +22,8 @@ const (
// Typed errors
var (
ErrDataSourceNotFound = errors.New("Data source not found")
ErrDataSourceNotFound = errors.New("Data source not found")
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
)
type DsAccess string

View File

@ -12,6 +12,10 @@ type SendEmailCommand struct {
Info string
}
type SendEmailCommandSync struct {
SendEmailCommand
}
type SendWebhook struct {
Url string
User string
@ -19,6 +23,13 @@ type SendWebhook struct {
Body string
}
type SendWebhookSync struct {
Url string
User string
Password string
Body string
}
type SendResetPasswordEmailCommand struct {
User *User
}

10
pkg/models/server.go Normal file
View File

@ -0,0 +1,10 @@
package models
import "context"
type GrafanaServer interface {
context.Context
Start()
Shutdown(code int, reason string)
}

View File

@ -82,7 +82,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
result := make(tsdb.TimeSeriesSlice, 0)
resp, err := c.HandleRequest(req)
resp, err := c.HandleRequest(context.Context, req)
if err != nil {
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
}

View File

@ -1,6 +1,7 @@
package conditions
import (
"context"
"testing"
null "gopkg.in/guregu/null.v3"
@ -137,7 +138,7 @@ func (ctx *queryConditionTestContext) exec() {
ctx.condition = condition
condition.HandleRequest = func(req *tsdb.Request) (*tsdb.Response, error) {
condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) {
return &tsdb.Response{
Results: map[string]*tsdb.QueryResult{
"A": {Series: ctx.series},

View File

@ -11,7 +11,6 @@ import (
type Engine struct {
execQueue chan *Job
resultQueue chan *EvalContext
clock clock.Clock
ticker *Ticker
scheduler Scheduler
@ -25,7 +24,6 @@ func NewEngine() *Engine {
e := &Engine{
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
execQueue: make(chan *Job, 1000),
resultQueue: make(chan *EvalContext, 1000),
scheduler: NewScheduler(),
evalHandler: NewEvalHandler(),
ruleReader: NewRuleReader(),
@ -39,23 +37,17 @@ func NewEngine() *Engine {
func (e *Engine) Run(ctx context.Context) error {
e.log.Info("Initializing Alerting")
g, ctx := errgroup.WithContext(ctx)
alertGroup, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return e.alertingTicker(ctx) })
g.Go(func() error { return e.execDispatcher(ctx) })
g.Go(func() error { return e.resultDispatcher(ctx) })
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
err := g.Wait()
err := alertGroup.Wait()
e.log.Info("Stopped Alerting", "reason", err)
return err
}
func (e *Engine) Stop() {
close(e.execQueue)
close(e.resultQueue)
}
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
defer func() {
if err := recover(); err != nil {
@ -81,70 +73,58 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
}
}
func (e *Engine) execDispatcher(grafanaCtx context.Context) error {
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
for {
select {
case <-grafanaCtx.Done():
close(e.resultQueue)
return grafanaCtx.Err()
return dispatcherGroup.Wait()
case job := <-e.execQueue:
go e.executeJob(grafanaCtx, job)
dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
}
}
}
func (e *Engine) executeJob(grafanaCtx context.Context, job *Job) error {
var (
unfinishedWorkTimeout time.Duration = time.Second * 5
alertTimeout time.Duration = time.Second * 30
)
func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
defer func() {
if err := recover(); err != nil {
e.log.Error("Execute Alert Panic", "error", err, "stack", log.Stack(1))
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
}
}()
done := make(chan *EvalContext, 1)
alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
job.Running = true
evalContext := NewEvalContext(alertCtx, job.Rule)
done := make(chan struct{})
go func() {
job.Running = true
context := NewEvalContext(job.Rule)
e.evalHandler.Eval(context)
job.Running = false
done <- context
e.evalHandler.Eval(evalContext)
e.resultHandler.Handle(evalContext)
close(done)
}()
var err error = nil
select {
case <-grafanaCtx.Done():
return grafanaCtx.Err()
case evalContext := <-done:
e.resultQueue <- evalContext
}
return nil
}
func (e *Engine) resultDispatcher(grafanaCtx context.Context) error {
for {
select {
case <-grafanaCtx.Done():
//handle all responses before shutting down.
for result := range e.resultQueue {
e.handleResponse(result)
}
return grafanaCtx.Err()
case result := <-e.resultQueue:
e.handleResponse(result)
case <-time.After(unfinishedWorkTimeout):
cancelFn()
err = grafanaCtx.Err()
case <-done:
}
case <-done:
}
}
func (e *Engine) handleResponse(result *EvalContext) {
defer func() {
if err := recover(); err != nil {
e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
}
}()
e.log.Info("rule", "nil", result.Rule == nil)
e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing)
e.resultHandler.Handle(result)
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
}

View File

@ -1,6 +1,7 @@
package alerting
import (
"context"
"fmt"
"time"
@ -20,14 +21,30 @@ type EvalContext struct {
StartTime time.Time
EndTime time.Time
Rule *Rule
DoneChan chan bool
CancelChan chan bool
log log.Logger
dashboardSlug string
ImagePublicUrl string
ImageOnDiskPath string
NoDataFound bool
RetryCount int
Context context.Context
}
func (evalContext *EvalContext) Deadline() (deadline time.Time, ok bool) {
return evalContext.Deadline()
}
func (evalContext *EvalContext) Done() <-chan struct{} {
return evalContext.Context.Done()
}
func (evalContext *EvalContext) Err() error {
return evalContext.Context.Err()
}
func (evalContext *EvalContext) Value(key interface{}) interface{} {
return evalContext.Context.Value(key)
}
type StateDescription struct {
@ -94,14 +111,13 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
}
}
func NewEvalContext(rule *Rule) *EvalContext {
func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext {
return &EvalContext{
Context: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
DoneChan: make(chan bool, 1),
CancelChan: make(chan bool, 1),
log: log.New("alerting.evalContext"),
RetryCount: 0,
}

View File

@ -1,17 +1,12 @@
package alerting
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
)
var (
MaxRetries int = 1
)
type DefaultEvalHandler struct {
log log.Logger
alertJobTimeout time.Duration
@ -20,49 +15,11 @@ type DefaultEvalHandler struct {
func NewEvalHandler() *DefaultEvalHandler {
return &DefaultEvalHandler{
log: log.New("alerting.evalHandler"),
alertJobTimeout: time.Second * 15,
alertJobTimeout: time.Second * 5,
}
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
go e.eval(context)
select {
case <-time.After(e.alertJobTimeout):
context.Error = fmt.Errorf("Execution timed out after %v", e.alertJobTimeout)
context.EndTime = time.Now()
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id, "timeout setting", e.alertJobTimeout)
e.retry(context)
case <-context.DoneChan:
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
if context.Error != nil {
e.retry(context)
}
}
}
func (e *DefaultEvalHandler) retry(context *EvalContext) {
e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
if context.RetryCount < MaxRetries {
context.DoneChan = make(chan bool, 1)
context.CancelChan = make(chan bool, 1)
context.RetryCount++
e.Eval(context)
}
}
func (e *DefaultEvalHandler) eval(context *EvalContext) {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1))
if panicErr, ok := err.(error); ok {
context.Error = panicErr
}
}
}()
for _, condition := range context.Rule.Conditions {
condition.Eval(context)
@ -80,5 +37,4 @@ func (e *DefaultEvalHandler) eval(context *EvalContext) {
context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
context.DoneChan <- true
}

View File

@ -1,6 +1,7 @@
package alerting
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
@ -19,25 +20,25 @@ func TestAlertingExecutor(t *testing.T) {
handler := NewEvalHandler()
Convey("Show return triggered with single passing condition", func() {
context := NewEvalContext(&Rule{
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{&conditionStub{
firing: true,
}},
})
handler.eval(context)
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
})
Convey("Show return false with not passing condition", func() {
context := NewEvalContext(&Rule{
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true},
&conditionStub{firing: false},
},
})
handler.eval(context)
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
})
})

View File

@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
continue
}
// backward compatability check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
if !hasEnabled || !enabled.MustBool() {
if hasEnabled && enabled.MustBool() == false {
continue
}

View File

@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"name": "name1",
"message": "desc1",
"handler": 1,
"enabled": true,
"frequency": "60s",
"conditions": [
{
@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"name": "name2",
"message": "desc2",
"handler": 0,
"enabled": true,
"frequency": "60s",
"severity": "warning",
"conditions": [

View File

@ -1,11 +1,9 @@
package alerting
import (
"time"
)
import "time"
type EvalHandler interface {
Eval(context *EvalContext)
Eval(evalContext *EvalContext)
}
type Scheduler interface {
@ -14,7 +12,7 @@ type Scheduler interface {
}
type Notifier interface {
Notify(alertResult *EvalContext)
Notify(evalContext *EvalContext) error
GetType() string
NeedsImage() bool
PassesFilter(rule *Rule) bool

View File

@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
"github.com/grafana/grafana/pkg/components/renderer"
@ -33,32 +35,36 @@ func (n *RootNotifier) PassesFilter(rule *Rule) bool {
return false
}
func (n *RootNotifier) Notify(context *EvalContext) {
func (n *RootNotifier) Notify(context *EvalContext) error {
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil {
n.log.Error("Failed to read notifications", "error", err)
return
return err
}
if len(notifiers) == 0 {
return
return nil
}
err = n.uploadImage(context)
if err != nil {
n.log.Error("Failed to upload alert panel image", "error", err)
return err
}
n.sendNotifications(notifiers, context)
return n.sendNotifications(context, notifiers)
}
func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) {
func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
g, _ := errgroup.WithContext(context.Context)
for _, notifier := range notifiers {
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
go notifier.Notify(context)
g.Go(func() error { return notifier.Notify(context) })
}
return g.Wait()
}
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {

View File

@ -22,7 +22,7 @@ func (fn *FakeNotifier) NeedsImage() bool {
return true
}
func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
return fn.FakeMatchResult

View File

@ -35,33 +35,39 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
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 := context.GetRuleUrl()
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return
return err
}
cmd := &m.SendEmailCommand{
Data: map[string]interface{}{
"Title": context.GetNotificationTitle(),
"State": context.Rule.State,
"Name": context.Rule.Name,
"StateModel": context.GetStateModel(),
"Message": context.Rule.Message,
"RuleUrl": ruleUrl,
"ImageLink": context.ImagePublicUrl,
"AlertPageUrl": setting.AppUrl + "alerting",
"EvalMatches": context.EvalMatches,
cmd := &m.SendEmailCommandSync{
SendEmailCommand: m.SendEmailCommand{
Data: map[string]interface{}{
"Title": evalContext.GetNotificationTitle(),
"State": evalContext.Rule.State,
"Name": evalContext.Rule.Name,
"StateModel": evalContext.GetStateModel(),
"Message": evalContext.Rule.Message,
"RuleUrl": ruleUrl,
"ImageLink": evalContext.ImagePublicUrl,
"AlertPageUrl": setting.AppUrl + "alerting",
"EvalMatches": evalContext.EvalMatches,
},
To: this.Addresses,
Template: "alert_notification.html",
},
To: this.Addresses,
Template: "alert_notification.html",
}
if err := bus.Dispatch(cmd); err != nil {
err = bus.DispatchCtx(evalContext, cmd)
if err != nil {
this.log.Error("Failed to send alert notification email", "error", err)
}
return nil
}

View File

@ -35,19 +35,19 @@ type SlackNotifier struct {
log log.Logger
}
func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name)
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 := context.GetRuleUrl()
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return
return err
}
fields := make([]map[string]interface{}, 0)
fieldLimitCount := 4
for index, evt := range context.EvalMatches {
for index, evt := range evalContext.EvalMatches {
fields = append(fields, map[string]interface{}{
"title": evt.Metric,
"value": evt.Value,
@ -58,44 +58,41 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
}
}
if context.Error != nil {
if evalContext.Error != nil {
fields = append(fields, map[string]interface{}{
"title": "Error message",
"value": context.Error.Error(),
"value": evalContext.Error.Error(),
"short": false,
})
}
message := ""
if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message = context.Rule.Message
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message = evalContext.Rule.Message
}
body := map[string]interface{}{
"attachments": []map[string]interface{}{
{
"color": context.GetStateModel().Color,
"title": context.GetNotificationTitle(),
"color": evalContext.GetStateModel().Color,
"title": evalContext.GetNotificationTitle(),
"title_link": ruleUrl,
"text": message,
"fields": fields,
"image_url": context.ImagePublicUrl,
"image_url": evalContext.ImagePublicUrl,
"footer": "Grafana v" + setting.BuildVersion,
"footer_icon": "http://grafana.org/assets/img/fav32.png",
"ts": time.Now().Unix(),
//"pretext": "Optional text that appears above the attachment block",
// "author_name": "Bobby Tables",
// "author_link": "http://flickr.com/bobby/",
// "author_icon": "http://flickr.com/icons/bobby.jpg",
// "thumb_url": "http://example.com/path/to/thumb.png",
},
},
}
data, _ := json.Marshal(&body)
cmd := &m.SendWebhook{Url: this.Url, Body: string(data)}
cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
if err := bus.Dispatch(cmd); err != nil {
if err := bus.DispatchCtx(evalContext, cmd); err != nil {
this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
}
return nil
}

View File

@ -36,36 +36,38 @@ type WebhookNotifier struct {
log log.Logger
}
func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
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", context.GetNotificationTitle())
bodyJSON.Set("ruleId", context.Rule.Id)
bodyJSON.Set("ruleName", context.Rule.Name)
bodyJSON.Set("state", context.Rule.State)
bodyJSON.Set("evalMatches", context.EvalMatches)
bodyJSON.Set("title", evalContext.GetNotificationTitle())
bodyJSON.Set("ruleId", evalContext.Rule.Id)
bodyJSON.Set("ruleName", evalContext.Rule.Name)
bodyJSON.Set("state", evalContext.Rule.State)
bodyJSON.Set("evalMatches", evalContext.EvalMatches)
ruleUrl, err := context.GetRuleUrl()
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
bodyJSON.Set("rule_url", ruleUrl)
}
if context.ImagePublicUrl != "" {
bodyJSON.Set("image_url", context.ImagePublicUrl)
if evalContext.ImagePublicUrl != "" {
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
}
body, _ := bodyJSON.MarshalJSON()
cmd := &m.SendWebhook{
cmd := &m.SendWebhookSync{
Url: this.Url,
User: this.User,
Password: this.Password,
Body: string(body),
}
if err := bus.Dispatch(cmd); err != nil {
if err := bus.DispatchCtx(evalContext, cmd); err != nil {
this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
}
return nil
}

View File

@ -12,7 +12,7 @@ import (
)
type ResultHandler interface {
Handle(ctx *EvalContext)
Handle(evalContext *EvalContext) error
}
type DefaultResultHandler struct {
@ -27,36 +27,36 @@ func NewResultHandler() *DefaultResultHandler {
}
}
func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
oldState := ctx.Rule.State
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
oldState := evalContext.Rule.State
exeuctionError := ""
annotationData := simplejson.New()
if ctx.Error != nil {
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
ctx.Rule.State = m.AlertStateExecError
exeuctionError = ctx.Error.Error()
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error", "ruleId", evalContext.Rule.Id, "error", evalContext.Error)
evalContext.Rule.State = m.AlertStateExecError
exeuctionError = evalContext.Error.Error()
annotationData.Set("errorMessage", exeuctionError)
} else if ctx.Firing {
ctx.Rule.State = m.AlertStateAlerting
annotationData = simplejson.NewFromAny(ctx.EvalMatches)
} else if evalContext.Firing {
evalContext.Rule.State = m.AlertStateAlerting
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
} else {
// handle no data case
if ctx.NoDataFound {
ctx.Rule.State = ctx.Rule.NoDataState
if evalContext.NoDataFound {
evalContext.Rule.State = evalContext.Rule.NoDataState
} else {
ctx.Rule.State = m.AlertStateOK
evalContext.Rule.State = m.AlertStateOK
}
}
countStateResult(ctx.Rule.State)
if ctx.Rule.State != oldState {
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
countStateResult(evalContext.Rule.State)
if evalContext.Rule.State != oldState {
handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "oldState", oldState)
cmd := &m.SetAlertStateCommand{
AlertId: ctx.Rule.Id,
OrgId: ctx.Rule.OrgId,
State: ctx.Rule.State,
AlertId: evalContext.Rule.Id,
OrgId: evalContext.Rule.OrgId,
State: evalContext.Rule.State,
Error: exeuctionError,
EvalData: annotationData,
}
@ -67,14 +67,14 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
// save annotation
item := annotations.Item{
OrgId: ctx.Rule.OrgId,
DashboardId: ctx.Rule.DashboardId,
PanelId: ctx.Rule.PanelId,
OrgId: evalContext.Rule.OrgId,
DashboardId: evalContext.Rule.DashboardId,
PanelId: evalContext.Rule.PanelId,
Type: annotations.AlertType,
AlertId: ctx.Rule.Id,
Title: ctx.Rule.Name,
Text: ctx.GetStateModel().Text,
NewState: string(ctx.Rule.State),
AlertId: evalContext.Rule.Id,
Title: evalContext.Rule.Name,
Text: evalContext.GetStateModel().Text,
NewState: string(evalContext.Rule.State),
PrevState: string(oldState),
Epoch: time.Now().Unix(),
Data: annotationData,
@ -85,8 +85,10 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
handler.log.Error("Failed to save annotation for new alert state", "error", err)
}
handler.notifier.Notify(ctx)
handler.notifier.Notify(evalContext)
}
return nil
}
func countStateResult(state m.AlertStateType) {

View File

@ -1,6 +1,8 @@
package alerting
import (
"context"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
@ -35,13 +37,12 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
return err
}
notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext())
notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
return nil
}
func createTestEvalContext() *EvalContext {
testRule := &Rule{
DashboardId: 1,
PanelId: 1,
@ -50,7 +51,7 @@ func createTestEvalContext() *EvalContext {
State: m.AlertStateAlerting,
}
ctx := NewEvalContext(testRule)
ctx := NewEvalContext(context.TODO(), testRule)
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
ctx.IsTestRun = true

View File

@ -1,6 +1,7 @@
package alerting
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/bus"
@ -48,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
func testAlertRule(rule *Rule) *EvalContext {
handler := NewEvalHandler()
context := NewEvalContext(rule)
context := NewEvalContext(context.TODO(), rule)
context.IsTestRun = true
handler.Eval(context)

View File

@ -5,8 +5,11 @@
package notifications
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"html/template"
"net"
"net/mail"
"net/smtp"
@ -15,6 +18,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
@ -185,3 +189,49 @@ func buildAndSend(msg *Message) (int, error) {
}
}
}
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")
}
var buffer bytes.Buffer
var err error
var subjectText interface{}
data := cmd.Data
if data == nil {
data = make(map[string]interface{}, 10)
}
setDefaultTemplateData(data, nil)
err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
if err != nil {
return nil, err
}
subjectData := data["Subject"].(map[string]interface{})
subjectText, hasSubject := subjectData["value"]
if !hasSubject {
return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
if err != nil {
return nil, err
}
var subjectBuffer bytes.Buffer
err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
if err != nil {
return nil, err
}
return &Message{
To: cmd.To,
From: setting.Smtp.FromAddress,
Subject: subjectBuffer.String(),
Body: buffer.String(),
}, nil
}

View File

@ -1,7 +1,7 @@
package notifications
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
@ -29,7 +29,10 @@ func Init() error {
bus.AddHandler("email", validateResetPasswordCode)
bus.AddHandler("email", sendEmailCommandHandler)
bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
bus.AddHandler("webhook", sendWebhook)
bus.AddCtxHandler("webhook", SendWebhookSync)
bus.AddEventListener(signUpStartedHandler)
bus.AddEventListener(signUpCompletedHandler)
@ -56,6 +59,15 @@ func Init() error {
return nil
}
func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
return sendWebRequestSync(ctx, &Webhook{
Url: cmd.Url,
User: cmd.User,
Password: cmd.Password,
Body: cmd.Body,
})
}
func sendWebhook(cmd *m.SendWebhook) error {
addToWebhookQueue(&Webhook{
Url: cmd.Url,
@ -72,51 +84,33 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string {
return ""
}
func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
if !setting.Smtp.Enabled {
return errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin")
}
var buffer bytes.Buffer
var err error
var subjectText interface{}
data := cmd.Data
if data == nil {
data = make(map[string]interface{}, 10)
}
setDefaultTemplateData(data, nil)
err = mailTemplates.ExecuteTemplate(&buffer, cmd.Template, data)
if err != nil {
return err
}
subjectData := data["Subject"].(map[string]interface{})
subjectText, hasSubject := subjectData["value"]
if !hasSubject {
return errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
}
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
if err != nil {
return err
}
var subjectBuffer bytes.Buffer
err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
if err != nil {
return err
}
addToMailQueue(&Message{
To: cmd.To,
From: setting.Smtp.FromAddress,
Subject: subjectBuffer.String(),
Body: buffer.String(),
func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
message, err := buildEmailMessage(&m.SendEmailCommand{
Data: cmd.Data,
Info: cmd.Info,
Massive: cmd.Massive,
Template: cmd.Template,
To: cmd.To,
})
if err != nil {
return err
}
_, err = buildAndSend(message)
return err
}
func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
message, err := buildEmailMessage(cmd)
if err != nil {
return err
}
addToMailQueue(message)
return nil
}

View File

@ -3,7 +3,6 @@ package notifications
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
@ -18,7 +17,7 @@ type testTriggeredAlert struct {
func TestNotifications(t *testing.T) {
Convey("Given the notifications service", t, func() {
bus.ClearBusHandlers()
//bus.ClearBusHandlers()
setting.StaticRootPath = "../../../public/"
setting.Smtp.Enabled = true

View File

@ -2,11 +2,14 @@ package notifications
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
"golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
)
@ -31,7 +34,7 @@ func processWebhookQueue() {
for {
select {
case webhook := <-webhookQueue:
err := sendWebRequest(webhook)
err := sendWebRequestSync(context.TODO(), webhook)
if err != nil {
webhookLog.Error("Failed to send webrequest ", "error", err)
@ -40,14 +43,14 @@ func processWebhookQueue() {
}
}
func sendWebRequest(webhook *Webhook) error {
func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
webhookLog.Debug("Sending webhook", "url", webhook.Url)
client := http.Client{
client := &http.Client{
Timeout: time.Duration(10 * time.Second),
}
request, err := http.NewRequest("POST", webhook.Url, bytes.NewReader([]byte(webhook.Body)))
request, err := http.NewRequest(http.MethodPost, webhook.Url, bytes.NewReader([]byte(webhook.Body)))
if webhook.User != "" && webhook.Password != "" {
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
}
@ -56,22 +59,23 @@ func sendWebRequest(webhook *Webhook) error {
return err
}
resp, err := client.Do(request)
resp, err := ctxhttp.Do(ctx, client, request)
if err != nil {
return err
}
_, err = ioutil.ReadAll(resp.Body)
if resp.StatusCode/100 == 2 {
return nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("Webhook response code %v", resp.StatusCode)
}
defer resp.Body.Close()
return nil
webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
return fmt.Errorf("Webhook response status %v", resp.Status)
}
var addToWebhookQueue = func(msg *Webhook) {

View File

@ -17,6 +17,7 @@ func init() {
bus.AddHandler("sql", DeleteAlertById)
bus.AddHandler("sql", GetAllAlertQueryHandler)
bus.AddHandler("sql", SetAlertState)
bus.AddHandler("sql", GetAlertStatesForDashboard)
}
func GetAlertById(query *m.GetAlertByIdQuery) error {
@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
return nil
})
}
func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
var rawSql = `SELECT
id,
dashboard_id,
panel_id,
state,
new_state_date
FROM alert
WHERE org_id = ? AND dashboard_id = ?`
query.Result = make([]*m.AlertStateInfoDTO, 0)
err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
return err
}

View File

@ -60,6 +60,13 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error {
func AddDataSource(cmd *m.AddDataSourceCommand) error {
return inTransaction(func(sess *xorm.Session) error {
existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name}
has, _ := sess.Get(&existing)
if has {
return m.ErrDataSourceNameExists
}
ds := &m.DataSource{
OrgId: cmd.OrgId,
Name: cmd.Name,

View File

@ -41,6 +41,7 @@ func TestDataAccess(t *testing.T) {
err := AddDataSource(&m.AddDataSourceCommand{
OrgId: 10,
Name: "laban",
Type: m.DS_INFLUXDB,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
@ -63,15 +64,19 @@ func TestDataAccess(t *testing.T) {
Convey("Given a datasource", func() {
AddDataSource(&m.AddDataSourceCommand{
err := AddDataSource(&m.AddDataSourceCommand{
OrgId: 10,
Name: "nisse",
Type: m.DS_GRAPHITE,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
})
So(err, ShouldBeNil)
query := m.GetDataSourcesQuery{OrgId: 10}
GetDataSources(&query)
err = GetDataSources(&query)
So(err, ShouldBeNil)
ds := query.Result[0]
Convey("Can delete datasource", func() {

View File

@ -92,44 +92,45 @@ func (mg *Migrator) Start() error {
mg.Logger.Debug("Executing", "sql", sql)
if err := mg.exec(m); err != nil {
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
record.Error = err.Error()
mg.x.Insert(&record)
err := mg.inTransaction(func(sess *xorm.Session) error {
if err := mg.exec(m, sess); err != nil {
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
record.Error = err.Error()
sess.Insert(&record)
return err
} else {
record.Success = true
sess.Insert(&record)
}
return nil
})
if err != nil {
return err
} else {
record.Success = true
mg.x.Insert(&record)
}
}
return nil
}
func (mg *Migrator) exec(m Migration) error {
func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
mg.Logger.Info("Executing migration", "id", m.Id())
err := mg.inTransaction(func(sess *xorm.Session) error {
condition := m.GetCondition()
if condition != nil {
sql, args := condition.Sql(mg.dialect)
results, err := sess.Query(sql, args...)
if err != nil || len(results) == 0 {
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
return sess.Rollback()
}
condition := m.GetCondition()
if condition != nil {
sql, args := condition.Sql(mg.dialect)
results, err := sess.Query(sql, args...)
if err != nil || len(results) == 0 {
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
return sess.Rollback()
}
}
_, err := sess.Exec(m.Sql(mg.dialect))
if err != nil {
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
return err
}
return nil
})
_, err := sess.Exec(m.Sql(mg.dialect))
if err != nil {
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
return err
}

View File

@ -1,6 +1,9 @@
package tsdb
import "errors"
import (
"context"
"errors"
)
type Batch struct {
DataSourceId int64
@ -20,7 +23,7 @@ func newBatch(dsId int64, queries QuerySlice) *Batch {
}
}
func (bg *Batch) process(context *QueryContext) {
func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) {
executor := getExecutorFor(bg.Queries[0].DataSource)
if executor == nil {
@ -32,13 +35,13 @@ func (bg *Batch) process(context *QueryContext) {
for _, query := range bg.Queries {
result.QueryResults[query.RefId] = &QueryResult{Error: result.Error}
}
context.ResultsChan <- result
queryContext.ResultsChan <- result
return
}
res := executor.Execute(bg.Queries, context)
res := executor.Execute(ctx, bg.Queries, queryContext)
bg.Done = true
context.ResultsChan <- res
queryContext.ResultsChan <- res
}
func (bg *Batch) addQuery(query *Query) {

View File

@ -1,7 +1,9 @@
package tsdb
import "context"
type Executor interface {
Execute(queries QuerySlice, context *QueryContext) *BatchResult
Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult
}
var registry map[string]GetExecutorFn

View File

@ -1,5 +1,7 @@
package tsdb
import "context"
type FakeExecutor struct {
results map[string]*QueryResult
resultsFn map[string]ResultsFn
@ -14,7 +16,7 @@ func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor {
}
}
func (e *FakeExecutor) Execute(queries QuerySlice, context *QueryContext) *BatchResult {
func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult {
result := &BatchResult{QueryResults: make(map[string]*QueryResult)}
for _, query := range queries {
if results, has := e.results[query.RefId]; has {

View File

@ -1,6 +1,7 @@
package graphite
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@ -11,6 +12,8 @@ import (
"strings"
"time"
"golang.org/x/net/context/ctxhttp"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
@ -26,7 +29,7 @@ func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
var (
glog log.Logger
HttpClient http.Client
HttpClient *http.Client
)
func init() {
@ -37,13 +40,13 @@ func init() {
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
HttpClient = http.Client{
HttpClient = &http.Client{
Timeout: time.Duration(15 * time.Second),
Transport: tr,
}
}
func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
result := &tsdb.BatchResult{}
formData := url.Values{
@ -66,7 +69,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
result.Error = err
return result
}
res, err := HttpClient.Do(req)
res, err := ctxhttp.Do(ctx, HttpClient, req)
if err != nil {
result.Error = err
return result

View File

@ -1,6 +1,7 @@
package prometheus
import (
"context"
"fmt"
"net/http"
"regexp"
@ -11,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/tsdb"
"github.com/prometheus/client_golang/api/prometheus"
pmodel "github.com/prometheus/common/model"
"golang.org/x/net/context"
)
type PrometheusExecutor struct {
@ -45,7 +45,7 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
return prometheus.NewQueryAPI(client), nil
}
func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
result := &tsdb.BatchResult{}
client, err := e.getClient()
@ -64,7 +64,7 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb
Step: query.Step,
}
value, err := client.QueryRange(context.Background(), query.Expr, timeRange)
value, err := client.QueryRange(ctx, query.Expr, timeRange)
if err != nil {
return resultWithError(result, err)

View File

@ -1,8 +1,10 @@
package tsdb
type HandleRequestFunc func(req *Request) (*Response, error)
import "context"
func HandleRequest(req *Request) (*Response, error) {
type HandleRequestFunc func(ctx context.Context, req *Request) (*Response, error)
func HandleRequest(ctx context.Context, req *Request) (*Response, error) {
context := NewQueryContext(req.Queries, req.TimeRange)
batches, err := getBatches(req)
@ -16,7 +18,7 @@ func HandleRequest(req *Request) (*Response, error) {
if len(batch.Depends) == 0 {
currentlyExecuting += 1
batch.Started = true
go batch.process(context)
go batch.process(ctx, context)
}
}
@ -46,7 +48,7 @@ func HandleRequest(req *Request) (*Response, error) {
if batch.allDependenciesAreIn(context) {
currentlyExecuting += 1
batch.Started = true
go batch.process(context)
go batch.process(ctx, context)
}
}
}

View File

@ -1,6 +1,8 @@
package testdata
import (
"context"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/tsdb"
)
@ -21,7 +23,7 @@ func init() {
tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
}
func (e *TestDataExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
func (e *TestDataExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
result := &tsdb.BatchResult{}
result.QueryResults = make(map[string]*tsdb.QueryResult)

View File

@ -1,6 +1,7 @@
package tsdb
import (
"context"
"testing"
"time"
@ -62,7 +63,7 @@ func TestMetricQuery(t *testing.T) {
fakeExecutor := registerFakeExecutor()
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
res, err := HandleRequest(req)
res, err := HandleRequest(context.TODO(), req)
So(err, ShouldBeNil)
Convey("Should return query results", func() {
@ -83,7 +84,7 @@ func TestMetricQuery(t *testing.T) {
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}})
res, err := HandleRequest(req)
res, err := HandleRequest(context.TODO(), req)
So(err, ShouldBeNil)
Convey("Should return query results", func() {
@ -106,7 +107,7 @@ func TestMetricQuery(t *testing.T) {
},
}
res, err := HandleRequest(req)
res, err := HandleRequest(context.TODO(), req)
So(err, ShouldBeNil)
Convey("Should have been batched in two requests", func() {
@ -121,7 +122,7 @@ func TestMetricQuery(t *testing.T) {
},
}
_, err := HandleRequest(req)
_, err := HandleRequest(context.TODO(), req)
So(err, ShouldNotBeNil)
})
@ -152,7 +153,7 @@ func TestMetricQuery(t *testing.T) {
}}
})
res, err := HandleRequest(req)
res, err := HandleRequest(context.TODO(), req)
So(err, ShouldBeNil)
Convey("Should have been batched in two requests", func() {

View File

@ -136,7 +136,7 @@ function (_, $, coreModule) {
$button.click(function() {
options = null;
$input.css('width', ($button.width() + 16) + 'px');
$input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
$button.hide();
$input.show();

View File

@ -236,7 +236,7 @@ function (angular, _, coreModule) {
var inputEl = elem.find('input');
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
inputEl.show();
linkEl.hide();

View File

@ -6,6 +6,7 @@ import {QueryPart} from 'app/core/components/query_part/query_part';
import alertDef from './alert_def';
import config from 'app/core/config';
import moment from 'moment';
import appEvents from 'app/core/app_events';
export class AlertTabCtrl {
panel: any;
@ -47,19 +48,18 @@ export class AlertTabCtrl {
$onInit() {
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
this.initModel();
this.validateModel();
// subscribe to graph threshold handle changes
var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
// set panel alert edit mode
// set panel alert edit mode
this.$scope.$on("$destroy", () => {
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
});
// subscribe to graph threshold handle changes
this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
// build notification model
// build notification model
this.notifications = [];
this.alertNotifications = [];
this.alertHistory = [];
@ -67,21 +67,8 @@ export class AlertTabCtrl {
return this.backendSrv.get('/api/alert-notifications').then(res => {
this.notifications = res;
_.each(this.alert.notifications, item => {
var model = _.find(this.notifications, {id: item.id});
if (model) {
model.iconClass = this.getNotificationIcon(model.type);
this.alertNotifications.push(model);
}
});
_.each(this.notifications, item => {
if (item.isDefault) {
item.iconClass = this.getNotificationIcon(item.type);
item.bgColor = "#00678b";
this.alertNotifications.push(item);
}
});
this.initModel();
this.validateModel();
});
}
@ -142,9 +129,8 @@ export class AlertTabCtrl {
}
initModel() {
var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
if (!this.alert.enabled) {
var alert = this.alert = this.panel.alert;
if (!alert) {
return;
}
@ -168,6 +154,22 @@ export class AlertTabCtrl {
ThresholdMapper.alertToGraphThresholds(this.panel);
for (let addedNotification of alert.notifications) {
var model = _.find(this.notifications, {id: addedNotification.id});
if (model) {
model.iconClass = this.getNotificationIcon(model.type);
this.alertNotifications.push(model);
}
}
for (let notification of this.notifications) {
if (notification.isDefault) {
notification.iconClass = this.getNotificationIcon(notification.type);
notification.bgColor = "#00678b";
this.alertNotifications.push(notification);
}
}
this.panelCtrl.editingThresholds = true;
this.panelCtrl.render();
}
@ -192,7 +194,7 @@ export class AlertTabCtrl {
}
validateModel() {
if (!this.alert.enabled) {
if (!this.alert) {
return;
}
@ -302,14 +304,24 @@ export class AlertTabCtrl {
}
delete() {
this.alert = this.panel.alert = {enabled: false};
this.panel.thresholds = [];
this.conditionModels = [];
this.panelCtrl.render();
appEvents.emit('confirm-modal', {
title: 'Delete Alert',
text: 'Are you sure you want to delete this alert rule?',
text2: 'You need to save dashboard for the delete to take effect',
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
delete this.panel.alert;
this.alert = null;
this.panel.thresholds = [];
this.conditionModels = [];
this.panelCtrl.render();
}
});
}
enable() {
this.alert.enabled = true;
this.panel.alert = {};
this.initModel();
}

View File

@ -7,7 +7,7 @@ import config from 'app/core/config';
export class AlertNotificationEditCtrl {
model: any;
showTest: boolean = false;
theForm: any;
testSeverity: string = "critical";
/** @ngInject */
@ -36,6 +36,10 @@ export class AlertNotificationEditCtrl {
}
save() {
if (!this.theForm.$valid) {
return;
}
if (this.model.id) {
this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
this.model = res;
@ -53,11 +57,11 @@ export class AlertNotificationEditCtrl {
this.model.settings = {};
}
toggleTest() {
this.showTest = !this.showTest;
}
testNotification() {
if (!this.theForm.$valid) {
return;
}
var payload = {
name: this.model.name,
type: this.model.type,

View File

@ -1,4 +1,4 @@
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert.enabled">
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-class="{active: ctrl.subTabIndex === 0}">
@ -151,7 +151,7 @@
</div>
</div>
<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
<div class="gf-form-group" ng-if="!ctrl.alert">
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.enable()">
<i class="icon-gf icon-gf-alert"></i>

View File

@ -6,81 +6,73 @@
</navbar>
<div class="page-container" >
<div class="page-header">
<h1>Alert notification</h1>
<div class="page-header">
<h1>Alert notification</h1>
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-12">Name</span>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Type</span>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input"
ng-model="ctrl.model.type"
ng-options="t for t in ['webhook', 'email', 'slack']"
ng-change="ctrl.typeChanged(notification, $index)">
</select>
</div>
</div>
<div class="gf-form">
<gf-form-switch
class="gf-form"
label="Send on all alerts"
label-class="width-12"
checked="ctrl.model.isDefault"
tooltip="Use this notification for all alerts">
</gf-form-switch>
</div>
</div>
<form name="ctrl.theForm">
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-12">Name</span>
<input type="text" required class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Type</span>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input" ng-model="ctrl.model.type" ng-options="t for t in ['webhook', 'email', 'slack']" ng-change="ctrl.typeChanged(notification, $index)">
</select>
</div>
</div>
<div class="gf-form">
<gf-form-switch class="gf-form" label="Send on all alerts" label-class="width-12" checked="ctrl.model.isDefault" tooltip="Use this notification for all alerts">
</gf-form-switch>
</div>
</div>
<div class="gf-form-group" ng-show="ctrl.model.type === 'webhook'">
<h3 class="page-heading">Webhook settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-6">Username</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-6">Password</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
</div>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.model.type === 'webhook'">
<h3 class="page-heading">Webhook settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-6">Username</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-6">Password</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
</div>
</div>
</div>
<div class="gf-form-group" ng-show="ctrl.model.type === 'slack'">
<h3 class="page-heading">Slack settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div>
</div>
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
<h3 class="page-heading">Slack settings</h3>
<div class="gf-form">
<span class="gf-form-label width-6">Url</span>
<input type="text" required class="gf-form-input max-width-30" ng-model="ctrl.model.settings.url" placeholder="Slack incoming webhook url"></input>
</div>
</div>
<div class="gf-form-group section" ng-show="ctrl.model.type === 'email'">
<h3 class="page-heading">Email addresses</h3>
<div class="gf-form">
<textarea rows="7" class="gf-form-input width-25" ng-model="ctrl.model.settings.addresses"></textarea>
</div>
</div>
<div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
<h3 class="page-heading">Email addresses</h3>
<div class="gf-form">
<textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form width-6">
<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
</div>
<div class="gf-form width-8">
<button ng-click="ctrl.toggleTest()" class="btn btn-secondary">Test</button>
</div>
<div class="gf-form width-20" ng-show="ctrl.showTest">
<div class="gf-form" ng-show="ctrl.showTest">
<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
</div>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form width-6">
<button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
</div>
<div class="gf-form width-20">
<div class="gf-form">
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
</div>
</div>
</div>
</div>
</form>
</div>

View File

@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
alertStatesPromise: any;
/** @ngInject */
constructor(private $rootScope,
@ -22,14 +23,27 @@ export class AnnotationsSrv {
clearCache() {
this.globalAnnotationsPromise = null;
this.alertStatesPromise = null;
}
getAnnotations(options) {
return this.$q.all([
this.getGlobalAnnotations(options),
this.getPanelAnnotations(options)
]).then(allResults => {
return _.flattenDeep(allResults);
this.getPanelAnnotations(options),
this.getAlertStates(options)
]).then(results => {
// combine the annotations and flatten results
var annotations = _.flattenDeep([results[0], results[1]]);
// look for alert state for this panel
var alertState = _.find(results[2], {panelId: options.panel.id});
return {
annotations: annotations,
alertState: alertState,
};
}).catch(err => {
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
});
@ -39,7 +53,7 @@ export class AnnotationsSrv {
var panel = options.panel;
var dashboard = options.dashboard;
if (panel && panel.alert && panel.alert.enabled) {
if (panel && panel.alert) {
return this.backendSrv.get('/api/annotations', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
@ -54,6 +68,28 @@ export class AnnotationsSrv {
return this.$q.when([]);
}
getAlertStates(options) {
if (!options.dashboard.id) {
return this.$q.when([]);
}
// ignore if no alerts
if (options.panel && !options.panel.alert) {
return this.$q.when([]);
}
if (options.range.raw.to !== 'now') {
return this.$q.when([]);
}
if (this.alertStatesPromise) {
return this.alertStatesPromise;
}
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
return this.alertStatesPromise;
}
getGlobalAnnotations(options) {
var dashboard = options.dashboard;

View File

@ -159,7 +159,7 @@ export class DashNavCtrl {
var confirmText = "";
var text2 = $scope.dashboard.title;
var alerts = $scope.dashboard.rows.reduce((memo, row) => {
memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
memo += row.panels.filter(panel => panel.alert).length;
return memo;
}, 0);

View File

@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
var intervalOverride = this.panel.interval;
// if no panel interval check datasource
if (!intervalOverride && this.datasource && this.datasource.interval) {
if (intervalOverride) {
intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
} else if (this.datasource && this.datasource.interval) {
intervalOverride = this.datasource.interval;
}

View File

@ -6,7 +6,7 @@ import $ from 'jquery';
var module = angular.module('grafana.directives');
var panelTemplate = `
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
<div class="panel-container">
<div class="panel-header">
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
<span data-placement="top" bs-tooltip="ctrl.error">
@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
// the reason for handling these classes this way is for performance
// limit the watchers on panels etc
ctrl.events.on('render', () => {
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
panelContainer.toggleClass('panel-has-alert', ctrl.panel.alert !== undefined);
if (panelContainer.hasClass('panel-has-alert')) {
panelContainer.removeClass('panel-alert-state--ok panel-alert-state--alerting');
}
// set special class for ok, or alerting states
if (ctrl.alertState) {
if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
}
}
});
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
panelContainer.css({minHeight: ctrl.containerHeight});
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);

View File

@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
.directive('panelMenu', function($compile, linkSrv) {
var linkTemplate =
'<span class="panel-title drag-handle pointer">' +
'<span class="icon-gf panel-alert-icon"></span>' +
'<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
'<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
'<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +

View File

@ -122,7 +122,7 @@ export class DataSourceEditCtrl {
});
}
saveChanges(test) {
saveChanges() {
if (!this.editForm.$valid) {
return;
}

View File

@ -8,11 +8,11 @@
<span class="gf-form-label width-6">Span</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
</div>
<div class="gf-form max-width-26">
<div class="gf-form">
<span class="gf-form-label width-8">Height</span>
<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
<editor-checkbox text="Transparent" model="ctrl.panel.transparent"></editor-checkbox>
</div>
<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form-inline">

View File

@ -10,7 +10,7 @@
<i class="fa fa-caret-down"></i>
</a>
<input type="text" class="hidden-input input-small gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<input type="text" class="gf-form-input" style="display: none" ng-keydown="vm.keyDown($event)" ng-model="vm.search.query" ng-change="vm.queryChanged()" ></input>
<div class="variable-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
<div class="variable-options-wrapper">

View File

@ -3,9 +3,10 @@ define([
'lodash',
'moment',
'app/core/utils/datemath',
'app/core/utils/kbn',
'./annotation_query',
],
function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
'use strict';
/** @ngInject */
@ -36,12 +37,9 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
query.statistics = target.statistics;
var range = end - start;
query.period = parseInt(target.period, 10) || (query.namespace === 'AWS/EC2' ? 300 : 60);
if (range / query.period >= 1440) {
query.period = Math.ceil(range / 1440 / 60) * 60;
}
target.period = query.period;
var period = this._getPeriod(target, query, options, start, end);
target.period = period;
query.period = period;
queries.push(query);
}.bind(this));
@ -69,6 +67,27 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
});
};
this._getPeriod = function(target, query, options, start, end) {
var period;
var range = end - start;
if (!target.period) {
period = (query.namespace === 'AWS/EC2') ? 300 : 60;
} else if (/^\d+$/.test(target.period)) {
period = parseInt(target.period, 10);
} else {
period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars));
}
if (query.period < 60) {
period = 60;
}
if (range / query.period >= 1440) {
period = Math.ceil(range / 1440 / 60) * 60;
}
return period;
};
this.performTimeSeriesQuery = function(query, start, end) {
return this.awsRequest({
region: query.region,

View File

@ -82,6 +82,35 @@ describe('CloudWatchDatasource', function() {
ctx.$rootScope.$apply();
});
it('should generate the correct query with interval variable', function(done) {
ctx.templateSrv.data = {
period: '10m'
};
var query = {
range: { from: 'now-1h', to: 'now' },
targets: [
{
region: 'us-east-1',
namespace: 'AWS/EC2',
metricName: 'CPUUtilization',
dimensions: {
InstanceId: 'i-12345678'
},
statistics: ['Average'],
period: '[[period]]'
}
]
};
ctx.ds.query(query).then(function() {
var params = requestParams.data.parameters;
expect(params.period).to.be(600);
done();
});
ctx.$rootScope.$apply();
});
it('should return series list', function(done) {
ctx.ds.query(query).then(function(result) {
expect(result.data[0].target).to.be('CPUUtilization_Average');

View File

@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
if (!data) {
return;
}
annotations = data.annotations || annotations;
annotations = ctrl.annotations;
render_panel();
});

View File

@ -22,6 +22,9 @@ class GraphCtrl extends MetricsPanelCtrl {
hiddenSeries: any = {};
seriesList: any = [];
dataList: any = [];
annotations: any = [];
alertState: any;
annotationsPromise: any;
datapointsCount: number;
datapointsOutside: boolean;
@ -167,11 +170,11 @@ class GraphCtrl extends MetricsPanelCtrl {
onDataError(err) {
this.seriesList = [];
this.annotations = [];
this.render([]);
}
onDataReceived(dataList) {
this.dataList = dataList;
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
@ -186,9 +189,10 @@ class GraphCtrl extends MetricsPanelCtrl {
}
}
this.annotationsPromise.then(annotations => {
this.annotationsPromise.then(result => {
this.loading = false;
this.seriesList.annotations = annotations;
this.alertState = result.alertState;
this.annotations = result.annotations;
this.render(this.seriesList);
}, () => {
this.loading = false;

View File

@ -13,7 +13,7 @@ export class ThresholdFormCtrl {
constructor($scope) {
this.panel = this.panelCtrl.panel;
if (this.panel.alert && this.panel.alert.enabled) {
if (this.panel.alert) {
this.disabled = true;
}

View File

@ -34,10 +34,7 @@
ng-change="editor.render()"
ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-4"
label="Scroll"
checked="editor.panel.scroll"
change="editor.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
<div class="gf-form max-width-17">
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper max-width-15">

View File

@ -38,3 +38,46 @@
top: 2px;
}
}
.panel-has-alert {
.panel-alert-icon:before {
content: "\e611";
position: relative;
top: 1px;
left: -3px;
}
}
.panel-alert-state {
&--alerting {
animation: alerting-panel 2s 0s infinite;
opacity: 1;
.panel-alert-icon:before {
color: $critical;
content: "\e610";
}
}
&--ok {
box-shadow: 0 0 5px rgba(0,200,0,10.8);
.panel-alert-icon:before {
color: $online;
content: "\e611";
}
}
}
@keyframes alerting-panel {
0% {
box-shadow: none;
}
50% {
box-shadow: 0 0 10px $critical;
}
100% {
box-shadow: none;
}
}