-
+
-
-
-
+
+
+
+
+ |
+
+
+
+ {{> body }}
|
-
+
+
+
-
diff --git a/emails/templates/new_user_invite.html b/emails/templates/new_user_invite.html
index 86948a665f6..6e75a203953 100644
--- a/emails/templates/new_user_invite.html
+++ b/emails/templates/new_user_invite.html
@@ -9,7 +9,7 @@
- You're invited to join [[.OrgName]]
+ You're invited to join [[.OrgName]]
|
|
@@ -25,7 +25,7 @@
- You've been invited to join the [[.OrgName]] organization by [[.InvitedBy]]. To accept your invitation and join the team, please click the link below:
+ You've been invited to join the [[.OrgName]] organization by [[.InvitedBy]]. To accept your invitation and join the team, please click the link below:
|
diff --git a/emails/templates/reset_password.html b/emails/templates/reset_password.html
index f3aca5da95e..e9d1527116c 100644
--- a/emails/templates/reset_password.html
+++ b/emails/templates/reset_password.html
@@ -7,7 +7,7 @@
- Hi [[.Name]]
+ Hi [[.Name]],
|
|
@@ -24,7 +24,7 @@
- Please click the following link to reset your password within [[.EmailCodeValidHours]] hours.
+ Please click the following link to reset your password within [[.EmailCodeValidHours]] hours.
[[.AppUrl]]user/password/reset?code=[[.Code]]
diff --git a/emails/templates/signup_started.html b/emails/templates/signup_started.html
index 39b369a1f7c..15689a0e62c 100644
--- a/emails/templates/signup_started.html
+++ b/emails/templates/signup_started.html
@@ -7,7 +7,7 @@
- Complete the signup
+ Complete the signup
|
|
diff --git a/emails/templates/welcome_on_signup.html b/emails/templates/welcome_on_signup.html
index b93c56b77b1..0e0e4dbe3bc 100644
--- a/emails/templates/welcome_on_signup.html
+++ b/emails/templates/welcome_on_signup.html
@@ -7,10 +7,15 @@
- Hi [[.Name]]
+ Hi [[.Name]],
|
|
+
+
+ Welcome! Ready to start building some beautiful metric and analytic dashboards?
+ |
+
@@ -29,6 +34,13 @@
|
+
+
+ Thank you for joining our community.
+
+ The Grafana Team
+ |
+
|
diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service
index e87635bb3de..ea7193c55a6 100644
--- a/packaging/deb/systemd/grafana-server.service
+++ b/packaging/deb/systemd/grafana-server.service
@@ -1,5 +1,5 @@
[Unit]
-Description=Starts and stops a single grafana instance on this system
+Description=Grafana instance
Documentation=http://docs.grafana.org
Wants=network-online.target
After=network-online.target
diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service
index d79c5987576..ec0ef7e804e 100644
--- a/packaging/rpm/systemd/grafana-server.service
+++ b/packaging/rpm/systemd/grafana-server.service
@@ -1,5 +1,5 @@
[Unit]
-Description=Starts and stops a single grafana instance on this system
+Description=Grafana instance
Documentation=http://docs.grafana.org
Wants=network-online.target
After=network-online.target
diff --git a/pkg/Godeps/Godeps.json b/pkg/Godeps/Godeps.json
deleted file mode 100644
index 5cff21cdba5..00000000000
--- a/pkg/Godeps/Godeps.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "ImportPath": "github.com/grafana/grafana/pkg",
- "GoVersion": "go1.6",
- "GodepVersion": "v60",
- "Packages": [
- "./pkg/..."
- ],
- "Deps": []
-}
diff --git a/pkg/Godeps/Readme b/pkg/Godeps/Readme
deleted file mode 100644
index 4cdaa53d56d..00000000000
--- a/pkg/Godeps/Readme
+++ /dev/null
@@ -1,5 +0,0 @@
-This directory tree is generated automatically by godep.
-
-Please do not edit.
-
-See https://github.com/tools/godep for more information.
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go
index 80b5108bbad..1233e09d504 100644
--- a/pkg/api/alerting.go
+++ b/pkg/api/alerting.go
@@ -8,7 +8,6 @@ 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) {
@@ -26,13 +25,18 @@ func ValidateOrgAlert(c *middleware.Context) {
}
}
-// GET /api/alerts/rules/
+// GET /api/alerts
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{
OrgId: c.OrgId,
- State: c.QueryStrings("state"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
+ Limit: c.QueryInt64("limit"),
+ }
+
+ states := c.QueryStrings("state")
+ if len(states) > 0 {
+ query.State = states
}
if err := bus.Dispatch(&query); err != nil {
@@ -50,7 +54,6 @@ func GetAlerts(c *middleware.Context) Response {
Name: alert.Name,
Message: alert.Message,
State: alert.State,
- Severity: alert.Severity,
EvalDate: alert.EvalDate,
NewStateDate: alert.NewStateDate,
ExecutionError: alert.ExecutionError,
@@ -147,7 +150,7 @@ func DelAlert(c *middleware.Context) Response {
}
func GetAlertNotifications(c *middleware.Context) Response {
- query := &models.GetAlertNotificationsQuery{OrgId: c.OrgId}
+ query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
if err := bus.Dispatch(query); err != nil {
return ApiError(500, "Failed to get alert notifications", err)
@@ -157,11 +160,12 @@ func GetAlertNotifications(c *middleware.Context) Response {
for _, notification := range query.Result {
result = append(result, dtos.AlertNotification{
- Id: notification.Id,
- Name: notification.Name,
- Type: notification.Type,
- Created: notification.Created,
- Updated: notification.Updated,
+ Id: notification.Id,
+ Name: notification.Name,
+ Type: notification.Type,
+ IsDefault: notification.IsDefault,
+ Created: notification.Created,
+ Updated: notification.Updated,
})
}
@@ -178,7 +182,7 @@ func GetAlertNotificationById(c *middleware.Context) Response {
return ApiError(500, "Failed to get alert notifications", err)
}
- return Json(200, query.Result[0])
+ return Json(200, query.Result)
}
func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
@@ -214,40 +218,19 @@ 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)
+//POST /api/alert-notifications/test
+func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) Response {
+ cmd := &alerting.NotificationTestCommand{
+ Name: dto.Name,
+ Type: dto.Type,
+ Settings: dto.Settings,
}
- query := &annotations.ItemQuery{
- AlertId: alertId,
- Type: annotations.AlertType,
- OrgId: c.OrgId,
- Limit: c.QueryInt64("limit"),
+ if err := bus.Dispatch(cmd); err != nil {
+ return ApiError(500, "Failed to send alert notifications", err)
}
- 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)
+ return ApiSuccess("Test notification sent")
}
func getAlertIdForRequest(c *middleware.Context) (int64, error) {
diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
new file mode 100644
index 00000000000..2803aa46435
--- /dev/null
+++ b/pkg/api/annotations.go
@@ -0,0 +1,46 @@
+package api
+
+import (
+ "github.com/grafana/grafana/pkg/api/dtos"
+ "github.com/grafana/grafana/pkg/middleware"
+ "github.com/grafana/grafana/pkg/services/annotations"
+)
+
+func GetAnnotations(c *middleware.Context) Response {
+
+ query := &annotations.ItemQuery{
+ From: c.QueryInt64("from") / 1000,
+ To: c.QueryInt64("to") / 1000,
+ Type: annotations.ItemType(c.Query("type")),
+ OrgId: c.OrgId,
+ AlertId: c.QueryInt64("alertId"),
+ DashboardId: c.QueryInt64("dashboardId"),
+ PanelId: c.QueryInt64("panelId"),
+ Limit: c.QueryInt64("limit"),
+ NewState: c.QueryStrings("newState"),
+ }
+
+ repo := annotations.GetRepository()
+
+ items, err := repo.Find(query)
+ if err != nil {
+ return ApiError(500, "Failed to get annotations", err)
+ }
+
+ result := make([]dtos.Annotation, 0)
+
+ for _, item := range items {
+ result = append(result, dtos.Annotation{
+ AlertId: item.AlertId,
+ Time: item.Epoch * 1000,
+ Data: item.Data,
+ NewState: item.NewState,
+ PrevState: item.PrevState,
+ Text: item.Text,
+ Metric: item.Metric,
+ Title: item.Title,
+ })
+ }
+
+ return Json(200, result)
+}
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 132c609dde4..35b667c55be 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -58,6 +58,7 @@ func Register(r *macaron.Macaron) {
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
r.Get("/dashboard/*", reqSignedIn, Index)
+ r.Get("/dashboard-solo/snapshot/*", Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index)
r.Get("/import/dashboard", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index)
@@ -254,17 +255,18 @@ func Register(r *macaron.Macaron) {
r.Get("/", wrap(GetAlerts))
})
- r.Get("/alert-history", wrap(GetAlertHistory))
-
r.Get("/alert-notifications", wrap(GetAlertNotifications))
r.Group("/alert-notifications", func() {
+ r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
r.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
r.Get("/:notificationId", wrap(GetAlertNotificationById))
r.Delete("/:notificationId", wrap(DeleteAlertNotification))
}, reqOrgAdmin)
+ r.Get("/annotations", wrap(GetAnnotations))
+
// error test
r.Get("/metrics/error", wrap(GenerateError))
diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go
index 4721924cff4..4d5fa1f52db 100644
--- a/pkg/api/cloudwatch/metrics.go
+++ b/pkg/api/cloudwatch/metrics.go
@@ -67,25 +67,29 @@ func init() {
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes",
"S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB",
"HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"},
- "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"},
- "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"},
- "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
- "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"},
- "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
- "AWS/ML": {"PredictCount", "PredictFailureCount"},
- "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
- "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
- "AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
- "AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
- "AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
- "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
- "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
+ "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"},
+ "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"},
+ "AWS/Firehose": {"DeliveryToElasticsearch.Bytes", "DeliveryToElasticsearch.Records", "DeliveryToElasticsearch.Success", "DeliveryToRedshift.Bytes", "DeliveryToRedshift.Records", "DeliveryToRedshift.Success", "DeliveryToS3.Bytes", "DeliveryToS3.DataFreshness", "DeliveryToS3.Records", "DeliveryToS3.Success", "IncomingBytes", "IncomingRecords", "DescribeDeliveryStream.Latency", "DescribeDeliveryStream.Requests", "ListDeliveryStreams.Latency", "ListDeliveryStreams.Requests", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Requests", "PutRecordBatch.Bytes", "PutRecordBatch.Latency", "PutRecordBatch.Records", "PutRecordBatch.Requests", "UpdateDeliveryStream.Latency", "UpdateDeliveryStream.Requests"},
+ "AWS/IoT": {"PublishIn.Success", "PublishOut.Success", "Subscribe.Success", "Ping.Success", "Connect.Success", "GetThingShadow.Accepted"},
+ "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"},
+ "AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"},
+ "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"},
+ "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"},
+ "AWS/ML": {"PredictCount", "PredictFailureCount"},
+ "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
+ "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+ "AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"},
+ "AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
+ "AWS/S3": {"BucketSizeBytes", "NumberOfObjects"},
+ "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"},
+ "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"},
"AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed",
"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"},
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
+ "KMS": {"SecondsUntilKeyMaterialExpiration"},
}
dimensionsMap = map[string][]string{
"AWS/ApiGateway": {"ApiName", "Method", "Resource", "Stage"},
@@ -106,7 +110,10 @@ func init() {
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {"ClientId", "DomainName"},
"AWS/Events": {"RuleName"},
+ "AWS/Firehose": {},
+ "AWS/IoT": {"Protocol"},
"AWS/Kinesis": {"StreamName", "ShardID"},
+ "AWS/KinesisAnalytics": {"Flow", "Id", "Application"},
"AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"},
"AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"},
"AWS/ML": {"MLModelId", "RequestMode"},
@@ -121,6 +128,7 @@ func init() {
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
"AWS/WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
+ "KMS": {"KeyId"},
}
customMetricsMetricsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
diff --git a/pkg/api/common.go b/pkg/api/common.go
index 9d3ad90783b..82eed0db5fe 100644
--- a/pkg/api/common.go
+++ b/pkg/api/common.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"net/http"
- "github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/macaron.v1"
@@ -88,10 +87,8 @@ func ApiError(status int, message string, err error) *NormalResponse {
switch status {
case 404:
- metrics.M_Api_Status_404.Inc(1)
data["message"] = "Not Found"
case 500:
- metrics.M_Api_Status_500.Inc(1)
data["message"] = "Internal Server Error"
}
diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go
index 66d654d4d93..97f2529c781 100644
--- a/pkg/api/dataproxy.go
+++ b/pkg/api/dataproxy.go
@@ -104,6 +104,22 @@ func ProxyDataSourceRequest(c *middleware.Context) {
}
proxyPath := c.Params("*")
+
+ if ds.Type == m.DS_ES {
+ if c.Req.Request.Method == "DELETE" {
+ c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
+ return
+ }
+ if c.Req.Request.Method == "PUT" {
+ c.JsonApiErr(403, "Puts not allowed on proxied Elasticsearch datasource", nil)
+ return
+ }
+ if c.Req.Request.Method == "POST" && proxyPath != "_msearch" {
+ c.JsonApiErr(403, "Posts not allowed on proxied Elasticsearch datasource except on /_msearch", nil)
+ return
+ }
+ }
+
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
proxy.Transport = dataProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go
index d5fbb2c3a9f..e024768cd5e 100644
--- a/pkg/api/dtos/alerting.go
+++ b/pkg/api/dtos/alerting.go
@@ -8,25 +8,25 @@ import (
)
type AlertRule struct {
- Id int64 `json:"id"`
- DashboardId int64 `json:"dashboardId"`
- PanelId int64 `json:"panelId"`
- Name string `json:"name"`
- Message string `json:"message"`
- State m.AlertStateType `json:"state"`
- Severity m.AlertSeverityType `json:"severity"`
- NewStateDate time.Time `json:"newStateDate"`
- EvalDate time.Time `json:"evalDate"`
- ExecutionError string `json:"executionError"`
- DashbboardUri string `json:"dashboardUri"`
+ Id int64 `json:"id"`
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ Name string `json:"name"`
+ Message string `json:"message"`
+ State m.AlertStateType `json:"state"`
+ NewStateDate time.Time `json:"newStateDate"`
+ EvalDate time.Time `json:"evalDate"`
+ ExecutionError string `json:"executionError"`
+ DashbboardUri string `json:"dashboardUri"`
}
type AlertNotification struct {
- Id int64 `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ IsDefault bool `json:"isDefault"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
}
type AlertTestCommand struct {
@@ -53,13 +53,8 @@ type EvalMatch struct {
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"`
+type NotificationTestCommand struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Settings *simplejson.Json `json:"settings"`
}
diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go
new file mode 100644
index 00000000000..a5d5823e1a4
--- /dev/null
+++ b/pkg/api/dtos/annotations.go
@@ -0,0 +1,17 @@
+package dtos
+
+import "github.com/grafana/grafana/pkg/components/simplejson"
+
+type Annotation struct {
+ AlertId int64 `json:"alertId"`
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ NewState string `json:"newState"`
+ PrevState string `json:"prevState"`
+ Time int64 `json:"time"`
+ Title string `json:"title"`
+ Text string `json:"text"`
+ Metric string `json:"metric"`
+
+ Data *simplejson.Json `json:"data"`
+}
diff --git a/pkg/api/dtos/playlist.go b/pkg/api/dtos/playlist.go
new file mode 100644
index 00000000000..317ff83339a
--- /dev/null
+++ b/pkg/api/dtos/playlist.go
@@ -0,0 +1,23 @@
+package dtos
+
+type PlaylistDashboard struct {
+ Id int64 `json:"id"`
+ Slug string `json:"slug"`
+ Title string `json:"title"`
+ Uri string `json:"uri"`
+ Order int `json:"order"`
+}
+
+type PlaylistDashboardsSlice []PlaylistDashboard
+
+func (slice PlaylistDashboardsSlice) Len() int {
+ return len(slice)
+}
+
+func (slice PlaylistDashboardsSlice) Less(i, j int) bool {
+ return slice[i].Order < slice[j].Order
+}
+
+func (slice PlaylistDashboardsSlice) Swap(i, j int) {
+ slice[i], slice[j] = slice[j], slice[i]
+}
diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go
index 5a84409d2c1..3a019e80c49 100644
--- a/pkg/api/frontendsettings.go
+++ b/pkg/api/frontendsettings.go
@@ -105,6 +105,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
grafanaDatasourceMeta, _ := plugins.DataSources["grafana"]
datasources["-- Grafana --"] = map[string]interface{}{
"type": "grafana",
+ "name": "-- Grafana --",
"meta": grafanaDatasourceMeta,
}
diff --git a/pkg/api/login.go b/pkg/api/login.go
index 4f976f753a2..789765ee01e 100644
--- a/pkg/api/login.go
+++ b/pkg/api/login.go
@@ -27,6 +27,8 @@ func LoginView(c *middleware.Context) {
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
+ viewData.Settings["genericOAuthEnabled"] = setting.OAuthService.Generic
+ viewData.Settings["oauthProviderName"] = setting.OAuthService.OAuthProviderName
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go
index 6512a827341..83c342937c1 100644
--- a/pkg/api/login_oauth.go
+++ b/pkg/api/login_oauth.go
@@ -3,7 +3,6 @@ package api
import (
"errors"
"fmt"
- "net/url"
"golang.org/x/oauth2"
@@ -46,9 +45,9 @@ func OAuthLogin(ctx *middleware.Context) {
userInfo, err := connect.UserInfo(token)
if err != nil {
if err == social.ErrMissingTeamMembership {
- ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
+ ctx.Redirect(setting.AppSubUrl + "/login?failCode=1000")
} else if err == social.ErrMissingOrganizationMembership {
- ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github organization membership not fulfilled"))
+ ctx.Redirect(setting.AppSubUrl + "/login?failCode=1001")
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
@@ -60,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
- ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
+ ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
return
}
diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go
index e4feb3442fb..780767531a8 100644
--- a/pkg/api/playlist_play.go
+++ b/pkg/api/playlist_play.go
@@ -1,16 +1,18 @@
package api
import (
+ "sort"
"strconv"
+ "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
_ "github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
)
-func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, error) {
- result := make([]m.PlaylistDashboardDto, 0)
+func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
+ result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByIds) > 0 {
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIds}
@@ -19,11 +21,12 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
}
for _, item := range dashboardQuery.Result {
- result = append(result, m.PlaylistDashboardDto{
+ result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Slug: item.Slug,
Title: item.Title,
Uri: "db/" + item.Slug,
+ Order: dashboardIdOrder[item.Id],
})
}
}
@@ -31,8 +34,8 @@ func populateDashboardsById(dashboardByIds []int64) ([]m.PlaylistDashboardDto, e
return result, nil
}
-func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.PlaylistDashboardDto {
- result := make([]m.PlaylistDashboardDto, 0)
+func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
+ result := make(dtos.PlaylistDashboardsSlice, 0)
if len(dashboardByTag) > 0 {
for _, tag := range dashboardByTag {
@@ -47,10 +50,11 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
if err := bus.Dispatch(&searchQuery); err == nil {
for _, item := range searchQuery.Result {
- result = append(result, m.PlaylistDashboardDto{
+ result = append(result, dtos.PlaylistDashboard{
Id: item.Id,
Title: item.Title,
Uri: item.Uri,
+ Order: dashboardTagOrder[tag],
})
}
}
@@ -60,28 +64,33 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string) []m.P
return result
}
-func LoadPlaylistDashboards(orgId, userId, playlistId int64) ([]m.PlaylistDashboardDto, error) {
+func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
playlistItems, _ := LoadPlaylistItems(playlistId)
dashboardByIds := make([]int64, 0)
dashboardByTag := make([]string, 0)
+ dashboardIdOrder := make(map[int64]int)
+ dashboardTagOrder := make(map[string]int)
for _, i := range playlistItems {
if i.Type == "dashboard_by_id" {
dashboardId, _ := strconv.ParseInt(i.Value, 10, 64)
dashboardByIds = append(dashboardByIds, dashboardId)
+ dashboardIdOrder[dashboardId] = i.Order
}
if i.Type == "dashboard_by_tag" {
dashboardByTag = append(dashboardByTag, i.Value)
+ dashboardTagOrder[i.Value] = i.Order
}
}
- result := make([]m.PlaylistDashboardDto, 0)
+ result := make(dtos.PlaylistDashboardsSlice, 0)
- var k, _ = populateDashboardsById(dashboardByIds)
+ var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
result = append(result, k...)
- result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag)...)
+ result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
+ sort.Sort(sort.Reverse(result))
return result, nil
}
diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go
index 401606e5ec8..1f391ea10b3 100644
--- a/pkg/cmd/grafana-cli/commands/install_command.go
+++ b/pkg/cmd/grafana-cli/commands/install_command.go
@@ -90,13 +90,12 @@ func InstallPlugin(pluginName, version string, c CommandLine) error {
logger.Infof("%s Installed %s successfully \n", color.GreenString("✔"), plugin.Id)
- /* Enable once we need support for downloading depedencies
res, _ := s.ReadPlugin(pluginFolder, pluginName)
- for _, v := range res.Dependency.Plugins {
+ for _, v := range res.Dependencies.Plugins {
InstallPlugin(v.Id, version, c)
- log.Infof("Installed dependency: %v ✔\n", v.Id)
+ logger.Infof("Installed dependency: %v ✔\n", v.Id)
}
- */
+
return err
}
diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go
index fbdec792322..658edd0f895 100644
--- a/pkg/cmd/grafana-cli/main.go
+++ b/pkg/cmd/grafana-cli/main.go
@@ -8,6 +8,7 @@ import (
"github.com/codegangsta/cli"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
+ "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
)
@@ -16,6 +17,8 @@ var version = "master"
func main() {
setupLogging()
+ services.Init(version)
+
app := cli.NewApp()
app.Name = "Grafana cli"
app.Usage = ""
diff --git a/pkg/cmd/grafana-cli/models/model.go b/pkg/cmd/grafana-cli/models/model.go
index 3a39c5fbe65..0700cb9a9e4 100644
--- a/pkg/cmd/grafana-cli/models/model.go
+++ b/pkg/cmd/grafana-cli/models/model.go
@@ -9,11 +9,11 @@ type InstalledPlugin struct {
Name string `json:"name"`
Type string `json:"type"`
- Info PluginInfo `json:"info"`
- Dependency Dependency `json:"dependencies"`
+ Info PluginInfo `json:"info"`
+ Dependencies Dependencies `json:"dependencies"`
}
-type Dependency struct {
+type Dependencies struct {
GrafanaVersion string `json:"grafanaVersion"`
Plugins []Plugin `json:"plugins"`
}
diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go
index c5c45460722..e2ccce8418f 100644
--- a/pkg/cmd/grafana-cli/services/services.go
+++ b/pkg/cmd/grafana-cli/services/services.go
@@ -1,35 +1,59 @@
package services
import (
+ "crypto/tls"
"encoding/json"
"errors"
"fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
"path"
+ "time"
- "github.com/franela/goreq"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
)
-var IoHelper m.IoUtil = IoUtilImp{}
+var (
+ IoHelper m.IoUtil = IoUtilImp{}
+ HttpClient http.Client
+ grafanaVersion string
+)
+
+func Init(version string) {
+ grafanaVersion = version
+
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
+ }
+
+ HttpClient = http.Client{
+ Timeout: time.Duration(10 * time.Second),
+ Transport: tr,
+ }
+}
func ListAllPlugins(repoUrl string) (m.PluginRepo, error) {
- fullUrl := repoUrl + "/repo"
- res, err := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do()
+ body, err := createRequest(repoUrl, "repo")
+
+ if err != nil {
+ logger.Info("Failed to create request", "error", err)
+ return m.PluginRepo{}, fmt.Errorf("Failed to create request. error: %v", err)
+ }
+
if err != nil {
return m.PluginRepo{}, err
}
- if res.StatusCode != 200 {
- return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode)
- }
- var resp m.PluginRepo
- err = res.Body.FromJsonTo(&resp)
+ var data m.PluginRepo
+ err = json.Unmarshal(body, &data)
if err != nil {
- return m.PluginRepo{}, errors.New("Could not load plugin data")
+ logger.Info("Failed to unmarshal graphite response error: %v", err)
+ return m.PluginRepo{}, err
}
- return resp, nil
+ return data, nil
}
func ReadPlugin(pluginDir, pluginName string) (m.InstalledPlugin, error) {
@@ -88,21 +112,46 @@ func RemoveInstalledPlugin(pluginPath, pluginName string) error {
}
func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
- fullUrl := repoUrl + "/repo/" + pluginId
+ body, err := createRequest(repoUrl, "repo", pluginId)
+
+ if err != nil {
+ logger.Info("Failed to create request", "error", err)
+ return m.Plugin{}, fmt.Errorf("Failed to create request. error: %v", err)
+ }
- res, err := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do()
if err != nil {
return m.Plugin{}, err
}
- if res.StatusCode != 200 {
- return m.Plugin{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode)
- }
- var resp m.Plugin
- err = res.Body.FromJsonTo(&resp)
+ var data m.Plugin
+ err = json.Unmarshal(body, &data)
if err != nil {
- return m.Plugin{}, errors.New("Could not load plugin data")
+ logger.Info("Failed to unmarshal graphite response error: %v", err)
+ return m.Plugin{}, err
}
- return resp, nil
+ return data, nil
+}
+
+func createRequest(repoUrl string, subPaths ...string) ([]byte, error) {
+ u, _ := url.Parse(repoUrl)
+ for _, v := range subPaths {
+ u.Path = path.Join(u.Path, v)
+ }
+
+ req, err := http.NewRequest(http.MethodGet, u.String(), nil)
+
+ req.Header.Set("grafana-version", grafanaVersion)
+ req.Header.Set("User-Agent", "grafana "+grafanaVersion)
+
+ if err != nil {
+ return []byte{}, err
+ }
+
+ res, err := HttpClient.Do(req)
+
+ body, err := ioutil.ReadAll(res.Body)
+ defer res.Body.Close()
+
+ return body, err
}
diff --git a/pkg/cmd/grafana-server/web.go b/pkg/cmd/grafana-server/web.go
index 51975ac5617..4c294a814ca 100644
--- a/pkg/cmd/grafana-server/web.go
+++ b/pkg/cmd/grafana-server/web.go
@@ -53,6 +53,7 @@ func newMacaron() *macaron.Macaron {
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
+ m.Use(middleware.RequestMetrics())
return m
}
diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go
index 5b383e69f34..1de46e7fd1d 100644
--- a/pkg/components/imguploader/imguploader.go
+++ b/pkg/components/imguploader/imguploader.go
@@ -19,9 +19,9 @@ func NewImageUploader() (ImageUploader, error) {
return nil, err
}
- bucket := s3sec.Key("secret_key").String()
- accessKey := s3sec.Key("access_key").String()
- secretKey := s3sec.Key("secret_key").String()
+ bucket := s3sec.Key("bucket_url").MustString("")
+ accessKey := s3sec.Key("access_key").MustString("")
+ secretKey := s3sec.Key("secret_key").MustString("")
if bucket == "" {
return nil, fmt.Errorf("Could not find bucket setting for image.uploader.s3")
diff --git a/pkg/components/imguploader/imguploader_test.go b/pkg/components/imguploader/imguploader_test.go
index d12464dae69..4a18f22c173 100644
--- a/pkg/components/imguploader/imguploader_test.go
+++ b/pkg/components/imguploader/imguploader_test.go
@@ -1,7 +1,6 @@
package imguploader
import (
- "reflect"
"testing"
"github.com/grafana/grafana/pkg/setting"
@@ -27,7 +26,12 @@ func TestImageUploaderFactory(t *testing.T) {
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
- So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&S3Uploader{}))
+ original, ok := uploader.(*S3Uploader)
+
+ So(ok, ShouldBeTrue)
+ So(original.accessKey, ShouldEqual, "access_key")
+ So(original.secretKey, ShouldEqual, "secret_key")
+ So(original.bucket, ShouldEqual, "bucket_url")
})
Convey("Webdav uploader", func() {
@@ -47,7 +51,12 @@ func TestImageUploaderFactory(t *testing.T) {
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
- So(reflect.TypeOf(uploader), ShouldEqual, reflect.TypeOf(&WebdavUploader{}))
+ original, ok := uploader.(*WebdavUploader)
+
+ So(ok, ShouldBeTrue)
+ So(original.url, ShouldEqual, "webdavUrl")
+ So(original.username, ShouldEqual, "username")
+ So(original.password, ShouldEqual, "password")
})
})
}
diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go
index af995d566c1..59ec598412b 100644
--- a/pkg/components/imguploader/s3uploader.go
+++ b/pkg/components/imguploader/s3uploader.go
@@ -3,7 +3,10 @@ package imguploader
import (
"io/ioutil"
"net/http"
+ "net/url"
+ "path"
+ "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
"github.com/kr/s3/s3util"
)
@@ -12,6 +15,7 @@ type S3Uploader struct {
bucket string
secretKey string
accessKey string
+ log log.Logger
}
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
@@ -19,10 +23,11 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
bucket: bucket,
accessKey: accessKey,
secretKey: secretKey,
+ log: log.New("s3uploader"),
}
}
-func (u *S3Uploader) Upload(path string) (string, error) {
+func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
s3util.DefaultConfig.AccessKey = u.accessKey
s3util.DefaultConfig.SecretKey = u.secretKey
@@ -31,15 +36,26 @@ func (u *S3Uploader) Upload(path string) (string, error) {
header.Add("x-amz-acl", "public-read")
header.Add("Content-Type", "image/png")
- fullUrl := u.bucket + util.GetRandomString(20) + ".png"
- writer, err := s3util.Create(fullUrl, header, nil)
+ var imageUrl *url.URL
+ var err error
+
+ if imageUrl, err = url.Parse(u.bucket); err != nil {
+ return "", err
+ }
+
+ // add image to url
+ imageUrl.Path = path.Join(imageUrl.Path, util.GetRandomString(20)+".png")
+ imageUrlString := imageUrl.String()
+ log.Debug("Uploading image to s3", "url", imageUrlString)
+
+ writer, err := s3util.Create(imageUrlString, header, nil)
if err != nil {
return "", err
}
defer writer.Close()
- imgData, err := ioutil.ReadFile(path)
+ imgData, err := ioutil.ReadFile(imageDiskPath)
if err != nil {
return "", err
}
@@ -49,5 +65,5 @@ func (u *S3Uploader) Upload(path string) (string, error) {
return "", err
}
- return fullUrl, nil
+ return imageUrlString, nil
}
diff --git a/pkg/components/imguploader/s3uploader_test.go b/pkg/components/imguploader/s3uploader_test.go
new file mode 100644
index 00000000000..1204a2db4e2
--- /dev/null
+++ b/pkg/components/imguploader/s3uploader_test.go
@@ -0,0 +1,23 @@
+package imguploader
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/setting"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUploadToS3(t *testing.T) {
+ SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
+ setting.NewConfigContext(&setting.CommandLineArgs{
+ HomePath: "../../../",
+ })
+
+ s3Uploader, _ := NewImageUploader()
+
+ path, err := s3Uploader.Upload("../../../public/img/logo_transparent_400x.png")
+
+ So(err, ShouldBeNil)
+ So(path, ShouldNotEqual, "")
+ })
+}
diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go
index 74444b1b123..3b59e1690fd 100644
--- a/pkg/components/imguploader/webdavuploader.go
+++ b/pkg/components/imguploader/webdavuploader.go
@@ -9,7 +9,6 @@ import (
"path"
"time"
- "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/util"
)
@@ -20,7 +19,6 @@ type WebdavUploader struct {
}
func (u *WebdavUploader) Upload(pa string) (string, error) {
- log.Error2("Hej")
client := http.Client{Timeout: time.Duration(10 * time.Second)}
url, _ := url.Parse(u.url)
diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go
index 4e8188e7a4b..d8c916bb765 100644
--- a/pkg/login/ldap.go
+++ b/pkg/login/ldap.go
@@ -48,7 +48,16 @@ func (a *ldapAuther) Dial() error {
ServerName: host,
RootCAs: certPool,
}
- a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
+ if a.server.StartTLS {
+ a.conn, err = ldap.Dial("tcp", address)
+ if err == nil {
+ if err = a.conn.StartTLS(tlsCfg); err == nil {
+ return nil
+ }
+ }
+ } else {
+ a.conn, err = ldap.DialTLS("tcp", address, tlsCfg)
+ }
} else {
a.conn, err = ldap.Dial("tcp", address)
}
diff --git a/pkg/login/settings.go b/pkg/login/settings.go
index e01c0e50992..e0713302a6d 100644
--- a/pkg/login/settings.go
+++ b/pkg/login/settings.go
@@ -19,6 +19,7 @@ type LdapServerConf struct {
Host string `toml:"host"`
Port int `toml:"port"`
UseSSL bool `toml:"use_ssl"`
+ StartTLS bool `toml:"start_tls"`
SkipVerifySSL bool `toml:"ssl_skip_verify"`
RootCACert string `toml:"root_ca_cert"`
BindDN string `toml:"bind_dn"`
diff --git a/pkg/metrics/gauge.go b/pkg/metrics/gauge.go
index 01cd584cb39..59758aa4ecb 100644
--- a/pkg/metrics/gauge.go
+++ b/pkg/metrics/gauge.go
@@ -24,10 +24,10 @@ func NewGauge(meta *MetricMeta) Gauge {
}
}
-func RegGauge(meta *MetricMeta) Gauge {
- g := NewGauge(meta)
- MetricStats.Register(g)
- return g
+func RegGauge(name string, tagStrings ...string) Gauge {
+ tr := NewGauge(NewMetricMeta(name, tagStrings))
+ MetricStats.Register(tr)
+ return tr
}
// GaugeSnapshot is a read-only copy of another Gauge.
diff --git a/pkg/metrics/graphite.go b/pkg/metrics/graphite.go
index e88df2ebb1b..59c992776de 100644
--- a/pkg/metrics/graphite.go
+++ b/pkg/metrics/graphite.go
@@ -63,6 +63,8 @@ func (this *GraphitePublisher) Publish(metrics []Metric) {
switch metric := m.(type) {
case Counter:
this.addCount(buf, metricName+".count", metric.Count(), now)
+ case Gauge:
+ this.addCount(buf, metricName, metric.Value(), now)
case Timer:
percentiles := metric.Percentiles([]float64{0.25, 0.75, 0.90, 0.99})
this.addCount(buf, metricName+".count", metric.Count(), now)
diff --git a/pkg/metrics/graphite_test.go b/pkg/metrics/graphite_test.go
index 2f866ddd7b6..ff2bf530d5e 100644
--- a/pkg/metrics/graphite_test.go
+++ b/pkg/metrics/graphite_test.go
@@ -10,6 +10,8 @@ import (
func TestGraphitePublisher(t *testing.T) {
+ setting.CustomInitPath = "conf/does_not_exist.ini"
+
Convey("Test graphite prefix replacement", t, func() {
var err error
err = setting.NewConfigContext(&setting.CommandLineArgs{
@@ -67,7 +69,6 @@ func TestGraphitePublisher(t *testing.T) {
_, err = setting.Cfg.NewSection("metrics.graphite")
- setting.InstanceName = "hostname.with.dots.com"
publisher, err := CreateGraphitePublisher()
So(err, ShouldBeNil)
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 80c7520eaf2..002f2369c9b 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -9,40 +9,52 @@ 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_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
+ M_Instance_Start Counter
+ M_Page_Status_200 Counter
+ M_Page_Status_500 Counter
+ M_Page_Status_404 Counter
+ M_Page_Status_Unknown Counter
+ M_Api_Status_200 Counter
+ M_Api_Status_404 Counter
+ M_Api_Status_500 Counter
+ M_Api_Status_Unknown Counter
+ M_Proxy_Status_200 Counter
+ M_Proxy_Status_404 Counter
+ M_Proxy_Status_500 Counter
+ M_Proxy_Status_Unknown 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_Alerting Counter
+ M_Alerting_Result_State_Ok Counter
+ M_Alerting_Result_State_Paused Counter
+ M_Alerting_Result_State_NoData Counter
+ M_Alerting_Result_State_ExecError 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
M_Alerting_Exeuction_Time Timer
+
+ // StatTotals
+ M_StatTotal_Dashboards Gauge
+ M_StatTotal_Users Gauge
+ M_StatTotal_Orgs Gauge
+ M_StatTotal_Playlists Gauge
)
func initMetricVars(settings *MetricSettings) {
@@ -54,9 +66,17 @@ func initMetricVars(settings *MetricSettings) {
M_Page_Status_200 = RegCounter("page.resp_status", "code", "200")
M_Page_Status_500 = RegCounter("page.resp_status", "code", "500")
M_Page_Status_404 = RegCounter("page.resp_status", "code", "404")
+ M_Page_Status_Unknown = RegCounter("page.resp_status", "code", "unknown")
- M_Api_Status_500 = RegCounter("api.resp_status", "code", "500")
+ M_Api_Status_200 = RegCounter("api.resp_status", "code", "200")
M_Api_Status_404 = RegCounter("api.resp_status", "code", "404")
+ M_Api_Status_500 = RegCounter("api.resp_status", "code", "500")
+ M_Api_Status_Unknown = RegCounter("api.resp_status", "code", "unknown")
+
+ M_Proxy_Status_200 = RegCounter("proxy.resp_status", "code", "200")
+ M_Proxy_Status_404 = RegCounter("proxy.resp_status", "code", "404")
+ M_Proxy_Status_500 = RegCounter("proxy.resp_status", "code", "500")
+ M_Proxy_Status_Unknown = RegCounter("proxy.resp_status", "code", "unknown")
M_Api_User_SignUpStarted = RegCounter("api.user.signup_started")
M_Api_User_SignUpCompleted = RegCounter("api.user.signup_completed")
@@ -77,12 +97,11 @@ func initMetricVars(settings *MetricSettings) {
M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
- M_Alerting_Result_State_Critical = RegCounter("alerting.result", "state", "critical")
- M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
+ M_Alerting_Result_State_Alerting = RegCounter("alerting.result", "state", "alerting")
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_Result_State_NoData = RegCounter("alerting.result", "state", "no_data")
+ M_Alerting_Result_State_ExecError = RegCounter("alerting.result", "state", "exec_error")
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
@@ -92,4 +111,10 @@ func initMetricVars(settings *MetricSettings) {
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time")
+
+ // StatTotals
+ M_StatTotal_Dashboards = RegGauge("stat_totals", "stat", "dashboards")
+ M_StatTotal_Users = RegGauge("stat_totals", "stat", "users")
+ M_StatTotal_Orgs = RegGauge("stat_totals", "stat", "orgs")
+ M_StatTotal_Playlists = RegGauge("stat_totals", "stat", "playlists")
}
diff --git a/pkg/metrics/publish.go b/pkg/metrics/publish.go
index 9c1de6e05d2..4255481b8d1 100644
--- a/pkg/metrics/publish.go
+++ b/pkg/metrics/publish.go
@@ -15,6 +15,7 @@ import (
)
var metricsLogger log.Logger = log.New("metrics")
+var metricPublishCounter int64 = 0
func Init() {
settings := readSettings()
@@ -45,12 +46,33 @@ func sendMetrics(settings *MetricSettings) {
return
}
+ updateTotalStats()
+
metrics := MetricStats.GetSnapshots()
for _, publisher := range settings.Publishers {
publisher.Publish(metrics)
}
}
+func updateTotalStats() {
+
+ // every interval also publish totals
+ metricPublishCounter++
+ if metricPublishCounter%10 == 0 {
+ // get stats
+ statsQuery := m.GetSystemStatsQuery{}
+ if err := bus.Dispatch(&statsQuery); err != nil {
+ metricsLogger.Error("Failed to get system stats", "error", err)
+ return
+ }
+
+ M_StatTotal_Dashboards.Update(statsQuery.Result.DashboardCount)
+ M_StatTotal_Users.Update(statsQuery.Result.UserCount)
+ M_StatTotal_Playlists.Update(statsQuery.Result.PlaylistCount)
+ M_StatTotal_Orgs.Update(statsQuery.Result.OrgCount)
+ }
+}
+
func sendUsageStats() {
if !setting.ReportingEnabled {
return
diff --git a/pkg/metrics/timer.go b/pkg/metrics/timer.go
index a22d61c408e..61c3bf9533d 100644
--- a/pkg/metrics/timer.go
+++ b/pkg/metrics/timer.go
@@ -222,7 +222,8 @@ func (t *StandardTimer) Update(d time.Duration) {
func (t *StandardTimer) UpdateSince(ts time.Time) {
t.mutex.Lock()
defer t.mutex.Unlock()
- t.histogram.Update(int64(time.Since(ts)))
+ sinceMs := time.Since(ts) / time.Millisecond
+ t.histogram.Update(int64(sinceMs))
t.meter.Mark(1)
}
diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go
index c6405ef80f9..9bed7cbe16b 100644
--- a/pkg/middleware/logger.go
+++ b/pkg/middleware/logger.go
@@ -49,9 +49,9 @@ func Logger() macaron.Handler {
if ctx, ok := c.Data["ctx"]; ok {
ctxTyped := ctx.(*Context)
if status == 500 {
- ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size())
+ ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ms", timeTakenMs, "size", rw.Size())
} else {
- ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size())
+ ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ms", timeTakenMs, "size", rw.Size())
}
}
}
diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go
index 5d52c68722e..df1768e1c3a 100644
--- a/pkg/middleware/middleware.go
+++ b/pkg/middleware/middleware.go
@@ -208,15 +208,6 @@ func (ctx *Context) Handle(status int, title string, err error) {
}
}
- switch status {
- case 200:
- metrics.M_Page_Status_200.Inc(1)
- case 404:
- metrics.M_Page_Status_404.Inc(1)
- case 500:
- metrics.M_Page_Status_500.Inc(1)
- }
-
ctx.Data["Title"] = title
ctx.HTML(status, strconv.Itoa(status))
}
@@ -243,10 +234,8 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) {
switch status {
case 404:
- metrics.M_Api_Status_404.Inc(1)
resp["message"] = "Not Found"
case 500:
- metrics.M_Api_Status_500.Inc(1)
resp["message"] = "Internal Server Error"
}
diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go
new file mode 100644
index 00000000000..417a1817d15
--- /dev/null
+++ b/pkg/middleware/request_metrics.go
@@ -0,0 +1,65 @@
+package middleware
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/grafana/grafana/pkg/metrics"
+ "gopkg.in/macaron.v1"
+)
+
+func RequestMetrics() macaron.Handler {
+ return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
+ rw := res.(macaron.ResponseWriter)
+ c.Next()
+
+ status := rw.Status()
+
+ if strings.HasPrefix(req.RequestURI, "/api/datasources/proxy") {
+ countProxyRequests(status)
+ } else if strings.HasPrefix(req.RequestURI, "/api/") {
+ countApiRequests(status)
+ } else {
+ countPageRequests(status)
+ }
+ }
+}
+
+func countApiRequests(status int) {
+ switch status {
+ case 200:
+ metrics.M_Api_Status_200.Inc(1)
+ case 404:
+ metrics.M_Api_Status_404.Inc(1)
+ case 500:
+ metrics.M_Api_Status_500.Inc(1)
+ default:
+ metrics.M_Api_Status_Unknown.Inc(1)
+ }
+}
+
+func countPageRequests(status int) {
+ switch status {
+ case 200:
+ metrics.M_Page_Status_200.Inc(1)
+ case 404:
+ metrics.M_Page_Status_404.Inc(1)
+ case 500:
+ metrics.M_Page_Status_500.Inc(1)
+ default:
+ metrics.M_Page_Status_Unknown.Inc(1)
+ }
+}
+
+func countProxyRequests(status int) {
+ switch status {
+ case 200:
+ metrics.M_Proxy_Status_200.Inc(1)
+ case 404:
+ metrics.M_Proxy_Status_404.Inc(1)
+ case 500:
+ metrics.M_Proxy_Status_500.Inc(1)
+ default:
+ metrics.M_Proxy_Status_Unknown.Inc(1)
+ }
+}
diff --git a/pkg/models/alert.go b/pkg/models/alert.go
index b7d0c3e0c1c..bf21b8e5ec6 100644
--- a/pkg/models/alert.go
+++ b/pkg/models/alert.go
@@ -10,27 +10,15 @@ type AlertStateType string
type AlertSeverityType string
const (
- AlertStatePending AlertStateType = "pending"
- AlertStateExeuctionError AlertStateType = "execution_error"
- AlertStatePaused AlertStateType = "paused"
- AlertStateCritical AlertStateType = "critical"
- AlertStateWarning AlertStateType = "warning"
- AlertStateOK AlertStateType = "ok"
+ AlertStateNoData AlertStateType = "no_data"
+ AlertStateExecError AlertStateType = "execution_error"
+ AlertStatePaused AlertStateType = "paused"
+ AlertStateAlerting AlertStateType = "alerting"
+ AlertStateOK AlertStateType = "ok"
)
func (s AlertStateType) IsValid() bool {
- return s == AlertStateOK || s == AlertStatePending || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
-}
-
-const (
- AlertSeverityCritical AlertSeverityType = "critical"
- AlertSeverityWarning AlertSeverityType = "warning"
- AlertSeverityInfo AlertSeverityType = "info"
- AlertSeverityOK AlertSeverityType = "ok"
-)
-
-func (s AlertSeverityType) IsValid() bool {
- return s == AlertSeverityCritical || s == AlertSeverityInfo || s == AlertSeverityWarning
+ return s == AlertStateOK || s == AlertStateNoData || s == AlertStateExecError || s == AlertStatePaused
}
type Alert struct {
@@ -41,7 +29,7 @@ type Alert struct {
PanelId int64
Name string
Message string
- Severity AlertSeverityType
+ Severity string
State AlertStateType
Handler int64
Silenced bool
@@ -114,10 +102,12 @@ type SaveAlertsCommand struct {
}
type SetAlertStateCommand struct {
- AlertId int64
- OrgId int64
- State AlertStateType
- Error string
+ AlertId int64
+ OrgId int64
+ State AlertStateType
+ Error string
+ EvalData *simplejson.Json
+
Timestamp time.Time
}
@@ -131,6 +121,7 @@ type GetAlertsQuery struct {
State []string
DashboardId int64
PanelId int64
+ Limit int64
Result []*Alert
}
diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go
index 464d6dc88da..87b515f370c 100644
--- a/pkg/models/alert_notifications.go
+++ b/pkg/models/alert_notifications.go
@@ -7,29 +7,32 @@ import (
)
type AlertNotification struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"-"`
- Name string `json:"name"`
- Type string `json:"type"`
- Settings *simplejson.Json `json:"settings"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ Id int64 `json:"id"`
+ OrgId int64 `json:"-"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
}
type CreateAlertNotificationCommand struct {
- Name string `json:"name" binding:"Required"`
- Type string `json:"type" binding:"Required"`
- Settings *simplejson.Json `json:"settings"`
+ Name string `json:"name" binding:"Required"`
+ Type string `json:"type" binding:"Required"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings"`
OrgId int64 `json:"-"`
Result *AlertNotification
}
type UpdateAlertNotificationCommand struct {
- Id int64 `json:"id" binding:"Required"`
- Name string `json:"name" binding:"Required"`
- Type string `json:"type" binding:"Required"`
- Settings *simplejson.Json `json:"settings" binding:"Required"`
+ Id int64 `json:"id" binding:"Required"`
+ Name string `json:"name" binding:"Required"`
+ Type string `json:"type" binding:"Required"`
+ IsDefault bool `json:"isDefault"`
+ Settings *simplejson.Json `json:"settings" binding:"Required"`
OrgId int64 `json:"-"`
Result *AlertNotification
@@ -43,8 +46,20 @@ type DeleteAlertNotificationCommand struct {
type GetAlertNotificationsQuery struct {
Name string
Id int64
+ OrgId int64
+
+ Result *AlertNotification
+}
+
+type GetAlertNotificationsToSendQuery struct {
Ids []int64
OrgId int64
Result []*AlertNotification
}
+
+type GetAllAlertNotificationsQuery struct {
+ OrgId int64
+
+ Result []*AlertNotification
+}
diff --git a/pkg/models/alert_state.go b/pkg/models/alert_state.go
deleted file mode 100644
index 5071efc2171..00000000000
--- a/pkg/models/alert_state.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package models
-
-// type AlertState struct {
-// Id int64 `json:"-"`
-// OrgId int64 `json:"-"`
-// AlertId int64 `json:"alertId"`
-// State string `json:"state"`
-// Created time.Time `json:"created"`
-// Info string `json:"info"`
-// TriggeredAlerts *simplejson.Json `json:"triggeredAlerts"`
-// }
-//
-// func (this *UpdateAlertStateCommand) IsValidState() bool {
-// for _, v := range alertstates.ValidStates {
-// if this.State == v {
-// return true
-// }
-// }
-// return false
-// }
-//
-// // Commands
-//
-// type UpdateAlertStateCommand struct {
-// AlertId int64 `json:"alertId" binding:"Required"`
-// OrgId int64 `json:"orgId" binding:"Required"`
-// State string `json:"state" binding:"Required"`
-// Info string `json:"info"`
-//
-// Result *Alert
-// }
-//
-// // Queries
-//
-// type GetAlertsStateQuery struct {
-// OrgId int64 `json:"orgId" binding:"Required"`
-// AlertId int64 `json:"alertId" binding:"Required"`
-//
-// Result *[]AlertState
-// }
-//
-// type GetLastAlertStateQuery struct {
-// AlertId int64
-// OrgId int64
-//
-// Result *AlertState
-// }
diff --git a/pkg/models/models.go b/pkg/models/models.go
index 189e594576b..5a53cfdabb3 100644
--- a/pkg/models/models.go
+++ b/pkg/models/models.go
@@ -6,4 +6,5 @@ const (
GITHUB OAuthType = iota + 1
GOOGLE
TWITTER
+ GENERIC
)
diff --git a/pkg/models/playlist.go b/pkg/models/playlist.go
index 4c6eacbb6a6..5c49bb9256c 100644
--- a/pkg/models/playlist.go
+++ b/pkg/models/playlist.go
@@ -57,17 +57,6 @@ func (this PlaylistDashboard) TableName() string {
type Playlists []*Playlist
type PlaylistDashboards []*PlaylistDashboard
-//
-// DTOS
-//
-
-type PlaylistDashboardDto struct {
- Id int64 `json:"id"`
- Slug string `json:"slug"`
- Title string `json:"title"`
- Uri string `json:"uri"`
-}
-
//
// COMMANDS
//
diff --git a/pkg/models/stats.go b/pkg/models/stats.go
index fa9cfdab6e8..067dec763e5 100644
--- a/pkg/models/stats.go
+++ b/pkg/models/stats.go
@@ -1,10 +1,10 @@
package models
type SystemStats struct {
- DashboardCount int
- UserCount int
- OrgCount int
- PlaylistCount int
+ DashboardCount int64
+ UserCount int64
+ OrgCount int64
+ PlaylistCount int64
}
type DataSourceStats struct {
diff --git a/pkg/services/alerting/conditions/common.go b/pkg/services/alerting/conditions/common.go
deleted file mode 100644
index 06702fd1e08..00000000000
--- a/pkg/services/alerting/conditions/common.go
+++ /dev/null
@@ -1 +0,0 @@
-package conditions
diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go
index 8c28a278d9f..18a2bf35262 100644
--- a/pkg/services/alerting/conditions/evaluator.go
+++ b/pkg/services/alerting/conditions/evaluator.go
@@ -5,25 +5,21 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting"
- "github.com/grafana/grafana/pkg/tsdb"
)
var (
- defaultTypes []string = []string{"gt", "lt"}
- rangedTypes []string = []string{"within_range", "outside_range"}
- paramlessTypes []string = []string{"no_value"}
+ defaultTypes []string = []string{"gt", "lt"}
+ rangedTypes []string = []string{"within_range", "outside_range"}
)
type AlertEvaluator interface {
- Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool
+ Eval(reducedValue *float64) bool
}
-type ParameterlessEvaluator struct {
- Type string
-}
+type NoDataEvaluator struct{}
-func (e *ParameterlessEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
- return len(series.Points) == 0
+func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
+ return reducedValue == nil
}
type ThresholdEvaluator struct {
@@ -47,14 +43,16 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
return defaultEval, nil
}
-func (e *ThresholdEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
+func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
+ if reducedValue == nil {
+ return false
+ }
+
switch e.Type {
case "gt":
- return reducedValue > e.Threshold
+ return *reducedValue > e.Threshold
case "lt":
- return reducedValue < e.Threshold
- case "no_value":
- return len(series.Points) == 0
+ return *reducedValue < e.Threshold
}
return false
@@ -88,12 +86,16 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
return rangedEval, nil
}
-func (e *RangedEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool {
+func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
+ if reducedValue == nil {
+ return false
+ }
+
switch e.Type {
case "within_range":
- return (e.Lower < reducedValue && e.Upper > reducedValue) || (e.Upper < reducedValue && e.Lower > reducedValue)
+ return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
case "outside_range":
- return (e.Upper < reducedValue && e.Lower < reducedValue) || (e.Upper > reducedValue && e.Lower > reducedValue)
+ return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
}
return false
@@ -113,8 +115,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
return newRangedEvaluator(typ, model)
}
- if inSlice(typ, paramlessTypes) {
- return &ParameterlessEvaluator{Type: typ}, nil
+ if typ == "no_data" {
+ return &NoDataEvaluator{}, nil
}
return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}
diff --git a/pkg/services/alerting/conditions/evaluator_test.go b/pkg/services/alerting/conditions/evaluator_test.go
index 8cc6899ff81..d2919f37d9d 100644
--- a/pkg/services/alerting/conditions/evaluator_test.go
+++ b/pkg/services/alerting/conditions/evaluator_test.go
@@ -4,7 +4,6 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
@@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
evaluator, err := NewAlertEvaluator(jsonModel)
So(err, ShouldBeNil)
- var timeserie [][2]float64
- dummieTimestamp := float64(521452145)
-
- for _, v := range datapoints {
- timeserie = append(timeserie, [2]float64{v, dummieTimestamp})
- }
-
- tsdb := &tsdb.TimeSeries{
- Name: "test time serie",
- Points: timeserie,
- }
-
- return evaluator.Eval(tsdb, reducedValue)
+ return evaluator.Eval(&reducedValue)
}
func TestEvalutors(t *testing.T) {
@@ -55,8 +42,15 @@ func TestEvalutors(t *testing.T) {
So(evalutorScenario(`{"type": "outside_range", "params": [100, 1] }`, 50), ShouldBeFalse)
})
- Convey("no_value", t, func() {
- So(evalutorScenario(`{"type": "no_value", "params": [] }`, 1000), ShouldBeTrue)
- So(evalutorScenario(`{"type": "no_value", "params": [] }`, 1000, 1, 2), ShouldBeFalse)
+ Convey("no_data", t, func() {
+ So(evalutorScenario(`{"type": "no_data", "params": [] }`, 50), ShouldBeFalse)
+
+ jsonModel, err := simplejson.NewJson([]byte(`{"type": "no_data", "params": [] }`))
+ So(err, ShouldBeNil)
+
+ evaluator, err := NewAlertEvaluator(jsonModel)
+ So(err, ShouldBeNil)
+
+ So(evaluator.Eval(nil), ShouldBeTrue)
})
}
diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go
index 0a661e8aaab..f966985f132 100644
--- a/pkg/services/alerting/conditions/query.go
+++ b/pkg/services/alerting/conditions/query.go
@@ -38,25 +38,32 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
return
}
+ emptySerieCount := 0
for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
- evalMatch := c.Evaluator.Eval(series, reducedValue)
+ evalMatch := c.Evaluator.Eval(reducedValue)
+
+ if reducedValue == nil {
+ emptySerieCount++
+ continue
+ }
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
- Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue),
+ Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
})
}
if evalMatch {
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
Metric: series.Name,
- Value: reducedValue,
+ Value: *reducedValue,
})
}
-
- context.Firing = evalMatch
}
+
+ context.NoDataFound = emptySerieCount == len(seriesList)
+ context.Firing = len(context.EvalMatches) > 0
}
func (c *QueryCondition) executeQuery(context *alerting.EvalContext) (tsdb.TimeSeriesSlice, error) {
diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go
index 07cc6871801..983e75c4c1b 100644
--- a/pkg/services/alerting/conditions/query_test.go
+++ b/pkg/services/alerting/conditions/query_test.go
@@ -41,7 +41,9 @@ func TestQueryCondition(t *testing.T) {
})
Convey("should fire when avg is above 100", func() {
- ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{120, 0}})}
+ one := float64(120)
+ two := float64(0)
+ ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
ctx.exec()
So(ctx.result.Error, ShouldBeNil)
@@ -49,12 +51,65 @@ func TestQueryCondition(t *testing.T) {
})
Convey("Should not fire when avg is below 100", func() {
- ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]float64{{90, 0}})}
+ one := float64(90)
+ two := float64(0)
+ ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}})}
ctx.exec()
So(ctx.result.Error, ShouldBeNil)
So(ctx.result.Firing, ShouldBeFalse)
})
+
+ Convey("Should fire if only first serie matches", func() {
+ one := float64(120)
+ two := float64(0)
+ ctx.series = tsdb.TimeSeriesSlice{
+ tsdb.NewTimeSeries("test1", [][2]*float64{{&one, &two}}),
+ tsdb.NewTimeSeries("test2", [][2]*float64{{&two, &two}}),
+ }
+ ctx.exec()
+
+ So(ctx.result.Error, ShouldBeNil)
+ So(ctx.result.Firing, ShouldBeTrue)
+ })
+
+ Convey("Empty series", func() {
+ Convey("Should set NoDataFound both series are empty", func() {
+ ctx.series = tsdb.TimeSeriesSlice{
+ tsdb.NewTimeSeries("test1", [][2]*float64{}),
+ tsdb.NewTimeSeries("test2", [][2]*float64{}),
+ }
+ ctx.exec()
+
+ So(ctx.result.Error, ShouldBeNil)
+ So(ctx.result.NoDataFound, ShouldBeTrue)
+ })
+
+ Convey("Should set NoDataFound both series contains null", func() {
+ one := float64(120)
+ ctx.series = tsdb.TimeSeriesSlice{
+ tsdb.NewTimeSeries("test1", [][2]*float64{{nil, &one}}),
+ tsdb.NewTimeSeries("test2", [][2]*float64{{nil, &one}}),
+ }
+ ctx.exec()
+
+ So(ctx.result.Error, ShouldBeNil)
+ So(ctx.result.NoDataFound, ShouldBeTrue)
+ })
+
+ Convey("Should not set NoDataFound if one serie is empty", func() {
+ one := float64(120)
+ two := float64(0)
+ ctx.series = tsdb.TimeSeriesSlice{
+ tsdb.NewTimeSeries("test1", [][2]*float64{}),
+ tsdb.NewTimeSeries("test2", [][2]*float64{{&one, &two}}),
+ }
+ ctx.exec()
+
+ So(ctx.result.Error, ShouldBeNil)
+ So(ctx.result.NoDataFound, ShouldBeFalse)
+ })
+ })
})
})
}
diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go
index f97ff6a56d7..2bb4cec00be 100644
--- a/pkg/services/alerting/conditions/reducer.go
+++ b/pkg/services/alerting/conditions/reducer.go
@@ -1,52 +1,73 @@
package conditions
-import "github.com/grafana/grafana/pkg/tsdb"
+import (
+ "math"
+
+ "github.com/grafana/grafana/pkg/tsdb"
+)
type QueryReducer interface {
- Reduce(timeSeries *tsdb.TimeSeries) float64
+ Reduce(timeSeries *tsdb.TimeSeries) *float64
}
type SimpleReducer struct {
Type string
}
-func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 {
- var value float64 = 0
+func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
+ if len(series.Points) == 0 {
+ return nil
+ }
+
+ value := float64(0)
+ allNull := true
switch s.Type {
case "avg":
for _, point := range series.Points {
- value += point[0]
+ if point[0] != nil {
+ value += *point[0]
+ allNull = false
+ }
}
value = value / float64(len(series.Points))
case "sum":
for _, point := range series.Points {
- value += point[0]
+ if point[0] != nil {
+ value += *point[0]
+ allNull = false
+ }
}
case "min":
- for i, point := range series.Points {
- if i == 0 {
- value = point[0]
- }
-
- if value > point[0] {
- value = point[0]
+ value = math.MaxFloat64
+ for _, point := range series.Points {
+ if point[0] != nil {
+ allNull = false
+ if value > *point[0] {
+ value = *point[0]
+ }
}
}
case "max":
+ value = -math.MaxFloat64
for _, point := range series.Points {
- if value < point[0] {
- value = point[0]
+ if point[0] != nil {
+ allNull = false
+ if value < *point[0] {
+ value = *point[0]
+ }
}
}
- case "mean":
- meanPosition := int64(len(series.Points) / 2)
- value = series.Points[meanPosition][0]
case "count":
value = float64(len(series.Points))
+ allNull = false
}
- return value
+ if allNull {
+ return nil
+ }
+
+ return &value
}
func NewSimpleReducer(typ string) *SimpleReducer {
diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go
index af242bbd668..f60154bc98d 100644
--- a/pkg/services/alerting/conditions/reducer_test.go
+++ b/pkg/services/alerting/conditions/reducer_test.go
@@ -10,44 +10,39 @@ import (
func TestSimpleReducer(t *testing.T) {
Convey("Test simple reducer by calculating", t, func() {
Convey("avg", func() {
- result := testReducer("avg", 1, 2, 3)
+ result := *testReducer("avg", 1, 2, 3)
So(result, ShouldEqual, float64(2))
})
Convey("sum", func() {
- result := testReducer("sum", 1, 2, 3)
+ result := *testReducer("sum", 1, 2, 3)
So(result, ShouldEqual, float64(6))
})
Convey("min", func() {
- result := testReducer("min", 3, 2, 1)
+ result := *testReducer("min", 3, 2, 1)
So(result, ShouldEqual, float64(1))
})
Convey("max", func() {
- result := testReducer("max", 1, 2, 3)
+ result := *testReducer("max", 1, 2, 3)
So(result, ShouldEqual, float64(3))
})
- Convey("mean odd numbers", func() {
- result := testReducer("mean", 1, 2, 3000)
- So(result, ShouldEqual, float64(2))
- })
-
Convey("count", func() {
- result := testReducer("count", 1, 2, 3000)
+ result := *testReducer("count", 1, 2, 3000)
So(result, ShouldEqual, float64(3))
})
})
}
-func testReducer(typ string, datapoints ...float64) float64 {
+func testReducer(typ string, datapoints ...float64) *float64 {
reducer := NewSimpleReducer(typ)
- var timeserie [][2]float64
+ var timeserie [][2]*float64
dummieTimestamp := float64(521452145)
- for _, v := range datapoints {
- timeserie = append(timeserie, [2]float64{v, dummieTimestamp})
+ for idx := range datapoints {
+ timeserie = append(timeserie, [2]*float64{&datapoints[idx], &dummieTimestamp})
}
tsdb := &tsdb.TimeSeries{
diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go
index 29c51e02abd..13067c25f08 100644
--- a/pkg/services/alerting/eval_context.go
+++ b/pkg/services/alerting/eval_context.go
@@ -26,6 +26,8 @@ type EvalContext struct {
dashboardSlug string
ImagePublicUrl string
ImageOnDiskPath string
+ NoDataFound bool
+ RetryCount int
}
type StateDescription struct {
@@ -35,30 +37,29 @@ type StateDescription struct {
}
func (c *EvalContext) GetStateModel() *StateDescription {
- if c.Error != nil {
- return &StateDescription{
- Color: "#D63232",
- Text: "EXECUTION ERROR",
- }
- }
-
- if !c.Firing {
+ switch c.Rule.State {
+ case m.AlertStateOK:
return &StateDescription{
Color: "#36a64f",
Text: "OK",
}
- }
-
- if c.Rule.Severity == m.AlertSeverityWarning {
+ case m.AlertStateNoData:
return &StateDescription{
- Color: "#fd821b",
- Text: "WARNING",
+ Color: "#888888",
+ Text: "No Data",
}
- } else {
+ case m.AlertStateExecError:
+ return &StateDescription{
+ Color: "#000",
+ Text: "Execution Error",
+ }
+ case m.AlertStateAlerting:
return &StateDescription{
Color: "#D63232",
- Text: "CRITICAL",
+ Text: "Alerting",
}
+ default:
+ panic("Unknown rule state " + c.Rule.State)
}
}
@@ -111,5 +112,6 @@ func NewEvalContext(rule *Rule) *EvalContext {
DoneChan: make(chan bool, 1),
CancelChan: make(chan bool, 1),
log: log.New("alerting.evalContext"),
+ RetryCount: 0,
}
}
diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go
index 3abcd978064..ab4c377197b 100644
--- a/pkg/services/alerting/eval_handler.go
+++ b/pkg/services/alerting/eval_handler.go
@@ -8,6 +8,10 @@ import (
"github.com/grafana/grafana/pkg/metrics"
)
+var (
+ MaxRetries int = 1
+)
+
type DefaultEvalHandler struct {
log log.Logger
alertJobTimeout time.Duration
@@ -16,26 +20,48 @@ type DefaultEvalHandler struct {
func NewEvalHandler() *DefaultEvalHandler {
return &DefaultEvalHandler{
log: log.New("alerting.evalHandler"),
- alertJobTimeout: time.Second * 5,
+ alertJobTimeout: time.Second * 10,
}
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
-
go e.eval(context)
select {
case <-time.After(e.alertJobTimeout):
- context.Error = fmt.Errorf("Timeout")
+ 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)
+ e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id, "timeout setting", e.alertJobTimeout)
+ e.retry(context)
case <-context.DoneChan:
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
- }
+ if context.Error != nil {
+ e.retry(context)
+ }
+ }
+}
+
+func (e *DefaultEvalHandler) retry(context *EvalContext) {
+ e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
+
+ if context.RetryCount < MaxRetries {
+ context.DoneChan = make(chan bool, 1)
+ context.CancelChan = make(chan bool, 1)
+ context.RetryCount++
+ e.Eval(context)
+ }
}
func (e *DefaultEvalHandler) eval(context *EvalContext) {
+ defer func() {
+ if err := recover(); err != nil {
+ e.log.Error("Alerting rule eval panic", "error", err, "stack", log.Stack(1))
+ if panicErr, ok := err.(error); ok {
+ context.Error = panicErr
+ }
+ }
+ }()
for _, condition := range context.Rule.Conditions {
condition.Eval(context)
@@ -52,7 +78,7 @@ func (e *DefaultEvalHandler) eval(context *EvalContext) {
}
context.EndTime = time.Now()
- elapsedTime := context.EndTime.Sub(context.StartTime)
+ elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
context.DoneChan <- true
}
diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go
index 039e9be9a30..ae5b4e4501d 100644
--- a/pkg/services/alerting/eval_handler_test.go
+++ b/pkg/services/alerting/eval_handler_test.go
@@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) {
handler.eval(context)
So(context.Firing, ShouldEqual, false)
})
-
})
}
diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go
index 7d3b800fb31..cf516786412 100644
--- a/pkg/services/alerting/extractor.go
+++ b/pkg/services/alerting/extractor.go
@@ -88,14 +88,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
Name: jsonAlert.Get("name").MustString(),
Handler: jsonAlert.Get("handler").MustInt64(),
Message: jsonAlert.Get("message").MustString(),
- Severity: m.AlertSeverityType(jsonAlert.Get("severity").MustString()),
Frequency: getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()),
}
- if !alert.Severity.IsValid() {
- return nil, ValidationError{Reason: "Invalid alert Severity"}
- }
-
for _, condition := range jsonAlert.Get("conditions").MustArray() {
jsonCondition := simplejson.NewFromAny(condition)
diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go
index e3df5e571d1..b25ce313b54 100644
--- a/pkg/services/alerting/extractor_test.go
+++ b/pkg/services/alerting/extractor_test.go
@@ -44,7 +44,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"handler": 1,
"enabled": true,
"frequency": "60s",
- "severity": "critical",
"conditions": [
{
"type": "query",
@@ -129,11 +128,6 @@ func TestAlertRuleExtraction(t *testing.T) {
So(alerts[1].Handler, ShouldEqual, 0)
})
- Convey("should extract Severity property", func() {
- So(alerts[0].Severity, ShouldEqual, "critical")
- So(alerts[1].Severity, ShouldEqual, "warning")
- })
-
Convey("should extract frequency in seconds", func() {
So(alerts[0].Frequency, ShouldEqual, 60)
So(alerts[1].Frequency, ShouldEqual, 60)
diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go
index 9688aba153a..78ffc280375 100644
--- a/pkg/services/alerting/interfaces.go
+++ b/pkg/services/alerting/interfaces.go
@@ -1,6 +1,8 @@
package alerting
-import "time"
+import (
+ "time"
+)
type EvalHandler interface {
Eval(context *EvalContext)
@@ -15,6 +17,7 @@ type Notifier interface {
Notify(alertResult *EvalContext)
GetType() string
NeedsImage() bool
+ PassesFilter(rule *Rule) bool
}
type Condition interface {
diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go
index e11e1e1aaaf..39e7eb83166 100644
--- a/pkg/services/alerting/models.go
+++ b/pkg/services/alerting/models.go
@@ -1,10 +1,11 @@
package alerting
type Job struct {
- Offset int64
- Delay bool
- Running bool
- Rule *Rule
+ Offset int64
+ OffsetWait bool
+ Delay bool
+ Running bool
+ Rule *Rule
}
type ResultLogEntry struct {
diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go
index c345594de2c..bb48f71cdcd 100644
--- a/pkg/services/alerting/notifier.go
+++ b/pkg/services/alerting/notifier.go
@@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool {
return false
}
+func (n *RootNotifier) PassesFilter(rule *Rule) bool {
+ return false
+}
+
func (n *RootNotifier) Notify(context *EvalContext) {
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
- notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
+ notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil {
n.log.Error("Failed to read notifications", "error", err)
return
@@ -46,6 +50,10 @@ func (n *RootNotifier) Notify(context *EvalContext) {
n.log.Error("Failed to upload alert panel image", "error", err)
}
+ n.sendNotifications(notifiers, context)
+}
+
+func (n *RootNotifier) sendNotifications(notifiers []Notifier, context *EvalContext) {
for _, notifier := range notifiers {
n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
go notifier.Notify(context)
@@ -53,7 +61,6 @@ func (n *RootNotifier) Notify(context *EvalContext) {
}
func (n *RootNotifier) uploadImage(context *EvalContext) error {
-
uploader, _ := imguploader.NewImageUploader()
imageUrl, err := context.GetImageUrl()
@@ -84,29 +91,28 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
return nil
}
-func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
- if len(notificationIds) == 0 {
- return []Notifier{}, nil
- }
+func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
+ query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
- query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil {
return nil, err
}
var result []Notifier
for _, notification := range query.Result {
- if not, err := n.getNotifierFor(notification); err != nil {
+ if not, err := n.createNotifierFor(notification); err != nil {
return nil, err
} else {
- result = append(result, not)
+ if shouldUseNotification(not, context) {
+ result = append(result, not)
+ }
}
}
return result, nil
}
-func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) {
+func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
factory, found := notifierFactories[model.Type]
if !found {
return nil, errors.New("Unsupported notification type")
@@ -115,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err
return factory(model)
}
+func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
+ if !context.Firing {
+ return true
+ }
+
+ if context.Error != nil {
+ return true
+ }
+
+ return notifier.PassesFilter(context.Rule)
+}
+
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go
index ebe305c6174..c854d8475b5 100644
--- a/pkg/services/alerting/notifier_test.go
+++ b/pkg/services/alerting/notifier_test.go
@@ -1,114 +1,81 @@
package alerting
-// func TestAlertNotificationExtraction(t *testing.T) {
-// Convey("Notifier tests", t, func() {
-// Convey("rules for sending notifications", func() {
-// dummieNotifier := NotifierImpl{}
-//
-// result := &AlertResult{
-// State: alertstates.Critical,
-// }
-//
-// notifier := &Notification{
-// Name: "Test Notifier",
-// Type: "TestType",
-// SendCritical: true,
-// SendWarning: true,
-// }
-//
-// Convey("Should send notification", func() {
-// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue)
-// })
-//
-// Convey("warn:false and state:warn should not send", func() {
-// result.State = alertstates.Warn
-// notifier.SendWarning = false
-// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse)
-// })
-// })
-//
-// Convey("Parsing alert notification from settings", func() {
-// Convey("Parsing email", func() {
-// Convey("empty settings should return error", func() {
-// json := `{ }`
-//
-// settingsJSON, _ := simplejson.NewJson([]byte(json))
-// model := &m.AlertNotification{
-// Name: "ops",
-// Type: "email",
-// Settings: settingsJSON,
-// }
-//
-// _, err := NewNotificationFromDBModel(model)
-// So(err, ShouldNotBeNil)
-// })
-//
-// Convey("from settings", func() {
-// json := `
-// {
-// "to": "ops@grafana.org"
-// }`
-//
-// settingsJSON, _ := simplejson.NewJson([]byte(json))
-// model := &m.AlertNotification{
-// Name: "ops",
-// Type: "email",
-// Settings: settingsJSON,
-// }
-//
-// not, err := NewNotificationFromDBModel(model)
-//
-// So(err, ShouldBeNil)
-// So(not.Name, ShouldEqual, "ops")
-// So(not.Type, ShouldEqual, "email")
-// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier")
-//
-// email := not.Notifierr.(*EmailNotifier)
-// So(email.To, ShouldEqual, "ops@grafana.org")
-// })
-// })
-//
-// Convey("Parsing webhook", func() {
-// Convey("empty settings should return error", func() {
-// json := `{ }`
-//
-// settingsJSON, _ := simplejson.NewJson([]byte(json))
-// model := &m.AlertNotification{
-// Name: "ops",
-// Type: "webhook",
-// Settings: settingsJSON,
-// }
-//
-// _, err := NewNotificationFromDBModel(model)
-// So(err, ShouldNotBeNil)
-// })
-//
-// Convey("from settings", func() {
-// json := `
-// {
-// "url": "http://localhost:3000",
-// "username": "username",
-// "password": "password"
-// }`
-//
-// settingsJSON, _ := simplejson.NewJson([]byte(json))
-// model := &m.AlertNotification{
-// Name: "slack",
-// Type: "webhook",
-// Settings: settingsJSON,
-// }
-//
-// not, err := NewNotificationFromDBModel(model)
-//
-// So(err, ShouldBeNil)
-// So(not.Name, ShouldEqual, "slack")
-// So(not.Type, ShouldEqual, "webhook")
-// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier")
-//
-// webhook := not.Notifierr.(*WebhookNotifier)
-// So(webhook.Url, ShouldEqual, "http://localhost:3000")
-// })
-// })
-// })
-// })
-// }
+import (
+ "testing"
+
+ "fmt"
+
+ "github.com/grafana/grafana/pkg/models"
+ m "github.com/grafana/grafana/pkg/models"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type FakeNotifier struct {
+ FakeMatchResult bool
+}
+
+func (fn *FakeNotifier) GetType() string {
+ return "FakeNotifier"
+}
+
+func (fn *FakeNotifier) NeedsImage() bool {
+ return true
+}
+
+func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
+
+func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
+ return fn.FakeMatchResult
+}
+
+func TestAlertNotificationExtraction(t *testing.T) {
+
+ Convey("Notifier tests", t, func() {
+ Convey("none firing alerts", func() {
+ ctx := &EvalContext{
+ Firing: false,
+ Rule: &Rule{
+ State: m.AlertStateAlerting,
+ },
+ }
+ notifier := &FakeNotifier{FakeMatchResult: false}
+
+ So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+ })
+
+ Convey("execution error cannot be ignored", func() {
+ ctx := &EvalContext{
+ Firing: true,
+ Error: fmt.Errorf("I used to be a programmer just like you"),
+ Rule: &Rule{
+ State: m.AlertStateOK,
+ },
+ }
+ notifier := &FakeNotifier{FakeMatchResult: false}
+
+ So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+ })
+
+ Convey("firing alert that match", func() {
+ ctx := &EvalContext{
+ Firing: true,
+ Rule: &Rule{
+ State: models.AlertStateAlerting,
+ },
+ }
+ notifier := &FakeNotifier{FakeMatchResult: true}
+
+ So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
+ })
+
+ Convey("firing alert that dont match", func() {
+ ctx := &EvalContext{
+ Firing: true,
+ Rule: &Rule{State: m.AlertStateOK},
+ }
+ notifier := &FakeNotifier{FakeMatchResult: false}
+
+ So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
+ })
+ })
+}
diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go
index 48fe6c4eaa5..27c2a625d17 100644
--- a/pkg/services/alerting/notifiers/base.go
+++ b/pkg/services/alerting/notifiers/base.go
@@ -1,10 +1,24 @@
package notifiers
+import (
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/services/alerting"
+)
+
type NotifierBase struct {
Name string
Type string
}
+func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase {
+ base := NotifierBase{Name: name, Type: notifierType}
+ return base
+}
+
+func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool {
+ return true
+}
+
func (n *NotifierBase) GetType() string {
return n.Type
}
diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go
new file mode 100644
index 00000000000..8cfc1ec3ae9
--- /dev/null
+++ b/pkg/services/alerting/notifiers/base_test.go
@@ -0,0 +1,30 @@
+package notifiers
+
+// import . "github.com/smartystreets/goconvey/convey"
+//
+// func TestBaseNotifier( t *testing.T ) {
+// Convey("Parsing base notification state", t, func() {
+//
+// Convey("matches", func() {
+// json := `
+// {
+// "states": "critical"
+// }`
+//
+// settingsJSON, _ := simplejson.NewJson([]byte(json))
+// not := NewNotifierBase("ops", "email", settingsJSON)
+// So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue)
+// })
+//
+// Convey("does not match", func() {
+// json := `
+// {
+// "severityFilter": "critical"
+// }`
+//
+// settingsJSON, _ := simplejson.NewJson([]byte(json))
+// not := NewNotifierBase("ops", "email", settingsJSON)
+// So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse)
+// })
+// })
+// }
diff --git a/pkg/services/alerting/notifiers/common.go b/pkg/services/alerting/notifiers/common.go
deleted file mode 100644
index 48b634c44d7..00000000000
--- a/pkg/services/alerting/notifiers/common.go
+++ /dev/null
@@ -1 +0,0 @@
-package notifiers
diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go
index d63ca7d13f5..eccd3ce9dfb 100644
--- a/pkg/services/alerting/notifiers/email.go
+++ b/pkg/services/alerting/notifiers/email.go
@@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &EmailNotifier{
- NotifierBase: NotifierBase{
- Name: model.Name,
- Type: model.Type,
- },
- Addresses: strings.Split(addressesString, "\n"),
- log: log.New("alerting.notifier.email"),
+ NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+ Addresses: strings.Split(addressesString, "\n"),
+ log: log.New("alerting.notifier.email"),
}, nil
}
@@ -50,16 +47,15 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
cmd := &m.SendEmailCommand{
Data: map[string]interface{}{
- "Title": context.GetNotificationTitle(),
- "State": context.Rule.State,
- "Name": context.Rule.Name,
- "Severity": context.Rule.Severity,
- "SeverityColor": context.GetStateModel().Color,
- "Message": context.Rule.Message,
- "RuleUrl": ruleUrl,
- "ImageLink": context.ImagePublicUrl,
- "AlertPageUrl": setting.AppUrl + "alerting",
- "EvalMatches": context.EvalMatches,
+ "Title": context.GetNotificationTitle(),
+ "State": context.Rule.State,
+ "Name": context.Rule.Name,
+ "StateModel": context.GetStateModel(),
+ "Message": context.Rule.Message,
+ "RuleUrl": ruleUrl,
+ "ImageLink": context.ImagePublicUrl,
+ "AlertPageUrl": setting.AppUrl + "alerting",
+ "EvalMatches": context.EvalMatches,
},
To: this.Addresses,
Template: "alert_notification.html",
diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go
index 837621e80bb..d0d67ca5a88 100644
--- a/pkg/services/alerting/notifiers/slack.go
+++ b/pkg/services/alerting/notifiers/slack.go
@@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &SlackNotifier{
- NotifierBase: NotifierBase{
- Name: model.Name,
- Type: model.Type,
- },
- Url: url,
- log: log.New("alerting.notifier.slack"),
+ NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+ Url: url,
+ log: log.New("alerting.notifier.slack"),
}, nil
}
diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go
index e725752778f..7e28b35cd0a 100644
--- a/pkg/services/alerting/notifiers/webhook.go
+++ b/pkg/services/alerting/notifiers/webhook.go
@@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &WebhookNotifier{
- NotifierBase: NotifierBase{
- Name: model.Name,
- Type: model.Type,
- },
- Url: url,
- User: model.Settings.Get("user").MustString(),
- Password: model.Settings.Get("password").MustString(),
- log: log.New("alerting.notifier.webhook"),
+ NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
+ Url: url,
+ User: model.Settings.Get("user").MustString(),
+ Password: model.Settings.Get("password").MustString(),
+ log: log.New("alerting.notifier.webhook"),
}, nil
}
@@ -48,7 +45,6 @@ func (this *WebhookNotifier) Notify(context *alerting.EvalContext) {
bodyJSON.Set("ruleId", context.Rule.Id)
bodyJSON.Set("ruleName", context.Rule.Name)
bodyJSON.Set("state", context.Rule.State)
- bodyJSON.Set("severity", context.Rule.Severity)
bodyJSON.Set("evalMatches", context.EvalMatches)
ruleUrl, err := context.GetRuleUrl()
diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go
index f22afb279d8..bb9f46d6084 100644
--- a/pkg/services/alerting/result_handler.go
+++ b/pkg/services/alerting/result_handler.go
@@ -34,14 +34,19 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
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
+ ctx.Rule.State = m.AlertStateExecError
exeuctionError = ctx.Error.Error()
annotationData.Set("errorMessage", exeuctionError)
} else if ctx.Firing {
- ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
+ ctx.Rule.State = m.AlertStateAlerting
annotationData = simplejson.NewFromAny(ctx.EvalMatches)
} else {
- ctx.Rule.State = m.AlertStateOK
+ // handle no data case
+ if ctx.NoDataFound {
+ ctx.Rule.State = ctx.Rule.NoDataState
+ } else {
+ ctx.Rule.State = m.AlertStateOK
+ }
}
countStateResult(ctx.Rule.State)
@@ -49,10 +54,11 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
handler.log.Info("New state change", "alertId", ctx.Rule.Id, "newState", ctx.Rule.State, "oldState", oldState)
cmd := &m.SetAlertStateCommand{
- AlertId: ctx.Rule.Id,
- OrgId: ctx.Rule.OrgId,
- State: ctx.Rule.State,
- Error: exeuctionError,
+ AlertId: ctx.Rule.Id,
+ OrgId: ctx.Rule.OrgId,
+ State: ctx.Rule.State,
+ Error: exeuctionError,
+ EvalData: annotationData,
}
if err := bus.Dispatch(cmd); err != nil {
@@ -61,15 +67,17 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
// save annotation
item := annotations.Item{
- OrgId: ctx.Rule.OrgId,
- Type: annotations.AlertType,
- AlertId: ctx.Rule.Id,
- Title: ctx.Rule.Name,
- Text: ctx.GetStateModel().Text,
- NewState: string(ctx.Rule.State),
- PrevState: string(oldState),
- Timestamp: time.Now(),
- Data: annotationData,
+ OrgId: ctx.Rule.OrgId,
+ DashboardId: ctx.Rule.DashboardId,
+ PanelId: ctx.Rule.PanelId,
+ Type: annotations.AlertType,
+ AlertId: ctx.Rule.Id,
+ Title: ctx.Rule.Name,
+ Text: ctx.GetStateModel().Text,
+ NewState: string(ctx.Rule.State),
+ PrevState: string(oldState),
+ Epoch: time.Now().Unix(),
+ Data: annotationData,
}
annotationRepo := annotations.GetRepository()
@@ -83,17 +91,15 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
func countStateResult(state m.AlertStateType) {
switch state {
- case m.AlertStateCritical:
- metrics.M_Alerting_Result_State_Critical.Inc(1)
- case m.AlertStateWarning:
- metrics.M_Alerting_Result_State_Warning.Inc(1)
+ case m.AlertStateAlerting:
+ metrics.M_Alerting_Result_State_Alerting.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)
+ case m.AlertStateNoData:
+ metrics.M_Alerting_Result_State_NoData.Inc(1)
+ case m.AlertStateExecError:
+ metrics.M_Alerting_Result_State_ExecError.Inc(1)
}
}
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index 8292041e04c..5f59b60b64f 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -18,8 +18,8 @@ type Rule struct {
Frequency int64
Name string
Message string
+ NoDataState m.AlertStateType
State m.AlertStateType
- Severity m.AlertSeverityType
Conditions []Condition
Notifications []int64
}
@@ -65,8 +65,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
model.Name = ruleDef.Name
model.Message = ruleDef.Message
model.Frequency = ruleDef.Frequency
- model.Severity = ruleDef.Severity
model.State = ruleDef.State
+ model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("no_data"))
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go
index 91b31d6d07f..622904ec3fc 100644
--- a/pkg/services/alerting/rule_test.go
+++ b/pkg/services/alerting/rule_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
- "github.com/grafana/grafana/pkg/models"
+ m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) {
"name": "name2",
"description": "desc2",
"handler": 0,
+ "noDataMode": "critical",
"enabled": true,
"frequency": "60s",
"conditions": [
@@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) {
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil)
- alert := &models.Alert{
+ alert := &m.Alert{
Id: 1,
OrgId: 1,
DashboardId: 1,
@@ -80,6 +81,11 @@ func TestAlertRuleModel(t *testing.T) {
Convey("Can read notifications", func() {
So(len(alertRule.Notifications), ShouldEqual, 2)
})
+ /*
+ Convey("Can read noDataMode", func() {
+ So(len(alertRule.NoDataMode), ShouldEqual, m.AlertStateCritical)
+ })
+ */
})
})
}
diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go
index ffac7ddb659..9d20796f3dc 100644
--- a/pkg/services/alerting/scheduler.go
+++ b/pkg/services/alerting/scheduler.go
@@ -1,6 +1,7 @@
package alerting
import (
+ "math"
"time"
"github.com/grafana/grafana/pkg/log"
@@ -34,8 +35,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
}
job.Rule = rule
- job.Offset = int64(i)
+ offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
+ job.Offset = int64(math.Floor(float64(offset) / 1000))
jobs[rule.Id] = job
}
@@ -46,9 +48,27 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
now := tickTime.Unix()
for _, job := range s.jobs {
- if now%job.Rule.Frequency == 0 && job.Running == false {
- s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name)
- execQueue <- job
+ if job.Running {
+ continue
+ }
+
+ if job.OffsetWait && now%job.Offset == 0 {
+ job.OffsetWait = false
+ s.enque(job, execQueue)
+ continue
+ }
+
+ if now%job.Rule.Frequency == 0 {
+ if job.Offset > 0 {
+ job.OffsetWait = true
+ } else {
+ s.enque(job, execQueue)
+ }
}
}
}
+
+func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
+ s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
+ execQueue <- job
+}
diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go
new file mode 100644
index 00000000000..de2cb981aaa
--- /dev/null
+++ b/pkg/services/alerting/test_notification.go
@@ -0,0 +1,77 @@
+package alerting
+
+import (
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/log"
+ m "github.com/grafana/grafana/pkg/models"
+)
+
+type NotificationTestCommand struct {
+ State m.AlertStateType
+ Name string
+ Type string
+ Settings *simplejson.Json
+}
+
+func init() {
+ bus.AddHandler("alerting", handleNotificationTestCommand)
+
+}
+
+func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
+ notifier := NewRootNotifier()
+
+ model := &m.AlertNotification{
+ Name: cmd.Name,
+ Type: cmd.Type,
+ Settings: cmd.Settings,
+ }
+
+ notifiers, err := notifier.createNotifierFor(model)
+
+ if err != nil {
+ log.Error2("Failed to create notifier", "error", err.Error())
+ return err
+ }
+
+ notifier.sendNotifications([]Notifier{notifiers}, createTestEvalContext())
+
+ return nil
+}
+
+func createTestEvalContext() *EvalContext {
+
+ testRule := &Rule{
+ DashboardId: 1,
+ PanelId: 1,
+ Name: "Test notification",
+ Message: "Someone is testing the alert notification within grafana.",
+ State: m.AlertStateAlerting,
+ }
+
+ ctx := NewEvalContext(testRule)
+ ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
+
+ ctx.IsTestRun = true
+ ctx.Firing = true
+ ctx.Error = nil
+ ctx.EvalMatches = evalMatchesBasedOnState()
+
+ return ctx
+}
+
+func evalMatchesBasedOnState() []*EvalMatch {
+ matches := make([]*EvalMatch, 0)
+ matches = append(matches, &EvalMatch{
+ Metric: "High value",
+ Value: 100,
+ })
+
+ matches = append(matches, &EvalMatch{
+ Metric: "Higher Value",
+ Value: 200,
+ })
+
+ return matches
+}
diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go
index 06be152ec31..189c3d823cf 100644
--- a/pkg/services/annotations/annotations.go
+++ b/pkg/services/annotations/annotations.go
@@ -1,10 +1,6 @@
package annotations
-import (
- "time"
-
- "github.com/grafana/grafana/pkg/components/simplejson"
-)
+import "github.com/grafana/grafana/pkg/components/simplejson"
type Repository interface {
Save(item *Item) error
@@ -12,9 +8,14 @@ type Repository interface {
}
type ItemQuery struct {
- OrgId int64 `json:"orgId"`
- Type ItemType `json:"type"`
- AlertId int64 `json:"alertId"`
+ OrgId int64 `json:"orgId"`
+ From int64 `json:"from"`
+ To int64 `json:"from"`
+ Type ItemType `json:"type"`
+ AlertId int64 `json:"alertId"`
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ NewState []string `json:"newState"`
Limit int64 `json:"alertId"`
}
@@ -36,17 +37,20 @@ const (
)
type Item struct {
- Id int64 `json:"id"`
- OrgId int64 `json:"orgId"`
- Type ItemType `json:"type"`
- Title string `json:"title"`
- Text string `json:"text"`
- Metric string `json:"metric"`
- AlertId int64 `json:"alertId"`
- UserId int64 `json:"userId"`
- PrevState string `json:"prevState"`
- NewState string `json:"newState"`
- Timestamp time.Time `json:"timestamp"`
+ Id int64 `json:"id"`
+ OrgId int64 `json:"orgId"`
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ CategoryId int64 `json:"panelId"`
+ Type ItemType `json:"type"`
+ Title string `json:"title"`
+ Text string `json:"text"`
+ Metric string `json:"metric"`
+ AlertId int64 `json:"alertId"`
+ UserId int64 `json:"userId"`
+ PrevState string `json:"prevState"`
+ NewState string `json:"newState"`
+ Epoch int64 `json:"epoch"`
Data *simplejson.Json `json:"data"`
}
diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go
index 309436cb7d9..91c75a1889e 100644
--- a/pkg/services/notifications/mailer.go
+++ b/pkg/services/notifications/mailer.go
@@ -12,6 +12,7 @@ import (
"net/smtp"
"os"
"strings"
+ "time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
@@ -66,7 +67,7 @@ func sendToSmtpServer(recipients []string, msgContent []byte) error {
tlsconfig.Certificates = []tls.Certificate{cert}
}
- conn, err := net.Dial("tcp", net.JoinHostPort(host, port))
+ conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), time.Second*10)
if err != nil {
return err
}
diff --git a/pkg/services/notifications/send_email_integration_test.go b/pkg/services/notifications/send_email_integration_test.go
index b91cc178240..1a4406dc13a 100644
--- a/pkg/services/notifications/send_email_integration_test.go
+++ b/pkg/services/notifications/send_email_integration_test.go
@@ -40,7 +40,7 @@ func TestEmailIntegrationTest(t *testing.T) {
"RuleUrl": "http://localhost:3000/dashboard/db/graphite-dashboard",
"ImageLink": "http://localhost:3000/render/dashboard-solo/db/graphite-dashboard?panelId=1&from=1471008499616&to=1471012099617&width=1000&height=500",
"AlertPageUrl": "http://localhost:3000/alerting",
- "Events": []map[string]string{
+ "EvalMatches": []map[string]string{
{
"Metric": "desktop",
"Value": "40",
diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go
index be67749a627..5a430238823 100644
--- a/pkg/services/sqlstore/alert.go
+++ b/pkg/services/sqlstore/alert.go
@@ -87,6 +87,13 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
sql.WriteString(")")
}
+ if query.Limit != 0 {
+ sql.WriteString(" LIMIT ?")
+ params = append(params, query.Limit)
+ }
+
+ sql.WriteString("ORDER BY name ASC")
+
alerts := make([]*m.Alert, 0)
if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
return err
@@ -159,7 +166,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
} else {
alert.Updated = time.Now()
alert.Created = time.Now()
- alert.State = m.AlertStatePending
+ alert.State = m.AlertStateNoData
alert.NewStateDate = time.Now()
_, err := sess.Insert(alert)
@@ -222,6 +229,8 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
alert.State = cmd.State
alert.StateChanges += 1
alert.NewStateDate = time.Now()
+ alert.EvalData = cmd.EvalData
+
if cmd.Error == "" {
alert.ExecutionError = " " //without this space, xorm skips updating this field
} else {
diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go
index c5b3a2f5975..5105ca39eff 100644
--- a/pkg/services/sqlstore/alert_notification.go
+++ b/pkg/services/sqlstore/alert_notification.go
@@ -16,6 +16,8 @@ func init() {
bus.AddHandler("sql", CreateAlertNotificationCommand)
bus.AddHandler("sql", UpdateAlertNotification)
bus.AddHandler("sql", DeleteAlertNotification)
+ bus.AddHandler("sql", GetAlertNotificationsToSend)
+ bus.AddHandler("sql", GetAllAlertNotifications)
}
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
@@ -32,73 +34,122 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
}
func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
- return getAlertNotificationsInternal(query, x.NewSession())
+ return getAlertNotificationInternal(query, x.NewSession())
}
-func getAlertNotificationsInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
+func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
+ results := make([]*m.AlertNotification, 0)
+ if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
+ return err
+ }
+
+ query.Result = results
+ return nil
+}
+
+func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`SELECT
- alert_notification.id,
- alert_notification.org_id,
- alert_notification.name,
- alert_notification.type,
- alert_notification.created,
- alert_notification.updated,
- alert_notification.settings
- FROM alert_notification
- `)
+ alert_notification.id,
+ alert_notification.org_id,
+ alert_notification.name,
+ alert_notification.type,
+ alert_notification.created,
+ alert_notification.updated,
+ alert_notification.settings,
+ alert_notification.is_default
+ FROM alert_notification
+ `)
sql.WriteString(` WHERE alert_notification.org_id = ?`)
params = append(params, query.OrgId)
- if query.Name != "" {
- sql.WriteString(` AND alert_notification.name = ?`)
- params = append(params, query.Name)
- }
-
- if query.Id != 0 {
- sql.WriteString(` AND alert_notification.id = ?`)
- params = append(params, query.Id)
- }
-
+ sql.WriteString(` AND ((alert_notification.is_default = 1)`)
if len(query.Ids) > 0 {
- sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
+ sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
for _, v := range query.Ids {
params = append(params, v)
}
}
+ sql.WriteString(`)`)
+
+ results := make([]*m.AlertNotification, 0)
+ if err := x.Sql(sql.String(), params...).Find(&results); err != nil {
+ return err
+ }
+
+ query.Result = results
+ return nil
+}
+
+func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *xorm.Session) error {
+ var sql bytes.Buffer
+ params := make([]interface{}, 0)
+
+ sql.WriteString(`SELECT
+ alert_notification.id,
+ alert_notification.org_id,
+ alert_notification.name,
+ alert_notification.type,
+ alert_notification.created,
+ alert_notification.updated,
+ alert_notification.settings,
+ alert_notification.is_default
+ FROM alert_notification
+ `)
+
+ sql.WriteString(` WHERE alert_notification.org_id = ?`)
+ params = append(params, query.OrgId)
+
+ if query.Name != "" || query.Id != 0 {
+ if query.Name != "" {
+ sql.WriteString(` AND alert_notification.name = ?`)
+ params = append(params, query.Name)
+ }
+
+ if query.Id != 0 {
+ sql.WriteString(` AND alert_notification.id = ?`)
+ params = append(params, query.Id)
+ }
+ }
results := make([]*m.AlertNotification, 0)
if err := sess.Sql(sql.String(), params...).Find(&results); err != nil {
return err
}
- query.Result = results
+ if len(results) == 0 {
+ query.Result = nil
+ } else {
+ query.Result = results[0]
+ }
+
return nil
}
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
return inTransaction(func(sess *xorm.Session) error {
existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
- err := getAlertNotificationsInternal(existingQuery, sess)
+ err := getAlertNotificationInternal(existingQuery, sess)
if err != nil {
return err
}
- if len(existingQuery.Result) > 0 {
+ if existingQuery.Result != nil {
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
}
alertNotification := &m.AlertNotification{
- OrgId: cmd.OrgId,
- Name: cmd.Name,
- Type: cmd.Type,
- Settings: cmd.Settings,
- Created: time.Now(),
- Updated: time.Now(),
+ OrgId: cmd.OrgId,
+ Name: cmd.Name,
+ Type: cmd.Type,
+ Settings: cmd.Settings,
+ Created: time.Now(),
+ Updated: time.Now(),
+ IsDefault: cmd.IsDefault,
}
if _, err = sess.Insert(alertNotification); err != nil {
@@ -120,11 +171,11 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
// check if name exists
sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
- if err := getAlertNotificationsInternal(sameNameQuery, sess); err != nil {
+ if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
return err
}
- if len(sameNameQuery.Result) > 0 && sameNameQuery.Result[0].Id != current.Id {
+ if sameNameQuery.Result != nil && sameNameQuery.Result.Id != current.Id {
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
}
@@ -132,6 +183,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
current.Settings = cmd.Settings
current.Name = cmd.Name
current.Type = cmd.Type
+ current.IsDefault = cmd.IsDefault
+
+ sess.UseBool("is_default")
if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
return err
diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go
index 4cbcf13a000..d37062fb58f 100644
--- a/pkg/services/sqlstore/alert_notification_test.go
+++ b/pkg/services/sqlstore/alert_notification_test.go
@@ -23,7 +23,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
err := GetAlertNotifications(cmd)
fmt.Printf("errror %v", err)
So(err, ShouldBeNil)
- So(len(cmd.Result), ShouldEqual, 0)
+ So(cmd.Result, ShouldBeNil)
})
Convey("Can save Alert Notification", func() {
@@ -63,20 +63,35 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()}
cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()}
+ cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()}
+
+ otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()}
So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil)
So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil)
So(CreateAlertNotificationCommand(&cmd3), ShouldBeNil)
+ So(CreateAlertNotificationCommand(&cmd4), ShouldBeNil)
+ So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
Convey("search", func() {
- query := &m.GetAlertNotificationsQuery{
+ query := &m.GetAlertNotificationsToSendQuery{
Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
OrgId: 1,
}
- err := GetAlertNotifications(query)
+ err := GetAlertNotificationsToSend(query)
So(err, ShouldBeNil)
- So(len(query.Result), ShouldEqual, 2)
+ So(len(query.Result), ShouldEqual, 3)
+ })
+
+ Convey("all", func() {
+ query := &m.GetAllAlertNotificationsQuery{
+ OrgId: 1,
+ }
+
+ err := GetAllAlertNotifications(query)
+ So(err, ShouldBeNil)
+ So(len(query.Result), ShouldEqual, 4)
})
})
})
diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go
index e22b1c48c47..02126c2984a 100644
--- a/pkg/services/sqlstore/alert_test.go
+++ b/pkg/services/sqlstore/alert_test.go
@@ -47,7 +47,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(err2, ShouldBeNil)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.Message, ShouldEqual, "Alerting message")
- So(alert.State, ShouldEqual, "pending")
+ So(alert.State, ShouldEqual, "no_data")
So(alert.Frequency, ShouldEqual, 1)
})
@@ -77,7 +77,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() {
- So(query.Result[0].State, ShouldEqual, "pending")
+ So(query.Result[0].State, ShouldEqual, "no_data")
})
})
diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
index 5cf4461b370..1b0f02fce09 100644
--- a/pkg/services/sqlstore/annotation.go
+++ b/pkg/services/sqlstore/annotation.go
@@ -3,6 +3,7 @@ package sqlstore
import (
"bytes"
"fmt"
+ "strings"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/services/annotations"
@@ -38,16 +39,43 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
params = append(params, query.AlertId)
}
+ if query.AlertId != 0 {
+ sql.WriteString(` AND alert_id = ?`)
+ params = append(params, query.AlertId)
+ }
+
+ if query.DashboardId != 0 {
+ sql.WriteString(` AND dashboard_id = ?`)
+ params = append(params, query.DashboardId)
+ }
+
+ if query.PanelId != 0 {
+ sql.WriteString(` AND panel_id = ?`)
+ params = append(params, query.PanelId)
+ }
+
+ if query.From > 0 && query.To > 0 {
+ sql.WriteString(` AND epoch BETWEEN ? AND ?`)
+ params = append(params, query.From, query.To)
+ }
+
if query.Type != "" {
sql.WriteString(` AND type = ?`)
params = append(params, string(query.Type))
}
+ if len(query.NewState) > 0 {
+ sql.WriteString(` AND new_state IN (?` + strings.Repeat(",?", len(query.NewState)-1) + ")")
+ for _, v := range query.NewState {
+ params = append(params, v)
+ }
+ }
+
if query.Limit == 0 {
query.Limit = 10
}
- sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
+ sql.WriteString(fmt.Sprintf("ORDER BY epoch DESC LIMIT %v", query.Limit))
items := make([]*annotations.Item, 0)
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go
index c028fd0fccc..2f1b40b2d61 100644
--- a/pkg/services/sqlstore/datasource.go
+++ b/pkg/services/sqlstore/datasource.go
@@ -43,7 +43,7 @@ func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error {
}
func GetDataSources(query *m.GetDataSourcesQuery) error {
- sess := x.Limit(100, 0).Where("org_id=?", query.OrgId).Asc("name")
+ sess := x.Limit(1000, 0).Where("org_id=?", query.OrgId).Asc("name")
query.Result = make([]*m.DataSource, 0)
return sess.Find(&query.Result)
diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go
index 63d61d8b196..b6956be41c7 100644
--- a/pkg/services/sqlstore/migrations/alert_mig.go
+++ b/pkg/services/sqlstore/migrations/alert_mig.go
@@ -62,5 +62,9 @@ func addAlertMigrations(mg *Migrator) {
}
mg.AddMigration("create alert_notification table v1", NewAddTableMigration(alert_notification))
+ mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{
+ Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0",
+ }))
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
+
}
diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
index 11b4eeed629..a307100ad3c 100644
--- a/pkg/services/sqlstore/migrations/annotation_mig.go
+++ b/pkg/services/sqlstore/migrations/annotation_mig.go
@@ -5,6 +5,7 @@ import (
)
func addAnnotationMig(mg *Migrator) {
+
table := Table{
Name: "annotation",
Columns: []*Column{
@@ -12,6 +13,9 @@ func addAnnotationMig(mg *Migrator) {
{Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "alert_id", Type: DB_BigInt, Nullable: true},
{Name: "user_id", Type: DB_BigInt, Nullable: true},
+ {Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
+ {Name: "panel_id", Type: DB_BigInt, Nullable: true},
+ {Name: "category_id", Type: DB_BigInt, Nullable: true},
{Name: "type", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "title", Type: DB_Text, Nullable: false},
{Name: "text", Type: DB_Text, Nullable: false},
@@ -19,20 +23,25 @@ func addAnnotationMig(mg *Migrator) {
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "data", Type: DB_Text, Nullable: false},
- {Name: "timestamp", Type: DB_DateTime, Nullable: false},
+ {Name: "epoch", Type: DB_BigInt, Nullable: false},
},
Indices: []*Index{
{Cols: []string{"org_id", "alert_id"}, Type: IndexType},
{Cols: []string{"org_id", "type"}, Type: IndexType},
- {Cols: []string{"timestamp"}, Type: IndexType},
+ {Cols: []string{"org_id", "category_id"}, Type: IndexType},
+ {Cols: []string{"org_id", "dashboard_id", "panel_id", "epoch"}, Type: IndexType},
+ {Cols: []string{"org_id", "epoch"}, Type: IndexType},
},
}
- mg.AddMigration("create annotation table v1", NewAddTableMigration(table))
+ mg.AddMigration("Drop old annotation table v4", NewDropTableMigration("annotation"))
+
+ mg.AddMigration("create annotation table v5", NewAddTableMigration(table))
// create indices
- mg.AddMigration("add index annotation org_id & alert_id ", NewAddIndexMigration(table, table.Indices[0]))
-
- mg.AddMigration("add index annotation org_id & type", NewAddIndexMigration(table, table.Indices[1]))
- mg.AddMigration("add index annotation timestamp", NewAddIndexMigration(table, table.Indices[2]))
+ mg.AddMigration("add index annotation 0 v3", NewAddIndexMigration(table, table.Indices[0]))
+ mg.AddMigration("add index annotation 1 v3", NewAddIndexMigration(table, table.Indices[1]))
+ mg.AddMigration("add index annotation 2 v3", NewAddIndexMigration(table, table.Indices[2]))
+ mg.AddMigration("add index annotation 3 v3", NewAddIndexMigration(table, table.Indices[3]))
+ mg.AddMigration("add index annotation 4 v3", NewAddIndexMigration(table, table.Indices[4]))
}
diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go
index 4f286dce68a..283501d366f 100644
--- a/pkg/services/sqlstore/migrations/dashboard_mig.go
+++ b/pkg/services/sqlstore/migrations/dashboard_mig.go
@@ -120,4 +120,9 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add index for plugin_id in dashboard", NewAddIndexMigration(dashboardV2, &Index{
Cols: []string{"org_id", "plugin_id"}, Type: IndexType,
}))
+
+ // dashboard_id index for dashboard_tag table
+ mg.AddMigration("Add index for dashboard_id in dashboard_tag", NewAddIndexMigration(dashboardTagV1, &Index{
+ Cols: []string{"dashboard_id"}, Type: IndexType,
+ }))
}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index fd010c42ca2..33b026713a1 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -47,10 +47,11 @@ var (
BuildStamp int64
// Paths
- LogsPath string
- HomePath string
- DataPath string
- PluginsPath string
+ LogsPath string
+ HomePath string
+ DataPath string
+ PluginsPath string
+ CustomInitPath = "conf/custom.ini"
// Log settings.
LogModes []string
@@ -312,7 +313,7 @@ func evalConfigValues() {
func loadSpecifedConfigFile(configFile string) error {
if configFile == "" {
- configFile = filepath.Join(HomePath, "conf/custom.ini")
+ configFile = filepath.Join(HomePath, CustomInitPath)
// return without error if custom file does not exist
if !pathExists(configFile) {
return nil
diff --git a/pkg/setting/setting_oauth.go b/pkg/setting/setting_oauth.go
index db2f0fb3802..71c4ade1468 100644
--- a/pkg/setting/setting_oauth.go
+++ b/pkg/setting/setting_oauth.go
@@ -11,8 +11,9 @@ type OAuthInfo struct {
}
type OAuther struct {
- GitHub, Google, Twitter bool
- OAuthInfos map[string]*OAuthInfo
+ GitHub, Google, Twitter, Generic bool
+ OAuthInfos map[string]*OAuthInfo
+ OAuthProviderName string
}
var OAuthService *OAuther
diff --git a/pkg/social/common.go b/pkg/social/common.go
new file mode 100644
index 00000000000..7bce5d2ae8f
--- /dev/null
+++ b/pkg/social/common.go
@@ -0,0 +1,20 @@
+package social
+
+import (
+ "fmt"
+ "strings"
+)
+
+func isEmailAllowed(email string, allowedDomains []string) bool {
+ if len(allowedDomains) == 0 {
+ return true
+ }
+
+ valid := false
+ for _, domain := range allowedDomains {
+ emailSuffix := fmt.Sprintf("@%s", domain)
+ valid = valid || strings.HasSuffix(email, emailSuffix)
+ }
+
+ return valid
+}
diff --git a/pkg/social/generic_oauth.go b/pkg/social/generic_oauth.go
new file mode 100644
index 00000000000..f016c87e201
--- /dev/null
+++ b/pkg/social/generic_oauth.go
@@ -0,0 +1,205 @@
+package social
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/grafana/grafana/pkg/models"
+
+ "golang.org/x/oauth2"
+)
+
+type GenericOAuth struct {
+ *oauth2.Config
+ allowedDomains []string
+ allowedOrganizations []string
+ apiUrl string
+ allowSignup bool
+ teamIds []int
+}
+
+func (s *GenericOAuth) Type() int {
+ return int(models.GENERIC)
+}
+
+func (s *GenericOAuth) IsEmailAllowed(email string) bool {
+ return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *GenericOAuth) IsSignupAllowed() bool {
+ return s.allowSignup
+}
+
+func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
+ if len(s.teamIds) == 0 {
+ return true
+ }
+
+ teamMemberships, err := s.FetchTeamMemberships(client)
+ if err != nil {
+ return false
+ }
+
+ for _, teamId := range s.teamIds {
+ for _, membershipId := range teamMemberships {
+ if teamId == membershipId {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
+ if len(s.allowedOrganizations) == 0 {
+ return true
+ }
+
+ organizations, err := s.FetchOrganizations(client)
+ if err != nil {
+ return false
+ }
+
+ for _, allowedOrganization := range s.allowedOrganizations {
+ for _, organization := range organizations {
+ if organization == allowedOrganization {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
+ type Record struct {
+ Email string `json:"email"`
+ Primary bool `json:"primary"`
+ Verified bool `json:"verified"`
+ }
+
+ emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
+ r, err := client.Get(emailsUrl)
+ if err != nil {
+ return "", err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return "", err
+ }
+
+ var email = ""
+ for _, record := range records {
+ if record.Primary {
+ email = record.Email
+ }
+ }
+
+ return email, nil
+}
+
+func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
+ type Record struct {
+ Id int `json:"id"`
+ }
+
+ membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
+ r, err := client.Get(membershipUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return nil, err
+ }
+
+ var ids = make([]int, len(records))
+ for i, record := range records {
+ ids[i] = record.Id
+ }
+
+ return ids, nil
+}
+
+func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
+ type Record struct {
+ Login string `json:"login"`
+ }
+
+ url := fmt.Sprintf(s.apiUrl + "/orgs")
+ r, err := client.Get(url)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return nil, err
+ }
+
+ var logins = make([]string, len(records))
+ for i, record := range records {
+ logins[i] = record.Login
+ }
+
+ return logins, nil
+}
+
+func (s *GenericOAuth) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+ var data struct {
+ Id int `json:"id"`
+ Name string `json:"login"`
+ Email string `json:"email"`
+ }
+
+ var err error
+ client := s.Client(oauth2.NoContext, token)
+ r, err := client.Get(s.apiUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+ return nil, err
+ }
+
+ userInfo := &BasicUserInfo{
+ Identity: strconv.Itoa(data.Id),
+ Name: data.Name,
+ Email: data.Email,
+ }
+
+ if !s.IsTeamMember(client) {
+ return nil, errors.New("User not a member of one of the required teams")
+ }
+
+ if !s.IsOrganizationMember(client) {
+ return nil, errors.New("User not a member of one of the required organizations")
+ }
+
+ if userInfo.Email == "" {
+ userInfo.Email, err = s.FetchPrivateEmail(client)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return userInfo, nil
+}
diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go
new file mode 100644
index 00000000000..40c8f2a2f7c
--- /dev/null
+++ b/pkg/social/github_oauth.go
@@ -0,0 +1,213 @@
+package social
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/grafana/grafana/pkg/models"
+
+ "golang.org/x/oauth2"
+)
+
+type SocialGithub struct {
+ *oauth2.Config
+ allowedDomains []string
+ allowedOrganizations []string
+ apiUrl string
+ allowSignup bool
+ teamIds []int
+}
+
+var (
+ ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
+)
+
+var (
+ ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
+)
+
+func (s *SocialGithub) Type() int {
+ return int(models.GITHUB)
+}
+
+func (s *SocialGithub) IsEmailAllowed(email string) bool {
+ return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGithub) IsSignupAllowed() bool {
+ return s.allowSignup
+}
+
+func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
+ if len(s.teamIds) == 0 {
+ return true
+ }
+
+ teamMemberships, err := s.FetchTeamMemberships(client)
+ if err != nil {
+ return false
+ }
+
+ for _, teamId := range s.teamIds {
+ for _, membershipId := range teamMemberships {
+ if teamId == membershipId {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
+ if len(s.allowedOrganizations) == 0 {
+ return true
+ }
+
+ organizations, err := s.FetchOrganizations(client)
+ if err != nil {
+ return false
+ }
+
+ for _, allowedOrganization := range s.allowedOrganizations {
+ for _, organization := range organizations {
+ if organization == allowedOrganization {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
+ type Record struct {
+ Email string `json:"email"`
+ Primary bool `json:"primary"`
+ Verified bool `json:"verified"`
+ }
+
+ emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
+ r, err := client.Get(emailsUrl)
+ if err != nil {
+ return "", err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return "", err
+ }
+
+ var email = ""
+ for _, record := range records {
+ if record.Primary {
+ email = record.Email
+ }
+ }
+
+ return email, nil
+}
+
+func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
+ type Record struct {
+ Id int `json:"id"`
+ }
+
+ membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
+ r, err := client.Get(membershipUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return nil, err
+ }
+
+ var ids = make([]int, len(records))
+ for i, record := range records {
+ ids[i] = record.Id
+ }
+
+ return ids, nil
+}
+
+func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
+ type Record struct {
+ Login string `json:"login"`
+ }
+
+ url := fmt.Sprintf(s.apiUrl + "/orgs")
+ r, err := client.Get(url)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ var records []Record
+
+ if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
+ return nil, err
+ }
+
+ var logins = make([]string, len(records))
+ for i, record := range records {
+ logins[i] = record.Login
+ }
+
+ return logins, nil
+}
+
+func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+ var data struct {
+ Id int `json:"id"`
+ Name string `json:"login"`
+ Email string `json:"email"`
+ }
+
+ var err error
+ client := s.Client(oauth2.NoContext, token)
+ r, err := client.Get(s.apiUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ defer r.Body.Close()
+
+ if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+ return nil, err
+ }
+
+ userInfo := &BasicUserInfo{
+ Identity: strconv.Itoa(data.Id),
+ Name: data.Name,
+ Email: data.Email,
+ }
+
+ if !s.IsTeamMember(client) {
+ return nil, ErrMissingTeamMembership
+ }
+
+ if !s.IsOrganizationMember(client) {
+ return nil, ErrMissingOrganizationMembership
+ }
+
+ if userInfo.Email == "" {
+ userInfo.Email, err = s.FetchPrivateEmail(client)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return userInfo, nil
+}
diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go
new file mode 100644
index 00000000000..7f0fdcc250a
--- /dev/null
+++ b/pkg/social/google_oauth.go
@@ -0,0 +1,52 @@
+package social
+
+import (
+ "encoding/json"
+
+ "github.com/grafana/grafana/pkg/models"
+
+ "golang.org/x/oauth2"
+)
+
+type SocialGoogle struct {
+ *oauth2.Config
+ allowedDomains []string
+ apiUrl string
+ allowSignup bool
+}
+
+func (s *SocialGoogle) Type() int {
+ return int(models.GOOGLE)
+}
+
+func (s *SocialGoogle) IsEmailAllowed(email string) bool {
+ return isEmailAllowed(email, s.allowedDomains)
+}
+
+func (s *SocialGoogle) IsSignupAllowed() bool {
+ return s.allowSignup
+}
+
+func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
+ var data struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ }
+ var err error
+
+ client := s.Client(oauth2.NoContext, token)
+ r, err := client.Get(s.apiUrl)
+ if err != nil {
+ return nil, err
+ }
+ defer r.Body.Close()
+ if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+ return nil, err
+ }
+ return &BasicUserInfo{
+ Identity: data.Id,
+ Name: data.Name,
+ Email: data.Email,
+ }, nil
+}
diff --git a/pkg/social/social.go b/pkg/social/social.go
index 4a8cb8bac5d..66d0f5fa778 100644
--- a/pkg/social/social.go
+++ b/pkg/social/social.go
@@ -1,14 +1,8 @@
package social
import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "strconv"
"strings"
- "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/net/context"
@@ -42,7 +36,7 @@ func NewOAuthService() {
setting.OAuthService = &setting.OAuther{}
setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo)
- allOauthes := []string{"github", "google"}
+ allOauthes := []string{"github", "google", "generic_oauth"}
for _, name := range allOauthes {
sec := setting.Cfg.Section("auth." + name)
@@ -98,269 +92,21 @@ func NewOAuthService() {
allowSignup: info.AllowSignup,
}
}
- }
-}
-func isEmailAllowed(email string, allowedDomains []string) bool {
- if len(allowedDomains) == 0 {
- return true
- }
-
- valid := false
- for _, domain := range allowedDomains {
- emailSuffix := fmt.Sprintf("@%s", domain)
- valid = valid || strings.HasSuffix(email, emailSuffix)
- }
-
- return valid
-}
-
-type SocialGithub struct {
- *oauth2.Config
- allowedDomains []string
- allowedOrganizations []string
- apiUrl string
- allowSignup bool
- teamIds []int
-}
-
-var (
- ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
-)
-
-var (
- ErrMissingOrganizationMembership = errors.New("User not a member of one of the required organizations")
-)
-
-func (s *SocialGithub) Type() int {
- return int(models.GITHUB)
-}
-
-func (s *SocialGithub) IsEmailAllowed(email string) bool {
- return isEmailAllowed(email, s.allowedDomains)
-}
-
-func (s *SocialGithub) IsSignupAllowed() bool {
- return s.allowSignup
-}
-
-func (s *SocialGithub) IsTeamMember(client *http.Client) bool {
- if len(s.teamIds) == 0 {
- return true
- }
-
- teamMemberships, err := s.FetchTeamMemberships(client)
- if err != nil {
- return false
- }
-
- for _, teamId := range s.teamIds {
- for _, membershipId := range teamMemberships {
- if teamId == membershipId {
- return true
+ // Generic - Uses the same scheme as Github.
+ if name == "generic_oauth" {
+ setting.OAuthService.Generic = true
+ setting.OAuthService.OAuthProviderName = sec.Key("oauth_provider_name").String()
+ teamIds := sec.Key("team_ids").Ints(",")
+ allowedOrganizations := sec.Key("allowed_organizations").Strings(" ")
+ SocialMap["generic_oauth"] = &GenericOAuth{
+ Config: &config,
+ allowedDomains: info.AllowedDomains,
+ apiUrl: info.ApiUrl,
+ allowSignup: info.AllowSignup,
+ teamIds: teamIds,
+ allowedOrganizations: allowedOrganizations,
}
}
}
-
- return false
-}
-
-func (s *SocialGithub) IsOrganizationMember(client *http.Client) bool {
- if len(s.allowedOrganizations) == 0 {
- return true
- }
-
- organizations, err := s.FetchOrganizations(client)
- if err != nil {
- return false
- }
-
- for _, allowedOrganization := range s.allowedOrganizations {
- for _, organization := range organizations {
- if organization == allowedOrganization {
- return true
- }
- }
- }
-
- return false
-}
-
-func (s *SocialGithub) FetchPrivateEmail(client *http.Client) (string, error) {
- type Record struct {
- Email string `json:"email"`
- Primary bool `json:"primary"`
- Verified bool `json:"verified"`
- }
-
- emailsUrl := fmt.Sprintf(s.apiUrl + "/emails")
- r, err := client.Get(emailsUrl)
- if err != nil {
- return "", err
- }
-
- defer r.Body.Close()
-
- var records []Record
-
- if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
- return "", err
- }
-
- var email = ""
- for _, record := range records {
- if record.Primary {
- email = record.Email
- }
- }
-
- return email, nil
-}
-
-func (s *SocialGithub) FetchTeamMemberships(client *http.Client) ([]int, error) {
- type Record struct {
- Id int `json:"id"`
- }
-
- membershipUrl := fmt.Sprintf(s.apiUrl + "/teams")
- r, err := client.Get(membershipUrl)
- if err != nil {
- return nil, err
- }
-
- defer r.Body.Close()
-
- var records []Record
-
- if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
- return nil, err
- }
-
- var ids = make([]int, len(records))
- for i, record := range records {
- ids[i] = record.Id
- }
-
- return ids, nil
-}
-
-func (s *SocialGithub) FetchOrganizations(client *http.Client) ([]string, error) {
- type Record struct {
- Login string `json:"login"`
- }
-
- url := fmt.Sprintf(s.apiUrl + "/orgs")
- r, err := client.Get(url)
- if err != nil {
- return nil, err
- }
-
- defer r.Body.Close()
-
- var records []Record
-
- if err = json.NewDecoder(r.Body).Decode(&records); err != nil {
- return nil, err
- }
-
- var logins = make([]string, len(records))
- for i, record := range records {
- logins[i] = record.Login
- }
-
- return logins, nil
-}
-
-func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
- var data struct {
- Id int `json:"id"`
- Name string `json:"login"`
- Email string `json:"email"`
- }
-
- var err error
- client := s.Client(oauth2.NoContext, token)
- r, err := client.Get(s.apiUrl)
- if err != nil {
- return nil, err
- }
-
- defer r.Body.Close()
-
- if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
- return nil, err
- }
-
- userInfo := &BasicUserInfo{
- Identity: strconv.Itoa(data.Id),
- Name: data.Name,
- Email: data.Email,
- }
-
- if !s.IsTeamMember(client) {
- return nil, ErrMissingTeamMembership
- }
-
- if !s.IsOrganizationMember(client) {
- return nil, ErrMissingOrganizationMembership
- }
-
- if userInfo.Email == "" {
- userInfo.Email, err = s.FetchPrivateEmail(client)
- if err != nil {
- return nil, err
- }
- }
-
- return userInfo, nil
-}
-
-// ________ .__
-// / _____/ ____ ____ ____ | | ____
-// / \ ___ / _ \ / _ \ / ___\| | _/ __ \
-// \ \_\ ( <_> | <_> ) /_/ > |_\ ___/
-// \______ /\____/ \____/\___ /|____/\___ >
-// \/ /_____/ \/
-
-type SocialGoogle struct {
- *oauth2.Config
- allowedDomains []string
- apiUrl string
- allowSignup bool
-}
-
-func (s *SocialGoogle) Type() int {
- return int(models.GOOGLE)
-}
-
-func (s *SocialGoogle) IsEmailAllowed(email string) bool {
- return isEmailAllowed(email, s.allowedDomains)
-}
-
-func (s *SocialGoogle) IsSignupAllowed() bool {
- return s.allowSignup
-}
-
-func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
- var data struct {
- Id string `json:"id"`
- Name string `json:"name"`
- Email string `json:"email"`
- }
- var err error
-
- client := s.Client(oauth2.NoContext, token)
- r, err := client.Get(s.apiUrl)
- if err != nil {
- return nil, err
- }
- defer r.Body.Close()
- if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
- return nil, err
- }
- return &BasicUserInfo{
- Identity: data.Id,
- Name: data.Name,
- Email: data.Email,
- }, nil
}
diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go
index 0247ad0dca8..4042702378c 100644
--- a/pkg/tsdb/graphite/graphite.go
+++ b/pkg/tsdb/graphite/graphite.go
@@ -1,6 +1,7 @@
package graphite
import (
+ "crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
@@ -11,13 +12,10 @@ import (
"time"
"github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb"
)
-var (
- HttpClient = http.Client{Timeout: time.Duration(10 * time.Second)}
-)
-
type GraphiteExecutor struct {
*tsdb.DataSourceInfo
}
@@ -26,11 +24,23 @@ func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor {
return &GraphiteExecutor{dsInfo}
}
-var glog log.Logger
+var (
+ glog log.Logger
+ HttpClient http.Client
+)
func init() {
glog = log.New("tsdb.graphite")
tsdb.RegisterExecutor("graphite", NewGraphiteExecutor)
+
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+
+ HttpClient = http.Client{
+ Timeout: time.Duration(10 * time.Second),
+ Transport: tr,
+ }
}
func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult {
@@ -47,7 +57,9 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
formData["target"] = []string{query.Query}
}
- glog.Info("Graphite request body", "formdata", formData.Encode())
+ if setting.Env == setting.DEV {
+ glog.Debug("Graphite request", "params", formData)
+ }
req, err := e.createRequest(formData)
if err != nil {
@@ -73,6 +85,10 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
Name: series.Target,
Points: series.DataPoints,
})
+
+ if setting.Env == setting.DEV {
+ glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
+ }
}
result.QueryResults["A"] = queryRes
diff --git a/pkg/tsdb/graphite/types.go b/pkg/tsdb/graphite/types.go
index 4cd1b601bbc..085b1fb2b94 100644
--- a/pkg/tsdb/graphite/types.go
+++ b/pkg/tsdb/graphite/types.go
@@ -1,6 +1,6 @@
package graphite
type TargetResponseDTO struct {
- Target string `json:"target"`
- DataPoints [][2]float64 `json:"datapoints"`
+ Target string `json:"target"`
+ DataPoints [][2]*float64 `json:"datapoints"`
}
diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go
index 2954c630b68..05a8b13ef84 100644
--- a/pkg/tsdb/models.go
+++ b/pkg/tsdb/models.go
@@ -46,13 +46,13 @@ type QueryResult struct {
}
type TimeSeries struct {
- Name string `json:"name"`
- Points [][2]float64 `json:"points"`
+ Name string `json:"name"`
+ Points [][2]*float64 `json:"points"`
}
type TimeSeriesSlice []*TimeSeries
-func NewTimeSeries(name string, points [][2]float64) *TimeSeries {
+func NewTimeSeries(name string, points [][2]*float64) *TimeSeries {
return &TimeSeries{
Name: name,
Points: points,
diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts
index 90724f65d2d..fcd337a0a26 100644
--- a/public/app/core/components/query_part/query_part.ts
+++ b/public/app/core/components/query_part/query_part.ts
@@ -54,9 +54,9 @@ export class QueryPart {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this.hasMultipleParamsInString(strValue, index)) {
- _.each(strValue.split(','), function(partVal: string, idx) {
+ _.each(strValue.split(','), (partVal, idx) => {
this.updateParam(partVal.trim(), idx);
- }, this);
+ });
return;
}
diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js
index 748b64dc5c1..8067202d511 100644
--- a/public/app/core/controllers/login_ctrl.js
+++ b/public/app/core/controllers/login_ctrl.js
@@ -6,6 +6,12 @@ define([
function (angular, coreModule, config) {
'use strict';
+ var failCodes = {
+ "1000": "Required Github team membership not fulfilled",
+ "1001": "Required Github organization membership not fulfilled",
+ "1002": "Required email domain not fulfilled",
+ };
+
coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
$scope.formModel = {
user: '',
@@ -17,8 +23,10 @@ function (angular, coreModule, config) {
$scope.googleAuthEnabled = config.googleAuthEnabled;
$scope.githubAuthEnabled = config.githubAuthEnabled;
- $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled;
+ $scope.oauthEnabled = config.githubAuthEnabled || config.googleAuthEnabled || config.genericOAuthEnabled;
$scope.allowUserPassLogin = config.allowUserPassLogin;
+ $scope.genericOAuthEnabled = config.genericOAuthEnabled;
+ $scope.oauthProviderName = config.oauthProviderName;
$scope.disableUserSignUp = config.disableUserSignUp;
$scope.loginHint = config.loginHint;
@@ -29,8 +37,8 @@ function (angular, coreModule, config) {
$scope.$watch("loginMode", $scope.loginModeChanged);
var params = $location.search();
- if (params.failedMsg) {
- $scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
+ if (params.failCode) {
+ $scope.appEvent('alert-warning', ['Login Failed', failCodes[params.failCode]]);
delete params.failedMsg;
$location.search(params);
}
diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js
index 36eac942c3e..d51260395de 100644
--- a/public/app/core/directives/metric_segment.js
+++ b/public/app/core/directives/metric_segment.js
@@ -40,7 +40,7 @@ function (_, $, coreModule) {
}
$scope.$apply(function() {
- var selected = _.findWhere($scope.altSegments, { value: value });
+ var selected = _.find($scope.altSegments, {value: value});
if (selected) {
segment.value = selected.value;
segment.html = selected.html;
@@ -76,10 +76,8 @@ function (_, $, coreModule) {
};
$scope.source = function(query, callback) {
- if (options) { return options; }
-
$scope.$apply(function() {
- $scope.getOptions().then(function(altSegments) {
+ $scope.getOptions({ measurementFilter: query }).then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) { return alt.value; });
@@ -174,7 +172,7 @@ function (_, $, coreModule) {
pre: function postLink($scope, elem, attrs) {
$scope.valueToSegment = function(value) {
- var option = _.findWhere($scope.options, {value: value});
+ var option = _.find($scope.options, {value: value});
var segment = {
cssClass: attrs.cssClass,
custom: attrs.custom,
@@ -197,7 +195,7 @@ function (_, $, coreModule) {
$scope.onSegmentChange = function() {
if ($scope.options) {
- var option = _.findWhere($scope.options, {text: $scope.segment.value});
+ var option = _.find($scope.options, {text: $scope.segment.value});
if (option && option.value !== $scope.property) {
$scope.property = option.value;
} else if (attrs.custom !== 'false') {
diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts
index dbe9932d574..60685fae74e 100644
--- a/public/app/core/directives/plugin_component.ts
+++ b/public/app/core/directives/plugin_component.ts
@@ -136,12 +136,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
}
// Annotations
case "annotations-query-ctrl": {
- return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
+ return System.import(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
return {
- baseUrl: scope.currentDatasource.meta.baseUrl,
- name: 'annotations-query-ctrl-' + scope.currentDatasource.meta.id,
+ baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
+ name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
bindings: {annotation: "=", datasource: "="},
- attrs: {"annotation": "currentAnnotation", datasource: "currentDatasource"},
+ attrs: {"annotation": "ctrl.currentAnnotation", datasource: "ctrl.currentDatasource"},
Component: dsModule.AnnotationsQueryCtrl,
};
});
diff --git a/public/app/core/directives/value_select_dropdown.js b/public/app/core/directives/value_select_dropdown.js
index 60d257d6fa8..d97124f3406 100644
--- a/public/app/core/directives/value_select_dropdown.js
+++ b/public/app/core/directives/value_select_dropdown.js
@@ -51,7 +51,7 @@ function (angular, _, coreModule) {
});
// convert values to text
- var currentTexts = _.pluck(selectedAndNotInTag, 'text');
+ var currentTexts = _.map(selectedAndNotInTag, 'text');
// join texts
vm.linkText = currentTexts.join(' + ');
@@ -167,7 +167,7 @@ function (angular, _, coreModule) {
_.each(vm.tags, function(tag) {
if (tag.selected) {
_.each(tag.values, function(value) {
- if (!_.findWhere(vm.selectedValues, {value: value})) {
+ if (!_.find(vm.selectedValues, {value: value})) {
tag.selected = false;
}
});
@@ -175,8 +175,8 @@ function (angular, _, coreModule) {
});
vm.selectedTags = _.filter(vm.tags, {selected: true});
- vm.variable.current.value = _.pluck(vm.selectedValues, 'value');
- vm.variable.current.text = _.pluck(vm.selectedValues, 'text').join(' + ');
+ vm.variable.current.value = _.map(vm.selectedValues, 'value');
+ vm.variable.current.text = _.map(vm.selectedValues, 'text').join(' + ');
vm.variable.current.tags = vm.selectedTags;
if (!vm.variable.multi) {
diff --git a/public/app/core/lodash_extended.js b/public/app/core/lodash_extended.js
index 4edae35f02d..6487b19193e 100644
--- a/public/app/core/lodash_extended.js
+++ b/public/app/core/lodash_extended.js
@@ -19,7 +19,7 @@ function () {
return variable === value ? alt : value;
},
toggleInOut: function(array,value) {
- if(_.contains(array,value)) {
+ if(_.includes(array,value)) {
array = _.without(array,value);
} else {
array.push(value);
diff --git a/public/app/core/routes/bundle_loader.ts b/public/app/core/routes/bundle_loader.ts
index 3d275aeda36..473ac66b4b9 100644
--- a/public/app/core/routes/bundle_loader.ts
+++ b/public/app/core/routes/bundle_loader.ts
@@ -2,21 +2,22 @@
export class BundleLoader {
lazy: any;
- loadingDefer: any;
constructor(bundleName) {
+ var defer = null;
+
this.lazy = ["$q", "$route", "$rootScope", ($q, $route, $rootScope) => {
- if (this.loadingDefer) {
- return this.loadingDefer.promise;
+ if (defer) {
+ return defer.promise;
}
- this.loadingDefer = $q.defer();
+ defer = $q.defer();
System.import(bundleName).then(() => {
- this.loadingDefer.resolve();
+ defer.resolve();
});
- return this.loadingDefer.promise;
+ return defer.promise;
}];
}
diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts
index 4ef329f975e..286f03db5f5 100644
--- a/public/app/core/services/alert_srv.ts
+++ b/public/app/core/services/alert_srv.ts
@@ -80,28 +80,27 @@ export class AlertSrv {
showConfirmModal(payload) {
var scope = this.$rootScope.$new();
- scope.title = payload.title;
- scope.text = payload.text;
- scope.text2 = payload.text2;
- scope.confirmTextRequired = payload.confirmText !== undefined && payload.confirmText !== "";
-
scope.onConfirm = function() {
- if (!scope.confirmTextRequired || (scope.confirmTextRequired && scope.confirmTextValid)) {
- payload.onConfirm();
- scope.dismiss();
- }
+ payload.onConfirm();
+ scope.dismiss();
};
scope.updateConfirmText = function(value) {
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
};
+ scope.title = payload.title;
+ scope.text = payload.text;
+ scope.text2 = payload.text2;
+ scope.confirmText = payload.confirmText;
+
scope.onConfirm = payload.onConfirm;
scope.onAltAction = payload.onAltAction;
scope.altActionText = payload.altActionText;
scope.icon = payload.icon || "fa-check";
scope.yesText = payload.yesText || "Yes";
scope.noText = payload.noText || "Cancel";
+ scope.confirmTextValid = scope.confirmText ? false : true;
var confirmModal = this.$modal({
template: 'public/app/partials/confirm_modal.html',
diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts
index 41d99c3198f..ac00528db20 100644
--- a/public/app/core/services/context_srv.ts
+++ b/public/app/core/services/context_srv.ts
@@ -9,6 +9,7 @@ export class User {
isGrafanaAdmin: any;
isSignedIn: any;
orgRole: any;
+ timezone: string;
constructor() {
if (config.bootData.user) {
diff --git a/public/app/core/utils/datemath.ts b/public/app/core/utils/datemath.ts
index 467d3750c9a..2aa793016ba 100644
--- a/public/app/core/utils/datemath.ts
+++ b/public/app/core/utils/datemath.ts
@@ -93,7 +93,7 @@ export function parseDateMath(mathString, time, roundUp?) {
}
unit = mathString.charAt(i++);
- if (!_.contains(units, unit)) {
+ if (!_.includes(units, unit)) {
return undefined;
} else {
if (type === 0) {
diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js
index c0d162b0ece..cf80d671d71 100644
--- a/public/app/core/utils/kbn.js
+++ b/public/app/core/utils/kbn.js
@@ -9,6 +9,10 @@ function($, _, moment) {
var kbn = {};
kbn.valueFormats = {};
+ kbn.regexEscape = function(value) {
+ return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
+ };
+
///// HELPER FUNCTIONS /////
kbn.round_interval = function(interval) {
diff --git a/public/app/features/admin/adminEditUserCtrl.js b/public/app/features/admin/adminEditUserCtrl.js
index d8500a845d3..104ccff99cd 100644
--- a/public/app/features/admin/adminEditUserCtrl.js
+++ b/public/app/features/admin/adminEditUserCtrl.js
@@ -81,20 +81,20 @@ function (angular, _) {
$scope.searchOrgs = function(queryStr, callback) {
if ($scope.orgsSearchCache.length > 0) {
- callback(_.pluck($scope.orgsSearchCache, "name"));
+ callback(_.map($scope.orgsSearchCache, "name"));
return;
}
backendSrv.get('/api/orgs', {query: ''}).then(function(result) {
$scope.orgsSearchCache = result;
- callback(_.pluck(result, "name"));
+ callback(_.map(result, "name"));
});
};
$scope.addOrgUser = function() {
if (!$scope.addOrgForm.$valid) { return; }
- var orgInfo = _.findWhere($scope.orgsSearchCache, {name: $scope.newOrg.name});
+ var orgInfo = _.find($scope.orgsSearchCache, {name: $scope.newOrg.name});
if (!orgInfo) { return; }
$scope.newOrg.loginOrEmail = $scope.user.login;
diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts
index 0b8efbe9889..8e9a86735ad 100644
--- a/public/app/features/alerting/alert_def.ts
+++ b/public/app/features/alerting/alert_def.ts
@@ -1,6 +1,6 @@
///
-
+import _ from 'lodash';
import {
QueryPartDef,
QueryPart,
@@ -36,15 +36,17 @@ var reducerTypes = [
{text: 'count()', value: 'count'},
];
+var noDataModes = [
+ {text: 'OK', value: 'ok'},
+ {text: 'Alerting', value: 'alerting'},
+ {text: 'No Data', value: 'no_data'},
+];
+
function createReducerPart(model) {
var def = new QueryPartDef({type: model.type, defaultParams: []});
return new QueryPart(model, def);
}
-var severityLevels = {
- 'critical': {text: 'Critical', iconClass: 'icon-gf icon-gf-critical', stateClass: 'alert-state-critical'},
- 'warning': {text: 'Warning', iconClass: 'icon-gf icon-gf-warning', stateClass: 'alert-state-warning'},
-};
function getStateDisplayModel(state) {
switch (state) {
@@ -55,23 +57,16 @@ function getStateDisplayModel(state) {
stateClass: 'alert-state-ok'
};
}
- case 'critical': {
+ case 'alerting': {
return {
- text: 'CRITICAL',
+ text: 'ALERTING',
iconClass: 'icon-gf icon-gf-critical',
stateClass: 'alert-state-critical'
};
}
- case 'warning': {
+ case 'no_data': {
return {
- text: 'WARNING',
- iconClass: 'icon-gf icon-gf-warning',
- stateClass: 'alert-state-warning'
- };
- }
- case 'pending': {
- return {
- text: 'PENDING',
+ text: 'NO DATA',
iconClass: "fa fa-question",
stateClass: 'alert-state-warning'
};
@@ -94,12 +89,23 @@ function getStateDisplayModel(state) {
}
}
+function joinEvalMatches(matches, seperator: string) {
+ return _.reduce(matches, (res, ev)=> {
+ if (ev.Metric !== undefined && ev.Value !== undefined) {
+ res.push(ev.Metric + "=" + ev.Value);
+ }
+
+ return res;
+ }, []).join(seperator);
+}
+
export default {
alertQueryDef: alertQueryDef,
getStateDisplayModel: getStateDisplayModel,
conditionTypes: conditionTypes,
evalFunctions: evalFunctions,
- severityLevels: severityLevels,
+ noDataModes: noDataModes,
reducerTypes: reducerTypes,
createReducerPart: createReducerPart,
+ joinEvalMatches: joinEvalMatches,
};
diff --git a/public/app/features/alerting/alert_list_ctrl.ts b/public/app/features/alerting/alert_list_ctrl.ts
index b1ae2730fbb..4b429e961a7 100644
--- a/public/app/features/alerting/alert_list_ctrl.ts
+++ b/public/app/features/alerting/alert_list_ctrl.ts
@@ -13,9 +13,8 @@ export class AlertListCtrl {
stateFilters = [
{text: 'All', value: null},
{text: 'OK', value: 'ok'},
- {text: 'Pending', value: 'pending'},
- {text: 'Warning', value: 'warning'},
- {text: 'Critical', value: 'critical'},
+ {text: 'Alerting', value: 'alerting'},
+ {text: 'No Data', value: 'no_data'},
{text: 'Execution Error', value: 'execution_error'},
];
diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts
index c5ed6187f35..01cacaf9740 100644
--- a/public/app/features/alerting/alert_tab_ctrl.ts
+++ b/public/app/features/alerting/alert_tab_ctrl.ts
@@ -17,7 +17,7 @@ export class AlertTabCtrl {
alert: any;
conditionModels: any;
evalFunctions: any;
- severityLevels: any;
+ noDataModes: any;
addNotificationSegment;
notifications;
alertNotifications;
@@ -40,7 +40,7 @@ export class AlertTabCtrl {
this.subTabIndex = 0;
this.evalFunctions = alertDef.evalFunctions;
this.conditionTypes = alertDef.conditionTypes;
- this.severityLevels = alertDef.severityLevels;
+ this.noDataModes = alertDef.noDataModes;
this.appSubUrl = config.appSubUrl;
}
@@ -68,24 +68,30 @@ export class AlertTabCtrl {
this.notifications = res;
_.each(this.alert.notifications, item => {
- var model = _.findWhere(this.notifications, {id: item.id});
+ var model = _.find(this.notifications, {id: item.id});
if (model) {
model.iconClass = this.getNotificationIcon(model.type);
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(', ');
+ _.each(this.notifications, item => {
+ if (item.isDefault) {
+ item.iconClass = this.getNotificationIcon(item.type);
+ item.bgColor = "#00678b";
+ this.alertNotifications.push(item);
+ }
+ });
+ });
+ }
- return ah;
- });
+ getAlertHistory() {
+ this.backendSrv.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50`).then(res => {
+ this.alertHistory = _.map(res, ah => {
+ ah.time = moment(ah.time).format('MMM D, YYYY HH:mm:ss');
+ ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
+ ah.metrics = alertDef.joinEvalMatches(ah.data, ', ');
+ return ah;
});
});
}
@@ -104,14 +110,25 @@ export class AlertTabCtrl {
}));
}
+ changeTabIndex(newTabIndex) {
+ this.subTabIndex = newTabIndex;
+
+ if (this.subTabIndex === 2) {
+ this.getAlertHistory();
+ }
+ }
notificationAdded() {
- var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
+ var model = _.find(this.notifications, {name: this.addNotificationSegment.value});
if (!model) {
return;
}
- this.alertNotifications.push({name: model.name, iconClass: this.getNotificationIcon(model.type)});
+ this.alertNotifications.push({
+ name: model.name,
+ iconClass: this.getNotificationIcon(model.type),
+ isDefault: false
+ });
this.alert.notifications.push({id: model.id});
// reset plus button
@@ -125,14 +142,18 @@ export class AlertTabCtrl {
}
initModel() {
- var alert = this.alert = this.panel.alert = this.panel.alert || {};
+ var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
+
+ if (!this.alert.enabled) {
+ return;
+ }
alert.conditions = alert.conditions || [];
if (alert.conditions.length === 0) {
alert.conditions.push(this.buildDefaultCondition());
}
- alert.severity = alert.severity || 'critical';
+ alert.noDataState = alert.noDataState || 'no_data';
alert.frequency = alert.frequency || '60s';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
@@ -145,11 +166,9 @@ export class AlertTabCtrl {
return memo;
}, []);
- if (this.alert.enabled) {
- this.panelCtrl.editingThresholds = true;
- }
-
ThresholdMapper.alertToGraphThresholds(this.panel);
+
+ this.panelCtrl.editingThresholds = true;
this.panelCtrl.render();
}
@@ -173,6 +192,10 @@ export class AlertTabCtrl {
}
validateModel() {
+ if (!this.alert.enabled) {
+ return;
+ }
+
let firstTarget;
var fixed = false;
let foundTarget = null;
@@ -295,11 +318,6 @@ export class AlertTabCtrl {
this.panelCtrl.render();
}
- severityChanged() {
- ThresholdMapper.alertToGraphThresholds(this.panel);
- this.panelCtrl.render();
- }
-
evaluatorTypeChanged(evaluator) {
// ensure params array is correct length
switch (evaluator.type) {
diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts
index fc33118cb00..de5703a0631 100644
--- a/public/app/features/alerting/notification_edit_ctrl.ts
+++ b/public/app/features/alerting/notification_edit_ctrl.ts
@@ -7,6 +7,8 @@ import config from 'app/core/config';
export class AlertNotificationEditCtrl {
model: any;
+ showTest: boolean = false;
+ testSeverity: string = "critical";
/** @ngInject */
constructor(private $routeParams, private backendSrv, private $scope, private $location) {
@@ -15,7 +17,10 @@ export class AlertNotificationEditCtrl {
} else {
this.model = {
type: 'email',
- settings: {}
+ settings: {
+ severityFilter: 'none'
+ },
+ isDefault: false
};
}
}
@@ -47,6 +52,23 @@ export class AlertNotificationEditCtrl {
typeChanged() {
this.model.settings = {};
}
+
+ toggleTest() {
+ this.showTest = !this.showTest;
+ }
+
+ testNotification() {
+ var payload = {
+ name: this.model.name,
+ type: this.model.type,
+ settings: this.model.settings,
+ };
+
+ this.backendSrv.post(`/api/alert-notifications/test`, payload)
+ .then(res => {
+ this.$scope.appEvent('alert-succes', ['Test notification sent', '']);
+ });
+ }
}
coreModule.controller('AlertNotificationEditCtrl', AlertNotificationEditCtrl);
diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html
index e755496b732..3d6e183d93f 100644
--- a/public/app/features/alerting/partials/alert_tab.html
+++ b/public/app/features/alerting/partials/alert_tab.html
@@ -2,15 +2,15 @@
|