Merge branch 'master' into graph-panel-non-timeseries

This commit is contained in:
Torkel Ödegaard
2016-09-05 09:58:07 +02:00
31 changed files with 378 additions and 106 deletions
+2
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -318,7 +318,7 @@ check_for_updates = true
# \______(_______;;;)__;;;)
[alerting]
enabled = false
;enabled = false
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /api/metrics
+1 -1
View File
@@ -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
+68
View File
@@ -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
View File
@@ -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() {
+1 -1
View File
@@ -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 {
+11
View File
@@ -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"`
}
+1 -1
View File
@@ -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
View File
@@ -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")
+10 -4
View File
@@ -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,
},
},
},
+35 -25
View File
@@ -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) {
-1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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,
+15 -2
View File
@@ -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,
+20 -11
View File
@@ -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)
}
}
+9
View File
@@ -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
+37
View File
@@ -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
}
+53 -11
View File
@@ -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
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -5,7 +5,7 @@
}
.edit-sidemenu-aside {
width: 14rem;
width: 16rem;
}
.edit-sidemenu {
-5
View File
@@ -1,5 +0,0 @@
#/bin/bash
#ln -s -f .hooks/* .git/hooks/
cd .git/hooks/
cp --symbolic-link -f ../../.hooks/* .