Merge branch 'master' of github.com:grafana/grafana
This commit is contained in:
@@ -15,11 +15,13 @@
|
||||
* **LDAP**: Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||
* **LDAP**: Now works with Auth Proxy, role and organisation mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf)
|
||||
* **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
|
||||
* **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
|
||||
|
||||
### Bugfixes
|
||||
* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679)
|
||||
* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443)
|
||||
* **Logging**: Fixed logging level confing issue [#6978](https://github.com/grafana/grafana/issues/6978)
|
||||
* **Notifications**: Remove html escaping the email subject. [#6905](https://github.com/grafana/grafana/issues/6905)
|
||||
|
||||
# 4.0.2 (2016-12-08)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ Name | Description
|
||||
------- | --------
|
||||
`regions()` | Returns a list of regions AWS provides their service.
|
||||
`namespaces()` | Returns a list of namespaces CloudWatch support.
|
||||
`metrics(namespace)` | Returns a list of metrics in the namespace.
|
||||
`metrics(namespace, [region])` | Returns a list of metrics in the namespace. (specify region for custom metrics)
|
||||
`dimension_keys(namespace)` | Returns a list of dimension keys in the namespace.
|
||||
`dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
|
||||
`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`.
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@@ -194,14 +195,12 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.GetMetricStatisticsInput{
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Dimensions: reqParam.Parameters.Dimensions,
|
||||
Statistics: reqParam.Parameters.Statistics,
|
||||
ExtendedStatistics: reqParam.Parameters.ExtendedStatistics,
|
||||
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
|
||||
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
|
||||
Period: aws.Int64(reqParam.Parameters.Period),
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Dimensions: reqParam.Parameters.Dimensions,
|
||||
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
|
||||
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
|
||||
Period: aws.Int64(reqParam.Parameters.Period),
|
||||
}
|
||||
if len(reqParam.Parameters.Statistics) != 0 {
|
||||
params.Statistics = reqParam.Parameters.Statistics
|
||||
@@ -215,6 +214,7 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc(1)
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
@@ -241,6 +241,7 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
for _, metric := range metrics {
|
||||
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -261,6 +262,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err := svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc(1)
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
for _, metric := range metrics {
|
||||
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
|
||||
|
||||
@@ -47,6 +47,8 @@ var (
|
||||
M_Alerting_Notification_Sent_PagerDuty Counter
|
||||
M_Alerting_Notification_Sent_Victorops Counter
|
||||
M_Alerting_Notification_Sent_OpsGenie Counter
|
||||
M_Aws_CloudWatch_GetMetricStatistics Counter
|
||||
M_Aws_CloudWatch_ListMetrics Counter
|
||||
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer Timer
|
||||
@@ -113,6 +115,9 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
|
||||
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
|
||||
|
||||
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
|
||||
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
|
||||
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
|
||||
M_Alerting_Execution_Time = RegTimer("alerting.execution_time")
|
||||
|
||||
@@ -7,6 +7,7 @@ var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
|
||||
type SendEmailCommand struct {
|
||||
To []string
|
||||
Template string
|
||||
Subject string
|
||||
Data map[string]interface{}
|
||||
Info string
|
||||
EmbededFiles []string
|
||||
|
||||
@@ -57,6 +57,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
|
||||
cmd := &m.SendEmailCommandSync{
|
||||
SendEmailCommand: m.SendEmailCommand{
|
||||
Subject: evalContext.GetNotificationTitle(),
|
||||
Data: map[string]interface{}{
|
||||
"Title": evalContext.GetNotificationTitle(),
|
||||
"State": evalContext.Rule.State,
|
||||
|
||||
@@ -111,7 +111,6 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
|
||||
var buffer bytes.Buffer
|
||||
var err error
|
||||
var subjectText interface{}
|
||||
|
||||
data := cmd.Data
|
||||
if data == nil {
|
||||
@@ -124,28 +123,34 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subjectData := data["Subject"].(map[string]interface{})
|
||||
subjectText, hasSubject := subjectData["value"]
|
||||
subject := cmd.Subject
|
||||
if cmd.Subject == "" {
|
||||
var subjectText interface{}
|
||||
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))
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
var subjectBuffer bytes.Buffer
|
||||
err = subjectTmpl.ExecuteTemplate(&subjectBuffer, "subject", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subject = subjectBuffer.String()
|
||||
}
|
||||
|
||||
return &Message{
|
||||
To: cmd.To,
|
||||
From: setting.Smtp.FromAddress,
|
||||
Subject: subjectBuffer.String(),
|
||||
Subject: subject,
|
||||
Body: buffer.String(),
|
||||
EmbededFiles: cmd.EmbededFiles,
|
||||
}, nil
|
||||
|
||||
@@ -80,6 +80,7 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn
|
||||
Template: cmd.Template,
|
||||
To: cmd.To,
|
||||
EmbededFiles: cmd.EmbededFiles,
|
||||
Subject: cmd.Subject,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
+34
-43
@@ -42,63 +42,54 @@ func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) {
|
||||
where := q.buildWhereClause()
|
||||
functions := q.buildFunctionList()
|
||||
|
||||
for _, v := range q.Metrics {
|
||||
if !containsWildcardPattern.Match([]byte(v.Metric)) {
|
||||
alias := ""
|
||||
if v.Alias != "" {
|
||||
alias = fmt.Sprintf(" {%s}", v.Alias)
|
||||
}
|
||||
for _, metric := range q.Metrics {
|
||||
alias := ""
|
||||
if metric.Alias != "" {
|
||||
alias = fmt.Sprintf(" {%s}", metric.Alias)
|
||||
}
|
||||
|
||||
rawQuery := fmt.Sprintf(
|
||||
"`%s`%s%s %s from %v to %v",
|
||||
v.Metric,
|
||||
functions,
|
||||
alias,
|
||||
where,
|
||||
q.TimeRange.GetFromAsMsEpoch(),
|
||||
q.TimeRange.GetToAsMsEpoch())
|
||||
if !containsWildcardPattern.Match([]byte(metric.Metric)) {
|
||||
rawQuery := q.renderQuerystring(metric.Metric, functions, alias, where, q.TimeRange)
|
||||
queriesToSend = append(queriesToSend, QueryToSend{
|
||||
RawQuery: rawQuery,
|
||||
QueryRef: q,
|
||||
})
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
m := strings.Replace(metric.Metric, "*", ".*", -1)
|
||||
mp, err := regexp.Compile(m)
|
||||
|
||||
m := strings.Replace(v.Metric, "*", ".*", -1)
|
||||
mp, err := regexp.Compile(m)
|
||||
if err != nil {
|
||||
log.Error2("failed to compile regex for ", "metric", m)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error2("failed to compile regex for ", "metric", m)
|
||||
continue
|
||||
}
|
||||
|
||||
//TODO: this lookup should be cached
|
||||
for _, a := range availableSeries {
|
||||
if mp.Match([]byte(a)) {
|
||||
alias := ""
|
||||
if v.Alias != "" {
|
||||
alias = fmt.Sprintf(" {%s}", v.Alias)
|
||||
//TODO: this lookup should be cached
|
||||
for _, wildcardMatch := range availableSeries {
|
||||
if mp.Match([]byte(wildcardMatch)) {
|
||||
rawQuery := q.renderQuerystring(wildcardMatch, functions, alias, where, q.TimeRange)
|
||||
queriesToSend = append(queriesToSend, QueryToSend{
|
||||
RawQuery: rawQuery,
|
||||
QueryRef: q,
|
||||
})
|
||||
}
|
||||
|
||||
rawQuery := fmt.Sprintf(
|
||||
"`%s`%s%s %s from %v to %v",
|
||||
a,
|
||||
functions,
|
||||
alias,
|
||||
where,
|
||||
q.TimeRange.GetFromAsMsEpoch(),
|
||||
q.TimeRange.GetToAsMsEpoch())
|
||||
|
||||
queriesToSend = append(queriesToSend, QueryToSend{
|
||||
RawQuery: rawQuery,
|
||||
QueryRef: q,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queriesToSend, nil
|
||||
}
|
||||
|
||||
func (q *Query) renderQuerystring(path, functions, alias, where string, timerange *tsdb.TimeRange) string {
|
||||
return fmt.Sprintf(
|
||||
"`%s`%s%s %s from %v to %v",
|
||||
path,
|
||||
functions,
|
||||
alias,
|
||||
where,
|
||||
q.TimeRange.GetFromAsMsEpoch(),
|
||||
q.TimeRange.GetToAsMsEpoch())
|
||||
}
|
||||
|
||||
func (q *Query) buildFunctionList() string {
|
||||
functions := ""
|
||||
for _, v := range q.FunctionList {
|
||||
|
||||
@@ -20,6 +20,14 @@ var conditionTypes = [
|
||||
{text: 'Query', value: 'query'},
|
||||
];
|
||||
|
||||
var alertStateSortScore = {
|
||||
alerting: 1,
|
||||
no_data: 2,
|
||||
pending: 3,
|
||||
ok: 4,
|
||||
paused: 5,
|
||||
};
|
||||
|
||||
var evalFunctions = [
|
||||
{text: 'IS ABOVE', value: 'gt'},
|
||||
{text: 'IS BELOW', value: 'lt'},
|
||||
@@ -129,4 +137,5 @@ export default {
|
||||
reducerTypes: reducerTypes,
|
||||
createReducerPart: createReducerPart,
|
||||
joinEvalMatches: joinEvalMatches,
|
||||
alertStateSortScore: alertStateSortScore,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
<span class="gf-form-label width-8">Max items</span>
|
||||
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.panel.limit" ng-change="ctrl.onRender()" />
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.show === 'current'">
|
||||
<span class="gf-form-label width-8">Sort order</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.sortOrder" ng-options="f.value as f.text for f in ctrl.sortOrderOptions" ng-change="ctrl.onRender()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Alerts from this dashboard" label-class="width-18" checked="ctrl.panel.onlyAlertsOnDashboard" on-change="ctrl.updateStateFilter()"></gf-form-switch>
|
||||
</div>
|
||||
<div class="section gf-form-group">
|
||||
|
||||
@@ -17,6 +17,12 @@ class AlertListPanel extends PanelCtrl {
|
||||
{text: 'Recent state changes', value: 'changes'}
|
||||
];
|
||||
|
||||
sortOrderOptions = [
|
||||
{text: 'Alphabetical (asc)', value: 1},
|
||||
{text: 'Alphabetical (desc)', value: 2},
|
||||
{text: 'Importance', value: 3},
|
||||
];
|
||||
|
||||
contentHeight: string;
|
||||
stateFilter: any = {};
|
||||
currentAlerts: any = [];
|
||||
@@ -26,10 +32,10 @@ class AlertListPanel extends PanelCtrl {
|
||||
show: 'current',
|
||||
limit: 10,
|
||||
stateFilter: [],
|
||||
onlyAlertsOnDashboard: false
|
||||
onlyAlertsOnDashboard: false,
|
||||
sortOrder: 1
|
||||
};
|
||||
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private $location, private backendSrv, private timeSrv, private templateSrv) {
|
||||
super($scope, $injector);
|
||||
@@ -44,6 +50,19 @@ class AlertListPanel extends PanelCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
sortResult(alerts) {
|
||||
if (this.panel.sortOrder === 3) {
|
||||
return _.sortBy(alerts, a => { return alertDef.alertStateSortScore[a.state]; });
|
||||
}
|
||||
|
||||
var result = _.sortBy(alerts, a => { return a.name.toLowerCase();});
|
||||
if (this.panel.sortOrder === 2) {
|
||||
result.reverse();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
updateStateFilter() {
|
||||
var result = [];
|
||||
|
||||
@@ -104,11 +123,11 @@ class AlertListPanel extends PanelCtrl {
|
||||
|
||||
this.backendSrv.get(`/api/alerts`, params)
|
||||
.then(res => {
|
||||
this.currentAlerts = _.map(res, al => {
|
||||
this.currentAlerts = this.sortResult(_.map(res, al => {
|
||||
al.stateModel = alertDef.getStateDisplayModel(al.state);
|
||||
al.newStateDateAgo = moment(al.newStateDate).fromNow().replace(" ago", "");
|
||||
return al;
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user