Merge branch 'master' into cli_db_commands

This commit is contained in:
bergquist
2016-12-09 12:36:05 +01:00
3934 changed files with 228148 additions and 815896 deletions
+286
View File
@@ -0,0 +1,286 @@
package api
import (
"fmt"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
func ValidateOrgAlert(c *middleware.Context) {
id := c.ParamsInt64(":alertId")
query := models.GetAlertByIdQuery{Id: id}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(404, "Alert not found", nil)
return
}
if c.OrgId != query.Result.OrgId {
c.JsonApiErr(403, "You are not allowed to edit/view alert", nil)
return
}
}
func GetAlertStatesForDashboard(c *middleware.Context) Response {
dashboardId := c.QueryInt64("dashboardId")
if dashboardId == 0 {
return ApiError(400, "Missing query parameter dashboardId", nil)
}
query := models.GetAlertStatesForDashboardQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch alert states", err)
}
return Json(200, query.Result)
}
// GET /api/alerts
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{
OrgId: c.OrgId,
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 {
return ApiError(500, "List alerts failed", err)
}
dashboardIds := make([]int64, 0)
alertDTOs := make([]*dtos.AlertRule, 0)
for _, alert := range query.Result {
dashboardIds = append(dashboardIds, alert.DashboardId)
alertDTOs = append(alertDTOs, &dtos.AlertRule{
Id: alert.Id,
DashboardId: alert.DashboardId,
PanelId: alert.PanelId,
Name: alert.Name,
Message: alert.Message,
State: alert.State,
EvalDate: alert.EvalDate,
NewStateDate: alert.NewStateDate,
ExecutionError: alert.ExecutionError,
})
}
dashboardsQuery := models.GetDashboardsQuery{
DashboardIds: dashboardIds,
}
if len(alertDTOs) > 0 {
if err := bus.Dispatch(&dashboardsQuery); err != nil {
return ApiError(500, "List alerts failed", err)
}
}
//TODO: should be possible to speed this up with lookup table
for _, alert := range alertDTOs {
for _, dash := range dashboardsQuery.Result {
if alert.DashboardId == dash.Id {
alert.DashbboardUri = "db/" + dash.Slug
}
}
}
return Json(200, alertDTOs)
}
// POST /api/alerts/test
func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
if _, idErr := dto.Dashboard.Get("id").Int64(); idErr != nil {
return ApiError(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
}
backendCmd := alerting.AlertTestCommand{
OrgId: c.OrgId,
Dashboard: dto.Dashboard,
PanelId: dto.PanelId,
}
if err := bus.Dispatch(&backendCmd); err != nil {
if validationErr, ok := err.(alerting.ValidationError); ok {
return ApiError(422, validationErr.Error(), nil)
}
return ApiError(500, "Failed to test rule", err)
}
res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{
Firing: res.Firing,
ConditionEvals: res.ConditionEvals,
}
if res.Error != nil {
dtoRes.Error = res.Error.Error()
}
for _, log := range res.Logs {
dtoRes.Logs = append(dtoRes.Logs, &dtos.AlertTestResultLog{Message: log.Message, Data: log.Data})
}
for _, match := range res.EvalMatches {
dtoRes.EvalMatches = append(dtoRes.EvalMatches, &dtos.EvalMatch{Metric: match.Metric, Value: match.Value})
}
dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
return Json(200, dtoRes)
}
// GET /api/alerts/:id
func GetAlert(c *middleware.Context) Response {
id := c.ParamsInt64(":alertId")
query := models.GetAlertByIdQuery{Id: id}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "List alerts failed", err)
}
return Json(200, &query.Result)
}
// DEL /api/alerts/:id
func DelAlert(c *middleware.Context) Response {
alertId := c.ParamsInt64(":alertId")
if alertId == 0 {
return ApiError(401, "Failed to parse alertid", nil)
}
cmd := models.DeleteAlertCommand{AlertId: alertId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete alert", err)
}
var resp = map[string]interface{}{"alertId": alertId}
return Json(200, resp)
}
func GetAlertNotifications(c *middleware.Context) Response {
query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
if err := bus.Dispatch(query); err != nil {
return ApiError(500, "Failed to get alert notifications", err)
}
result := make([]*dtos.AlertNotification, 0)
for _, notification := range query.Result {
result = append(result, &dtos.AlertNotification{
Id: notification.Id,
Name: notification.Name,
Type: notification.Type,
IsDefault: notification.IsDefault,
Created: notification.Created,
Updated: notification.Updated,
})
}
return Json(200, result)
}
func GetAlertNotificationById(c *middleware.Context) Response {
query := &models.GetAlertNotificationsQuery{
OrgId: c.OrgId,
Id: c.ParamsInt64("notificationId"),
}
if err := bus.Dispatch(query); err != nil {
return ApiError(500, "Failed to get alert notifications", err)
}
return Json(200, query.Result)
}
func CreateAlertNotification(c *middleware.Context, cmd models.CreateAlertNotificationCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to create alert notification", err)
}
return Json(200, cmd.Result)
}
func UpdateAlertNotification(c *middleware.Context, cmd models.UpdateAlertNotificationCommand) Response {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update alert notification", err)
}
return Json(200, cmd.Result)
}
func DeleteAlertNotification(c *middleware.Context) Response {
cmd := models.DeleteAlertNotificationCommand{
OrgId: c.OrgId,
Id: c.ParamsInt64("notificationId"),
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to delete alert notification", err)
}
return ApiSuccess("Notification deleted")
}
//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,
}
if err := bus.Dispatch(cmd); err != nil {
return ApiError(500, "Failed to send alert notifications", err)
}
return ApiSuccess("Test notification sent")
}
//POST /api/alerts/:alertId/pause
func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
cmd := models.PauseAlertCommand{
OrgId: c.OrgId,
AlertId: c.ParamsInt64("alertId"),
Paused: dto.Paused,
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "", err)
}
var response models.AlertStateType = models.AlertStatePending
pausedState := "un paused"
if cmd.Paused {
response = models.AlertStatePaused
pausedState = "paused"
}
result := map[string]interface{}{
"alertId": cmd.AlertId,
"state": response,
"message": "alert " + pausedState,
}
return Json(200, result)
}
+62
View File
@@ -0,0 +1,62 @@
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)
}
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
AlertId: cmd.PanelId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
})
if err != nil {
return ApiError(500, "Failed to delete annotations", err)
}
return ApiSuccess("Annotations deleted")
}
+35 -3
View File
@@ -19,6 +19,9 @@ func Register(r *macaron.Macaron) {
quota := middleware.Quota
bind := binding.Bind
// automatically set HEAD for every GET
r.SetAutoHead(true)
// not logged in views
r.Get("/", reqSignedIn, Index)
r.Get("/logout", Logout)
@@ -55,12 +58,15 @@ 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)
r.Get("/playlists/", reqSignedIn, Index)
r.Get("/playlists/*", reqSignedIn, Index)
r.Get("/alerting/", reqSignedIn, Index)
r.Get("/alerting/*", reqSignedIn, Index)
// sign up
r.Get("/signup", Index)
@@ -107,6 +113,9 @@ func Register(r *macaron.Macaron) {
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
r.Get("/quotas", wrap(GetUserQuotas))
r.Put("/helpflags/:id", wrap(SetHelpFlag))
// For dev purpose
r.Get("/helpflags/clear", wrap(ClearHelpFlags))
r.Get("/preferences", wrap(GetUserPreferences))
r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
@@ -187,7 +196,7 @@ func Register(r *macaron.Macaron) {
r.Group("/datasources", func() {
r.Get("/", GetDataSources)
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSource)
r.Get("/:id", wrap(GetDataSourceById))
r.Get("/name/:name", wrap(GetDataSourceByName))
@@ -197,9 +206,9 @@ func Register(r *macaron.Macaron) {
r.Get("/plugins", wrap(GetPluginList))
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
r.Group("/plugins", func() {
r.Get("/:pluginId/readme", wrap(GetPluginReadme))
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
}, reqOrgAdmin)
@@ -238,11 +247,33 @@ func Register(r *macaron.Macaron) {
r.Get("/search/", Search)
// metrics
r.Get("/metrics/test", wrap(GetTestMetrics))
r.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
r.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
// metrics
r.Get("/metrics", wrap(GetInternalMetrics))
r.Group("/alerts", func() {
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
r.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
r.Get("/", wrap(GetAlerts))
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
})
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))
}, reqEditorRole)
r.Get("/annotations", wrap(GetAnnotations))
r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
// error test
r.Get("/metrics/error", wrap(GenerateError))
@@ -279,4 +310,5 @@ func Register(r *macaron.Macaron) {
InitAppPluginRoutes(r)
r.NotFound(NotFoundHandler)
}
+16 -1
View File
@@ -1,6 +1,11 @@
package api
import (
"crypto/tls"
"net"
"net/http"
"time"
"gopkg.in/macaron.v1"
"github.com/grafana/grafana/pkg/api/pluginproxy"
@@ -11,6 +16,16 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var pluginProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func InitAppPluginRoutes(r *macaron.Macaron) {
for _, plugin := range plugins.Apps {
for _, route := range plugin.Routes {
@@ -40,7 +55,7 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appId string) macaron.Handler
path := c.Params("*")
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appId)
proxy.Transport = dataProxyTransport
proxy.Transport = pluginProxyTransport
proxy.ServeHTTP(c.Resp, c.Req.Request)
}
}
+78 -28
View File
@@ -33,6 +33,39 @@ type cwRequest struct {
DataSource *m.DataSource
}
type datasourceInfo struct {
Profile string
Region string
AssumeRoleArn string
Namespace string
AccessKey string
SecretKey string
}
func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
accessKey := ""
secretKey := ""
for key, value := range req.DataSource.SecureJsonData.Decrypt() {
if key == "accessKey" {
accessKey = value
}
if key == "secretKey" {
secretKey = value
}
}
return &datasourceInfo{
AssumeRoleArn: assumeRoleArn,
Region: req.Region,
Profile: req.DataSource.Database,
AccessKey: accessKey,
SecretKey: secretKey,
}
}
func init() {
actionHandlers = map[string]actionHandler{
"GetMetricStatistics": handleGetMetricStatistics,
@@ -56,8 +89,8 @@ type cache struct {
var awsCredentialCache map[string]cache = make(map[string]cache)
var credentialCacheLock sync.RWMutex
func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials {
cacheKey := profile + ":" + assumeRoleArn
func getCredentials(dsInfo *datasourceInfo) *credentials.Credentials {
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
credentialCacheLock.RLock()
if _, ok := awsCredentialCache[cacheKey]; ok {
if awsCredentialCache[cacheKey].expiration != nil &&
@@ -74,9 +107,9 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
sessionToken := ""
var expiration *time.Time
expiration = nil
if strings.Index(assumeRoleArn, "arn:aws:iam:") == 0 {
if strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
params := &sts.AssumeRoleInput{
RoleArn: aws.String(assumeRoleArn),
RoleArn: aws.String(dsInfo.AssumeRoleArn),
RoleSessionName: aws.String("GrafanaSession"),
DurationSeconds: aws.Int64(900),
}
@@ -85,13 +118,14 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
stsCreds := credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: profile},
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(stsSess), ExpiryWindow: 5 * time.Minute},
})
stsConfig := &aws.Config{
Region: aws.String(region),
Region: aws.String(dsInfo.Region),
Credentials: stsCreds,
}
svc := sts.New(session.New(stsConfig), stsConfig)
resp, err := svc.AssumeRole(params)
if err != nil {
@@ -115,9 +149,14 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
SessionToken: sessionToken,
}},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{Filename: "", Profile: profile},
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: dsInfo.AccessKey,
SecretAccessKey: dsInfo.SecretKey,
}},
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
credentialCacheLock.Lock()
awsCredentialCache[cacheKey] = cache{
credential: creds,
@@ -129,10 +168,9 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
}
func getAwsConfig(req *cwRequest) *aws.Config {
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
cfg := &aws.Config{
Region: aws.String(req.Region),
Credentials: getCredentials(req.DataSource.Database, req.Region, assumeRoleArn),
Credentials: getCredentials(req.GetDatasourceInfo()),
}
return cfg
}
@@ -143,25 +181,33 @@ func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
reqParam := &struct {
Parameters struct {
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistics []*string `json:"statistics"`
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Period int64 `json:"period"`
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistics []*string `json:"statistics"`
ExtendedStatistics []*string `json:"extendedStatistics"`
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
Period int64 `json:"period"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.GetMetricStatisticsInput{
Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName),
Dimensions: reqParam.Parameters.Dimensions,
Statistics: reqParam.Parameters.Statistics,
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
Period: aws.Int64(reqParam.Parameters.Period),
Namespace: aws.String(reqParam.Parameters.Namespace),
MetricName: aws.String(reqParam.Parameters.MetricName),
Dimensions: reqParam.Parameters.Dimensions,
Statistics: reqParam.Parameters.Statistics,
ExtendedStatistics: reqParam.Parameters.ExtendedStatistics,
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
Period: aws.Int64(reqParam.Parameters.Period),
}
if len(reqParam.Parameters.Statistics) != 0 {
params.Statistics = reqParam.Parameters.Statistics
}
if len(reqParam.Parameters.ExtendedStatistics) != 0 {
params.ExtendedStatistics = reqParam.Parameters.ExtendedStatistics
}
resp, err := svc.GetMetricStatistics(params)
@@ -254,11 +300,12 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
reqParam := &struct {
Parameters struct {
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistic string `json:"statistic"`
Period int64 `json:"period"`
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
Statistic string `json:"statistic"`
ExtendedStatistic string `json:"extendedStatistic"`
Period int64 `json:"period"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
@@ -274,6 +321,9 @@ func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
if reqParam.Parameters.Statistic != "" {
params.Statistic = aws.String(reqParam.Parameters.Statistic)
}
if reqParam.Parameters.ExtendedStatistic != "" {
params.ExtendedStatistic = aws.String(reqParam.Parameters.ExtendedStatistic)
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
+69 -55
View File
@@ -28,7 +28,8 @@ var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCa
func init() {
metricsMap = map[string][]string{
"AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnhealthyHostCount"},
"AWS/ApiGateway": {"4XXError", "5XXError", "CacheHitCount", "CacheMissCount", "Count", "IntegrationLatency", "Latency"},
"AWS/ApplicationELB": {"ActiveConnectionCount", "ClientTLSNegotiationErrorCount", "HealthyHostCount", "HTTPCode_ELB_4XX_Count", "HTTPCode_ELB_5XX_Count", "HTTPCode_Target_2XX_Count", "HTTPCode_Target_3XX_Count", "HTTPCode_Target_4XX_Count", "HTTPCode_Target_5XX_Count", "NewConnectionCount", "ProcessedBytes", "RejectedConnectionCount", "RequestCount", "TargetConnectionErrorCount", "TargetResponseTime", "TargetTLSNegotiationErrorCount", "UnHealthyHostCount"},
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
@@ -66,27 +67,32 @@ 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": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "FailedSqlStatements", "FreeableMemory", "FreeStorageSpace", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
"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"},
"AWS/ApplicationELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"},
"AWS/AutoScaling": {"AutoScalingGroupName"},
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
@@ -104,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"},
@@ -119,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)
@@ -130,7 +140,7 @@ func init() {
func handleGetRegions(req *cwRequest, c *middleware.Context) {
regions := []string{
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "cn-north-1",
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2",
"eu-central-1", "eu-west-1", "sa-east-1", "us-east-1", "us-west-1", "us-west-2", "us-gov-west-1",
}
result := []interface{}{}
@@ -182,8 +192,10 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
}
} else {
var err error
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
if namespaceMetrics, err = getMetricsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, assumeRoleArn, getAllMetrics); err != nil {
cwData := req.GetDatasourceInfo()
cwData.Namespace = reqParam.Parameters.Namespace
if namespaceMetrics, err = getMetricsForCustomMetrics(cwData, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
@@ -216,8 +228,10 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
}
} else {
var err error
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
if dimensionValues, err = getDimensionsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, assumeRoleArn, getAllMetrics); err != nil {
dsInfo := req.GetDatasourceInfo()
dsInfo.Namespace = reqParam.Parameters.Namespace
if dimensionValues, err = getDimensionsForCustomMetrics(dsInfo, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
@@ -232,16 +246,16 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
c.JSON(200, result)
}
func getAllMetrics(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) {
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
cfg := &aws.Config{
Region: aws.String(region),
Credentials: getCredentials(database, region, assumeRoleArn),
Region: aws.String(cwData.Region),
Credentials: getCredentials(cwData),
}
svc := cloudwatch.New(session.New(cfg), cfg)
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(namespace),
Namespace: aws.String(cwData.Namespace),
}
var resp cloudwatch.ListMetricsOutput
@@ -262,8 +276,8 @@ func getAllMetrics(region string, namespace string, database string, assumeRoleA
var metricsCacheLock sync.Mutex
func getMetricsForCustomMetrics(region string, namespace string, database string, assumeRoleArn string, getAllMetrics func(string, string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database, assumeRoleArn)
func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
@@ -271,37 +285,37 @@ func getMetricsForCustomMetrics(region string, namespace string, database string
metricsCacheLock.Lock()
defer metricsCacheLock.Unlock()
if _, ok := customMetricsMetricsMap[database]; !ok {
customMetricsMetricsMap[database] = make(map[string]map[string]*CustomMetricsCache)
if _, ok := customMetricsMetricsMap[dsInfo.Profile]; !ok {
customMetricsMetricsMap[dsInfo.Profile] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region]; !ok {
customMetricsMetricsMap[database][region] = make(map[string]*CustomMetricsCache)
if _, ok := customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region]; !ok {
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region][namespace]; !ok {
customMetricsMetricsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
if _, ok := customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace]; !ok {
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace] = &CustomMetricsCache{}
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
}
if customMetricsMetricsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsMetricsMap[database][region][namespace].Cache, nil
if customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsMetricsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
if isDuplicate(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) {
if isDuplicate(customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *metric.MetricName) {
continue
}
customMetricsMetricsMap[database][region][namespace].Cache = append(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName)
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = append(customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *metric.MetricName)
}
return customMetricsMetricsMap[database][region][namespace].Cache, nil
return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
var dimensionsCacheLock sync.Mutex
func getDimensionsForCustomMetrics(region string, namespace string, database string, assumeRoleArn string, getAllMetrics func(string, string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database, assumeRoleArn)
func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(dsInfo)
if err != nil {
return []string{}, err
}
@@ -309,33 +323,33 @@ func getDimensionsForCustomMetrics(region string, namespace string, database str
dimensionsCacheLock.Lock()
defer dimensionsCacheLock.Unlock()
if _, ok := customMetricsDimensionsMap[database]; !ok {
customMetricsDimensionsMap[database] = make(map[string]map[string]*CustomMetricsCache)
if _, ok := customMetricsDimensionsMap[dsInfo.Profile]; !ok {
customMetricsDimensionsMap[dsInfo.Profile] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region]; !ok {
customMetricsDimensionsMap[database][region] = make(map[string]*CustomMetricsCache)
if _, ok := customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region]; !ok {
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region][namespace]; !ok {
customMetricsDimensionsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
if _, ok := customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace]; !ok {
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace] = &CustomMetricsCache{}
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
}
if customMetricsDimensionsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
if customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsDimensionsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
for _, dimension := range metric.Dimensions {
if isDuplicate(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) {
if isDuplicate(customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *dimension.Name) {
continue
}
customMetricsDimensionsMap[database][region][namespace].Cache = append(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name)
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = append(customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, *dimension.Name)
}
}
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
}
func isDuplicate(nameList []string, target string) bool {
+16 -12
View File
@@ -11,11 +11,13 @@ import (
func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
assumeRoleArn := ""
f := func(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) {
dsInfo := &datasourceInfo{
Region: "us-east-1",
Namespace: "Foo",
Profile: "default",
AssumeRoleArn: "",
}
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
@@ -29,7 +31,7 @@ func TestCloudWatchMetrics(t *testing.T) {
},
}, nil
}
metrics, _ := getMetricsForCustomMetrics(region, namespace, database, assumeRoleArn, f)
metrics, _ := getMetricsForCustomMetrics(dsInfo, f)
Convey("Should contain Test_MetricName", func() {
So(metrics, ShouldContain, "Test_MetricName")
@@ -37,11 +39,13 @@ func TestCloudWatchMetrics(t *testing.T) {
})
Convey("When calling getDimensionsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
assumeRoleArn := ""
f := func(region string, namespace string, database string, assumeRoleArn string) (cloudwatch.ListMetricsOutput, error) {
dsInfo := &datasourceInfo{
Region: "us-east-1",
Namespace: "Foo",
Profile: "default",
AssumeRoleArn: "",
}
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
@@ -55,7 +59,7 @@ func TestCloudWatchMetrics(t *testing.T) {
},
}, nil
}
dimensionKeys, _ := getDimensionsForCustomMetrics(region, namespace, database, assumeRoleArn, f)
dimensionKeys, _ := getDimensionsForCustomMetrics(dsInfo, f)
Convey("Should contain Test_DimensionName", func() {
So(dimensionKeys, ShouldContain, "Test_DimensionName")
+1 -4
View File
@@ -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"
@@ -80,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse {
func ApiSuccess(message string) *NormalResponse {
resp := make(map[string]interface{})
resp["message"] = message
return Respond(200, resp)
return Json(200, resp)
}
func ApiError(status int, message string, err error) *NormalResponse {
@@ -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"
}
+45
View File
@@ -8,11 +8,13 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -120,6 +122,10 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
dash := cmd.GetDashboardModel()
// Check if Title is empty
if dash.Title == "" {
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
}
if dash.Id == 0 {
limitReached, err := middleware.QuotaReached(c, "dashboard")
if err != nil {
@@ -130,6 +136,16 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
}
}
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
OrgId: c.OrgId,
UserId: c.UserId,
Dashboard: dash,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
}
err := bus.Dispatch(&cmd)
if err != nil {
if err == m.ErrDashboardWithSameNameExists {
@@ -152,6 +168,16 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
return ApiError(500, "Failed to save dashboard", err)
}
alertCmd := alerting.UpdateDashboardAlertsCommand{
OrgId: c.OrgId,
UserId: c.UserId,
Dashboard: cmd.Result,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return ApiError(500, "Failed to save alerts", err)
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
}
@@ -191,9 +217,28 @@ func GetHomeDashboard(c *middleware.Context) Response {
return ApiError(500, "Failed to load home dashboard", err)
}
if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
addGettingStartedPanelToHomeDashboard(dash.Dashboard)
}
return Json(200, &dash)
}
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
rows := dash.Get("rows").MustArray()
row := simplejson.NewFromAny(rows[0])
newpanel := simplejson.NewFromAny(map[string]interface{}{
"type": "gettingstarted",
"id": 123123,
"span": 12,
})
panels := row.Get("panels").MustArray()
panels = append(panels, newpanel)
row.Set("panels", panels)
}
func GetDashboardFromJsonFile(c *middleware.Context) {
file := c.Params(":file")
+29 -14
View File
@@ -1,8 +1,6 @@
package api
import (
"crypto/tls"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -17,16 +15,6 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var dataProxyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
director := func(req *http.Request) {
req.URL.Scheme = targetUrl.Scheme
@@ -77,7 +65,7 @@ func getDatasource(id int64, orgId int64) (*m.DataSource, error) {
return nil, err
}
return &query.Result, nil
return query.Result, nil
}
func ProxyDataSourceRequest(c *middleware.Context) {
@@ -95,6 +83,13 @@ func ProxyDataSourceRequest(c *middleware.Context) {
return
}
if ds.Type == m.DS_INFLUXDB {
if c.Query("db") != ds.Database {
c.JsonApiErr(403, "Datasource is not configured to allow this database", nil)
return
}
}
targetUrl, _ := url.Parse(ds.Url)
if len(setting.DataProxyWhiteList) > 0 {
if _, exists := setting.DataProxyWhiteList[targetUrl.Host]; !exists {
@@ -104,8 +99,28 @@ 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.Transport, err = ds.GetHttpTransport()
if err != nil {
c.JsonApiErr(400, "Unable to load TLS certificate", err)
return
}
proxy.ServeHTTP(c.Resp, c.Req.Request)
c.Resp.Header().Del("Set-Cookie")
}
+7 -4
View File
@@ -11,11 +11,16 @@ import (
)
func TestDataSourceProxy(t *testing.T) {
Convey("When getting graphite datasource proxy", t, func() {
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
targetUrl, _ := url.Parse(ds.Url)
targetUrl, err := url.Parse(ds.Url)
proxy := NewReverseProxy(&ds, "/render", targetUrl)
proxy.Transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
transport, ok := proxy.Transport.(*http.Transport)
So(ok, ShouldBeTrue)
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
requestUrl, _ := url.Parse("http://grafana.com/sub")
req := http.Request{URL: requestUrl}
@@ -54,7 +59,5 @@ func TestDataSourceProxy(t *testing.T) {
So(queryVals["u"][0], ShouldEqual, "user")
So(queryVals["p"][0], ShouldEqual, "password")
})
})
}
+68 -13
View File
@@ -5,10 +5,9 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/plugins"
//"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/util"
)
@@ -22,7 +21,6 @@ func GetDataSources(c *middleware.Context) {
result := make(dtos.DataSourceList, 0)
for _, ds := range query.Result {
dsItem := dtos.DataSource{
Id: ds.Id,
OrgId: ds.OrgId,
@@ -35,6 +33,7 @@ func GetDataSources(c *middleware.Context) {
User: ds.User,
BasicAuth: ds.BasicAuth,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
}
if plugin, exists := plugins.DataSources[ds.Type]; exists {
@@ -92,6 +91,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
cmd.OrgId = c.OrgId
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrDataSourceNameExists {
c.JsonApiErr(409, err.Error(), err)
return
}
c.JsonApiErr(500, "Failed to add datasource", err)
return
}
@@ -99,17 +103,56 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) {
c.JSON(200, util.DynMap{"message": "Datasource added", "id": cmd.Result.Id})
}
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) {
func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
err := bus.Dispatch(&cmd)
err := fillWithSecureJsonData(&cmd)
if err != nil {
c.JsonApiErr(500, "Failed to update datasource", err)
return
return ApiError(500, "Failed to update datasource", err)
}
c.JsonOK("Datasource updated")
err = bus.Dispatch(&cmd)
if err != nil {
return ApiError(500, "Failed to update datasource", err)
}
return Json(200, util.DynMap{"message": "Datasource updated"})
}
func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error {
if len(cmd.SecureJsonData) == 0 {
return nil
}
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
if err != nil {
return err
}
secureJsonData := ds.SecureJsonData.Decrypt()
for k, v := range secureJsonData {
if _, ok := cmd.SecureJsonData[k]; !ok {
cmd.SecureJsonData[k] = v
}
}
return nil
}
func getRawDataSourceById(id int64, orgId int64) (*m.DataSource, error) {
query := m.GetDataSourceByIdQuery{
Id: id,
OrgId: orgId,
}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
return query.Result, nil
}
// Get /api/datasources/name/:name
@@ -123,9 +166,7 @@ func GetDataSourceByName(c *middleware.Context) Response {
return ApiError(500, "Failed to query datasources", err)
}
ds := query.Result
dtos := convertModelToDtos(ds)
dtos := convertModelToDtos(query.Result)
return Json(200, &dtos)
}
@@ -148,8 +189,8 @@ func GetDataSourceIdByName(c *middleware.Context) Response {
return Json(200, &dtos)
}
func convertModelToDtos(ds m.DataSource) dtos.DataSource {
return dtos.DataSource{
func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
dto := dtos.DataSource{
Id: ds.Id,
OrgId: ds.OrgId,
Name: ds.Name,
@@ -166,4 +207,18 @@ func convertModelToDtos(ds m.DataSource) dtos.DataSource {
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
}
if len(ds.SecureJsonData) > 0 {
dto.TLSAuth.CACertSet = len(ds.SecureJsonData["tlsCACert"]) > 0
dto.TLSAuth.ClientCertSet = len(ds.SecureJsonData["tlsClientCert"]) > 0
dto.TLSAuth.ClientKeySet = len(ds.SecureJsonData["tlsClientKey"]) > 0
}
for k, v := range ds.SecureJsonData {
if len(v) > 0 {
dto.EncryptedFields = append(dto.EncryptedFields, k)
}
}
return dto
}
+66
View File
@@ -0,0 +1,66 @@
package dtos
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
)
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"`
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"`
IsDefault bool `json:"isDefault"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type AlertTestCommand struct {
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
PanelId int64 `json:"panelId" binding:"Required"`
}
type AlertTestResult struct {
Firing bool `json:"firing"`
ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
EvalMatches []*EvalMatch `json:"matches,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"`
}
type AlertTestResultLog struct {
Message string `json:"message"`
Data interface{} `json:"data"`
}
type EvalMatch struct {
Tags map[string]string `json:"tags,omitempty"`
Metric string `json:"metric"`
Value float64 `json:"value"`
}
type NotificationTestCommand struct {
Name string `json:"name"`
Type string `json:"type"`
Settings *simplejson.Json `json:"settings"`
}
type PauseAlertCommand struct {
AlertId int64 `json:"alertId"`
Paused bool `json:"paused"`
}
+23
View File
@@ -0,0 +1,23 @@
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"`
}
type DeleteAnnotationsCmd struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
}
+27 -20
View File
@@ -22,19 +22,20 @@ type LoginCommand struct {
}
type CurrentUser struct {
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
IsSignedIn bool `json:"isSignedIn"`
Id int64 `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
}
type DashboardMeta struct {
@@ -80,6 +81,15 @@ type DataSource struct {
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData,omitempty"`
TLSAuth TLSAuth `json:"tlsAuth,omitempty"`
EncryptedFields []string `json:"encryptedFields"`
}
// TLSAuth is used to show if TLS certs have been uploaded already
type TLSAuth struct {
CACertSet bool `json:"tlsCACertSet"`
ClientCertSet bool `json:"tlsClientCertSet"`
ClientKeySet bool `json:"tlsClientKeySet"`
}
type DataSourceList []DataSource
@@ -96,13 +106,10 @@ func (slice DataSourceList) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}
type MetricQueryResultDto struct {
Data []MetricQueryResultDataDto `json:"data"`
}
type MetricQueryResultDataDto struct {
Target string `json:"target"`
DataPoints [][2]float64 `json:"datapoints"`
type MetricRequest struct {
From string `json:"from"`
To string `json:"to"`
Queries []*simplejson.Json `json:"queries"`
}
type UserStars struct {
+23
View File
@@ -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]
}
+28 -6
View File
@@ -38,7 +38,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
url := ds.Url
if ds.Access == m.DS_ACCESS_PROXY {
url = setting.AppSubUrl + "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
}
var dsMap = map[string]interface{}{
@@ -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,
}
@@ -121,11 +122,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
panels := map[string]interface{}{}
for _, panel := range enabledPlugins.Panels {
panels[panel.Id] = map[string]interface{}{
"module": panel.Module,
"baseUrl": panel.BaseUrl,
"name": panel.Name,
"id": panel.Id,
"info": panel.Info,
"module": panel.Module,
"baseUrl": panel.BaseUrl,
"name": panel.Name,
"id": panel.Id,
"info": panel.Info,
"hideFromList": panel.HideFromList,
"sort": getPanelSort(panel.Id),
}
}
@@ -149,6 +152,25 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
return jsonObj, nil
}
func getPanelSort(id string) int {
sort := 100
switch id {
case "graph":
sort = 1
case "singlestat":
sort = 2
case "table":
sort = 3
case "text":
sort = 4
case "alertlist":
sort = 5
case "dashlist":
sort = 6
}
return sort
}
func GetFrontendSettings(c *middleware.Context) {
settings, err := getFrontendSettingsMap(c)
if err != nil {
+1
View File
@@ -36,6 +36,7 @@ func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")
req.Header.Del("Authorization")
}
return &httputil.ReverseProxy{Director: director}
+29 -3
View File
@@ -1,6 +1,7 @@
package api
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
@@ -32,6 +33,16 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
locale = parts[0]
}
appUrl := setting.AppUrl
appSubUrl := setting.AppSubUrl
// special case when doing localhost call from phantomjs
if c.IsRenderCall {
appUrl = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
appSubUrl = ""
settings["appSubUrl"] = ""
}
var data = dtos.IndexViewData{
User: &dtos.CurrentUser{
Id: c.UserId,
@@ -47,10 +58,11 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
Locale: locale,
HelpFlags1: c.HelpFlags1,
},
Settings: settings,
AppUrl: setting.AppUrl,
AppSubUrl: setting.AppSubUrl,
AppUrl: appUrl,
AppSubUrl: appSubUrl,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId,
BuildVersion: setting.BuildVersion,
@@ -91,6 +103,20 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Children: dashboardChildNavs,
})
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
alertChildNavs := []*dtos.NavLink{
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
{Text: "Notifications", Url: setting.AppSubUrl + "/alerting/notifications"},
}
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Alerting",
Icon: "icon-gf icon-gf-alert",
Url: setting.AppSubUrl + "/alerting/list",
Children: alertChildNavs,
})
}
if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Data Sources",
@@ -140,7 +166,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
}
}
if c.OrgRole == m.ROLE_ADMIN {
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
}
+7 -3
View File
@@ -25,11 +25,15 @@ func LoginView(c *middleware.Context) {
return
}
viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google
viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub
enabledOAuths := make(map[string]interface{})
for key, oauth := range setting.OAuthService.OAuthInfos {
enabledOAuths[key] = map[string]string{"name": oauth.Name}
}
viewData.Settings["oauth"] = enabledOAuths
viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
viewData.Settings["loginHint"] = setting.LoginHint
viewData.Settings["allowUserPassLogin"] = setting.AllowUserPassLogin
viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, VIEW_INDEX, viewData)
+88 -16
View File
@@ -1,14 +1,20 @@
package api
import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/url"
"io/ioutil"
"log"
"net/http"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@@ -16,6 +22,12 @@ import (
"github.com/grafana/grafana/pkg/social"
)
func GenStateString() string {
rnd := make([]byte, 32)
rand.Read(rnd)
return base64.StdEncoding.EncodeToString(rnd)
}
func OAuthLogin(ctx *middleware.Context) {
if setting.OAuthService == nil {
ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil)
@@ -29,43 +41,102 @@ func OAuthLogin(ctx *middleware.Context) {
return
}
error := ctx.Query("error")
if error != "" {
errorDesc := ctx.Query("error_description")
ctx.Logger.Info("OAuthLogin Failed", "error", error, "errorDesc", errorDesc)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1003")
return
}
code := ctx.Query("code")
if code == "" {
ctx.Redirect(connect.AuthCodeURL("", oauth2.AccessTypeOnline))
state := GenStateString()
ctx.Session.Set(middleware.SESS_KEY_OAUTH_STATE, state)
if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
}else{
ctx.Redirect(connect.AuthCodeURL(state, oauth2.SetParam("hd", setting.OAuthService.OAuthInfos[name].HostedDomain), oauth2.AccessTypeOnline));
}
return
}
// verify state string
savedState := ctx.Session.Get(middleware.SESS_KEY_OAUTH_STATE).(string)
queryState := ctx.Query("state")
if savedState != queryState {
ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
return
}
// handle call back
token, err := connect.Exchange(oauth2.NoContext, code)
// initialize oauth2 context
oauthCtx := oauth2.NoContext
if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" {
cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
if err != nil {
log.Fatal(err)
}
// Load CA cert
caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
},
}
sslcli := &http.Client{Transport: tr}
oauthCtx = context.Background()
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
}
// get token from provider
token, err := connect.Exchange(oauthCtx, code)
if err != nil {
ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
return
}
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer"
log.Trace("login.OAuthLogin(Got token)")
ctx.Logger.Debug("OAuthLogin Got token")
userInfo, err := connect.UserInfo(token)
// set up oauth2 client
client := connect.Client(oauthCtx, token)
// get user info
userInfo, err := connect.UserInfo(client)
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)
}
return
}
log.Trace("login.OAuthLogin(social login): %s", userInfo)
ctx.Logger.Debug("OAuthLogin got user info", "userInfo", userInfo)
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
ctx.Logger.Info("OAuth login attempt with unallowed email", "email", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?failCode=1002")
return
}
userQuery := m.GetUserByLoginQuery{LoginOrEmail: userInfo.Email}
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
err = bus.Dispatch(&userQuery)
// create account if missing
@@ -84,10 +155,11 @@ func OAuthLogin(ctx *middleware.Context) {
return
}
cmd := m.CreateUserCommand{
Login: userInfo.Email,
Email: userInfo.Email,
Name: userInfo.Name,
Company: userInfo.Company,
Login: userInfo.Login,
Email: userInfo.Email,
Name: userInfo.Name,
Company: userInfo.Company,
DefaultOrgRole: userInfo.Role,
}
if err = bus.Dispatch(&cmd); err != nil {
+42 -21
View File
@@ -1,40 +1,57 @@
package api
import (
"context"
"encoding/json"
"math/rand"
"net/http"
"strconv"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/testdata"
"github.com/grafana/grafana/pkg/util"
)
func GetTestMetrics(c *middleware.Context) Response {
from := c.QueryInt64("from")
to := c.QueryInt64("to")
maxDataPoints := c.QueryInt64("maxDataPoints")
stepInSeconds := (to - from) / maxDataPoints
// POST /api/tsdb/query
func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response {
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
result := dtos.MetricQueryResultDto{}
result.Data = make([]dtos.MetricQueryResultDataDto, 1)
request := &tsdb.Request{TimeRange: timeRange}
for seriesIndex := range result.Data {
points := make([][2]float64, maxDataPoints)
walker := rand.Float64() * 100
time := from
for _, query := range reqDto.Queries {
request.Queries = append(request.Queries, &tsdb.Query{
RefId: query.Get("refId").MustString("A"),
MaxDataPoints: query.Get("maxDataPoints").MustInt64(100),
IntervalMs: query.Get("intervalMs").MustInt64(1000),
Model: query,
DataSource: &models.DataSource{
Name: "Grafana TestDataDB",
Type: "grafana-testdata-datasource",
},
})
}
for i := range points {
points[i][0] = walker
points[i][1] = float64(time)
walker += rand.Float64() - 0.5
time += stepInSeconds
}
resp, err := tsdb.HandleRequest(context.TODO(), request)
if err != nil {
return ApiError(500, "Metric request error", err)
}
result.Data[seriesIndex].Target = "test-series-" + strconv.Itoa(seriesIndex)
result.Data[seriesIndex].DataPoints = points
return Json(200, &resp)
}
// GET /api/tsdb/testdata/scenarios
func GetTestDataScenarios(c *middleware.Context) Response {
result := make([]interface{}, 0)
for _, scenario := range testdata.ScenarioRegistry {
result = append(result, map[string]interface{}{
"id": scenario.Id,
"name": scenario.Name,
"description": scenario.Description,
"stringInput": scenario.StringInput,
})
}
return Json(200, &result)
@@ -53,6 +70,10 @@ func GetInternalMetrics(c *middleware.Context) Response {
metricName := m.Name() + m.StringifyTags()
switch metric := m.(type) {
case metrics.Gauge:
resp[metricName] = map[string]interface{}{
"value": metric.Value(),
}
case metrics.Counter:
resp[metricName] = map[string]interface{}{
"count": metric.Count(),
+3
View File
@@ -152,6 +152,9 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgId int64) Respons
// GET /api/orgs/:orgId
func DeleteOrgById(c *middleware.Context) Response {
if err := bus.Dispatch(&m.DeleteOrgCommand{Id: c.ParamsInt64(":orgId")}); err != nil {
if err == m.ErrOrgNotFound {
return ApiError(404, "Failed to delete organization. ID not found", nil)
}
return ApiError(500, "Failed to update organization", err)
}
return ApiSuccess("Organization deleted")
+3
View File
@@ -38,6 +38,9 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
cmd.UserId = userToAdd.Id
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrOrgUserAlreadyAdded {
return ApiError(409, "User is already member of this organization", nil)
}
return ApiError(500, "Could not add user to organization", err)
}
+12
View File
@@ -26,6 +26,18 @@ func ValidateOrgPlaylist(c *middleware.Context) {
c.JsonApiErr(403, "You are not allowed to edit/view playlist", nil)
return
}
items, itemsErr := LoadPlaylistItemDTOs(id)
if itemsErr != nil {
c.JsonApiErr(404, "Playlist items not found", err)
return
}
if len(items) == 0 {
c.JsonApiErr(404, "Playlist is empty", itemsErr)
return
}
}
func SearchPlaylists(c *middleware.Context) Response {
+19 -10
View File
@@ -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
}
+5 -19
View File
@@ -6,35 +6,21 @@ import (
"github.com/grafana/grafana/pkg/components/renderer"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func RenderToPng(c *middleware.Context) {
queryReader := util.NewUrlQueryReader(c.Req.URL)
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
sessionId := c.Session.ID()
// Handle api calls authenticated without session
if sessionId == "" && c.ApiKeyId != 0 {
c.Session.Start(c)
c.Session.Set(middleware.SESS_KEY_APIKEY, c.ApiKeyId)
// release will make sure the new session is persisted before
// we spin up phantomjs
c.Session.Release()
// cleanup session after render is complete
defer func() { c.Session.Destory(c) }()
}
renderOpts := &renderer.RenderOpts{
Url: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
SessionId: c.Session.ID(),
Timeout: queryReader.Get("timeout", "30"),
Path: c.Params("*") + queryParams,
Width: queryReader.Get("width", "800"),
Height: queryReader.Get("height", "400"),
OrgId: c.OrgId,
Timeout: queryReader.Get("timeout", "30"),
}
renderOpts.Url = setting.ToAbsUrl(renderOpts.Url)
pngPath, err := renderer.RenderToPng(renderOpts)
if err != nil {
+31
View File
@@ -180,3 +180,34 @@ func SearchUsers(c *middleware.Context) Response {
return Json(200, query.Result)
}
func SetHelpFlag(c *middleware.Context) Response {
flag := c.ParamsInt64(":id")
bitmask := &c.HelpFlags1
bitmask.AddFlag(m.HelpFlags1(flag))
cmd := m.SetUserHelpFlagCommand{
UserId: c.UserId,
HelpFlags1: *bitmask,
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update help flag", err)
}
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
}
func ClearHelpFlags(c *middleware.Context) Response {
cmd := m.SetUserHelpFlagCommand{
UserId: c.UserId,
HelpFlags1: m.HelpFlags1(0),
}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed to update help flag", err)
}
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
}
+40
View File
@@ -1,18 +1,22 @@
package bus
import (
"context"
"fmt"
"reflect"
)
type HandlerFunc interface{}
type CtxHandlerFunc func()
type Msg interface{}
type Bus interface {
Dispatch(msg Msg) error
DispatchCtx(ctx context.Context, msg Msg) error
Publish(msg Msg) error
AddHandler(handler HandlerFunc)
AddCtxHandler(handler HandlerFunc)
AddEventListener(handler HandlerFunc)
AddWildcardListener(handler HandlerFunc)
}
@@ -34,6 +38,27 @@ func New() Bus {
return bus
}
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
var handler = b.handlers[msgName]
if handler == nil {
return fmt.Errorf("handler not found for %s", msgName)
}
var params = make([]reflect.Value, 2)
params[0] = reflect.ValueOf(ctx)
params[1] = reflect.ValueOf(msg)
ret := reflect.ValueOf(handler).Call(params)
err := ret[0].Interface()
if err == nil {
return nil
} else {
return err.(error)
}
}
func (b *InProcBus) Dispatch(msg Msg) error {
var msgName = reflect.TypeOf(msg).Elem().Name()
@@ -90,6 +115,12 @@ func (b *InProcBus) AddHandler(handler HandlerFunc) {
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddCtxHandler(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
queryTypeName := handlerType.In(1).Elem().Name()
b.handlers[queryTypeName] = handler
}
func (b *InProcBus) AddEventListener(handler HandlerFunc) {
handlerType := reflect.TypeOf(handler)
eventName := handlerType.In(0).Elem().Name()
@@ -105,6 +136,11 @@ func AddHandler(implName string, handler HandlerFunc) {
globalBus.AddHandler(handler)
}
// Package level functions
func AddCtxHandler(implName string, handler HandlerFunc) {
globalBus.AddCtxHandler(handler)
}
// Package level functions
func AddEventListener(handler HandlerFunc) {
globalBus.AddEventListener(handler)
@@ -118,6 +154,10 @@ func Dispatch(msg Msg) error {
return globalBus.Dispatch(msg)
}
func DispatchCtx(ctx context.Context, msg Msg) error {
return globalBus.DispatchCtx(ctx, msg)
}
func Publish(msg Msg) error {
return globalBus.Publish(msg)
}
+1 -1
View File
@@ -45,7 +45,7 @@ func runPluginCommand(command func(commandLine CommandLine) error) func(context
cmd := &contextCommandLine{context}
if err := command(cmd); err != nil {
logger.Errorf("\n%s: ", color.RedString("Error"))
logger.Errorf("%s\n\n", err)
logger.Errorf("%s %s\n\n", color.RedString("✗"), err)
cmd.ShowHelp()
os.Exit(1)
@@ -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
}
@@ -53,8 +53,16 @@ func upgradeAllCommand(c CommandLine) error {
for _, p := range pluginsToUpgrade {
logger.Infof("Updating %v \n", p.Id)
s.RemoveInstalledPlugin(pluginsDir, p.Id)
InstallPlugin(p.Id, "", c)
var err error
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
if err != nil {
return err
}
err = InstallPlugin(p.Id, "", c)
if err != nil {
return err
}
}
return nil
+3
View File
@@ -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 = ""
+3 -3
View File
@@ -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"`
}
+87 -20
View File
@@ -1,35 +1,70 @@
package services
import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"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{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
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 := sendRequest(repoUrl, "repo")
if err != nil {
logger.Info("Failed to send request", "error", err)
return m.PluginRepo{}, fmt.Errorf("Failed to send 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 +123,53 @@ func RemoveInstalledPlugin(pluginPath, pluginName string) error {
}
func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
fullUrl := repoUrl + "/repo/" + pluginId
body, err := sendRequest(repoUrl, "repo", pluginId)
if err != nil {
logger.Info("Failed to send request", "error", err)
return m.Plugin{}, fmt.Errorf("Failed to send 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 sendRequest(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)
if err != nil {
return []byte{}, err
}
if res.StatusCode/100 != 2 {
return []byte{}, fmt.Errorf("Api returned invalid status: %s", res.Status)
}
body, err := ioutil.ReadAll(res.Body)
defer res.Body.Close()
return body, err
}
+19 -38
View File
@@ -13,18 +13,20 @@ import (
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
_ "github.com/grafana/grafana/pkg/tsdb/opentsdb"
_ "github.com/grafana/grafana/pkg/tsdb/prometheus"
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
)
var version = "3.1.0"
var version = "4.1.0"
var commit = "NA"
var buildstamp string
var build_date string
@@ -55,25 +57,8 @@ func main() {
setting.BuildCommit = commit
setting.BuildStamp = buildstampInt64
go listenToSystemSignels()
flag.Parse()
writePIDFile()
initRuntime()
metrics.Init()
search.Init()
login.Init()
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
if err := notifications.Init(); err != nil {
log.Fatal(3, "Notification service failed to initialize", err)
}
StartServer()
exitChan <- 0
server := NewGrafanaServer()
server.Start()
}
func initRuntime() {
@@ -91,7 +76,9 @@ func initRuntime() {
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
setting.LogConfigurationInfo()
}
func initSql() {
sqlstore.NewEngine()
sqlstore.EnsureAdminUser()
}
@@ -114,24 +101,18 @@ func writePIDFile() {
}
}
func listenToSystemSignels() {
func listenToSystemSignals(server models.GrafanaServer) {
signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
code := 0
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
select {
case sig := <-signalChan:
log.Info("Received signal %s. shutting down", sig)
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
case code = <-exitChan:
switch code {
case 0:
log.Info("Shutting down")
default:
log.Warn("Shutting down")
}
server.Shutdown(code, "startup error")
}
log.Close()
os.Exit(code)
}
+150
View File
@@ -0,0 +1,150 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"gopkg.in/macaron.v1"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/eventpublisher"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/social"
)
func NewGrafanaServer() models.GrafanaServer {
rootCtx, shutdownFn := context.WithCancel(context.Background())
childRoutines, childCtx := errgroup.WithContext(rootCtx)
return &GrafanaServerImpl{
context: childCtx,
shutdownFn: shutdownFn,
childRoutines: childRoutines,
log: log.New("server"),
}
}
type GrafanaServerImpl struct {
context context.Context
shutdownFn context.CancelFunc
childRoutines *errgroup.Group
log log.Logger
}
func (g *GrafanaServerImpl) Start() {
go listenToSystemSignals(g)
writePIDFile()
initRuntime()
initSql()
metrics.Init()
search.Init()
login.Init()
social.NewOAuthService()
eventpublisher.Init()
plugins.Init()
// init alerting
if setting.ExecuteAlerts {
engine := alerting.NewEngine()
g.childRoutines.Go(func() error { return engine.Run(g.context) })
}
// cleanup service
cleanUpService := cleanup.NewCleanUpService()
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
if err := notifications.Init(); err != nil {
g.log.Error("Notification service failed to initialize", "erro", err)
g.Shutdown(1, "Startup failed")
return
}
g.startHttpServer()
}
func (g *GrafanaServerImpl) startHttpServer() {
logger = log.New("http.server")
var err error
m := newMacaron()
api.Register(m)
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl)
switch setting.Protocol {
case setting.HTTP:
err = http.ListenAndServe(listenAddr, m)
case setting.HTTPS:
err = ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
default:
g.log.Error("Invalid protocol", "protocol", setting.Protocol)
g.Shutdown(1, "Startup failed")
}
if err != nil {
g.log.Error("Fail to start server", "error", err)
g.Shutdown(1, "Startup failed")
return
}
}
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
g.log.Info("Shutdown started", "code", code, "reason", reason)
g.shutdownFn()
err := g.childRoutines.Wait()
g.log.Info("Shutdown completed", "reason", err)
log.Close()
os.Exit(code)
}
func ListenAndServeTLS(listenAddr, certfile, keyfile string, m *macaron.Macaron) error {
if certfile == "" {
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
if keyfile == "" {
return fmt.Errorf("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(setting.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`Cannot find SSL cert_file at %v`, setting.CertFile)
}
if _, err := os.Stat(setting.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`Cannot find SSL key_file at %v`, setting.KeyFile)
}
return http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
}
// implement context.Context
func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) {
return g.context.Deadline()
}
func (g *GrafanaServerImpl) Done() <-chan struct{} {
return g.context.Done()
}
func (g *GrafanaServerImpl) Err() error {
return g.context.Err()
}
func (g *GrafanaServerImpl) Value(key interface{}) interface{} {
return g.context.Value(key)
}
+10 -7
View File
@@ -6,7 +6,6 @@ package main
import (
"fmt"
"net/http"
"os"
"path"
"gopkg.in/macaron.v1"
@@ -47,13 +46,15 @@ func newMacaron() *macaron.Macaron {
Delims: macaron.Delims{Left: "[[", Right: "]]"},
}))
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
m.Use(middleware.RequestMetrics())
// needs to be after context handler
if setting.EnforceDomain {
m.Use(middleware.ValidateHostHeader(setting.Domain))
}
m.Use(middleware.GetContextHandler())
m.Use(middleware.Sessioner(&setting.SessionOptions))
return m
}
@@ -78,7 +79,7 @@ func mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
))
}
func StartServer() {
func StartServer() int {
logger = log.New("server")
var err error
@@ -94,11 +95,13 @@ func StartServer() {
err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m)
default:
logger.Error("Invalid protocol", "protocol", setting.Protocol)
os.Exit(1)
return 1
}
if err != nil {
logger.Error("Fail to start server", "error", err)
os.Exit(1)
return 1
}
return 0
}
+64
View File
@@ -0,0 +1,64 @@
package imguploader
import (
"fmt"
"github.com/grafana/grafana/pkg/setting"
)
type ImageUploader interface {
Upload(path string) (string, error)
}
type NopImageUploader struct {
}
func (NopImageUploader) Upload(path string) (string, error) {
return "", nil
}
func NewImageUploader() (ImageUploader, error) {
switch setting.ImageUploadProvider {
case "s3":
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
if err != nil {
return nil, err
}
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")
}
if accessKey == "" {
return nil, fmt.Errorf("Could not find accessKey setting for image.uploader.s3")
}
if secretKey == "" {
return nil, fmt.Errorf("Could not find secretKey setting for image.uploader.s3")
}
return NewS3Uploader(bucket, accessKey, secretKey), nil
case "webdav":
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
if err != nil {
return nil, err
}
url := webdavSec.Key("url").String()
if url == "" {
return nil, fmt.Errorf("Could not find url key for image.uploader.webdav")
}
username := webdavSec.Key("username").String()
password := webdavSec.Key("password").String()
return NewWebdavImageUploader(url, username, password)
}
return NopImageUploader{}, nil
}
@@ -0,0 +1,62 @@
package imguploader
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestImageUploaderFactory(t *testing.T) {
Convey("Can create image uploader for ", t, func() {
Convey("S3ImageUploader", func() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "s3"
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
s3sec.NewKey("bucket_url", "bucket_url")
s3sec.NewKey("access_key", "access_key")
s3sec.NewKey("secret_key", "secret_key")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
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() {
var err error
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
setting.ImageUploadProvider = "webdav"
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
webdavSec.NewKey("url", "webdavUrl")
webdavSec.NewKey("username", "username")
webdavSec.NewKey("password", "password")
uploader, err := NewImageUploader()
So(err, ShouldBeNil)
original, ok := uploader.(*WebdavUploader)
So(ok, ShouldBeTrue)
So(original.url, ShouldEqual, "webdavUrl")
So(original.username, ShouldEqual, "username")
So(original.password, ShouldEqual, "password")
})
})
}
+69
View File
@@ -0,0 +1,69 @@
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"
)
type S3Uploader struct {
bucket string
secretKey string
accessKey string
log log.Logger
}
func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
return &S3Uploader{
bucket: bucket,
accessKey: accessKey,
secretKey: secretKey,
log: log.New("s3uploader"),
}
}
func (u *S3Uploader) Upload(imageDiskPath string) (string, error) {
s3util.DefaultConfig.AccessKey = u.accessKey
s3util.DefaultConfig.SecretKey = u.secretKey
header := make(http.Header)
header.Add("x-amz-acl", "public-read")
header.Add("Content-Type", "image/png")
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(imageDiskPath)
if err != nil {
return "", err
}
_, err = writer.Write(imgData)
if err != nil {
return "", err
}
return imageUrlString, nil
}
@@ -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, "")
})
}
@@ -0,0 +1,51 @@
package imguploader
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"github.com/grafana/grafana/pkg/util"
)
type WebdavUploader struct {
url string
username string
password string
}
func (u *WebdavUploader) Upload(pa string) (string, error) {
url, _ := url.Parse(u.url)
url.Path = path.Join(url.Path, util.GetRandomString(20)+".png")
imgData, err := ioutil.ReadFile(pa)
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
if u.username != "" {
req.SetBasicAuth(u.username, u.password)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if res.StatusCode != http.StatusCreated {
body, _ := ioutil.ReadAll(res.Body)
return "", fmt.Errorf("Failed to upload image. Returned statuscode %v body %s", res.StatusCode, body)
}
return url.String(), nil
}
func NewWebdavImageUploader(url, username, passwrod string) (*WebdavUploader, error) {
return &WebdavUploader{
url: url,
username: username,
password: passwrod,
}, nil
}
@@ -0,0 +1,18 @@
package imguploader
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestUploadToWebdav(t *testing.T) {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:9998/dav/", "username", "password")
SkipConvey("[Integration test] for external_image_store.webdav", t, func() {
path, err := webdavUploader.Upload("../../../public/img/logo_transparent_400x.png")
So(err, ShouldBeNil)
So(path, ShouldNotEqual, "")
})
}
+36 -11
View File
@@ -9,36 +9,60 @@ import (
"runtime"
"time"
"strconv"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"strconv"
)
type RenderOpts struct {
Url string
Width string
Height string
SessionId string
Timeout string
Path string
Width string
Height string
Timeout string
OrgId int64
}
var rendererLog log.Logger = log.New("png-renderer")
func RenderToPng(params *RenderOpts) (string, error) {
log.Info("PhantomRenderer::renderToPng url %v", params.Url)
rendererLog.Info("Rendering", "path", params.Path)
var executable = "phantomjs"
if runtime.GOOS == "windows" {
executable = executable + ".exe"
}
localDomain := "localhost"
if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR {
localDomain = setting.HttpAddr
}
url := fmt.Sprintf("%s://%s:%s/%s", setting.Protocol, localDomain, setting.HttpPort, params.Path)
binPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, executable))
scriptPath, _ := filepath.Abs(filepath.Join(setting.PhantomDir, "render.js"))
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
pngPath = pngPath + ".png"
cmd := exec.Command(binPath, "--ignore-ssl-errors=true", scriptPath, "url="+params.Url, "width="+params.Width,
"height="+params.Height, "png="+pngPath, "cookiename="+setting.SessionOptions.CookieName,
"domain="+setting.Domain, "sessionid="+params.SessionId)
renderKey := middleware.AddRenderAuthKey(params.OrgId)
defer middleware.RemoveRenderAuthKey(renderKey)
cmdArgs := []string{
"--ignore-ssl-errors=true",
"--web-security=false",
scriptPath,
"url=" + url,
"width=" + params.Width,
"height=" + params.Height,
"png=" + pngPath,
"domain=" + localDomain,
"renderKey=" + renderKey,
}
cmd := exec.Command(binPath, cmdArgs...)
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -71,11 +95,12 @@ func RenderToPng(params *RenderOpts) (string, error) {
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
log.Error(4, "failed to kill: %v", err)
rendererLog.Error("failed to kill", "error", err)
}
return "", fmt.Errorf("PhantomRenderer::renderToPng timeout (>%vs)", timeout)
case <-done:
}
rendererLog.Debug("Image rendered", "path", pngPath)
return pngPath, nil
}
@@ -0,0 +1,24 @@
package securejsondata
import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
}
return decrypted
}
func GetEncryptedJsonData(sjd map[string]string) SecureJsonData {
encrypted := make(SecureJsonData)
for key, data := range sjd {
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
return encrypted
}
+42 -5
View File
@@ -12,6 +12,7 @@ import (
"gopkg.in/ini.v1"
"github.com/go-stack/stack"
"github.com/inconshreveable/log15"
"github.com/inconshreveable/log15/term"
)
@@ -31,11 +32,25 @@ func New(logger string, ctx ...interface{}) Logger {
}
func Trace(format string, v ...interface{}) {
Root.Debug(fmt.Sprintf(format, v))
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
} else {
message = format
}
Root.Debug(message)
}
func Debug(format string, v ...interface{}) {
Root.Debug(fmt.Sprintf(format, v))
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
} else {
message = format
}
Root.Debug(message)
}
func Debug2(message string, v ...interface{}) {
@@ -43,7 +58,14 @@ func Debug2(message string, v ...interface{}) {
}
func Info(format string, v ...interface{}) {
Root.Info(fmt.Sprintf(format, v))
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
} else {
message = format
}
Root.Info(message)
}
func Info2(message string, v ...interface{}) {
@@ -51,7 +73,14 @@ func Info2(message string, v ...interface{}) {
}
func Warn(format string, v ...interface{}) {
Root.Warn(fmt.Sprintf(format, v))
var message string
if len(v) > 0 {
message = fmt.Sprintf(format, v)
} else {
message = format
}
Root.Warn(message)
}
func Warn2(message string, v ...interface{}) {
@@ -115,7 +144,9 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl {
for _, filterStr := range filterStrArray {
parts := strings.Split(filterStr, ":")
filterMap[parts[0]] = getLogLevelFromString(parts[1])
if len(parts) > 1 {
filterMap[parts[0]] = getLogLevelFromString(parts[1])
}
}
return filterMap
@@ -218,3 +249,9 @@ func LogFilterHandler(maxLevel log15.Lvl, filters map[string]log15.Lvl, h log15.
return r.Lvl <= maxLevel
}, h)
}
func Stack(skip int) string {
call := stack.Caller(skip)
s := stack.Trace().TrimBelow(call).TrimRuntime()
return s.String()
}
+15 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
type ldapAuther struct {
@@ -29,7 +30,7 @@ func (a *ldapAuther) Dial() error {
var err error
var certPool *x509.CertPool
if a.server.RootCACert != "" {
certPool := x509.NewCertPool()
certPool = x509.NewCertPool()
for _, caCertFile := range strings.Split(a.server.RootCACert, " ") {
if pem, err := ioutil.ReadFile(caCertFile); err != nil {
return err
@@ -48,7 +49,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)
}
@@ -123,8 +133,10 @@ func (a *ldapAuther) getGrafanaUserFor(ldapUser *ldapUserInfo) (*m.User, error)
// get user from grafana db
userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
if err := bus.Dispatch(&userQuery); err != nil {
if err == m.ErrUserNotFound {
if err == m.ErrUserNotFound && setting.LdapAllowSignup {
return a.createGrafanaUser(ldapUser)
} else if err == m.ErrUserNotFound {
return nil, ErrInvalidCredentials
} else {
return nil, err
}
+1
View File
@@ -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"`
+7 -6
View File
@@ -24,16 +24,16 @@ 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.
type GaugeSnapshot struct {
*MetricMeta
value int64
*MetricMeta
}
// Snapshot returns the snapshot.
@@ -61,9 +61,10 @@ func (NilGauge) Value() int64 { return 0 }
// StandardGauge is the standard implementation of a Gauge and uses the
// sync/atomic package to manage a single int64 value.
// atomic needs 64-bit aligned memory which is ensure for first word
type StandardGauge struct {
*MetricMeta
value int64
*MetricMeta
}
// Snapshot returns a read-only copy of the gauge.
+2
View File
@@ -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)
+2 -1
View File
@@ -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)
+76 -21
View File
@@ -9,29 +9,55 @@ 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_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_Pending Counter
M_Alerting_Notification_Sent_Slack Counter
M_Alerting_Notification_Sent_Email Counter
M_Alerting_Notification_Sent_Webhook Counter
M_Alerting_Notification_Sent_PagerDuty Counter
M_Alerting_Notification_Sent_Victorops Counter
M_Alerting_Notification_Sent_OpsGenie Counter
// Timers
M_DataSource_ProxyReq_Timer Timer
M_Alerting_Exeuction_Time Timer
// StatTotals
M_Alerting_Active_Alerts Gauge
M_StatTotal_Dashboards Gauge
M_StatTotal_Users Gauge
M_StatTotal_Orgs Gauge
M_StatTotal_Playlists Gauge
)
func initMetricVars(settings *MetricSettings) {
@@ -43,9 +69,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")
@@ -66,6 +100,27 @@ func initMetricVars(settings *MetricSettings) {
M_Models_Dashboard_Insert = RegCounter("models.dashboard.insert")
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_NoData = RegCounter("alerting.result", "state", "no_data")
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending")
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
// Timers
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
M_Alerting_Exeuction_Time = RegTimer("alerting.execution_time")
// StatTotals
M_Alerting_Active_Alerts = RegGauge("alerting.active_alerts")
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")
}
+23
View File
@@ -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
@@ -79,6 +101,7 @@ func sendUsageStats() {
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
metrics["stats.alerts.count"] = statsQuery.Result.AlertCount
dsStats := m.GetDataSourceStatsQuery{}
if err := bus.Dispatch(&dsStats); err != nil {
+2 -1
View File
@@ -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)
}
+2 -2
View File
@@ -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())
}
}
}
+8 -36
View File
@@ -22,6 +22,7 @@ type Context struct {
Session SessionStore
IsSignedIn bool
IsRenderCall bool
AllowAnonymous bool
Logger log.Logger
}
@@ -42,11 +43,11 @@ func GetContextHandler() macaron.Handler {
// then init session and look for userId in session
// then look for api key in session (special case for render calls via api)
// then test if anonymous access is enabled
if initContextWithApiKey(ctx) ||
if initContextWithRenderAuth(ctx) ||
initContextWithApiKey(ctx) ||
initContextWithBasicAuth(ctx) ||
initContextWithAuthProxy(ctx) ||
initContextWithUserSessionCookie(ctx) ||
initContextWithApiKeyFromSession(ctx) ||
initContextWithAnonymousUser(ctx) {
}
@@ -176,29 +177,6 @@ func initContextWithBasicAuth(ctx *Context) bool {
}
}
// special case for panel render calls with api key
func initContextWithApiKeyFromSession(ctx *Context) bool {
keyId := ctx.Session.Get(SESS_KEY_APIKEY)
if keyId == nil {
return false
}
keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)}
if err := bus.Dispatch(&keyQuery); err != nil {
ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err)
return false
} else {
apikey := keyQuery.Result
ctx.IsSignedIn = true
ctx.SignedInUser = &m.SignedInUser{}
ctx.OrgRole = apikey.Role
ctx.ApiKeyId = apikey.Id
ctx.OrgId = apikey.OrgId
return true
}
}
// Handle handles and logs error by given status.
func (ctx *Context) Handle(status int, title string, err error) {
if err != nil {
@@ -208,16 +186,8 @@ 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.Data["AppSubUrl"] = setting.AppSubUrl
ctx.HTML(status, strconv.Itoa(status))
}
@@ -243,10 +213,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"
}
@@ -261,6 +229,10 @@ func (ctx *Context) HasUserRole(role m.RoleType) bool {
return ctx.OrgRole.Includes(role)
}
func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
return ctx.HelpFlags1.HasFlag(flag)
}
func (ctx *Context) TimeRequest(timer metrics.Timer) {
ctx.Data["perfmon.timer"] = timer
}
+24 -50
View File
@@ -19,53 +19,14 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"runtime"
"gopkg.in/macaron.v1"
"github.com/go-macaron/inject"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
const (
panicHtml = `<html>
<head><title>PANIC: %s</title>
<meta charset="utf-8" />
<style type="text/css">
html, body {
font-family: "Roboto", sans-serif;
color: #333333;
background-color: #ea5343;
margin: 0px;
}
h1 {
color: #d04526;
background-color: #ffffff;
padding: 20px;
border-bottom: 1px dashed #2b3848;
}
pre {
margin: 20px;
padding: 20px;
border: 2px solid #2b3848;
background-color: #ffffff;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
</head><body>
<h1>PANIC</h1>
<pre style="font-weight: bold;">%s</pre>
<pre>%s</pre>
</body>
</html>`
)
var (
dunno = []byte("???")
centerDot = []byte("·")
@@ -151,21 +112,34 @@ func Recovery() macaron.Handler {
panicLogger.Error("Request error", "error", err, "stack", string(stack))
// Lookup the current responsewriter
val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
res := val.Interface().(http.ResponseWriter)
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
if theErr, ok := err.(error); ok {
c.Data["Title"] = theErr.Error()
}
// respond with panic message while in development mode
var body []byte
if setting.Env == setting.DEV {
res.Header().Set("Content-Type", "text/html")
body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
c.Data["ErrorMsg"] = string(stack)
}
res.WriteHeader(http.StatusInternalServerError)
if nil != body {
res.Write(body)
}
c.HTML(500, "500")
// // Lookup the current responsewriter
// val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
// res := val.Interface().(http.ResponseWriter)
//
// // respond with panic message while in development mode
// var body []byte
// if setting.Env == setting.DEV {
// res.Header().Set("Content-Type", "text/html")
// body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
// }
//
// res.WriteHeader(http.StatusInternalServerError)
// if nil != body {
// res.Write(body)
// }
}
}()
+55
View File
@@ -0,0 +1,55 @@
package middleware
import (
"sync"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
var renderKeysLock sync.Mutex
var renderKeys map[string]*m.SignedInUser = make(map[string]*m.SignedInUser)
func initContextWithRenderAuth(ctx *Context) bool {
key := ctx.GetCookie("renderKey")
if key == "" {
return false
}
renderKeysLock.Lock()
defer renderKeysLock.Unlock()
if renderUser, exists := renderKeys[key]; !exists {
ctx.JsonApiErr(401, "Invalid Render Key", nil)
return true
} else {
ctx.IsSignedIn = true
ctx.SignedInUser = renderUser
ctx.IsRenderCall = true
return true
}
}
type renderContextFunc func(key string) (string, error)
func AddRenderAuthKey(orgId int64) string {
renderKeysLock.Lock()
key := util.GetRandomString(32)
renderKeys[key] = &m.SignedInUser{
OrgId: orgId,
OrgRole: m.ROLE_VIEWER,
}
renderKeysLock.Unlock()
return key
}
func RemoveRenderAuthKey(key string) {
renderKeysLock.Lock()
delete(renderKeys, key)
renderKeysLock.Unlock()
}
+65
View File
@@ -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)
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ import (
const (
SESS_KEY_USERID = "uid"
SESS_KEY_APIKEY = "apikey_id" // used fror render requests with api keys
SESS_KEY_OAUTH_STATE = "state"
)
var sessionManager *session.Manager
+6 -1
View File
@@ -8,7 +8,12 @@ import (
)
func ValidateHostHeader(domain string) macaron.Handler {
return func(c *macaron.Context) {
return func(c *Context) {
// ignore local render calls
if c.IsRenderCall {
return
}
h := c.Req.Host
if i := strings.Index(h, ":"); i >= 0 {
h = h[:i]
+187
View File
@@ -0,0 +1,187 @@
package models
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
type AlertStateType string
type AlertSeverityType string
type NoDataOption string
type ExecutionErrorOption string
const (
AlertStateNoData AlertStateType = "no_data"
AlertStatePaused AlertStateType = "paused"
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
)
const (
NoDataSetNoData NoDataOption = "no_data"
NoDataSetAlerting NoDataOption = "alerting"
NoDataKeepState NoDataOption = "keep_state"
)
const (
ExecutionErrorSetAlerting ExecutionErrorOption = "alerting"
ExecutionErrorKeepState ExecutionErrorOption = "keep_state"
)
func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
}
func (s NoDataOption) IsValid() bool {
return s == NoDataSetNoData || s == NoDataSetAlerting || s == NoDataKeepState
}
func (s NoDataOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
func (s ExecutionErrorOption) IsValid() bool {
return s == ExecutionErrorSetAlerting || s == ExecutionErrorKeepState
}
func (s ExecutionErrorOption) ToAlertState() AlertStateType {
return AlertStateType(s)
}
type Alert struct {
Id int64
Version int64
OrgId int64
DashboardId int64
PanelId int64
Name string
Message string
Severity string
State AlertStateType
Handler int64
Silenced bool
ExecutionError string
Frequency int64
EvalData *simplejson.Json
EvalDate time.Time
NewStateDate time.Time
StateChanges int
Created time.Time
Updated time.Time
Settings *simplejson.Json
}
func (alert *Alert) ValidToSave() bool {
return alert.DashboardId != 0 && alert.OrgId != 0 && alert.PanelId != 0
}
func (alert *Alert) ShouldUpdateState(newState AlertStateType) bool {
return alert.State != newState
}
func (this *Alert) ContainsUpdates(other *Alert) bool {
result := false
result = result || this.Name != other.Name
result = result || this.Message != other.Message
if this.Settings != nil && other.Settings != nil {
json1, err1 := this.Settings.Encode()
json2, err2 := other.Settings.Encode()
if err1 != nil || err2 != nil {
return false
}
result = result || string(json1) != string(json2)
}
//don't compare .State! That would be insane.
return result
}
type AlertingClusterInfo struct {
ServerId string
ClusterSize int
UptimePosition int
}
type HeartBeat struct {
Id int64
ServerId string
Updated time.Time
Created time.Time
}
type HeartBeatCommand struct {
ServerId string
Result AlertingClusterInfo
}
type SaveAlertsCommand struct {
DashboardId int64
UserId int64
OrgId int64
Alerts []*Alert
}
type PauseAlertCommand struct {
OrgId int64
AlertId int64
Paused bool
}
type SetAlertStateCommand struct {
AlertId int64
OrgId int64
State AlertStateType
Error string
EvalData *simplejson.Json
Timestamp time.Time
}
type DeleteAlertCommand struct {
AlertId int64
}
//Queries
type GetAlertsQuery struct {
OrgId int64
State []string
DashboardId int64
PanelId int64
Limit int64
Result []*Alert
}
type GetAllAlertsQuery struct {
Result []*Alert
}
type GetAlertByIdQuery struct {
Id int64
Result *Alert
}
type GetAlertStatesForDashboardQuery struct {
OrgId int64
DashboardId int64
Result []*AlertStateInfoDTO
}
type AlertStateInfoDTO struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
}
+65
View File
@@ -0,0 +1,65 @@
package models
import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
)
type AlertNotification struct {
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"`
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"`
IsDefault bool `json:"isDefault"`
Settings *simplejson.Json `json:"settings" binding:"Required"`
OrgId int64 `json:"-"`
Result *AlertNotification
}
type DeleteAlertNotificationCommand struct {
Id int64
OrgId int64
}
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
}
+39
View File
@@ -0,0 +1,39 @@
package models
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingModelTest(t *testing.T) {
Convey("Testing Alerting model", t, func() {
json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`))
json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`))
rule1 := &Alert{
Settings: json1,
Name: "Namn",
Message: "Message",
}
rule2 := &Alert{
Settings: json2,
Name: "Namn",
Message: "Message",
}
Convey("Testing AlertRule equals", func() {
So(rule1.ContainsUpdates(rule2), ShouldBeFalse)
})
Convey("Changing the expression should contain update", func() {
json2, _ := simplejson.NewJson([]byte(`{ "field": "newValue" }`))
rule1.Settings = json2
So(rule1.ContainsUpdates(rule2), ShouldBeTrue)
})
})
}
+3
View File
@@ -63,6 +63,9 @@ type DeleteDashboardSnapshotCommand struct {
DeleteKey string `json:"-"`
}
type DeleteExpiredSnapshotsCommand struct {
}
type GetDashboardSnapshotQuery struct {
Key string
+1
View File
@@ -15,6 +15,7 @@ var (
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
)
type UpdatePluginDashboardError struct {
+34 -29
View File
@@ -4,6 +4,7 @@ import (
"errors"
"time"
"github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/components/simplejson"
)
@@ -22,7 +23,8 @@ const (
// Typed errors
var (
ErrDataSourceNotFound = errors.New("Data source not found")
ErrDataSourceNotFound = errors.New("Data source not found")
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
)
type DsAccess string
@@ -45,6 +47,7 @@ type DataSource struct {
WithCredentials bool
IsDefault bool
JsonData *simplejson.Json
SecureJsonData securejsondata.SecureJsonData
Created time.Time
Updated time.Time
@@ -76,19 +79,20 @@ func IsKnownDataSourcePlugin(dsType string) bool {
// Also acts as api DTO
type AddDataSourceCommand struct {
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"`
Password string `json:"password"`
Database string `json:"database"`
User string `json:"user"`
BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"`
Password string `json:"password"`
Database string `json:"database"`
User string `json:"user"`
BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
OrgId int64 `json:"-"`
@@ -97,19 +101,20 @@ type AddDataSourceCommand struct {
// Also acts as api DTO
type UpdateDataSourceCommand struct {
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"`
Password string `json:"password"`
User string `json:"user"`
Database string `json:"database"`
BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"`
Name string `json:"name" binding:"Required"`
Type string `json:"type" binding:"Required"`
Access DsAccess `json:"access" binding:"Required"`
Url string `json:"url"`
Password string `json:"password"`
User string `json:"user"`
Database string `json:"database"`
BasicAuth bool `json:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials"`
IsDefault bool `json:"isDefault"`
JsonData *simplejson.Json `json:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData"`
OrgId int64 `json:"-"`
Id int64 `json:"-"`
@@ -131,13 +136,13 @@ type GetDataSourcesQuery struct {
type GetDataSourceByIdQuery struct {
Id int64
OrgId int64
Result DataSource
Result *DataSource
}
type GetDataSourceByNameQuery struct {
Name string
OrgId int64
Result DataSource
Result *DataSource
}
// ---------------------
+95
View File
@@ -0,0 +1,95 @@
package models
import (
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"sync"
"time"
)
type proxyTransportCache struct {
cache map[int64]cachedTransport
sync.Mutex
}
type cachedTransport struct {
updated time.Time
*http.Transport
}
var ptc = proxyTransportCache{
cache: make(map[int64]cachedTransport),
}
func (ds *DataSource) GetHttpClient() (*http.Client, error) {
transport, err := ds.GetHttpTransport()
if err != nil {
return nil, err
}
return &http.Client{
Timeout: time.Duration(30 * time.Second),
Transport: transport,
}, nil
}
func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
ptc.Lock()
defer ptc.Unlock()
if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) {
return t.Transport, nil
}
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
}
var tlsAuth, tlsAuthWithCACert bool
if ds.JsonData != nil {
tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false)
tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false)
}
if tlsAuth {
transport.TLSClientConfig.InsecureSkipVerify = false
decrypted := ds.SecureJsonData.Decrypt()
if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 {
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"]))
if ok {
transport.TLSClientConfig.RootCAs = caPool
}
}
cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"]))
if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
}
ptc.cache[ds.Id] = cachedTransport{
Transport: transport,
updated: ds.Updated,
}
return transport, nil
}
+157
View File
@@ -0,0 +1,157 @@
package models
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func TestDataSourceCache(t *testing.T) {
Convey("When caching a datasource proxy", t, func() {
clearCache()
ds := DataSource{
Id: 1,
Url: "http://k8s:8001",
Type: "Kubernetes",
}
t1, err := ds.GetHttpTransport()
So(err, ShouldBeNil)
t2, err := ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should be using the cached proxy", func() {
So(t2, ShouldEqual, t1)
})
})
Convey("When getting kubernetes datasource proxy", t, func() {
clearCache()
setting.SecretKey = "password"
json := simplejson.New()
json.Set("tlsAuth", true)
json.Set("tlsAuthWithCACert", true)
t := time.Now()
ds := DataSource{
Url: "http://k8s:8001",
Type: "Kubernetes",
Updated: t.Add(-2 * time.Minute),
}
transport, err := ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should have no cert", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
})
ds.JsonData = json
ds.SecureJsonData = map[string][]byte{
"tlsCACert": util.Encrypt([]byte(caCert), "password"),
"tlsClientCert": util.Encrypt([]byte(clientCert), "password"),
"tlsClientKey": util.Encrypt([]byte(clientKey), "password"),
}
ds.Updated = t.Add(-1 * time.Minute)
transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should add cert", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false)
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1)
})
ds.JsonData = nil
ds.SecureJsonData = map[string][]byte{}
ds.Updated = t
transport, err = ds.GetHttpTransport()
So(err, ShouldBeNil)
Convey("Should remove cert", func() {
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true)
So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0)
})
})
}
func clearCache() {
ptc.Lock()
defer ptc.Unlock()
ptc.cache = make(map[int64]cachedTransport)
}
const caCert string = `-----BEGIN CERTIFICATE-----
MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda
MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg
QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4
4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d
1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN
iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ
0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx
2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5
VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9
I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB
CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7
adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k
MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh
FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n
3lb92xM=
-----END CERTIFICATE-----`
const clientCert string = `-----BEGIN CERTIFICATE-----
MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w
GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2
FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b
Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo
GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8
SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4
YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP
ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw
AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q
4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe
58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5
llG/Sw5+FquFuChaA6l5KWy7F3bQyA==
-----END CERTIFICATE-----`
const clientKey string = `-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV
u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn
Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega
0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI
LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi
dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs
Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk
CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x
64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM
8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh
WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf
vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz
k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs
DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35
aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ
Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo
jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01
hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0
M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8
v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX
xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL
Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0
Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD
FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD
+VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg=
-----END RSA PRIVATE KEY-----`
-22
View File
@@ -1,22 +0,0 @@
package models
import "errors"
var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
type SendEmailCommand struct {
To []string
Template string
Data map[string]interface{}
Massive bool
Info string
}
type SendResetPasswordEmailCommand struct {
User *User
}
type ValidateResetPasswordCodeQuery struct {
Code string
Result *User
}
+18
View File
@@ -0,0 +1,18 @@
package models
type HelpFlags1 uint64
const (
HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota
HelpFlagDashboardHelp1
)
func (f HelpFlags1) HasFlag(flag HelpFlags1) bool { return f&flag != 0 }
func (f *HelpFlags1) AddFlag(flag HelpFlags1) { *f |= flag }
func (f *HelpFlags1) ClearFlag(flag HelpFlags1) { *f &= ^flag }
func (f *HelpFlags1) ToggleFlag(flag HelpFlags1) { *f ^= flag }
type SetUserHelpFlagCommand struct {
HelpFlags1 HelpFlags1
UserId int64
}
+2
View File
@@ -6,4 +6,6 @@ const (
GITHUB OAuthType = iota + 1
GOOGLE
TWITTER
GENERIC
GRAFANANET
)
+34
View File
@@ -0,0 +1,34 @@
package models
import "errors"
var ErrInvalidEmailCode = errors.New("Invalid or expired email code")
type SendEmailCommand struct {
To []string
Template string
Data map[string]interface{}
Info string
EmbededFiles []string
}
type SendEmailCommandSync struct {
SendEmailCommand
}
type SendWebhookSync struct {
Url string
User string
Password string
Body string
HttpMethod string
}
type SendResetPasswordEmailCommand struct {
User *User
}
type ValidateResetPasswordCodeQuery struct {
Code string
Result *User
}
-11
View File
@@ -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
//
+4 -19
View File
@@ -4,8 +4,7 @@ import (
"errors"
"time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/components/securejsondata"
)
var (
@@ -19,23 +18,13 @@ type PluginSetting struct {
Enabled bool
Pinned bool
JsonData map[string]interface{}
SecureJsonData SecureJsonData
SecureJsonData securejsondata.SecureJsonData
PluginVersion string
Created time.Time
Updated time.Time
}
type SecureJsonData map[string][]byte
func (s SecureJsonData) Decrypt() map[string]string {
decrypted := make(map[string]string)
for key, data := range s {
decrypted[key] = string(util.Decrypt(data, setting.SecretKey))
}
return decrypted
}
// ----------------------
// COMMANDS
@@ -58,12 +47,8 @@ type UpdatePluginSettingVersionCmd struct {
OrgId int64 `json:"-"`
}
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() SecureJsonData {
encrypted := make(SecureJsonData)
for key, data := range cmd.SecureJsonData {
encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey)
}
return encrypted
func (cmd *UpdatePluginSettingCmd) GetEncryptedJsonData() securejsondata.SecureJsonData {
return securejsondata.GetEncryptedJsonData(cmd.SecureJsonData)
}
// ---------------------
+10
View File
@@ -0,0 +1,10 @@
package models
import "context"
type GrafanaServer interface {
context.Context
Start()
Shutdown(code int, reason string)
}
+6 -4
View File
@@ -1,10 +1,11 @@
package models
type SystemStats struct {
DashboardCount int
UserCount int
OrgCount int
PlaylistCount int
DashboardCount int64
UserCount int64
OrgCount int64
PlaylistCount int64
AlertCount int64
}
type DataSourceStats struct {
@@ -29,6 +30,7 @@ type AdminStats struct {
DataSourceCount int `json:"data_source_count"`
PlaylistCount int `json:"playlist_count"`
StarredDbCount int `json:"starred_db_count"`
AlertCount int `json:"alert_count"`
}
type GetAdminStatsQuery struct {
+17 -9
View File
@@ -22,6 +22,7 @@ type User struct {
Company string
EmailVerified bool
Theme string
HelpFlags1 HelpFlags1
IsAdmin bool
OrgId int64
@@ -44,15 +45,16 @@ func (u *User) NameOrFallback() string {
// COMMANDS
type CreateUserCommand struct {
Email string
Login string
Name string
Company string
OrgName string
Password string
EmailVerified bool
IsAdmin bool
SkipOrgSetup bool
Email string
Login string
Name string
Company string
OrgName string
Password string
EmailVerified bool
IsAdmin bool
SkipOrgSetup bool
DefaultOrgRole string
Result User
}
@@ -95,6 +97,11 @@ type GetUserByLoginQuery struct {
Result *User
}
type GetUserByEmailQuery struct {
Email string
Result *User
}
type GetUserByIdQuery struct {
Id int64
Result *User
@@ -138,6 +145,7 @@ type SignedInUser struct {
Email string
ApiKeyId int64
IsGrafanaAdmin bool
HelpFlags1 HelpFlags1
}
type UserProfileDTO struct {
+1
View File
@@ -6,6 +6,7 @@ type DataSourcePlugin struct {
FrontendPluginBase
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
BuiltIn bool `json:"builtIn"`
Mixed bool `json:"mixed"`
App string `json:"app"`
+6 -1
View File
@@ -43,7 +43,12 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
fp.IncludedInAppId = app.Id
fp.BaseUrl = app.BaseUrl
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
if isExternalPlugin(app.PluginDir) {
fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module"
} else {
fp.Module = util.JoinUrlFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
}
}
func (fp *FrontendPluginBase) handleModuleDefaults() {
+1
View File
@@ -38,6 +38,7 @@ type PluginBase struct {
Includes []*PluginInclude `json:"includes"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
HideFromList bool `json:"hideFromList"`
IncludedInAppId string `json:"-"`
PluginDir string `json:"-"`
+26 -9
View File
@@ -9,6 +9,11 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/hashicorp/go-version"
)
var (
httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
)
type GrafanaNetPlugin struct {
@@ -39,26 +44,23 @@ func StartPluginUpdateChecker() {
}
func getAllExternalPluginSlugs() string {
str := ""
var result []string
for _, plug := range Plugins {
if plug.IsCorePlugin {
continue
}
str += plug.Id + ","
result = append(result, plug.Id)
}
return str
return strings.Join(result, ",")
}
func checkForUpdates() {
log.Trace("Checking for updates")
client := http.Client{Timeout: time.Duration(5 * time.Second)}
pluginSlugs := getAllExternalPluginSlugs()
resp, err := client.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
resp, err := httpClient.Get("https://grafana.net/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
if err != nil {
log.Trace("Failed to get plugins repo from grafana.net, %v", err.Error())
@@ -84,12 +86,20 @@ func checkForUpdates() {
for _, gplug := range gNetPlugins {
if gplug.Slug == plug.Id {
plug.GrafanaNetVersion = gplug.Version
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
plugVersion, err1 := version.NewVersion(plug.Info.Version)
gplugVersion, err2 := version.NewVersion(gplug.Version)
if err1 != nil || err2 != nil {
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
} else {
plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
}
}
}
}
resp2, err := client.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
if err != nil {
log.Trace("Failed to get latest.json repo from github: %v", err.Error())
return
@@ -116,4 +126,11 @@ func checkForUpdates() {
GrafanaLatestVersion = githubLatest.Stable
GrafanaHasUpdate = githubLatest.Stable != setting.BuildVersion
}
currVersion, err1 := version.NewVersion(setting.BuildVersion)
latestVersion, err2 := version.NewVersion(GrafanaLatestVersion)
if err1 == nil && err2 == nil {
GrafanaHasUpdate = currVersion.LessThan(latestVersion)
}
}
+55
View File
@@ -0,0 +1,55 @@
package alerting
import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
func init() {
bus.AddHandler("alerting", updateDashboardAlerts)
bus.AddHandler("alerting", validateDashboardAlerts)
}
func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if _, err := extractor.GetAlerts(); err != nil {
return err
}
return nil
}
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
saveAlerts := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,
DashboardId: cmd.Dashboard.Id,
}
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
if alerts, err := extractor.GetAlerts(); err != nil {
return err
} else {
saveAlerts.Alerts = alerts
}
if err := bus.Dispatch(&saveAlerts); err != nil {
return err
}
return nil
}
@@ -0,0 +1,135 @@
package conditions
import (
"encoding/json"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting"
"gopkg.in/guregu/null.v3"
)
var (
defaultTypes []string = []string{"gt", "lt"}
rangedTypes []string = []string{"within_range", "outside_range"}
)
type AlertEvaluator interface {
Eval(reducedValue null.Float) bool
}
type NoValueEvaluator struct{}
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
return reducedValue.Valid == false
}
type ThresholdEvaluator struct {
Type string
Threshold float64
}
func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) {
params := model.Get("params").MustArray()
if len(params) == 0 {
return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"}
}
firstParam, ok := params[0].(json.Number)
if !ok {
return nil, alerting.ValidationError{Reason: "Evaluator has invalid parameter"}
}
defaultEval := &ThresholdEvaluator{Type: typ}
defaultEval.Threshold, _ = firstParam.Float64()
return defaultEval, nil
}
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
return false
}
switch e.Type {
case "gt":
return reducedValue.Float64 > e.Threshold
case "lt":
return reducedValue.Float64 < e.Threshold
}
return false
}
type RangedEvaluator struct {
Type string
Lower float64
Upper float64
}
func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, error) {
params := model.Get("params").MustArray()
if len(params) == 0 {
return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"}
}
firstParam, ok := params[0].(json.Number)
if !ok {
return nil, alerting.ValidationError{Reason: "Evaluator has invalid parameter"}
}
secondParam, ok := params[1].(json.Number)
if !ok {
return nil, alerting.ValidationError{Reason: "Evaluator has invalid second parameter"}
}
rangedEval := &RangedEvaluator{Type: typ}
rangedEval.Lower, _ = firstParam.Float64()
rangedEval.Upper, _ = secondParam.Float64()
return rangedEval, nil
}
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
if reducedValue.Valid == false {
return false
}
floatValue := reducedValue.Float64
switch e.Type {
case "within_range":
return (e.Lower < floatValue && e.Upper > floatValue) || (e.Upper < floatValue && e.Lower > floatValue)
case "outside_range":
return (e.Upper < floatValue && e.Lower < floatValue) || (e.Upper > floatValue && e.Lower > floatValue)
}
return false
}
func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
typ := model.Get("type").MustString()
if typ == "" {
return nil, alerting.ValidationError{Reason: "Evaluator missing type property"}
}
if inSlice(typ, defaultTypes) {
return newThresholdEvaluator(typ, model)
}
if inSlice(typ, rangedTypes) {
return newRangedEvaluator(typ, model)
}
if typ == "no_value" {
return &NoValueEvaluator{}, nil
}
return nil, alerting.ValidationError{Reason: "Evaluator invalid evaluator type: " + typ}
}
func inSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
@@ -0,0 +1,63 @@
package conditions
import (
"testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func evalutorScenario(json string, reducedValue float64, datapoints ...float64) bool {
jsonModel, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
evaluator, err := NewAlertEvaluator(jsonModel)
So(err, ShouldBeNil)
return evaluator.Eval(null.FloatFrom(reducedValue))
}
func TestEvalutors(t *testing.T) {
Convey("greater then", t, func() {
So(evalutorScenario(`{"type": "gt", "params": [1] }`, 3), ShouldBeTrue)
So(evalutorScenario(`{"type": "gt", "params": [3] }`, 1), ShouldBeFalse)
})
Convey("less then", t, func() {
So(evalutorScenario(`{"type": "lt", "params": [1] }`, 3), ShouldBeFalse)
So(evalutorScenario(`{"type": "lt", "params": [3] }`, 1), ShouldBeTrue)
})
Convey("within_range", t, func() {
So(evalutorScenario(`{"type": "within_range", "params": [1, 100] }`, 3), ShouldBeTrue)
So(evalutorScenario(`{"type": "within_range", "params": [1, 100] }`, 300), ShouldBeFalse)
So(evalutorScenario(`{"type": "within_range", "params": [100, 1] }`, 3), ShouldBeTrue)
So(evalutorScenario(`{"type": "within_range", "params": [100, 1] }`, 300), ShouldBeFalse)
})
Convey("outside_range", t, func() {
So(evalutorScenario(`{"type": "outside_range", "params": [1, 100] }`, 1000), ShouldBeTrue)
So(evalutorScenario(`{"type": "outside_range", "params": [1, 100] }`, 50), ShouldBeFalse)
So(evalutorScenario(`{"type": "outside_range", "params": [100, 1] }`, 1000), ShouldBeTrue)
So(evalutorScenario(`{"type": "outside_range", "params": [100, 1] }`, 50), ShouldBeFalse)
})
Convey("no_value", t, func() {
Convey("should be false if serie have values", func() {
So(evalutorScenario(`{"type": "no_value", "params": [] }`, 50), ShouldBeFalse)
})
Convey("should be true when the serie have no value", func() {
jsonModel, err := simplejson.NewJson([]byte(`{"type": "no_value", "params": [] }`))
So(err, ShouldBeNil)
evaluator, err := NewAlertEvaluator(jsonModel)
So(err, ShouldBeNil)
So(evaluator.Eval(null.FloatFromPtr(nil)), ShouldBeTrue)
})
})
}
+191
View File
@@ -0,0 +1,191 @@
package conditions
import (
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb"
)
func init() {
alerting.RegisterCondition("query", func(model *simplejson.Json, index int) (alerting.Condition, error) {
return NewQueryCondition(model, index)
})
}
type QueryCondition struct {
Index int
Query AlertQuery
Reducer QueryReducer
Evaluator AlertEvaluator
Operator string
HandleRequest tsdb.HandleRequestFunc
}
type AlertQuery struct {
Model *simplejson.Json
DatasourceId int64
From string
To string
}
func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.ConditionResult, error) {
timeRange := tsdb.NewTimeRange(c.Query.From, c.Query.To)
seriesList, err := c.executeQuery(context, timeRange)
if err != nil {
return nil, err
}
emptySerieCount := 0
evalMatchCount := 0
var matches []*alerting.EvalMatch
for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(reducedValue)
if reducedValue.Valid == false {
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.Float64),
})
}
if evalMatch {
evalMatchCount++
matches = append(matches, &alerting.EvalMatch{
Metric: series.Name,
Value: reducedValue.Float64,
})
}
}
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
Operator: c.Operator,
EvalMatches: matches,
}, nil
}
func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *tsdb.TimeRange) (tsdb.TimeSeriesSlice, error) {
getDsInfo := &m.GetDataSourceByIdQuery{
Id: c.Query.DatasourceId,
OrgId: context.Rule.OrgId,
}
if err := bus.Dispatch(getDsInfo); err != nil {
return nil, fmt.Errorf("Could not find datasource")
}
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
result := make(tsdb.TimeSeriesSlice, 0)
resp, err := c.HandleRequest(context.Ctx, req)
if err != nil {
return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err)
}
for _, v := range resp.Results {
if v.Error != nil {
return nil, fmt.Errorf("tsdb.HandleRequest() response error %v", v)
}
result = append(result, v.Series...)
if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Query Result", c.Index),
Data: v.Series,
})
}
}
return result, nil
}
func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request {
req := &tsdb.Request{
TimeRange: timeRange,
Queries: []*tsdb.Query{
{
RefId: "A",
Model: c.Query.Model,
DataSource: datasource,
},
},
}
return req
}
func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, error) {
condition := QueryCondition{}
condition.Index = index
condition.HandleRequest = tsdb.HandleRequest
queryJson := model.Get("query")
condition.Query.Model = queryJson.Get("model")
condition.Query.From = queryJson.Get("params").MustArray()[1].(string)
condition.Query.To = queryJson.Get("params").MustArray()[2].(string)
if err := validateFromValue(condition.Query.From); err != nil {
return nil, err
}
if err := validateToValue(condition.Query.To); err != nil {
return nil, err
}
condition.Query.DatasourceId = queryJson.Get("datasourceId").MustInt64()
reducerJson := model.Get("reducer")
condition.Reducer = NewSimpleReducer(reducerJson.Get("type").MustString())
evaluatorJson := model.Get("evaluator")
evaluator, err := NewAlertEvaluator(evaluatorJson)
if err != nil {
return nil, err
}
condition.Evaluator = evaluator
operatorJson := model.Get("operator")
operator := operatorJson.Get("type").MustString("and")
condition.Operator = operator
return &condition, nil
}
func validateFromValue(from string) error {
fromRaw := strings.Replace(from, "now-", "", 1)
_, err := time.ParseDuration("-" + fromRaw)
return err
}
func validateToValue(to string) error {
if to == "now" {
return nil
} else if strings.HasPrefix(to, "now-") {
withoutNow := strings.Replace(to, "now-", "", 1)
_, err := time.ParseDuration("-" + withoutNow)
if err == nil {
return nil
}
}
_, err := time.ParseDuration(to)
return err
}
@@ -0,0 +1,167 @@
package conditions
import (
"context"
"testing"
null "gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
func TestQueryCondition(t *testing.T) {
Convey("when evaluating query condition", t, func() {
queryConditionScenario("Given avg() and > 100", func(ctx *queryConditionTestContext) {
ctx.reducer = `{"type": "avg"}`
ctx.evaluator = `{"type": "gt", "params": [100]}`
Convey("Can read query condition from json model", func() {
ctx.exec()
So(ctx.condition.Query.From, ShouldEqual, "5m")
So(ctx.condition.Query.To, ShouldEqual, "now")
So(ctx.condition.Query.DatasourceId, ShouldEqual, 1)
Convey("Can read query reducer", func() {
reducer, ok := ctx.condition.Reducer.(*SimpleReducer)
So(ok, ShouldBeTrue)
So(reducer.Type, ShouldEqual, "avg")
})
Convey("Can read evaluator", func() {
evaluator, ok := ctx.condition.Evaluator.(*ThresholdEvaluator)
So(ok, ShouldBeTrue)
So(evaluator.Type, ShouldEqual, "gt")
})
})
Convey("should fire when avg is above 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(120, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Should not fire when avg is below 100", func() {
points := tsdb.NewTimeSeriesPointsFromArgs(90, 0)
ctx.series = tsdb.TimeSeriesSlice{tsdb.NewTimeSeries("test1", points)}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeFalse)
})
Convey("Should fire if only first serie matches", func() {
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(0, 0)),
}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.Firing, ShouldBeTrue)
})
Convey("Empty series", func() {
Convey("Should set NoDataFound both series are empty", func() {
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs()),
}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should set NoDataFound both series contains null", func() {
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
tsdb.NewTimeSeries("test2", tsdb.TimeSeriesPoints{tsdb.TimePoint{null.FloatFromPtr(nil), null.FloatFrom(0)}}),
}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeTrue)
})
Convey("Should not set NoDataFound if one serie is empty", func() {
ctx.series = tsdb.TimeSeriesSlice{
tsdb.NewTimeSeries("test1", tsdb.NewTimeSeriesPointsFromArgs()),
tsdb.NewTimeSeries("test2", tsdb.NewTimeSeriesPointsFromArgs(120, 0)),
}
cr, err := ctx.exec()
So(err, ShouldBeNil)
So(cr.NoDataFound, ShouldBeFalse)
})
})
})
})
}
type queryConditionTestContext struct {
reducer string
evaluator string
series tsdb.TimeSeriesSlice
result *alerting.EvalContext
condition *QueryCondition
}
type queryConditionScenarioFunc func(c *queryConditionTestContext)
func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) {
jsonModel, err := simplejson.NewJson([]byte(`{
"type": "query",
"query": {
"params": ["A", "5m", "now"],
"datasourceId": 1,
"model": {"target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
},
"reducer":` + ctx.reducer + `,
"evaluator":` + ctx.evaluator + `
}`))
So(err, ShouldBeNil)
condition, err := NewQueryCondition(jsonModel, 0)
So(err, ShouldBeNil)
ctx.condition = condition
condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) {
return &tsdb.Response{
Results: map[string]*tsdb.QueryResult{
"A": {Series: ctx.series},
},
}, nil
}
return condition.Eval(ctx.result)
}
func queryConditionScenario(desc string, fn queryConditionScenarioFunc) {
Convey(desc, func() {
bus.AddHandler("test", func(query *m.GetDataSourceByIdQuery) error {
query.Result = &m.DataSource{Id: 1, Type: "graphite"}
return nil
})
ctx := &queryConditionTestContext{}
ctx.result = &alerting.EvalContext{
Rule: &alerting.Rule{},
}
fn(ctx)
})
}
+108
View File
@@ -0,0 +1,108 @@
package conditions
import (
"math"
"sort"
"github.com/grafana/grafana/pkg/tsdb"
"gopkg.in/guregu/null.v3"
)
type QueryReducer interface {
Reduce(timeSeries *tsdb.TimeSeries) null.Float
}
type SimpleReducer struct {
Type string
}
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
if len(series.Points) == 0 {
return null.FloatFromPtr(nil)
}
value := float64(0)
allNull := true
switch s.Type {
case "avg":
validPointsCount := 0
for _, point := range series.Points {
if point[0].Valid {
value += point[0].Float64
validPointsCount += 1
allNull = false
}
}
if validPointsCount > 0 {
value = value / float64(validPointsCount)
}
case "sum":
for _, point := range series.Points {
if point[0].Valid {
value += point[0].Float64
allNull = false
}
}
case "min":
value = math.MaxFloat64
for _, point := range series.Points {
if point[0].Valid {
allNull = false
if value > point[0].Float64 {
value = point[0].Float64
}
}
}
case "max":
value = -math.MaxFloat64
for _, point := range series.Points {
if point[0].Valid {
allNull = false
if value < point[0].Float64 {
value = point[0].Float64
}
}
}
case "count":
value = float64(len(series.Points))
allNull = false
case "last":
points := series.Points
for i := len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
value = points[i][0].Float64
allNull = false
break
}
}
case "median":
var values []float64
for _, v := range series.Points {
if v[0].Valid {
allNull = false
values = append(values, v[0].Float64)
}
}
if len(values) >= 1 {
sort.Float64s(values)
length := len(values)
if length%2 == 1 {
value = values[(length-1)/2]
} else {
value = (values[(length/2)-1] + values[length/2]) / 2
}
}
}
if allNull {
return null.FloatFromPtr(nil)
}
return null.FloatFrom(value)
}
func NewSimpleReducer(typ string) *SimpleReducer {
return &SimpleReducer{Type: typ}
}
@@ -0,0 +1,86 @@
package conditions
import (
"testing"
"gopkg.in/guregu/null.v3"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
)
func TestSimpleReducer(t *testing.T) {
Convey("Test simple reducer by calculating", t, func() {
Convey("sum", func() {
result := testReducer("sum", 1, 2, 3)
So(result, ShouldEqual, float64(6))
})
Convey("min", func() {
result := testReducer("min", 3, 2, 1)
So(result, ShouldEqual, float64(1))
})
Convey("max", func() {
result := testReducer("max", 1, 2, 3)
So(result, ShouldEqual, float64(3))
})
Convey("count", func() {
result := testReducer("count", 1, 2, 3000)
So(result, ShouldEqual, float64(3))
})
Convey("last", func() {
result := testReducer("last", 1, 2, 3000)
So(result, ShouldEqual, float64(3000))
})
Convey("median odd amount of numbers", func() {
result := testReducer("median", 1, 2, 3000)
So(result, ShouldEqual, float64(2))
})
Convey("median even amount of numbers", func() {
result := testReducer("median", 1, 2, 4, 3000)
So(result, ShouldEqual, float64(3))
})
Convey("median with one values", func() {
result := testReducer("median", 1)
So(result, ShouldEqual, float64(1))
})
Convey("avg", func() {
result := testReducer("avg", 1, 2, 3)
So(result, ShouldEqual, float64(2))
})
Convey("avg of number values and null values should ignore nulls", func() {
reducer := NewSimpleReducer("avg")
series := &tsdb.TimeSeries{
Name: "test time serie",
}
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3))
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4))
So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
})
})
}
func testReducer(typ string, datapoints ...float64) float64 {
reducer := NewSimpleReducer(typ)
series := &tsdb.TimeSeries{
Name: "test time serie",
}
for idx := range datapoints {
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(datapoints[idx]), 1234134))
}
return reducer.Reduce(series).Float64
}
+137
View File
@@ -0,0 +1,137 @@
package alerting
import (
"context"
"time"
"github.com/benbjohnson/clock"
"github.com/grafana/grafana/pkg/log"
"golang.org/x/sync/errgroup"
)
type Engine struct {
execQueue chan *Job
clock clock.Clock
ticker *Ticker
scheduler Scheduler
evalHandler EvalHandler
ruleReader RuleReader
log log.Logger
resultHandler ResultHandler
}
func NewEngine() *Engine {
e := &Engine{
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
execQueue: make(chan *Job, 1000),
scheduler: NewScheduler(),
evalHandler: NewEvalHandler(),
ruleReader: NewRuleReader(),
log: log.New("alerting.engine"),
resultHandler: NewResultHandler(),
}
return e
}
func (e *Engine) Run(ctx context.Context) error {
e.log.Info("Initializing Alerting")
alertGroup, ctx := errgroup.WithContext(ctx)
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
err := alertGroup.Wait()
e.log.Info("Stopped Alerting", "reason", err)
return err
}
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
defer func() {
if err := recover(); err != nil {
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
}
}()
tickIndex := 0
for {
select {
case <-grafanaCtx.Done():
return grafanaCtx.Err()
case tick := <-e.ticker.C:
// TEMP SOLUTION update rules ever tenth tick
if tickIndex%10 == 0 {
e.scheduler.Update(e.ruleReader.Fetch())
}
e.scheduler.Tick(tick, e.execQueue)
tickIndex++
}
}
}
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
for {
select {
case <-grafanaCtx.Done():
return dispatcherGroup.Wait()
case job := <-e.execQueue:
dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
}
}
}
var (
unfinishedWorkTimeout time.Duration = time.Second * 5
alertTimeout time.Duration = time.Second * 30
)
func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
}
}()
alertCtx, cancelFn := context.WithTimeout(context.TODO(), alertTimeout)
job.Running = true
evalContext := NewEvalContext(alertCtx, job.Rule)
done := make(chan struct{})
go func() {
defer func() {
if err := recover(); err != nil {
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
close(done)
}
}()
e.evalHandler.Eval(evalContext)
e.resultHandler.Handle(evalContext)
close(done)
}()
var err error = nil
select {
case <-grafanaCtx.Done():
select {
case <-time.After(unfinishedWorkTimeout):
cancelFn()
err = grafanaCtx.Err()
case <-done:
}
case <-done:
}
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
job.Running = false
cancelFn()
return err
}
+119
View File
@@ -0,0 +1,119 @@
package alerting
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
type EvalContext struct {
Firing bool
IsTestRun bool
EvalMatches []*EvalMatch
Logs []*ResultLogEntry
Error error
ConditionEvals string
StartTime time.Time
EndTime time.Time
Rule *Rule
log log.Logger
dashboardSlug string
ImagePublicUrl string
ImageOnDiskPath string
NoDataFound bool
PrevAlertState m.AlertStateType
Ctx context.Context
}
func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext {
return &EvalContext{
Ctx: alertCtx,
StartTime: time.Now(),
Rule: rule,
Logs: make([]*ResultLogEntry, 0),
EvalMatches: make([]*EvalMatch, 0),
log: log.New("alerting.evalContext"),
PrevAlertState: rule.State,
}
}
type StateDescription struct {
Color string
Text string
Data string
}
func (c *EvalContext) GetStateModel() *StateDescription {
switch c.Rule.State {
case m.AlertStateOK:
return &StateDescription{
Color: "#36a64f",
Text: "OK",
}
case m.AlertStateNoData:
return &StateDescription{
Color: "#888888",
Text: "No Data",
}
case m.AlertStateAlerting:
return &StateDescription{
Color: "#D63232",
Text: "Alerting",
}
default:
panic("Unknown rule state " + c.Rule.State)
}
}
func (c *EvalContext) ShouldUpdateAlertState() bool {
return c.Rule.State != c.PrevAlertState
}
func (c *EvalContext) ShouldSendNotification() bool {
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
return false
}
return true
}
func (a *EvalContext) GetDurationMs() float64 {
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
}
func (c *EvalContext) GetNotificationTitle() string {
return "[" + c.GetStateModel().Text + "] " + c.Rule.Name
}
func (c *EvalContext) GetDashboardSlug() (string, error) {
if c.dashboardSlug != "" {
return c.dashboardSlug, nil
}
slugQuery := &m.GetDashboardSlugByIdQuery{Id: c.Rule.DashboardId}
if err := bus.Dispatch(slugQuery); err != nil {
return "", err
}
c.dashboardSlug = slugQuery.Result
return c.dashboardSlug, nil
}
func (c *EvalContext) GetRuleUrl() (string, error) {
if c.IsTestRun {
return setting.AppUrl, nil
}
if slug, err := c.GetDashboardSlug(); err != nil {
return "", err
} else {
ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
return ruleUrl, nil
}
}
@@ -0,0 +1,48 @@
package alerting
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertingEvalContext(t *testing.T) {
Convey("Eval context", t, func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldUpdateAlertState(), ShouldBeTrue)
})
Convey("ok -> ok", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
})
Convey("Should send notifications", func() {
Convey("pending -> ok", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldSendNotification(), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldSendNotification(), ShouldBeTrue)
})
})
})
}
+65
View File
@@ -0,0 +1,65 @@
package alerting
import (
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
)
type DefaultEvalHandler struct {
log log.Logger
alertJobTimeout time.Duration
}
func NewEvalHandler() *DefaultEvalHandler {
return &DefaultEvalHandler{
log: log.New("alerting.evalHandler"),
alertJobTimeout: time.Second * 5,
}
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
noDataFound := true
conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context)
if err != nil {
context.Error = err
}
// break if condition could not be evaluated
if context.Error != nil {
break
}
// calculating Firing based on operator
if cr.Operator == "or" {
firing = firing || cr.Firing
noDataFound = noDataFound || cr.NoDataFound
} else {
firing = firing && cr.Firing
noDataFound = noDataFound && cr.NoDataFound
}
if i > 0 {
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
} else {
conditionEvals = strconv.FormatBool(firing)
}
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
}
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
context.Firing = firing
context.NoDataFound = noDataFound
context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond
metrics.M_Alerting_Exeuction_Time.Update(elapsedTime)
}
+168
View File
@@ -0,0 +1,168 @@
package alerting
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
type conditionStub struct {
firing bool
operator string
matches []*EvalMatch
noData bool
}
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator, NoDataFound: c.noData}, nil
}
func TestAlertingExecutor(t *testing.T) {
Convey("Test alert execution", t, func() {
handler := NewEvalHandler()
Convey("Show return triggered with single passing condition", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{&conditionStub{
firing: true,
}},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
})
Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if any of the condition is passing with OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
})
Convey("Show return false if any of the condition is failing with AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if one condition is failing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
})
Convey("Show return false if one condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
})
Convey("Show return false if a condition is failing with nested AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
})
Convey("Show return true if a condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
})
Convey("Should return no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.NoDataFound, ShouldBeTrue)
})
Convey("Should return no data if both conditions have no data and using AND", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{operator: "and", noData: true},
&conditionStub{operator: "and", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeFalse)
})
Convey("Should not return no data if both conditions have no data and using OR", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{operator: "or", noData: true},
&conditionStub{operator: "or", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeTrue)
})
})
}
+148
View File
@@ -0,0 +1,148 @@
package alerting
import (
"errors"
"fmt"
"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 DashAlertExtractor struct {
Dash *m.Dashboard
OrgId int64
log log.Logger
}
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
return &DashAlertExtractor{
Dash: dash,
OrgId: orgId,
log: log.New("alerting.extractor"),
}
}
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
if dsName == "" {
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return nil, err
} else {
for _, ds := range query.Result {
if ds.IsDefault {
return ds, nil
}
}
}
} else {
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return nil, err
} else {
return query.Result, nil
}
}
return nil, errors.New("Could not find datasource id for " + dsName)
}
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == refId {
return target
}
}
return nil
}
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
e.log.Debug("GetAlerts")
alerts := make([]*m.Alert, 0)
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
jsonAlert, hasAlert := panel.CheckGet("alert")
if !hasAlert {
continue
}
// backward compatability check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
if hasEnabled && enabled.MustBool() == false {
continue
}
frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
if err != nil {
return nil, ValidationError{Reason: "Could not parse frequency"}
}
alert := &m.Alert{
DashboardId: e.Dash.Id,
OrgId: e.OrgId,
PanelId: panel.Get("id").MustInt64(),
Id: jsonAlert.Get("id").MustInt64(),
Name: jsonAlert.Get("name").MustString(),
Handler: jsonAlert.Get("handler").MustInt64(),
Message: jsonAlert.Get("message").MustString(),
Frequency: frequency,
}
for _, condition := range jsonAlert.Get("conditions").MustArray() {
jsonCondition := simplejson.NewFromAny(condition)
jsonQuery := jsonCondition.Get("query")
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
panelQuery := findPanelQueryByRefId(panel, queryRefId)
if panelQuery == nil {
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
return nil, ValidationError{Reason: reason}
}
dsName := ""
if panelQuery.Get("datasource").MustString() != "" {
dsName = panelQuery.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
dsName = panel.Get("datasource").MustString()
}
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
return nil, err
} else {
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
}
if interval, err := panel.Get("interval").String(); err == nil {
panelQuery.Set("interval", interval)
}
jsonQuery.Set("model", panelQuery.Interface())
}
alert.Settings = jsonAlert
// validate
_, err = NewRuleFromDBAlert(alert)
if err == nil && alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
e.log.Error("Failed to extract alerts from dashboard", "error", err)
return nil, err
}
}
}
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
return alerts, nil
}
+487
View File
@@ -0,0 +1,487 @@
package alerting
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing alert rules from dashboard json", t, func() {
RegisterCondition("query", func(model *simplejson.Json, index int) (Condition, error) {
return &FakeCondition{}, nil
})
setting.NewConfigContext(&setting.CommandLineArgs{
HomePath: "../../../",
})
// mock data
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
return nil
})
bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
if query.Name == defaultDs.Name {
query.Result = defaultDs
}
if query.Name == graphite2Ds.Name {
query.Result = graphite2Ds
}
if query.Name == influxDBDs.Name {
query.Result = influxDBDs
}
return nil
})
json := `
{
"id": 57,
"title": "Graphite 4",
"originalTitle": "Graphite 4",
"tags": ["graphite"],
"rows": [
{
"panels": [
{
"title": "Active desktop users",
"editable": true,
"type": "graph",
"id": 3,
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
}
],
"datasource": null,
"alert": {
"name": "name1",
"message": "desc1",
"handler": 1,
"frequency": "60s",
"conditions": [
{
"type": "query",
"query": {"params": ["A", "5m", "now"]},
"reducer": {"type": "avg", "params": []},
"evaluator": {"type": ">", "params": [100]}
}
]
}
},
{
"title": "Active mobile users",
"id": 4,
"targets": [
{"refId": "A", "target": ""},
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
],
"datasource": "graphite2",
"alert": {
"name": "name2",
"message": "desc2",
"handler": 0,
"frequency": "60s",
"severity": "warning",
"conditions": [
{
"type": "query",
"query": {"params": ["B", "5m", "now"]},
"reducer": {"type": "avg", "params": []},
"evaluator": {"type": ">", "params": [100]}
}
]
}
}
]
}
]
}`
Convey("Parsing and validating dashboard containing graphite alerts", func() {
dashJson, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
alerts, err := extractor.GetAlerts()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
Convey("all properties have been set", func() {
So(len(alerts), ShouldEqual, 2)
for _, v := range alerts {
So(v.DashboardId, ShouldEqual, 57)
So(v.Name, ShouldNotBeEmpty)
So(v.Message, ShouldNotBeEmpty)
settings := simplejson.NewFromAny(v.Settings)
So(settings.Get("interval").MustString(""), ShouldEqual, "")
}
Convey("should extract handler property", func() {
So(alerts[0].Handler, ShouldEqual, 1)
So(alerts[1].Handler, ShouldEqual, 0)
})
Convey("should extract frequency in seconds", func() {
So(alerts[0].Frequency, ShouldEqual, 60)
So(alerts[1].Frequency, ShouldEqual, 60)
})
Convey("should extract panel idc", func() {
So(alerts[0].PanelId, ShouldEqual, 3)
So(alerts[1].PanelId, ShouldEqual, 4)
})
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Message, ShouldEqual, "desc1")
So(alerts[1].Name, ShouldEqual, "name2")
So(alerts[1].Message, ShouldEqual, "desc2")
})
Convey("should set datasourceId", func() {
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
query := condition.Get("query")
So(query.Get("datasourceId").MustInt64(), ShouldEqual, 12)
})
Convey("should copy query model to condition", func() {
condition := simplejson.NewFromAny(alerts[0].Settings.Get("conditions").MustArray()[0])
model := condition.Get("query").Get("model")
So(model.Get("target").MustString(), ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)")
})
})
})
Convey("Parse and validate dashboard containing influxdb alert", func() {
json2 := `{
"id": 4,
"title": "Influxdb",
"tags": [
"apa"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "450px",
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
10
],
"type": "gt"
},
"query": {
"params": [
"B",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"frequency": "3s",
"handler": 1,
"name": "Influxdb",
"noDataState": "no_data",
"notifications": [
{
"id": 6
}
]
},
"alerting": {},
"aliasColors": {
"logins.count.count": "#890F02"
},
"bars": false,
"datasource": "InfluxDB",
"editable": true,
"error": false,
"fill": 1,
"grid": {},
"id": 1,
"interval": ">10s",
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"datacenter"
],
"type": "tag"
},
{
"params": [
"none"
],
"type": "fill"
}
],
"hide": false,
"measurement": "logins.count",
"policy": "default",
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
"rawQuery": true,
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "count"
}
]
],
"tags": []
},
{
"dsType": "influxdb",
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"hide": true,
"measurement": "cpu",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
],
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "sum"
}
]
],
"tags": []
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 10
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"msResolution": false,
"ordering": "alphabetical",
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"editable": true,
"error": false,
"id": 2,
"isNew": true,
"limit": 10,
"links": [],
"show": "current",
"span": 2,
"stateFilter": [
"alerting"
],
"title": "Alert status",
"type": "alertlist"
}
],
"title": "Row"
}
],
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"now": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 13,
"version": 120,
"links": [],
"gnetId": null
}`
dashJson, err := simplejson.NewJson([]byte(json2))
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
alerts, err := extractor.GetAlerts()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
Convey("should be able to read interval", func() {
So(len(alerts), ShouldEqual, 1)
for _, alert := range alerts {
So(alert.DashboardId, ShouldEqual, 4)
conditions := alert.Settings.Get("conditions").MustArray()
cond := simplejson.NewFromAny(conditions[0])
So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
}
})
})
})
}
+33
View File
@@ -0,0 +1,33 @@
package alerting
import "time"
type EvalHandler interface {
Eval(evalContext *EvalContext)
}
type Scheduler interface {
Tick(time time.Time, execQueue chan *Job)
Update(rules []*Rule)
}
type Notifier interface {
Notify(evalContext *EvalContext) error
GetType() string
NeedsImage() bool
PassesFilter(rule *Rule) bool
GetNotifierId() int64
GetIsDefault() bool
}
type ConditionResult struct {
Firing bool
NoDataFound bool
Operator string
EvalMatches []*EvalMatch
}
type Condition interface {
Eval(result *EvalContext) (*ConditionResult, error)
}
+25
View File
@@ -0,0 +1,25 @@
package alerting
type Job struct {
Offset int64
OffsetWait bool
Delay bool
Running bool
Rule *Rule
}
type ResultLogEntry struct {
Message string
Data interface{}
}
type EvalMatch struct {
Value float64 `json:"value"`
Metric string `json:"metric"`
Tags map[string]string `json:"tags"`
}
type Level struct {
Operator string
Value float64
}
+159
View File
@@ -0,0 +1,159 @@
package alerting
import (
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/imguploader"
"github.com/grafana/grafana/pkg/components/renderer"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type RootNotifier struct {
log log.Logger
}
func NewRootNotifier() *RootNotifier {
return &RootNotifier{
log: log.New("alerting.notifier"),
}
}
func (n *RootNotifier) GetType() string {
return "root"
}
func (n *RootNotifier) NeedsImage() bool {
return false
}
func (n *RootNotifier) PassesFilter(rule *Rule) bool {
return false
}
func (n *RootNotifier) GetNotifierId() int64 {
return 0
}
func (n *RootNotifier) GetIsDefault() bool {
return false
}
func (n *RootNotifier) Notify(context *EvalContext) error {
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil {
return err
}
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 {
return nil
}
if err = n.uploadImage(context); err != nil {
n.log.Error("Failed to upload alert panel image.", "error", err)
}
return n.sendNotifications(context, notifiers)
}
func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error {
g, _ := errgroup.WithContext(context.Ctx)
for _, notifier := range notifiers {
not := notifier //avoid updating scope variable in go routine
n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
g.Go(func() error { return not.Notify(context) })
}
return g.Wait()
}
func (n *RootNotifier) uploadImage(context *EvalContext) (err error) {
uploader, err := imguploader.NewImageUploader()
if err != nil {
return err
}
renderOpts := &renderer.RenderOpts{
Width: "800",
Height: "400",
Timeout: "30",
OrgId: context.Rule.OrgId,
}
if slug, err := context.GetDashboardSlug(); err != nil {
return err
} else {
renderOpts.Path = fmt.Sprintf("dashboard-solo/db/%s?&panelId=%d", slug, context.Rule.PanelId)
}
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
return err
} else {
context.ImageOnDiskPath = imagePath
}
context.ImagePublicUrl, err = uploader.Upload(context.ImageOnDiskPath)
if err != nil {
return err
}
n.log.Info("uploaded", "url", context.ImagePublicUrl)
return nil
}
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
query := &m.GetAlertNotificationsToSendQuery{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.createNotifierFor(notification); err != nil {
return nil, err
} else {
if shouldUseNotification(not, context) {
result = append(result, not)
}
}
}
return result, nil
}
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
factory, found := notifierFactories[model.Type]
if !found {
return nil, errors.New("Unsupported notification type")
}
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)
func RegisterNotifier(typeName string, factory NotifierFactory) {
notifierFactories[typeName] = factory
}

Some files were not shown because too many files have changed in this diff Show More