Merge branch 'master' into cli_db_commands
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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, "")
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
// }
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -63,6 +63,9 @@ type DeleteDashboardSnapshotCommand struct {
|
||||
DeleteKey string `json:"-"`
|
||||
}
|
||||
|
||||
type DeleteExpiredSnapshotsCommand struct {
|
||||
}
|
||||
|
||||
type GetDashboardSnapshotQuery struct {
|
||||
Key string
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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-----`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,4 +6,6 @@ const (
|
||||
GITHUB OAuthType = iota + 1
|
||||
GOOGLE
|
||||
TWITTER
|
||||
GENERIC
|
||||
GRAFANANET
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type GrafanaServer interface {
|
||||
context.Context
|
||||
|
||||
Start()
|
||||
Shutdown(code int, reason string)
|
||||
}
|
||||
+6
-4
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user