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) |
|
[Website](http://grafana.org) |
|
||||||
[Twitter](https://twitter.com/grafana) |
|
[Twitter](https://twitter.com/grafana) |
|
||||||
[IRC](https://webchat.freenode.net/?channels=grafana) |
|
[IRC](https://webchat.freenode.net/?channels=grafana) |
|
||||||

|
[](http://slack.raintank.io)
|
||||||
[Slack](http://slack.raintank.io) |
|
[Slack](http://slack.raintank.io) |
|
||||||
[Email](mailto:contact@grafana.org)
|
[Email](mailto:contact@grafana.org)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an
|
|||||||
### ** Data Source **
|
### ** 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.
|
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.
|
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.
|
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.
|
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.
|
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!
|
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.
|
6. Settings: Manage Dashboard settings and features such as Templating and Annotations.
|
||||||
|
|
||||||
## Dashboards, Panels, Rows, the building blocks of Grafana...
|
## 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
|
## Adding & Editing Graphs and Panels
|
||||||
|
@ -492,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul
|
|||||||
### path
|
### path
|
||||||
The full path to a directory containing your json dashboards.
|
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]
|
## [log]
|
||||||
|
|
||||||
### mode
|
### 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
|
// GET /api/alerts
|
||||||
func GetAlerts(c *middleware.Context) Response {
|
func GetAlerts(c *middleware.Context) Response {
|
||||||
query := models.GetAlertsQuery{
|
query := models.GetAlertsQuery{
|
||||||
|
@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
||||||
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
||||||
r.Get("/", wrap(GetAlerts))
|
r.Get("/", wrap(GetAlerts))
|
||||||
|
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||||
|
@ -79,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse {
|
|||||||
func ApiSuccess(message string) *NormalResponse {
|
func ApiSuccess(message string) *NormalResponse {
|
||||||
resp := make(map[string]interface{})
|
resp := make(map[string]interface{})
|
||||||
resp["message"] = message
|
resp["message"] = message
|
||||||
return Respond(200, resp)
|
return Json(200, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApiError(status int, message string, err error) *NormalResponse {
|
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
|
cmd.OrgId = c.OrgId
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
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)
|
c.JsonApiErr(500, "Failed to add datasource", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"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 {
|
if err != nil {
|
||||||
return ApiError(500, "Metric request error", err)
|
return ApiError(500, "Metric request error", err)
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
package bus
|
package bus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandlerFunc interface{}
|
type HandlerFunc interface{}
|
||||||
|
type CtxHandlerFunc func()
|
||||||
type Msg interface{}
|
type Msg interface{}
|
||||||
|
|
||||||
type Bus interface {
|
type Bus interface {
|
||||||
Dispatch(msg Msg) error
|
Dispatch(msg Msg) error
|
||||||
|
DispatchCtx(ctx context.Context, msg Msg) error
|
||||||
Publish(msg Msg) error
|
Publish(msg Msg) error
|
||||||
|
|
||||||
AddHandler(handler HandlerFunc)
|
AddHandler(handler HandlerFunc)
|
||||||
|
AddCtxHandler(handler HandlerFunc)
|
||||||
AddEventListener(handler HandlerFunc)
|
AddEventListener(handler HandlerFunc)
|
||||||
AddWildcardListener(handler HandlerFunc)
|
AddWildcardListener(handler HandlerFunc)
|
||||||
}
|
}
|
||||||
@ -34,6 +38,27 @@ func New() Bus {
|
|||||||
return 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 {
|
func (b *InProcBus) Dispatch(msg Msg) error {
|
||||||
var msgName = reflect.TypeOf(msg).Elem().Name()
|
var msgName = reflect.TypeOf(msg).Elem().Name()
|
||||||
|
|
||||||
@ -90,6 +115,12 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) {
|
|||||||
b.handlers[queryTypeName] = handler
|
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) {
|
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
|
||||||
handlerType := reflect.TypeOf(handler)
|
handlerType := reflect.TypeOf(handler)
|
||||||
eventName := handlerType.In(0).Elem().Name()
|
eventName := handlerType.In(0).Elem().Name()
|
||||||
@ -105,6 +136,11 @@ func AddHandler(implName string, handler HandlerFunc) {
|
|||||||
globalBus.AddHandler(handler)
|
globalBus.AddHandler(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package level functions
|
||||||
|
func AddCtxHandler(implName string, handler HandlerFunc) {
|
||||||
|
globalBus.AddCtxHandler(handler)
|
||||||
|
}
|
||||||
|
|
||||||
// Package level functions
|
// Package level functions
|
||||||
func AddEventListener(handler HandlerFunc) {
|
func AddEventListener(handler HandlerFunc) {
|
||||||
globalBus.AddEventListener(handler)
|
globalBus.AddEventListener(handler)
|
||||||
@ -118,6 +154,10 @@ func Dispatch(msg Msg) error {
|
|||||||
return globalBus.Dispatch(msg)
|
return globalBus.Dispatch(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DispatchCtx(ctx context.Context, msg Msg) error {
|
||||||
|
return globalBus.DispatchCtx(ctx, msg)
|
||||||
|
}
|
||||||
|
|
||||||
func Publish(msg Msg) error {
|
func Publish(msg Msg) error {
|
||||||
return globalBus.Publish(msg)
|
return globalBus.Publish(msg)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -13,21 +12,11 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/login"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"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/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"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/conditions"
|
||||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||||
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||||
@ -66,41 +55,8 @@ func main() {
|
|||||||
setting.BuildCommit = commit
|
setting.BuildCommit = commit
|
||||||
setting.BuildStamp = buildstampInt64
|
setting.BuildStamp = buildstampInt64
|
||||||
|
|
||||||
appContext, shutdownFn := context.WithCancel(context.Background())
|
server := NewGrafanaServer()
|
||||||
grafanaGroup, appContext := errgroup.WithContext(appContext)
|
server.Start()
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRuntime() {
|
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)
|
signalChan := make(chan os.Signal, 1)
|
||||||
code := 0
|
code := 0
|
||||||
|
|
||||||
@ -151,18 +107,8 @@ func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Gro
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case sig := <-signalChan:
|
case sig := <-signalChan:
|
||||||
log.Info2("Received system signal. Shutting down", "signal", sig)
|
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||||
case code = <-exitChan:
|
case code = <-exitChan:
|
||||||
switch code {
|
server.Shutdown(code, "startup error")
|
||||||
case 0:
|
|
||||||
log.Info("Shutting down")
|
|
||||||
default:
|
|
||||||
log.Warn("Shutting down")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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
|
// Typed errors
|
||||||
var (
|
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
|
type DsAccess string
|
||||||
|
@ -12,6 +12,10 @@ type SendEmailCommand struct {
|
|||||||
Info string
|
Info string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendEmailCommandSync struct {
|
||||||
|
SendEmailCommand
|
||||||
|
}
|
||||||
|
|
||||||
type SendWebhook struct {
|
type SendWebhook struct {
|
||||||
Url string
|
Url string
|
||||||
User string
|
User string
|
||||||
@ -19,6 +23,13 @@ type SendWebhook struct {
|
|||||||
Body string
|
Body string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendWebhookSync struct {
|
||||||
|
Url string
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Body string
|
||||||
|
}
|
||||||
|
|
||||||
type SendResetPasswordEmailCommand struct {
|
type SendResetPasswordEmailCommand struct {
|
||||||
User *User
|
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)
|
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
|
||||||
result := make(tsdb.TimeSeriesSlice, 0)
|
result := make(tsdb.TimeSeriesSlice, 0)
|
||||||
|
|
||||||
resp, err := c.HandleRequest(req)
|
resp, err := c.HandleRequest(context.Context, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
|
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package conditions
|
package conditions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
null "gopkg.in/guregu/null.v3"
|
null "gopkg.in/guregu/null.v3"
|
||||||
@ -137,7 +138,7 @@ func (ctx *queryConditionTestContext) exec() {
|
|||||||
|
|
||||||
ctx.condition = condition
|
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{
|
return &tsdb.Response{
|
||||||
Results: map[string]*tsdb.QueryResult{
|
Results: map[string]*tsdb.QueryResult{
|
||||||
"A": {Series: ctx.series},
|
"A": {Series: ctx.series},
|
||||||
|
@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
execQueue chan *Job
|
execQueue chan *Job
|
||||||
resultQueue chan *EvalContext
|
|
||||||
clock clock.Clock
|
clock clock.Clock
|
||||||
ticker *Ticker
|
ticker *Ticker
|
||||||
scheduler Scheduler
|
scheduler Scheduler
|
||||||
@ -25,7 +24,6 @@ func NewEngine() *Engine {
|
|||||||
e := &Engine{
|
e := &Engine{
|
||||||
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
|
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
|
||||||
execQueue: make(chan *Job, 1000),
|
execQueue: make(chan *Job, 1000),
|
||||||
resultQueue: make(chan *EvalContext, 1000),
|
|
||||||
scheduler: NewScheduler(),
|
scheduler: NewScheduler(),
|
||||||
evalHandler: NewEvalHandler(),
|
evalHandler: NewEvalHandler(),
|
||||||
ruleReader: NewRuleReader(),
|
ruleReader: NewRuleReader(),
|
||||||
@ -39,23 +37,17 @@ func NewEngine() *Engine {
|
|||||||
func (e *Engine) Run(ctx context.Context) error {
|
func (e *Engine) Run(ctx context.Context) error {
|
||||||
e.log.Info("Initializing Alerting")
|
e.log.Info("Initializing Alerting")
|
||||||
|
|
||||||
g, ctx := errgroup.WithContext(ctx)
|
alertGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
g.Go(func() error { return e.alertingTicker(ctx) })
|
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
|
||||||
g.Go(func() error { return e.execDispatcher(ctx) })
|
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
|
||||||
g.Go(func() error { return e.resultDispatcher(ctx) })
|
|
||||||
|
|
||||||
err := g.Wait()
|
err := alertGroup.Wait()
|
||||||
|
|
||||||
e.log.Info("Stopped Alerting", "reason", err)
|
e.log.Info("Stopped Alerting", "reason", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) Stop() {
|
|
||||||
close(e.execQueue)
|
|
||||||
close(e.resultQueue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-grafanaCtx.Done():
|
case <-grafanaCtx.Done():
|
||||||
close(e.resultQueue)
|
return dispatcherGroup.Wait()
|
||||||
return grafanaCtx.Err()
|
|
||||||
case job := <-e.execQueue:
|
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() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
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() {
|
go func() {
|
||||||
job.Running = true
|
e.evalHandler.Eval(evalContext)
|
||||||
context := NewEvalContext(job.Rule)
|
e.resultHandler.Handle(evalContext)
|
||||||
e.evalHandler.Eval(context)
|
|
||||||
job.Running = false
|
|
||||||
done <- context
|
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
var err error = nil
|
||||||
select {
|
select {
|
||||||
|
|
||||||
case <-grafanaCtx.Done():
|
case <-grafanaCtx.Done():
|
||||||
return grafanaCtx.Err()
|
|
||||||
case evalContext := <-done:
|
|
||||||
e.resultQueue <- evalContext
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) resultDispatcher(grafanaCtx context.Context) error {
|
|
||||||
for {
|
|
||||||
select {
|
select {
|
||||||
case <-grafanaCtx.Done():
|
case <-time.After(unfinishedWorkTimeout):
|
||||||
//handle all responses before shutting down.
|
cancelFn()
|
||||||
for result := range e.resultQueue {
|
err = grafanaCtx.Err()
|
||||||
e.handleResponse(result)
|
case <-done:
|
||||||
}
|
|
||||||
|
|
||||||
return grafanaCtx.Err()
|
|
||||||
case result := <-e.resultQueue:
|
|
||||||
e.handleResponse(result)
|
|
||||||
}
|
}
|
||||||
|
case <-done:
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) handleResponse(result *EvalContext) {
|
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
|
||||||
defer func() {
|
job.Running = false
|
||||||
if err := recover(); err != nil {
|
cancelFn()
|
||||||
e.log.Error("Panic in resultDispatcher", "error", err, "stack", log.Stack(1))
|
return err
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -20,14 +21,30 @@ type EvalContext struct {
|
|||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
Rule *Rule
|
Rule *Rule
|
||||||
DoneChan chan bool
|
|
||||||
CancelChan chan bool
|
|
||||||
log log.Logger
|
log log.Logger
|
||||||
dashboardSlug string
|
dashboardSlug string
|
||||||
ImagePublicUrl string
|
ImagePublicUrl string
|
||||||
ImageOnDiskPath string
|
ImageOnDiskPath string
|
||||||
NoDataFound bool
|
NoDataFound bool
|
||||||
RetryCount int
|
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 {
|
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{
|
return &EvalContext{
|
||||||
|
Context: alertCtx,
|
||||||
StartTime: time.Now(),
|
StartTime: time.Now(),
|
||||||
Rule: rule,
|
Rule: rule,
|
||||||
Logs: make([]*ResultLogEntry, 0),
|
Logs: make([]*ResultLogEntry, 0),
|
||||||
EvalMatches: make([]*EvalMatch, 0),
|
EvalMatches: make([]*EvalMatch, 0),
|
||||||
DoneChan: make(chan bool, 1),
|
|
||||||
CancelChan: make(chan bool, 1),
|
|
||||||
log: log.New("alerting.evalContext"),
|
log: log.New("alerting.evalContext"),
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
MaxRetries int = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
type DefaultEvalHandler struct {
|
type DefaultEvalHandler struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
alertJobTimeout time.Duration
|
alertJobTimeout time.Duration
|
||||||
@ -20,49 +15,11 @@ type DefaultEvalHandler struct {
|
|||||||
func NewEvalHandler() *DefaultEvalHandler {
|
func NewEvalHandler() *DefaultEvalHandler {
|
||||||
return &DefaultEvalHandler{
|
return &DefaultEvalHandler{
|
||||||
log: log.New("alerting.evalHandler"),
|
log: log.New("alerting.evalHandler"),
|
||||||
alertJobTimeout: time.Second * 15,
|
alertJobTimeout: time.Second * 5,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
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 {
|
for _, condition := range context.Rule.Conditions {
|
||||||
condition.Eval(context)
|
condition.Eval(context)
|
||||||
|
|
||||||
@ -80,5 +37,4 @@ func (e *DefaultEvalHandler) eval(context *EvalContext) {
|
|||||||
context.EndTime = time.Now()
|
context.EndTime = time.Now()
|
||||||
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
|
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
|
||||||
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
|
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
|
||||||
context.DoneChan <- true
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@ -19,25 +20,25 @@ func TestAlertingExecutor(t *testing.T) {
|
|||||||
handler := NewEvalHandler()
|
handler := NewEvalHandler()
|
||||||
|
|
||||||
Convey("Show return triggered with single passing condition", func() {
|
Convey("Show return triggered with single passing condition", func() {
|
||||||
context := NewEvalContext(&Rule{
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
Conditions: []Condition{&conditionStub{
|
Conditions: []Condition{&conditionStub{
|
||||||
firing: true,
|
firing: true,
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
|
|
||||||
handler.eval(context)
|
handler.Eval(context)
|
||||||
So(context.Firing, ShouldEqual, true)
|
So(context.Firing, ShouldEqual, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Show return false with not passing condition", func() {
|
Convey("Show return false with not passing condition", func() {
|
||||||
context := NewEvalContext(&Rule{
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
Conditions: []Condition{
|
Conditions: []Condition{
|
||||||
&conditionStub{firing: true},
|
&conditionStub{firing: true},
|
||||||
&conditionStub{firing: false},
|
&conditionStub{firing: false},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
handler.eval(context)
|
handler.Eval(context)
|
||||||
So(context.Firing, ShouldEqual, false)
|
So(context.Firing, ShouldEqual, false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backward compatability check, can be removed later
|
||||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||||
|
if hasEnabled && enabled.MustBool() == false {
|
||||||
if !hasEnabled || !enabled.MustBool() {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
"name": "name1",
|
"name": "name1",
|
||||||
"message": "desc1",
|
"message": "desc1",
|
||||||
"handler": 1,
|
"handler": 1,
|
||||||
"enabled": true,
|
|
||||||
"frequency": "60s",
|
"frequency": "60s",
|
||||||
"conditions": [
|
"conditions": [
|
||||||
{
|
{
|
||||||
@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
"name": "name2",
|
"name": "name2",
|
||||||
"message": "desc2",
|
"message": "desc2",
|
||||||
"handler": 0,
|
"handler": 0,
|
||||||
"enabled": true,
|
|
||||||
"frequency": "60s",
|
"frequency": "60s",
|
||||||
"severity": "warning",
|
"severity": "warning",
|
||||||
"conditions": [
|
"conditions": [
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EvalHandler interface {
|
type EvalHandler interface {
|
||||||
Eval(context *EvalContext)
|
Eval(evalContext *EvalContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scheduler interface {
|
type Scheduler interface {
|
||||||
@ -14,7 +12,7 @@ type Scheduler interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
Notify(alertResult *EvalContext)
|
Notify(evalContext *EvalContext) error
|
||||||
GetType() string
|
GetType() string
|
||||||
NeedsImage() bool
|
NeedsImage() bool
|
||||||
PassesFilter(rule *Rule) bool
|
PassesFilter(rule *Rule) bool
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/imguploader"
|
"github.com/grafana/grafana/pkg/components/imguploader"
|
||||||
"github.com/grafana/grafana/pkg/components/renderer"
|
"github.com/grafana/grafana/pkg/components/renderer"
|
||||||
@ -33,32 +35,36 @@ func (n *RootNotifier) PassesFilter(rule *Rule) bool {
|
|||||||
return false
|
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)
|
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
|
||||||
|
|
||||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("Failed to read notifications", "error", err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(notifiers) == 0 {
|
if len(notifiers) == 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = n.uploadImage(context)
|
err = n.uploadImage(context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("Failed to upload alert panel image", "error", err)
|
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 {
|
for _, notifier := range notifiers {
|
||||||
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
|
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) {
|
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
|
||||||
|
@ -22,7 +22,7 @@ func (fn *FakeNotifier) NeedsImage() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
|
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
|
||||||
|
|
||||||
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
|
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
|
||||||
return fn.FakeMatchResult
|
return fn.FakeMatchResult
|
||||||
|
@ -35,33 +35,39 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
|||||||
}, nil
|
}, 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)
|
this.log.Info("Sending alert notification to", "addresses", this.Addresses)
|
||||||
metrics.M_Alerting_Notification_Sent_Email.Inc(1)
|
metrics.M_Alerting_Notification_Sent_Email.Inc(1)
|
||||||
|
|
||||||
ruleUrl, err := context.GetRuleUrl()
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.log.Error("Failed get rule link", "error", err)
|
this.log.Error("Failed get rule link", "error", err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &m.SendEmailCommand{
|
cmd := &m.SendEmailCommandSync{
|
||||||
Data: map[string]interface{}{
|
SendEmailCommand: m.SendEmailCommand{
|
||||||
"Title": context.GetNotificationTitle(),
|
Data: map[string]interface{}{
|
||||||
"State": context.Rule.State,
|
"Title": evalContext.GetNotificationTitle(),
|
||||||
"Name": context.Rule.Name,
|
"State": evalContext.Rule.State,
|
||||||
"StateModel": context.GetStateModel(),
|
"Name": evalContext.Rule.Name,
|
||||||
"Message": context.Rule.Message,
|
"StateModel": evalContext.GetStateModel(),
|
||||||
"RuleUrl": ruleUrl,
|
"Message": evalContext.Rule.Message,
|
||||||
"ImageLink": context.ImagePublicUrl,
|
"RuleUrl": ruleUrl,
|
||||||
"AlertPageUrl": setting.AppUrl + "alerting",
|
"ImageLink": evalContext.ImagePublicUrl,
|
||||||
"EvalMatches": context.EvalMatches,
|
"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)
|
this.log.Error("Failed to send alert notification email", "error", err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -35,19 +35,19 @@ type SlackNotifier struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
this.log.Info("Executing slack notification", "ruleId", context.Rule.Id, "notification", this.Name)
|
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
|
||||||
metrics.M_Alerting_Notification_Sent_Slack.Inc(1)
|
metrics.M_Alerting_Notification_Sent_Slack.Inc(1)
|
||||||
|
|
||||||
ruleUrl, err := context.GetRuleUrl()
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
this.log.Error("Failed get rule link", "error", err)
|
this.log.Error("Failed get rule link", "error", err)
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := make([]map[string]interface{}, 0)
|
fields := make([]map[string]interface{}, 0)
|
||||||
fieldLimitCount := 4
|
fieldLimitCount := 4
|
||||||
for index, evt := range context.EvalMatches {
|
for index, evt := range evalContext.EvalMatches {
|
||||||
fields = append(fields, map[string]interface{}{
|
fields = append(fields, map[string]interface{}{
|
||||||
"title": evt.Metric,
|
"title": evt.Metric,
|
||||||
"value": evt.Value,
|
"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{}{
|
fields = append(fields, map[string]interface{}{
|
||||||
"title": "Error message",
|
"title": "Error message",
|
||||||
"value": context.Error.Error(),
|
"value": evalContext.Error.Error(),
|
||||||
"short": false,
|
"short": false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
message := ""
|
message := ""
|
||||||
if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||||
message = context.Rule.Message
|
message = evalContext.Rule.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
body := map[string]interface{}{
|
body := map[string]interface{}{
|
||||||
"attachments": []map[string]interface{}{
|
"attachments": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"color": context.GetStateModel().Color,
|
"color": evalContext.GetStateModel().Color,
|
||||||
"title": context.GetNotificationTitle(),
|
"title": evalContext.GetNotificationTitle(),
|
||||||
"title_link": ruleUrl,
|
"title_link": ruleUrl,
|
||||||
"text": message,
|
"text": message,
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
"image_url": context.ImagePublicUrl,
|
"image_url": evalContext.ImagePublicUrl,
|
||||||
"footer": "Grafana v" + setting.BuildVersion,
|
"footer": "Grafana v" + setting.BuildVersion,
|
||||||
"footer_icon": "http://grafana.org/assets/img/fav32.png",
|
"footer_icon": "http://grafana.org/assets/img/fav32.png",
|
||||||
"ts": time.Now().Unix(),
|
"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)
|
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)
|
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
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
|
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
this.log.Info("Sending webhook")
|
this.log.Info("Sending webhook")
|
||||||
metrics.M_Alerting_Notification_Sent_Webhook.Inc(1)
|
metrics.M_Alerting_Notification_Sent_Webhook.Inc(1)
|
||||||
|
|
||||||
bodyJSON := simplejson.New()
|
bodyJSON := simplejson.New()
|
||||||
bodyJSON.Set("title", context.GetNotificationTitle())
|
bodyJSON.Set("title", evalContext.GetNotificationTitle())
|
||||||
bodyJSON.Set("ruleId", context.Rule.Id)
|
bodyJSON.Set("ruleId", evalContext.Rule.Id)
|
||||||
bodyJSON.Set("ruleName", context.Rule.Name)
|
bodyJSON.Set("ruleName", evalContext.Rule.Name)
|
||||||
bodyJSON.Set("state", context.Rule.State)
|
bodyJSON.Set("state", evalContext.Rule.State)
|
||||||
bodyJSON.Set("evalMatches", context.EvalMatches)
|
bodyJSON.Set("evalMatches", evalContext.EvalMatches)
|
||||||
|
|
||||||
ruleUrl, err := context.GetRuleUrl()
|
ruleUrl, err := evalContext.GetRuleUrl()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
bodyJSON.Set("rule_url", ruleUrl)
|
bodyJSON.Set("rule_url", ruleUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.ImagePublicUrl != "" {
|
if evalContext.ImagePublicUrl != "" {
|
||||||
bodyJSON.Set("image_url", context.ImagePublicUrl)
|
bodyJSON.Set("image_url", evalContext.ImagePublicUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := bodyJSON.MarshalJSON()
|
body, _ := bodyJSON.MarshalJSON()
|
||||||
|
|
||||||
cmd := &m.SendWebhook{
|
cmd := &m.SendWebhookSync{
|
||||||
Url: this.Url,
|
Url: this.Url,
|
||||||
User: this.User,
|
User: this.User,
|
||||||
Password: this.Password,
|
Password: this.Password,
|
||||||
Body: string(body),
|
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)
|
this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ResultHandler interface {
|
type ResultHandler interface {
|
||||||
Handle(ctx *EvalContext)
|
Handle(evalContext *EvalContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultResultHandler struct {
|
type DefaultResultHandler struct {
|
||||||
@ -27,36 +27,36 @@ func NewResultHandler() *DefaultResultHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||||
oldState := ctx.Rule.State
|
oldState := evalContext.Rule.State
|
||||||
|
|
||||||
exeuctionError := ""
|
exeuctionError := ""
|
||||||
annotationData := simplejson.New()
|
annotationData := simplejson.New()
|
||||||
if ctx.Error != nil {
|
if evalContext.Error != nil {
|
||||||
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
|
handler.log.Error("Alert Rule Result Error", "ruleId", evalContext.Rule.Id, "error", evalContext.Error)
|
||||||
ctx.Rule.State = m.AlertStateExecError
|
evalContext.Rule.State = m.AlertStateExecError
|
||||||
exeuctionError = ctx.Error.Error()
|
exeuctionError = evalContext.Error.Error()
|
||||||
annotationData.Set("errorMessage", exeuctionError)
|
annotationData.Set("errorMessage", exeuctionError)
|
||||||
} else if ctx.Firing {
|
} else if evalContext.Firing {
|
||||||
ctx.Rule.State = m.AlertStateAlerting
|
evalContext.Rule.State = m.AlertStateAlerting
|
||||||
annotationData = simplejson.NewFromAny(ctx.EvalMatches)
|
annotationData = simplejson.NewFromAny(evalContext.EvalMatches)
|
||||||
} else {
|
} else {
|
||||||
// handle no data case
|
// handle no data case
|
||||||
if ctx.NoDataFound {
|
if evalContext.NoDataFound {
|
||||||
ctx.Rule.State = ctx.Rule.NoDataState
|
evalContext.Rule.State = evalContext.Rule.NoDataState
|
||||||
} else {
|
} else {
|
||||||
ctx.Rule.State = m.AlertStateOK
|
evalContext.Rule.State = m.AlertStateOK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
countStateResult(ctx.Rule.State)
|
countStateResult(evalContext.Rule.State)
|
||||||
if ctx.Rule.State != oldState {
|
if evalContext.Rule.State != oldState {
|
||||||
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
|
handler.log.Info("New state change", "alertId", evalContext.Rule.Id, "newState", evalContext.Rule.State, "oldState", oldState)
|
||||||
|
|
||||||
cmd := &m.SetAlertStateCommand{
|
cmd := &m.SetAlertStateCommand{
|
||||||
AlertId: ctx.Rule.Id,
|
AlertId: evalContext.Rule.Id,
|
||||||
OrgId: ctx.Rule.OrgId,
|
OrgId: evalContext.Rule.OrgId,
|
||||||
State: ctx.Rule.State,
|
State: evalContext.Rule.State,
|
||||||
Error: exeuctionError,
|
Error: exeuctionError,
|
||||||
EvalData: annotationData,
|
EvalData: annotationData,
|
||||||
}
|
}
|
||||||
@ -67,14 +67,14 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
|||||||
|
|
||||||
// save annotation
|
// save annotation
|
||||||
item := annotations.Item{
|
item := annotations.Item{
|
||||||
OrgId: ctx.Rule.OrgId,
|
OrgId: evalContext.Rule.OrgId,
|
||||||
DashboardId: ctx.Rule.DashboardId,
|
DashboardId: evalContext.Rule.DashboardId,
|
||||||
PanelId: ctx.Rule.PanelId,
|
PanelId: evalContext.Rule.PanelId,
|
||||||
Type: annotations.AlertType,
|
Type: annotations.AlertType,
|
||||||
AlertId: ctx.Rule.Id,
|
AlertId: evalContext.Rule.Id,
|
||||||
Title: ctx.Rule.Name,
|
Title: evalContext.Rule.Name,
|
||||||
Text: ctx.GetStateModel().Text,
|
Text: evalContext.GetStateModel().Text,
|
||||||
NewState: string(ctx.Rule.State),
|
NewState: string(evalContext.Rule.State),
|
||||||
PrevState: string(oldState),
|
PrevState: string(oldState),
|
||||||
Epoch: time.Now().Unix(),
|
Epoch: time.Now().Unix(),
|
||||||
Data: annotationData,
|
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.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) {
|
func countStateResult(state m.AlertStateType) {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
@ -35,13 +37,12 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext())
|
notifier.sendNotifications(createTestEvalContext(), []Notifier{notifiers})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestEvalContext() *EvalContext {
|
func createTestEvalContext() *EvalContext {
|
||||||
|
|
||||||
testRule := &Rule{
|
testRule := &Rule{
|
||||||
DashboardId: 1,
|
DashboardId: 1,
|
||||||
PanelId: 1,
|
PanelId: 1,
|
||||||
@ -50,7 +51,7 @@ func createTestEvalContext() *EvalContext {
|
|||||||
State: m.AlertStateAlerting,
|
State: m.AlertStateAlerting,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := NewEvalContext(testRule)
|
ctx := NewEvalContext(context.TODO(), testRule)
|
||||||
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
|
||||||
|
|
||||||
ctx.IsTestRun = true
|
ctx.IsTestRun = true
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -48,7 +49,7 @@ func handleAlertTestCommand(cmd *AlertTestCommand) error {
|
|||||||
func testAlertRule(rule *Rule) *EvalContext {
|
func testAlertRule(rule *Rule) *EvalContext {
|
||||||
handler := NewEvalHandler()
|
handler := NewEvalHandler()
|
||||||
|
|
||||||
context := NewEvalContext(rule)
|
context := NewEvalContext(context.TODO(), rule)
|
||||||
context.IsTestRun = true
|
context.IsTestRun = true
|
||||||
|
|
||||||
handler.Eval(context)
|
handler.Eval(context)
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
@ -15,6 +18,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"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
|
package notifications
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -29,7 +29,10 @@ func Init() error {
|
|||||||
bus.AddHandler("email", validateResetPasswordCode)
|
bus.AddHandler("email", validateResetPasswordCode)
|
||||||
bus.AddHandler("email", sendEmailCommandHandler)
|
bus.AddHandler("email", sendEmailCommandHandler)
|
||||||
|
|
||||||
|
bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
|
||||||
|
|
||||||
bus.AddHandler("webhook", sendWebhook)
|
bus.AddHandler("webhook", sendWebhook)
|
||||||
|
bus.AddCtxHandler("webhook", SendWebhookSync)
|
||||||
|
|
||||||
bus.AddEventListener(signUpStartedHandler)
|
bus.AddEventListener(signUpStartedHandler)
|
||||||
bus.AddEventListener(signUpCompletedHandler)
|
bus.AddEventListener(signUpCompletedHandler)
|
||||||
@ -56,6 +59,15 @@ func Init() error {
|
|||||||
return nil
|
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 {
|
func sendWebhook(cmd *m.SendWebhook) error {
|
||||||
addToWebhookQueue(&Webhook{
|
addToWebhookQueue(&Webhook{
|
||||||
Url: cmd.Url,
|
Url: cmd.Url,
|
||||||
@ -72,51 +84,33 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
|
func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
|
||||||
if !setting.Smtp.Enabled {
|
message, err := buildEmailMessage(&m.SendEmailCommand{
|
||||||
return errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin")
|
Data: cmd.Data,
|
||||||
}
|
Info: cmd.Info,
|
||||||
|
Massive: cmd.Massive,
|
||||||
var buffer bytes.Buffer
|
Template: cmd.Template,
|
||||||
var err error
|
To: cmd.To,
|
||||||
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(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ package notifications
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
@ -18,7 +17,7 @@ type testTriggeredAlert struct {
|
|||||||
func TestNotifications(t *testing.T) {
|
func TestNotifications(t *testing.T) {
|
||||||
|
|
||||||
Convey("Given the notifications service", t, func() {
|
Convey("Given the notifications service", t, func() {
|
||||||
bus.ClearBusHandlers()
|
//bus.ClearBusHandlers()
|
||||||
|
|
||||||
setting.StaticRootPath = "../../../public/"
|
setting.StaticRootPath = "../../../public/"
|
||||||
setting.Smtp.Enabled = true
|
setting.Smtp.Enabled = true
|
||||||
|
@ -2,11 +2,14 @@ package notifications
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -31,7 +34,7 @@ func processWebhookQueue() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case webhook := <-webhookQueue:
|
case webhook := <-webhookQueue:
|
||||||
err := sendWebRequest(webhook)
|
err := sendWebRequestSync(context.TODO(), webhook)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webhookLog.Error("Failed to send webrequest ", "error", err)
|
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)
|
webhookLog.Debug("Sending webhook", "url", webhook.Url)
|
||||||
|
|
||||||
client := http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Duration(10 * time.Second),
|
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 != "" {
|
if webhook.User != "" && webhook.Password != "" {
|
||||||
request.Header.Add("Authorization", util.GetBasicAuthHeader(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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(request)
|
resp, err := ctxhttp.Do(ctx, client, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ioutil.ReadAll(resp.Body)
|
if resp.StatusCode/100 == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return fmt.Errorf("Webhook response code %v", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
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) {
|
var addToWebhookQueue = func(msg *Webhook) {
|
||||||
|
@ -17,6 +17,7 @@ func init() {
|
|||||||
bus.AddHandler("sql", DeleteAlertById)
|
bus.AddHandler("sql", DeleteAlertById)
|
||||||
bus.AddHandler("sql", GetAllAlertQueryHandler)
|
bus.AddHandler("sql", GetAllAlertQueryHandler)
|
||||||
bus.AddHandler("sql", SetAlertState)
|
bus.AddHandler("sql", SetAlertState)
|
||||||
|
bus.AddHandler("sql", GetAlertStatesForDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlertById(query *m.GetAlertByIdQuery) error {
|
func GetAlertById(query *m.GetAlertByIdQuery) error {
|
||||||
@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
|
|||||||
return nil
|
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 {
|
func AddDataSource(cmd *m.AddDataSourceCommand) error {
|
||||||
|
|
||||||
return inTransaction(func(sess *xorm.Session) 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{
|
ds := &m.DataSource{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
|
@ -41,6 +41,7 @@ func TestDataAccess(t *testing.T) {
|
|||||||
|
|
||||||
err := AddDataSource(&m.AddDataSourceCommand{
|
err := AddDataSource(&m.AddDataSourceCommand{
|
||||||
OrgId: 10,
|
OrgId: 10,
|
||||||
|
Name: "laban",
|
||||||
Type: m.DS_INFLUXDB,
|
Type: m.DS_INFLUXDB,
|
||||||
Access: m.DS_ACCESS_DIRECT,
|
Access: m.DS_ACCESS_DIRECT,
|
||||||
Url: "http://test",
|
Url: "http://test",
|
||||||
@ -63,15 +64,19 @@ func TestDataAccess(t *testing.T) {
|
|||||||
|
|
||||||
Convey("Given a datasource", func() {
|
Convey("Given a datasource", func() {
|
||||||
|
|
||||||
AddDataSource(&m.AddDataSourceCommand{
|
err := AddDataSource(&m.AddDataSourceCommand{
|
||||||
OrgId: 10,
|
OrgId: 10,
|
||||||
|
Name: "nisse",
|
||||||
Type: m.DS_GRAPHITE,
|
Type: m.DS_GRAPHITE,
|
||||||
Access: m.DS_ACCESS_DIRECT,
|
Access: m.DS_ACCESS_DIRECT,
|
||||||
Url: "http://test",
|
Url: "http://test",
|
||||||
})
|
})
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
query := m.GetDataSourcesQuery{OrgId: 10}
|
query := m.GetDataSourcesQuery{OrgId: 10}
|
||||||
GetDataSources(&query)
|
err = GetDataSources(&query)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
ds := query.Result[0]
|
ds := query.Result[0]
|
||||||
|
|
||||||
Convey("Can delete datasource", func() {
|
Convey("Can delete datasource", func() {
|
||||||
|
@ -92,44 +92,45 @@ func (mg *Migrator) Start() error {
|
|||||||
|
|
||||||
mg.Logger.Debug("Executing", "sql", sql)
|
mg.Logger.Debug("Executing", "sql", sql)
|
||||||
|
|
||||||
if err := mg.exec(m); err != nil {
|
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||||
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
|
||||||
record.Error = err.Error()
|
if err := mg.exec(m, sess); err != nil {
|
||||||
mg.x.Insert(&record)
|
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
|
return err
|
||||||
} else {
|
|
||||||
record.Success = true
|
|
||||||
mg.x.Insert(&record)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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())
|
mg.Logger.Info("Executing migration", "id", m.Id())
|
||||||
|
|
||||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
condition := m.GetCondition()
|
||||||
|
if condition != nil {
|
||||||
condition := m.GetCondition()
|
sql, args := condition.Sql(mg.dialect)
|
||||||
if condition != nil {
|
results, err := sess.Query(sql, args...)
|
||||||
sql, args := condition.Sql(mg.dialect)
|
if err != nil || len(results) == 0 {
|
||||||
results, err := sess.Query(sql, args...)
|
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
|
||||||
if err != nil || len(results) == 0 {
|
return sess.Rollback()
|
||||||
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
|
|
||||||
return sess.Rollback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := sess.Exec(m.Sql(mg.dialect))
|
_, 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
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package tsdb
|
package tsdb
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
type Batch struct {
|
type Batch struct {
|
||||||
DataSourceId int64
|
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)
|
executor := getExecutorFor(bg.Queries[0].DataSource)
|
||||||
|
|
||||||
if executor == nil {
|
if executor == nil {
|
||||||
@ -32,13 +35,13 @@ func (bg *Batch) process(context *QueryContext) {
|
|||||||
for _, query := range bg.Queries {
|
for _, query := range bg.Queries {
|
||||||
result.QueryResults[query.RefId] = &QueryResult{Error: result.Error}
|
result.QueryResults[query.RefId] = &QueryResult{Error: result.Error}
|
||||||
}
|
}
|
||||||
context.ResultsChan <- result
|
queryContext.ResultsChan <- result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := executor.Execute(bg.Queries, context)
|
res := executor.Execute(ctx, bg.Queries, queryContext)
|
||||||
bg.Done = true
|
bg.Done = true
|
||||||
context.ResultsChan <- res
|
queryContext.ResultsChan <- res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bg *Batch) addQuery(query *Query) {
|
func (bg *Batch) addQuery(query *Query) {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package tsdb
|
package tsdb
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
type Executor interface {
|
type Executor interface {
|
||||||
Execute(queries QuerySlice, context *QueryContext) *BatchResult
|
Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult
|
||||||
}
|
}
|
||||||
|
|
||||||
var registry map[string]GetExecutorFn
|
var registry map[string]GetExecutorFn
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package tsdb
|
package tsdb
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
type FakeExecutor struct {
|
type FakeExecutor struct {
|
||||||
results map[string]*QueryResult
|
results map[string]*QueryResult
|
||||||
resultsFn map[string]ResultsFn
|
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)}
|
result := &BatchResult{QueryResults: make(map[string]*QueryResult)}
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
if results, has := e.results[query.RefId]; has {
|
if results, has := e.results[query.RefId]; has {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package graphite
|
package graphite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -11,6 +12,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
@ -26,7 +29,7 @@ func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
glog log.Logger
|
glog log.Logger
|
||||||
HttpClient http.Client
|
HttpClient *http.Client
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -37,13 +40,13 @@ func init() {
|
|||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpClient = http.Client{
|
HttpClient = &http.Client{
|
||||||
Timeout: time.Duration(15 * time.Second),
|
Timeout: time.Duration(15 * time.Second),
|
||||||
Transport: tr,
|
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{}
|
result := &tsdb.BatchResult{}
|
||||||
|
|
||||||
formData := url.Values{
|
formData := url.Values{
|
||||||
@ -66,7 +69,8 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
|||||||
result.Error = err
|
result.Error = err
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
res, err := HttpClient.Do(req)
|
|
||||||
|
res, err := ctxhttp.Do(ctx, HttpClient, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = err
|
result.Error = err
|
||||||
return result
|
return result
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package prometheus
|
package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -11,7 +12,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
"github.com/prometheus/client_golang/api/prometheus"
|
"github.com/prometheus/client_golang/api/prometheus"
|
||||||
pmodel "github.com/prometheus/common/model"
|
pmodel "github.com/prometheus/common/model"
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PrometheusExecutor struct {
|
type PrometheusExecutor struct {
|
||||||
@ -45,7 +45,7 @@ func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) {
|
|||||||
return prometheus.NewQueryAPI(client), nil
|
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{}
|
result := &tsdb.BatchResult{}
|
||||||
|
|
||||||
client, err := e.getClient()
|
client, err := e.getClient()
|
||||||
@ -64,7 +64,7 @@ func (e *PrometheusExecutor) Execute(queries tsdb.QuerySlice, queryContext *tsdb
|
|||||||
Step: query.Step,
|
Step: query.Step,
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := client.QueryRange(context.Background(), query.Expr, timeRange)
|
value, err := client.QueryRange(ctx, query.Expr, timeRange)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resultWithError(result, err)
|
return resultWithError(result, err)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package tsdb
|
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)
|
context := NewQueryContext(req.Queries, req.TimeRange)
|
||||||
|
|
||||||
batches, err := getBatches(req)
|
batches, err := getBatches(req)
|
||||||
@ -16,7 +18,7 @@ func HandleRequest(req *Request) (*Response, error) {
|
|||||||
if len(batch.Depends) == 0 {
|
if len(batch.Depends) == 0 {
|
||||||
currentlyExecuting += 1
|
currentlyExecuting += 1
|
||||||
batch.Started = true
|
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) {
|
if batch.allDependenciesAreIn(context) {
|
||||||
currentlyExecuting += 1
|
currentlyExecuting += 1
|
||||||
batch.Started = true
|
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
|
package testdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
)
|
)
|
||||||
@ -21,7 +23,7 @@ func init() {
|
|||||||
tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor)
|
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 := &tsdb.BatchResult{}
|
||||||
result.QueryResults = make(map[string]*tsdb.QueryResult)
|
result.QueryResults = make(map[string]*tsdb.QueryResult)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package tsdb
|
package tsdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ func TestMetricQuery(t *testing.T) {
|
|||||||
fakeExecutor := registerFakeExecutor()
|
fakeExecutor := registerFakeExecutor()
|
||||||
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
|
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
|
||||||
|
|
||||||
res, err := HandleRequest(req)
|
res, err := HandleRequest(context.TODO(), req)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should return query results", func() {
|
Convey("Should return query results", func() {
|
||||||
@ -83,7 +84,7 @@ func TestMetricQuery(t *testing.T) {
|
|||||||
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
|
fakeExecutor.Return("A", TimeSeriesSlice{&TimeSeries{Name: "argh"}})
|
||||||
fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}})
|
fakeExecutor.Return("B", TimeSeriesSlice{&TimeSeries{Name: "barg"}})
|
||||||
|
|
||||||
res, err := HandleRequest(req)
|
res, err := HandleRequest(context.TODO(), req)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should return query results", func() {
|
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)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should have been batched in two requests", func() {
|
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)
|
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)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("Should have been batched in two requests", func() {
|
Convey("Should have been batched in two requests", func() {
|
||||||
|
@ -136,7 +136,7 @@ function (_, $, coreModule) {
|
|||||||
|
|
||||||
$button.click(function() {
|
$button.click(function() {
|
||||||
options = null;
|
options = null;
|
||||||
$input.css('width', ($button.width() + 16) + 'px');
|
$input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
|
||||||
|
|
||||||
$button.hide();
|
$button.hide();
|
||||||
$input.show();
|
$input.show();
|
||||||
|
@ -236,7 +236,7 @@ function (angular, _, coreModule) {
|
|||||||
var inputEl = elem.find('input');
|
var inputEl = elem.find('input');
|
||||||
|
|
||||||
function openDropdown() {
|
function openDropdown() {
|
||||||
inputEl.css('width', Math.max(linkEl.width(), 30) + 'px');
|
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
|
||||||
|
|
||||||
inputEl.show();
|
inputEl.show();
|
||||||
linkEl.hide();
|
linkEl.hide();
|
||||||
|
@ -6,6 +6,7 @@ import {QueryPart} from 'app/core/components/query_part/query_part';
|
|||||||
import alertDef from './alert_def';
|
import alertDef from './alert_def';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export class AlertTabCtrl {
|
export class AlertTabCtrl {
|
||||||
panel: any;
|
panel: any;
|
||||||
@ -47,19 +48,18 @@ export class AlertTabCtrl {
|
|||||||
$onInit() {
|
$onInit() {
|
||||||
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
|
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
|
||||||
|
|
||||||
this.initModel();
|
// subscribe to graph threshold handle changes
|
||||||
this.validateModel();
|
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.$scope.$on("$destroy", () => {
|
||||||
|
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
|
||||||
this.panelCtrl.editingThresholds = false;
|
this.panelCtrl.editingThresholds = false;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
// subscribe to graph threshold handle changes
|
// build notification model
|
||||||
this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
|
|
||||||
|
|
||||||
// build notification model
|
|
||||||
this.notifications = [];
|
this.notifications = [];
|
||||||
this.alertNotifications = [];
|
this.alertNotifications = [];
|
||||||
this.alertHistory = [];
|
this.alertHistory = [];
|
||||||
@ -67,21 +67,8 @@ export class AlertTabCtrl {
|
|||||||
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
||||||
this.notifications = res;
|
this.notifications = res;
|
||||||
|
|
||||||
_.each(this.alert.notifications, item => {
|
this.initModel();
|
||||||
var model = _.find(this.notifications, {id: item.id});
|
this.validateModel();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,9 +129,8 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initModel() {
|
initModel() {
|
||||||
var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
|
var alert = this.alert = this.panel.alert;
|
||||||
|
if (!alert) {
|
||||||
if (!this.alert.enabled) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +154,22 @@ export class AlertTabCtrl {
|
|||||||
|
|
||||||
ThresholdMapper.alertToGraphThresholds(this.panel);
|
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.editingThresholds = true;
|
||||||
this.panelCtrl.render();
|
this.panelCtrl.render();
|
||||||
}
|
}
|
||||||
@ -192,7 +194,7 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validateModel() {
|
validateModel() {
|
||||||
if (!this.alert.enabled) {
|
if (!this.alert) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,14 +304,24 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
this.alert = this.panel.alert = {enabled: false};
|
appEvents.emit('confirm-modal', {
|
||||||
this.panel.thresholds = [];
|
title: 'Delete Alert',
|
||||||
this.conditionModels = [];
|
text: 'Are you sure you want to delete this alert rule?',
|
||||||
this.panelCtrl.render();
|
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() {
|
enable() {
|
||||||
this.alert.enabled = true;
|
this.panel.alert = {};
|
||||||
this.initModel();
|
this.initModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import config from 'app/core/config';
|
|||||||
|
|
||||||
export class AlertNotificationEditCtrl {
|
export class AlertNotificationEditCtrl {
|
||||||
model: any;
|
model: any;
|
||||||
showTest: boolean = false;
|
theForm: any;
|
||||||
testSeverity: string = "critical";
|
testSeverity: string = "critical";
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -36,6 +36,10 @@ export class AlertNotificationEditCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
|
if (!this.theForm.$valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.model.id) {
|
if (this.model.id) {
|
||||||
this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
|
this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
|
||||||
this.model = res;
|
this.model = res;
|
||||||
@ -53,11 +57,11 @@ export class AlertNotificationEditCtrl {
|
|||||||
this.model.settings = {};
|
this.model.settings = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTest() {
|
|
||||||
this.showTest = !this.showTest;
|
|
||||||
}
|
|
||||||
|
|
||||||
testNotification() {
|
testNotification() {
|
||||||
|
if (!this.theForm.$valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
name: this.model.name,
|
name: this.model.name,
|
||||||
type: this.model.type,
|
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">
|
<aside class="edit-sidemenu-aside">
|
||||||
<ul class="edit-sidemenu">
|
<ul class="edit-sidemenu">
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||||
@ -151,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="gf-form-button-row">
|
||||||
<button class="btn btn-inverse" ng-click="ctrl.enable()">
|
<button class="btn btn-inverse" ng-click="ctrl.enable()">
|
||||||
<i class="icon-gf icon-gf-alert"></i>
|
<i class="icon-gf icon-gf-alert"></i>
|
||||||
|
@ -6,81 +6,73 @@
|
|||||||
</navbar>
|
</navbar>
|
||||||
|
|
||||||
<div class="page-container" >
|
<div class="page-container" >
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Alert notification</h1>
|
<h1>Alert notification</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group">
|
<form name="ctrl.theForm">
|
||||||
<div class="gf-form">
|
<div class="gf-form-group">
|
||||||
<span class="gf-form-label width-12">Name</span>
|
<div class="gf-form">
|
||||||
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
<span class="gf-form-label width-12">Name</span>
|
||||||
</div>
|
<input type="text" required class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
||||||
<div class="gf-form">
|
</div>
|
||||||
<span class="gf-form-label width-12">Type</span>
|
<div class="gf-form">
|
||||||
<div class="gf-form-select-wrapper width-15">
|
<span class="gf-form-label width-12">Type</span>
|
||||||
<select class="gf-form-input"
|
<div class="gf-form-select-wrapper width-15">
|
||||||
ng-model="ctrl.model.type"
|
<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)">
|
||||||
ng-options="t for t in ['webhook', 'email', 'slack']"
|
</select>
|
||||||
ng-change="ctrl.typeChanged(notification, $index)">
|
</div>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
<div class="gf-form">
|
||||||
</div>
|
<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">
|
||||||
<div class="gf-form">
|
</gf-form-switch>
|
||||||
<gf-form-switch
|
</div>
|
||||||
class="gf-form"
|
</div>
|
||||||
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'">
|
<div class="gf-form-group" ng-if="ctrl.model.type === 'webhook'">
|
||||||
<h3 class="page-heading">Webhook settings</h3>
|
<h3 class="page-heading">Webhook settings</h3>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-6">Url</span>
|
<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>
|
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-6">Username</span>
|
<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>
|
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-6">Password</span>
|
<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>
|
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group" ng-show="ctrl.model.type === 'slack'">
|
<div class="gf-form-group" ng-if="ctrl.model.type === 'slack'">
|
||||||
<h3 class="page-heading">Slack settings</h3>
|
<h3 class="page-heading">Slack settings</h3>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-6">Url</span>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group section" ng-show="ctrl.model.type === 'email'">
|
<div class="gf-form-group section" ng-if="ctrl.model.type === 'email'">
|
||||||
<h3 class="page-heading">Email addresses</h3>
|
<h3 class="page-heading">Email addresses</h3>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<textarea rows="7" class="gf-form-input width-25" ng-model="ctrl.model.settings.addresses"></textarea>
|
<textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form width-6">
|
<div class="gf-form width-6">
|
||||||
<button ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
<button type="submit" ng-click="ctrl.save()" class="btn btn-success">Save</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form width-8">
|
<div class="gf-form width-20">
|
||||||
<button ng-click="ctrl.toggleTest()" class="btn btn-secondary">Test</button>
|
<div class="gf-form">
|
||||||
</div>
|
<button type="submit" ng-click="ctrl.testNotification()" class="btn btn-secondary">Send Test</button>
|
||||||
<div class="gf-form width-20" ng-show="ctrl.showTest">
|
</div>
|
||||||
<div class="gf-form" ng-show="ctrl.showTest">
|
</div>
|
||||||
<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
|
|||||||
|
|
||||||
export class AnnotationsSrv {
|
export class AnnotationsSrv {
|
||||||
globalAnnotationsPromise: any;
|
globalAnnotationsPromise: any;
|
||||||
|
alertStatesPromise: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope,
|
constructor(private $rootScope,
|
||||||
@ -22,14 +23,27 @@ export class AnnotationsSrv {
|
|||||||
|
|
||||||
clearCache() {
|
clearCache() {
|
||||||
this.globalAnnotationsPromise = null;
|
this.globalAnnotationsPromise = null;
|
||||||
|
this.alertStatesPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnnotations(options) {
|
getAnnotations(options) {
|
||||||
return this.$q.all([
|
return this.$q.all([
|
||||||
this.getGlobalAnnotations(options),
|
this.getGlobalAnnotations(options),
|
||||||
this.getPanelAnnotations(options)
|
this.getPanelAnnotations(options),
|
||||||
]).then(allResults => {
|
this.getAlertStates(options)
|
||||||
return _.flattenDeep(allResults);
|
]).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 => {
|
}).catch(err => {
|
||||||
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
|
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
|
||||||
});
|
});
|
||||||
@ -39,7 +53,7 @@ export class AnnotationsSrv {
|
|||||||
var panel = options.panel;
|
var panel = options.panel;
|
||||||
var dashboard = options.dashboard;
|
var dashboard = options.dashboard;
|
||||||
|
|
||||||
if (panel && panel.alert && panel.alert.enabled) {
|
if (panel && panel.alert) {
|
||||||
return this.backendSrv.get('/api/annotations', {
|
return this.backendSrv.get('/api/annotations', {
|
||||||
from: options.range.from.valueOf(),
|
from: options.range.from.valueOf(),
|
||||||
to: options.range.to.valueOf(),
|
to: options.range.to.valueOf(),
|
||||||
@ -54,6 +68,28 @@ export class AnnotationsSrv {
|
|||||||
return this.$q.when([]);
|
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) {
|
getGlobalAnnotations(options) {
|
||||||
var dashboard = options.dashboard;
|
var dashboard = options.dashboard;
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ export class DashNavCtrl {
|
|||||||
var confirmText = "";
|
var confirmText = "";
|
||||||
var text2 = $scope.dashboard.title;
|
var text2 = $scope.dashboard.title;
|
||||||
var alerts = $scope.dashboard.rows.reduce((memo, row) => {
|
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;
|
return memo;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
var intervalOverride = this.panel.interval;
|
var intervalOverride = this.panel.interval;
|
||||||
|
|
||||||
// if no panel interval check datasource
|
// 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;
|
intervalOverride = this.datasource.interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import $ from 'jquery';
|
|||||||
var module = angular.module('grafana.directives');
|
var module = angular.module('grafana.directives');
|
||||||
|
|
||||||
var panelTemplate = `
|
var panelTemplate = `
|
||||||
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
|
<div class="panel-container">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
|
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
|
||||||
<span data-placement="top" bs-tooltip="ctrl.error">
|
<span data-placement="top" bs-tooltip="ctrl.error">
|
||||||
@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
|
|||||||
link: function(scope, elem) {
|
link: function(scope, elem) {
|
||||||
var panelContainer = elem.find('.panel-container');
|
var panelContainer = elem.find('.panel-container');
|
||||||
var ctrl = scope.ctrl;
|
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() {
|
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
|
||||||
panelContainer.css({minHeight: ctrl.containerHeight});
|
panelContainer.css({minHeight: ctrl.containerHeight});
|
||||||
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
|
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
|
||||||
|
@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
|
|||||||
.directive('panelMenu', function($compile, linkSrv) {
|
.directive('panelMenu', function($compile, linkSrv) {
|
||||||
var linkTemplate =
|
var linkTemplate =
|
||||||
'<span class="panel-title drag-handle pointer">' +
|
'<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-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-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>' +
|
'<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) {
|
if (!this.editForm.$valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@
|
|||||||
<span class="gf-form-label width-6">Span</span>
|
<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>
|
<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>
|
||||||
<div class="gf-form max-width-26">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-8">Height</span>
|
<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>
|
<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>
|
</div>
|
||||||
|
<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<i class="fa fa-caret-down"></i>
|
<i class="fa fa-caret-down"></i>
|
||||||
</a>
|
</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-value-dropdown" ng-if="vm.dropdownVisible" ng-class="{'multi': vm.variable.multi, 'single': !vm.variable.multi}">
|
||||||
<div class="variable-options-wrapper">
|
<div class="variable-options-wrapper">
|
||||||
|
@ -3,9 +3,10 @@ define([
|
|||||||
'lodash',
|
'lodash',
|
||||||
'moment',
|
'moment',
|
||||||
'app/core/utils/datemath',
|
'app/core/utils/datemath',
|
||||||
|
'app/core/utils/kbn',
|
||||||
'./annotation_query',
|
'./annotation_query',
|
||||||
],
|
],
|
||||||
function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
|
function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -36,12 +37,9 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
|
|||||||
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
|
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
|
||||||
query.statistics = target.statistics;
|
query.statistics = target.statistics;
|
||||||
|
|
||||||
var range = end - start;
|
var period = this._getPeriod(target, query, options, start, end);
|
||||||
query.period = parseInt(target.period, 10) || (query.namespace === 'AWS/EC2' ? 300 : 60);
|
target.period = period;
|
||||||
if (range / query.period >= 1440) {
|
query.period = period;
|
||||||
query.period = Math.ceil(range / 1440 / 60) * 60;
|
|
||||||
}
|
|
||||||
target.period = query.period;
|
|
||||||
|
|
||||||
queries.push(query);
|
queries.push(query);
|
||||||
}.bind(this));
|
}.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) {
|
this.performTimeSeriesQuery = function(query, start, end) {
|
||||||
return this.awsRequest({
|
return this.awsRequest({
|
||||||
region: query.region,
|
region: query.region,
|
||||||
|
@ -82,6 +82,35 @@ describe('CloudWatchDatasource', function() {
|
|||||||
ctx.$rootScope.$apply();
|
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) {
|
it('should return series list', function(done) {
|
||||||
ctx.ds.query(query).then(function(result) {
|
ctx.ds.query(query).then(function(result) {
|
||||||
expect(result.data[0].target).to.be('CPUUtilization_Average');
|
expect(result.data[0].target).to.be('CPUUtilization_Average');
|
||||||
|
@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
annotations = data.annotations || annotations;
|
annotations = ctrl.annotations;
|
||||||
render_panel();
|
render_panel();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
hiddenSeries: any = {};
|
hiddenSeries: any = {};
|
||||||
seriesList: any = [];
|
seriesList: any = [];
|
||||||
dataList: any = [];
|
dataList: any = [];
|
||||||
|
annotations: any = [];
|
||||||
|
alertState: any;
|
||||||
|
|
||||||
annotationsPromise: any;
|
annotationsPromise: any;
|
||||||
datapointsCount: number;
|
datapointsCount: number;
|
||||||
datapointsOutside: boolean;
|
datapointsOutside: boolean;
|
||||||
@ -167,11 +170,11 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
onDataError(err) {
|
onDataError(err) {
|
||||||
this.seriesList = [];
|
this.seriesList = [];
|
||||||
|
this.annotations = [];
|
||||||
this.render([]);
|
this.render([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDataReceived(dataList) {
|
onDataReceived(dataList) {
|
||||||
|
|
||||||
this.dataList = dataList;
|
this.dataList = dataList;
|
||||||
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
|
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.loading = false;
|
||||||
this.seriesList.annotations = annotations;
|
this.alertState = result.alertState;
|
||||||
|
this.annotations = result.annotations;
|
||||||
this.render(this.seriesList);
|
this.render(this.seriesList);
|
||||||
}, () => {
|
}, () => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
@ -13,7 +13,7 @@ export class ThresholdFormCtrl {
|
|||||||
constructor($scope) {
|
constructor($scope) {
|
||||||
this.panel = this.panelCtrl.panel;
|
this.panel = this.panelCtrl.panel;
|
||||||
|
|
||||||
if (this.panel.alert && this.panel.alert.enabled) {
|
if (this.panel.alert) {
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,10 +34,7 @@
|
|||||||
ng-change="editor.render()"
|
ng-change="editor.render()"
|
||||||
ng-model-onblur>
|
ng-model-onblur>
|
||||||
</div>
|
</div>
|
||||||
<gf-form-switch class="gf-form" label-class="width-4"
|
<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
|
||||||
label="Scroll"
|
|
||||||
checked="editor.panel.scroll"
|
|
||||||
change="editor.render()"></gf-form-switch>
|
|
||||||
<div class="gf-form max-width-17">
|
<div class="gf-form max-width-17">
|
||||||
<label class="gf-form-label width-6">Font size</label>
|
<label class="gf-form-label width-6">Font size</label>
|
||||||
<div class="gf-form-select-wrapper max-width-15">
|
<div class="gf-form-select-wrapper max-width-15">
|
||||||
|
@ -38,3 +38,46 @@
|
|||||||
top: 2px;
|
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