Merge branch 'master' into graph-panel-non-timeseries
This commit is contained in:
@@ -9,12 +9,14 @@
|
||||
* **Navigation**: Add search to org swithcer, closes [#2609](https://github.com/grafana/grafana/issues/2609)
|
||||
* **Database**: Allow database config using one propertie, closes [#5456](https://github.com/grafana/grafana/pull/5456)
|
||||
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
|
||||
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
|
||||
|
||||
# 3.1.2 (unreleased)
|
||||
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
|
||||
* **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
|
||||
* **Internal Metrics**: Fixed issue with dots in instance_name when sending internal metrics to Graphite, fixes [#5739](https://github.com/grafana/grafana/issues/5739)
|
||||
* **Grafana-CLI**: Add default plugin path for MAC OS, fixes [#5806](https://github.com/grafana/grafana/issues/5806)
|
||||
* **Grafana-CLI**: Improve error message for upgrade-all command, fixes [#5885](https://github.com/grafana/grafana/issues/5885)
|
||||
|
||||
# 3.1.1 (2016-08-01)
|
||||
* **IFrame embedding**: Fixed issue of using full iframe height, fixes [#5605](https://github.com/grafana/grafana/issues/5606)
|
||||
|
||||
@@ -78,7 +78,7 @@ the latest master builds [here](http://grafana.org/download/builds)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Go 1.5
|
||||
- Go 1.6
|
||||
- NodeJS v4+
|
||||
- [Godep](https://github.com/tools/godep)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ var (
|
||||
binaries []string = []string{"grafana-server", "grafana-cli"}
|
||||
)
|
||||
|
||||
const minGoVersion = 1.3
|
||||
const minGoVersion = 1.6
|
||||
|
||||
func main() {
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
+1
-1
@@ -31,4 +31,4 @@ deployment:
|
||||
branch: master
|
||||
owner: grafana
|
||||
commands:
|
||||
- ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
- ./trigger_grafana_packer.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}
|
||||
+1
-1
@@ -318,7 +318,7 @@ check_for_updates = true
|
||||
# \______(_______;;;)__;;;)
|
||||
|
||||
[alerting]
|
||||
enabled = false
|
||||
;enabled = false
|
||||
|
||||
#################################### Internal Grafana Metrics ##########################
|
||||
# Metrics available at HTTP API Url /api/metrics
|
||||
|
||||
@@ -77,7 +77,7 @@ Example dimension queries which will return list of resources for individual AWS
|
||||
|
||||
Service | Query
|
||||
------- | -----
|
||||
EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
|
||||
ELB | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
|
||||
ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
|
||||
RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
|
||||
RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
|
||||
|
||||
@@ -88,6 +88,8 @@ Another way is put a webserver like Nginx or Apache in front of Grafana and have
|
||||
|
||||
`http` or `https`
|
||||
|
||||
> **Note** Grafana versions earlier than 3.0 are vulnerable to [POODLE](https://en.wikipedia.org/wiki/POODLE). So we strongly recommend to upgrade to 3.x or use a reverse proxy for ssl termination.
|
||||
|
||||
### domain
|
||||
|
||||
This setting is only used in as a part of the `root_url` setting (see below). Important if you
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
func ValidateOrgAlert(c *middleware.Context) {
|
||||
@@ -212,3 +213,70 @@ func DeleteAlertNotification(c *middleware.Context) Response {
|
||||
|
||||
return ApiSuccess("Notification deleted")
|
||||
}
|
||||
|
||||
func GetAlertHistory(c *middleware.Context) Response {
|
||||
alertId, err := getAlertIdForRequest(c)
|
||||
if err != nil {
|
||||
return ApiError(400, "Invalid request", err)
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
AlertId: alertId,
|
||||
Type: annotations.AlertType,
|
||||
OrgId: c.OrgId,
|
||||
Limit: c.QueryInt64("limit"),
|
||||
}
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
items, err := repo.Find(query)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get history for alert", err)
|
||||
}
|
||||
|
||||
var result []dtos.AlertHistory
|
||||
for _, item := range items {
|
||||
result = append(result, dtos.AlertHistory{
|
||||
AlertId: item.AlertId,
|
||||
Timestamp: item.Timestamp,
|
||||
Data: item.Data,
|
||||
NewState: item.NewState,
|
||||
Text: item.Text,
|
||||
Metric: item.Metric,
|
||||
Title: item.Title,
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
}
|
||||
|
||||
func getAlertIdForRequest(c *middleware.Context) (int64, error) {
|
||||
alertId := c.QueryInt64("alertId")
|
||||
panelId := c.QueryInt64("panelId")
|
||||
dashboardId := c.QueryInt64("dashboardId")
|
||||
|
||||
if alertId == 0 && dashboardId == 0 && panelId == 0 {
|
||||
return 0, fmt.Errorf("Missing alertId or dashboardId and panelId")
|
||||
}
|
||||
|
||||
if alertId == 0 {
|
||||
//fetch alertId
|
||||
query := models.GetAlertsQuery{
|
||||
OrgId: c.OrgId,
|
||||
DashboardId: dashboardId,
|
||||
PanelId: panelId,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(query.Result) != 1 {
|
||||
return 0, fmt.Errorf("PanelId is not unique on dashboard")
|
||||
}
|
||||
|
||||
alertId = query.Result[0].Id
|
||||
}
|
||||
|
||||
return alertId, nil
|
||||
}
|
||||
|
||||
+5
-1
@@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) {
|
||||
quota := middleware.Quota
|
||||
bind := binding.Bind
|
||||
|
||||
// automatically set HEAD for every GET
|
||||
r.SetAutoHead(true)
|
||||
|
||||
// not logged in views
|
||||
r.Get("/", reqSignedIn, Index)
|
||||
r.Get("/logout", Logout)
|
||||
@@ -247,11 +250,12 @@ func Register(r *macaron.Macaron) {
|
||||
|
||||
r.Group("/alerts", func() {
|
||||
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
|
||||
//r.Get("/:alertId/states", wrap(GetAlertStates))
|
||||
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
|
||||
r.Get("/", wrap(GetAlerts))
|
||||
})
|
||||
|
||||
r.Get("/alert-history", wrap(GetAlertHistory))
|
||||
|
||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||
|
||||
r.Group("/alert-notifications", func() {
|
||||
|
||||
@@ -22,7 +22,6 @@ func GetDataSources(c *middleware.Context) {
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
for _, ds := range query.Result {
|
||||
|
||||
dsItem := dtos.DataSource{
|
||||
Id: ds.Id,
|
||||
OrgId: ds.OrgId,
|
||||
@@ -35,6 +34,7 @@ func GetDataSources(c *middleware.Context) {
|
||||
User: ds.User,
|
||||
BasicAuth: ds.BasicAuth,
|
||||
IsDefault: ds.IsDefault,
|
||||
JsonData: ds.JsonData,
|
||||
}
|
||||
|
||||
if plugin, exists := plugins.DataSources[ds.Type]; exists {
|
||||
|
||||
@@ -52,3 +52,14 @@ type EvalMatch struct {
|
||||
Metric string `json:"metric"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type AlertHistory struct {
|
||||
AlertId int64 `json:"alertId"`
|
||||
NewState string `json:"newState"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Metric string `json:"metric"`
|
||||
|
||||
Data *simplejson.Json `json:"data"`
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
|
||||
cmd := &contextCommandLine{context}
|
||||
if err := command(cmd); err != nil {
|
||||
logger.Errorf("\n%s: ", color.RedString("Error"))
|
||||
logger.Errorf("%s\n\n", err)
|
||||
logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
|
||||
|
||||
cmd.ShowHelp()
|
||||
os.Exit(1)
|
||||
|
||||
@@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error {
|
||||
for _, p := range pluginsToUpgrade {
|
||||
logger.Infof("Updating %v \n", p.Id)
|
||||
|
||||
s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
||||
InstallPlugin(p.Id, "", c)
|
||||
var err error
|
||||
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = InstallPlugin(p.Id, "", c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+37
-32
@@ -9,34 +9,36 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
M_Instance_Start Counter
|
||||
M_Page_Status_200 Counter
|
||||
M_Page_Status_500 Counter
|
||||
M_Page_Status_404 Counter
|
||||
M_Api_Status_500 Counter
|
||||
M_Api_Status_404 Counter
|
||||
M_Api_User_SignUpStarted Counter
|
||||
M_Api_User_SignUpCompleted Counter
|
||||
M_Api_User_SignUpInvite Counter
|
||||
M_Api_Dashboard_Save Timer
|
||||
M_Api_Dashboard_Get Timer
|
||||
M_Api_Dashboard_Search Timer
|
||||
M_Api_Admin_User_Create Counter
|
||||
M_Api_Login_Post Counter
|
||||
M_Api_Login_OAuth Counter
|
||||
M_Api_Org_Create Counter
|
||||
M_Api_Dashboard_Snapshot_Create Counter
|
||||
M_Api_Dashboard_Snapshot_External Counter
|
||||
M_Api_Dashboard_Snapshot_Get Counter
|
||||
M_Models_Dashboard_Insert Counter
|
||||
M_Alerting_Result_Critical Counter
|
||||
M_Alerting_Result_Warning Counter
|
||||
M_Alerting_Result_Info Counter
|
||||
M_Alerting_Result_Ok Counter
|
||||
M_Alerting_Active_Alerts Counter
|
||||
M_Alerting_Notification_Sent_Slack Counter
|
||||
M_Alerting_Notification_Sent_Email Counter
|
||||
M_Alerting_Notification_Sent_Webhook Counter
|
||||
M_Instance_Start Counter
|
||||
M_Page_Status_200 Counter
|
||||
M_Page_Status_500 Counter
|
||||
M_Page_Status_404 Counter
|
||||
M_Api_Status_500 Counter
|
||||
M_Api_Status_404 Counter
|
||||
M_Api_User_SignUpStarted Counter
|
||||
M_Api_User_SignUpCompleted Counter
|
||||
M_Api_User_SignUpInvite Counter
|
||||
M_Api_Dashboard_Save Timer
|
||||
M_Api_Dashboard_Get Timer
|
||||
M_Api_Dashboard_Search Timer
|
||||
M_Api_Admin_User_Create Counter
|
||||
M_Api_Login_Post Counter
|
||||
M_Api_Login_OAuth Counter
|
||||
M_Api_Org_Create Counter
|
||||
M_Api_Dashboard_Snapshot_Create Counter
|
||||
M_Api_Dashboard_Snapshot_External Counter
|
||||
M_Api_Dashboard_Snapshot_Get Counter
|
||||
M_Models_Dashboard_Insert Counter
|
||||
M_Alerting_Result_State_Critical Counter
|
||||
M_Alerting_Result_State_Warning Counter
|
||||
M_Alerting_Result_State_Ok Counter
|
||||
M_Alerting_Result_State_Paused Counter
|
||||
M_Alerting_Result_State_Pending Counter
|
||||
M_Alerting_Result_State_ExecutionError Counter
|
||||
M_Alerting_Active_Alerts Counter
|
||||
M_Alerting_Notification_Sent_Slack Counter
|
||||
M_Alerting_Notification_Sent_Email Counter
|
||||
M_Alerting_Notification_Sent_Webhook Counter
|
||||
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer Timer
|
||||
@@ -75,10 +77,13 @@ func initMetricVars(settings *MetricSettings) {
|
||||
|
||||
M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
|
||||
|
||||
M_Alerting_Result_Critical = RegCounter("alerting.result", "severity", "critical")
|
||||
M_Alerting_Result_Warning = RegCounter("alerting.result", "severity", "warning")
|
||||
M_Alerting_Result_Info = RegCounter("alerting.result", "severity", "info")
|
||||
M_Alerting_Result_Ok = RegCounter("alerting.result", "severity", "ok")
|
||||
M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical")
|
||||
M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
|
||||
M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
|
||||
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
|
||||
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
|
||||
M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error")
|
||||
|
||||
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
|
||||
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
|
||||
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
|
||||
|
||||
@@ -106,10 +106,16 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource) *tsdb.
|
||||
RefId: "A",
|
||||
Query: c.Query.Model.Get("target").MustString(),
|
||||
DataSource: &tsdb.DataSourceInfo{
|
||||
Id: datasource.Id,
|
||||
Name: datasource.Name,
|
||||
PluginId: datasource.Type,
|
||||
Url: datasource.Url,
|
||||
Id: datasource.Id,
|
||||
Name: datasource.Name,
|
||||
PluginId: datasource.Type,
|
||||
Url: datasource.Url,
|
||||
User: datasource.User,
|
||||
Password: datasource.Password,
|
||||
Database: datasource.Database,
|
||||
BasicAuth: datasource.BasicAuth,
|
||||
BasicAuthUser: datasource.BasicAuthUser,
|
||||
BasicAuthPassword: datasource.BasicAuthPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,36 +28,46 @@ type EvalContext struct {
|
||||
ImageOnDiskPath string
|
||||
}
|
||||
|
||||
type StateDescription struct {
|
||||
Color string
|
||||
Text string
|
||||
Data string
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetStateModel() *StateDescription {
|
||||
if c.Error != nil {
|
||||
return &StateDescription{
|
||||
Color: "#D63232",
|
||||
Text: "EXECUTION ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
if !c.Firing {
|
||||
return &StateDescription{
|
||||
Color: "#36a64f",
|
||||
Text: "OK",
|
||||
}
|
||||
}
|
||||
|
||||
if c.Rule.Severity == m.AlertSeverityWarning {
|
||||
return &StateDescription{
|
||||
Color: "#fd821b",
|
||||
Text: "WARNING",
|
||||
}
|
||||
} else {
|
||||
return &StateDescription{
|
||||
Color: "#D63232",
|
||||
Text: "CRITICAL",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *EvalContext) GetDurationMs() float64 {
|
||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetColor() string {
|
||||
if !c.Firing {
|
||||
return "#36a64f"
|
||||
}
|
||||
|
||||
if c.Rule.Severity == m.AlertSeverityWarning {
|
||||
return "#fd821b"
|
||||
} else {
|
||||
return "#D63232"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetStateText() string {
|
||||
if !c.Firing {
|
||||
return "OK"
|
||||
}
|
||||
|
||||
if c.Rule.Severity == m.AlertSeverityWarning {
|
||||
return "WARNING"
|
||||
} else {
|
||||
return "CRITICAL"
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetNotificationTitle() string {
|
||||
return "[" + c.GetStateText() + "] " + c.Rule.Name
|
||||
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
|
||||
}
|
||||
|
||||
func (c *EvalContext) getDashboardSlug() (string, error) {
|
||||
|
||||
@@ -48,7 +48,6 @@ func (n *RootNotifier) Notify(context *EvalContext) {
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
|
||||
|
||||
go notifier.Notify(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
|
||||
"State": context.Rule.State,
|
||||
"Name": context.Rule.Name,
|
||||
"Severity": context.Rule.Severity,
|
||||
"SeverityColor": context.GetColor(),
|
||||
"SeverityColor": context.GetStateModel().Color,
|
||||
"Message": context.Rule.Message,
|
||||
"RuleUrl": ruleUrl,
|
||||
"ImageLink": context.ImagePublicUrl,
|
||||
|
||||
@@ -61,13 +61,26 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
|
||||
}
|
||||
}
|
||||
|
||||
if context.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": context.Error.Error(),
|
||||
"short": false,
|
||||
})
|
||||
}
|
||||
|
||||
message := ""
|
||||
if context.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
message = context.Rule.Message
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"attachments": []map[string]interface{}{
|
||||
{
|
||||
"color": context.GetColor(),
|
||||
"color": context.GetStateModel().Color,
|
||||
"title": context.GetNotificationTitle(),
|
||||
"title_link": ruleUrl,
|
||||
"text": context.Rule.Message,
|
||||
"text": message,
|
||||
"fields": fields,
|
||||
"image_url": context.ImagePublicUrl,
|
||||
"footer": "Grafana v" + setting.BuildVersion,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -30,17 +31,20 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
||||
oldState := ctx.Rule.State
|
||||
|
||||
exeuctionError := ""
|
||||
annotationData := simplejson.New()
|
||||
if ctx.Error != nil {
|
||||
handler.log.Error("Alert Rule Result Error", "ruleId", ctx.Rule.Id, "error", ctx.Error)
|
||||
ctx.Rule.State = m.AlertStateExeuctionError
|
||||
exeuctionError = ctx.Error.Error()
|
||||
annotationData.Set("errorMessage", exeuctionError)
|
||||
} else if ctx.Firing {
|
||||
ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
|
||||
annotationData = simplejson.NewFromAny(ctx.EvalMatches)
|
||||
} else {
|
||||
ctx.Rule.State = m.AlertStateOK
|
||||
}
|
||||
|
||||
countSeverity(ctx.Rule.Severity)
|
||||
countStateResult(ctx.Rule.State)
|
||||
if ctx.Rule.State != oldState {
|
||||
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
|
||||
|
||||
@@ -61,10 +65,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
||||
Type: annotations.AlertType,
|
||||
AlertId: ctx.Rule.Id,
|
||||
Title: ctx.Rule.Name,
|
||||
Text: ctx.GetStateText(),
|
||||
Text: ctx.GetStateModel().Text,
|
||||
NewState: string(ctx.Rule.State),
|
||||
PrevState: string(oldState),
|
||||
Timestamp: time.Now(),
|
||||
Data: annotationData,
|
||||
}
|
||||
|
||||
annotationRepo := annotations.GetRepository()
|
||||
@@ -76,15 +81,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
||||
}
|
||||
}
|
||||
|
||||
func countSeverity(state m.AlertSeverityType) {
|
||||
func countStateResult(state m.AlertStateType) {
|
||||
switch state {
|
||||
case m.AlertSeverityOK:
|
||||
metrics.M_Alerting_Result_Ok.Inc(1)
|
||||
case m.AlertSeverityInfo:
|
||||
metrics.M_Alerting_Result_Info.Inc(1)
|
||||
case m.AlertSeverityWarning:
|
||||
metrics.M_Alerting_Result_Warning.Inc(1)
|
||||
case m.AlertSeverityCritical:
|
||||
metrics.M_Alerting_Result_Critical.Inc(1)
|
||||
case m.AlertStateCritical:
|
||||
metrics.M_Alerting_Result_State_Critical.Inc(1)
|
||||
case m.AlertStateWarning:
|
||||
metrics.M_Alerting_Result_State_Warning.Inc(1)
|
||||
case m.AlertStateOK:
|
||||
metrics.M_Alerting_Result_State_Ok.Inc(1)
|
||||
case m.AlertStatePaused:
|
||||
metrics.M_Alerting_Result_State_Paused.Inc(1)
|
||||
case m.AlertStatePending:
|
||||
metrics.M_Alerting_Result_State_Pending.Inc(1)
|
||||
case m.AlertStateExeuctionError:
|
||||
metrics.M_Alerting_Result_State_ExecutionError.Inc(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ import (
|
||||
|
||||
type Repository interface {
|
||||
Save(item *Item) error
|
||||
Find(query *ItemQuery) ([]*Item, error)
|
||||
}
|
||||
|
||||
type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
Type ItemType `json:"type"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
|
||||
Limit int64 `json:"alertId"`
|
||||
}
|
||||
|
||||
var repositoryInstance Repository
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
@@ -17,5 +20,39 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`SELECT *
|
||||
from annotation
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.Type != "" {
|
||||
sql.WriteString(` AND type = ?`)
|
||||
params = append(params, string(query.Type))
|
||||
}
|
||||
|
||||
if query.Limit == 0 {
|
||||
query.Limit = 10
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
|
||||
|
||||
items := make([]*annotations.Item, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package graphite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
var (
|
||||
HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||
)
|
||||
|
||||
type GraphiteExecutor struct {
|
||||
*tsdb.DataSourceInfo
|
||||
}
|
||||
@@ -30,7 +36,7 @@ func init() {
|
||||
func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
|
||||
result := &tsdb.BatchResult{}
|
||||
|
||||
params := url.Values{
|
||||
formData := url.Values{
|
||||
"from": []string{"-" + formatTimeRange(context.TimeRange.From)},
|
||||
"until": []string{formatTimeRange(context.TimeRange.To)},
|
||||
"format": []string{"json"},
|
||||
@@ -38,28 +44,24 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
params["target"] = []string{query.Query}
|
||||
glog.Debug("Graphite request", "query", query.Query)
|
||||
formData["target"] = []string{query.Query}
|
||||
}
|
||||
|
||||
client := http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||
res, err := client.PostForm(e.Url+"/render?", params)
|
||||
glog.Info("Graphite request body", "formdata", formData.Encode())
|
||||
|
||||
req, err := e.createRequest(formData)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
res, err := HttpClient.Do(req)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
|
||||
var data []TargetResponseDTO
|
||||
err = json.Unmarshal(body, &data)
|
||||
data, err := e.parseResponse(res)
|
||||
if err != nil {
|
||||
glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body))
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
@@ -77,6 +79,46 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
defer res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
glog.Info("Request is Unauthorized", "status", res.Status, "body", string(body))
|
||||
return nil, fmt.Errorf("Request is Unauthorized status: %v body: %s", res.Status, string(body))
|
||||
}
|
||||
|
||||
var data []TargetResponseDTO
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) {
|
||||
u, _ := url.Parse(e.Url)
|
||||
u.Path = path.Join(u.Path, "render")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
glog.Info("Failed to create request", "error", err)
|
||||
return nil, fmt.Errorf("Failed to create request. error: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if e.BasicAuth {
|
||||
req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
|
||||
}
|
||||
|
||||
return req, err
|
||||
}
|
||||
|
||||
func formatTimeRange(input string) string {
|
||||
if input == "now" {
|
||||
return input
|
||||
|
||||
@@ -113,7 +113,7 @@ function (_, $, coreModule) {
|
||||
if (str[0] === '/') { str = str.substring(1); }
|
||||
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
|
||||
try {
|
||||
return item.toLowerCase().match(str);
|
||||
return item.toLowerCase().match(str.toLowerCase());
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {ThresholdMapper} from './threshold_mapper';
|
||||
import {QueryPart} from 'app/core/components/query_part/query_part';
|
||||
import alertDef from './alert_def';
|
||||
import config from 'app/core/config';
|
||||
import moment from 'moment';
|
||||
|
||||
export class AlertTabCtrl {
|
||||
panel: any;
|
||||
@@ -22,6 +23,7 @@ export class AlertTabCtrl {
|
||||
alertNotifications;
|
||||
error: string;
|
||||
appSubUrl: string;
|
||||
alertHistory: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope,
|
||||
@@ -60,6 +62,7 @@ export class AlertTabCtrl {
|
||||
// build notification model
|
||||
this.notifications = [];
|
||||
this.alertNotifications = [];
|
||||
this.alertHistory = [];
|
||||
|
||||
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
||||
this.notifications = res;
|
||||
@@ -71,6 +74,19 @@ export class AlertTabCtrl {
|
||||
this.alertNotifications.push(model);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => {
|
||||
this.alertHistory = _.map(res, ah => {
|
||||
ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
|
||||
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
|
||||
|
||||
ah.metrics = _.map(ah.data, ev=> {
|
||||
return ev.Metric + "=" + ev.Value;
|
||||
}).join(', ');
|
||||
|
||||
return ah;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,6 +208,8 @@ export class AlertTabCtrl {
|
||||
this.error = 'Currently the alerting backend only supports Graphite queries';
|
||||
} else if (this.templateSrv.variableExists(foundTarget.target)) {
|
||||
this.error = 'Template variables are not supported in alert queries';
|
||||
} else {
|
||||
this.error = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ export class AlertNotificationEditCtrl {
|
||||
});
|
||||
} else {
|
||||
this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
|
||||
this.$location.path('alerting/notification/' + res.id + '/edit');
|
||||
this.$scope.appEvent('alert-success', ['Notification created', '']);
|
||||
this.$location.path('alerting/notifications');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,31 @@
|
||||
<textarea class="gf-form-input width-20" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<h5 class="section-heading">Alert history</h5>
|
||||
<section class="card-section card-list-layout-list">
|
||||
<ol class="card-list" >
|
||||
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-sub-name">
|
||||
<span class="alert-list-item-state {{ah.stateModel.stateClass}}">
|
||||
<i class="{{ah.stateModel.iconClass}}"></i>
|
||||
{{ah.stateModel.text}}
|
||||
</span> {{ah.metrics}}
|
||||
</div>
|
||||
<div class="card-item-sub-name">
|
||||
{{ah.time}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
|
||||
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-model="ctrl.jsonText"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
||||
<i class="fa fa-paste"></i>
|
||||
|
||||
@@ -254,6 +254,15 @@ register({
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
register({
|
||||
type: 'elapsed',
|
||||
addStrategy: addTransformationStrategy,
|
||||
category: categories.Transformations,
|
||||
params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}],
|
||||
defaultParams: ['10s'],
|
||||
renderer: functionRenderer,
|
||||
});
|
||||
|
||||
// Selectors
|
||||
register({
|
||||
type: 'bottom',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.edit-sidemenu-aside {
|
||||
width: 14rem;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
.edit-sidemenu {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#/bin/bash
|
||||
|
||||
#ln -s -f .hooks/* .git/hooks/
|
||||
cd .git/hooks/
|
||||
cp --symbolic-link -f ../../.hooks/* .
|
||||
Reference in New Issue
Block a user