mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alerting_opentsdb
This commit is contained in:
commit
8368a4a88a
@ -3,7 +3,7 @@
|
||||
[Website](http://grafana.org) |
|
||||
[Twitter](https://twitter.com/grafana) |
|
||||
[IRC](https://webchat.freenode.net/?channels=grafana) |
|
||||

|
||||
[](http://slack.raintank.io)
|
||||
[Slack](http://slack.raintank.io) |
|
||||
[Email](mailto:contact@grafana.org)
|
||||
|
||||
|
@ -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!
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
150
pkg/cmd/grafana-server/server.go
Normal file
150
pkg/cmd/grafana-server/server.go
Normal 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)
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
10
pkg/models/server.go
Normal file
@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type GrafanaServer interface {
|
||||
context.Context
|
||||
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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": [
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
pkg/tsdb/testdata/testdata.go
vendored
4
pkg/tsdb/testdata/testdata.go
vendored
@ -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)
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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>' +
|
||||
|
@ -122,7 +122,7 @@ export class DataSourceEditCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
saveChanges(test) {
|
||||
saveChanges() {
|
||||
if (!this.editForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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');
|
||||
|
@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
annotations = data.annotations || annotations;
|
||||
annotations = ctrl.annotations;
|
||||
render_panel();
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user