Merge remote-tracking branch 'grafana/master' into annotations-created
* grafana/master: (26 commits) notes about closing #11306 cleanup changelog: unix socket permissions Adjust permissions of unix socket docs: tweaks changelog: adds note about closing #5855 legend: small refactoring Make golint happier mysql: skip tests by default mssql: update query editor help mysql: fix precision for the time column in table/annotation query mode postgres: fix precision for the time column in table/annotation query mode mssql: fix precision for the time column in table/annotation query mode mssql: remove UTC conversion in macro functions mssql: fix timeGroup macro so that it properly creates correct groups small screen legend right also work like legend under in render + set scrollbar to undefined in destroyScrollbar so it doesnt become disabled when toggeling between right and under fixed so legend right works like legend under on small screens Alerting: move getNewState to EvalContext graphite: adds more traces for alerting sql datasource: extract common logic for converting time column to epoch time in ms ...
This commit is contained in:
+36
-36
@@ -29,7 +29,7 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
|
||||
dashboardID := c.QueryInt64("dashboardId")
|
||||
|
||||
if dashboardID == 0 {
|
||||
return ApiError(400, "Missing query parameter dashboardId", nil)
|
||||
return Error(400, "Missing query parameter dashboardId", nil)
|
||||
}
|
||||
|
||||
query := m.GetAlertStatesForDashboardQuery{
|
||||
@@ -38,10 +38,10 @@ func GetAlertStatesForDashboard(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to fetch alert states", err)
|
||||
return Error(500, "Failed to fetch alert states", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/alerts
|
||||
@@ -60,20 +60,20 @@ func GetAlerts(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "List alerts failed", err)
|
||||
return Error(500, "List alerts failed", err)
|
||||
}
|
||||
|
||||
for _, alert := range query.Result {
|
||||
alert.Url = m.GetDashboardUrl(alert.DashboardUid, alert.DashboardSlug)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// POST /api/alerts/test
|
||||
func AlertTest(c *m.ReqContext, 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)
|
||||
return Error(400, "The dashboard needs to be saved at least once before you can test an alert rule", nil)
|
||||
}
|
||||
|
||||
backendCmd := alerting.AlertTestCommand{
|
||||
@@ -84,9 +84,9 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&backendCmd); err != nil {
|
||||
if validationErr, ok := err.(alerting.ValidationError); ok {
|
||||
return ApiError(422, validationErr.Error(), nil)
|
||||
return Error(422, validationErr.Error(), nil)
|
||||
}
|
||||
return ApiError(500, "Failed to test rule", err)
|
||||
return Error(500, "Failed to test rule", err)
|
||||
}
|
||||
|
||||
res := backendCmd.Result
|
||||
@@ -109,7 +109,7 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response {
|
||||
|
||||
dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
|
||||
|
||||
return Json(200, dtoRes)
|
||||
return JSON(200, dtoRes)
|
||||
}
|
||||
|
||||
// GET /api/alerts/:id
|
||||
@@ -118,21 +118,21 @@ func GetAlert(c *m.ReqContext) Response {
|
||||
query := m.GetAlertByIdQuery{Id: id}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "List alerts failed", err)
|
||||
return Error(500, "List alerts failed", err)
|
||||
}
|
||||
|
||||
return Json(200, &query.Result)
|
||||
return JSON(200, &query.Result)
|
||||
}
|
||||
|
||||
func GetAlertNotifiers(c *m.ReqContext) Response {
|
||||
return Json(200, alerting.GetNotifiers())
|
||||
return JSON(200, alerting.GetNotifiers())
|
||||
}
|
||||
|
||||
func GetAlertNotifications(c *m.ReqContext) Response {
|
||||
query := &m.GetAllAlertNotificationsQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return ApiError(500, "Failed to get alert notifications", err)
|
||||
return Error(500, "Failed to get alert notifications", err)
|
||||
}
|
||||
|
||||
result := make([]*dtos.AlertNotification, 0)
|
||||
@@ -148,7 +148,7 @@ func GetAlertNotifications(c *m.ReqContext) Response {
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func GetAlertNotificationByID(c *m.ReqContext) Response {
|
||||
@@ -158,30 +158,30 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return ApiError(500, "Failed to get alert notifications", err)
|
||||
return Error(500, "Failed to get alert notifications", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to create alert notification", err)
|
||||
return Error(500, "Failed to create alert notification", err)
|
||||
}
|
||||
|
||||
return Json(200, cmd.Result)
|
||||
return JSON(200, cmd.Result)
|
||||
}
|
||||
|
||||
func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update alert notification", err)
|
||||
return Error(500, "Failed to update alert notification", err)
|
||||
}
|
||||
|
||||
return Json(200, cmd.Result)
|
||||
return JSON(200, cmd.Result)
|
||||
}
|
||||
|
||||
func DeleteAlertNotification(c *m.ReqContext) Response {
|
||||
@@ -191,10 +191,10 @@ func DeleteAlertNotification(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete alert notification", err)
|
||||
return Error(500, "Failed to delete alert notification", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Notification deleted")
|
||||
return Success("Notification deleted")
|
||||
}
|
||||
|
||||
//POST /api/alert-notifications/test
|
||||
@@ -207,41 +207,41 @@ func NotificationTest(c *m.ReqContext, dto dtos.NotificationTestCommand) Respons
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
if err == m.ErrSmtpNotEnabled {
|
||||
return ApiError(412, err.Error(), err)
|
||||
return Error(412, err.Error(), err)
|
||||
}
|
||||
return ApiError(500, "Failed to send alert notifications", err)
|
||||
return Error(500, "Failed to send alert notifications", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Test notification sent")
|
||||
return Success("Test notification sent")
|
||||
}
|
||||
|
||||
//POST /api/alerts/:alertId/pause
|
||||
func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
|
||||
alertId := c.ParamsInt64("alertId")
|
||||
alertID := c.ParamsInt64("alertId")
|
||||
|
||||
query := m.GetAlertByIdQuery{Id: alertId}
|
||||
query := m.GetAlertByIdQuery{Id: alertID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Get Alert failed", err)
|
||||
return Error(500, "Get Alert failed", err)
|
||||
}
|
||||
|
||||
guardian := guardian.New(query.Result.DashboardId, c.OrgId, c.SignedInUser)
|
||||
if canEdit, err := guardian.CanEdit(); err != nil || !canEdit {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking permissions for Alert", err)
|
||||
return Error(500, "Error while checking permissions for Alert", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Access denied to this dashboard and alert", nil)
|
||||
return Error(403, "Access denied to this dashboard and alert", nil)
|
||||
}
|
||||
|
||||
cmd := m.PauseAlertCommand{
|
||||
OrgId: c.OrgId,
|
||||
AlertIds: []int64{alertId},
|
||||
AlertIds: []int64{alertID},
|
||||
Paused: dto.Paused,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "", err)
|
||||
return Error(500, "", err)
|
||||
}
|
||||
|
||||
var response m.AlertStateType = m.AlertStatePending
|
||||
@@ -252,12 +252,12 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"alertId": alertId,
|
||||
"alertId": alertID,
|
||||
"state": response,
|
||||
"message": "Alert " + pausedState,
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
//POST /api/admin/pause-all-alerts
|
||||
@@ -267,7 +267,7 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&updateCmd); err != nil {
|
||||
return ApiError(500, "Failed to pause alerts", err)
|
||||
return Error(500, "Failed to pause alerts", err)
|
||||
}
|
||||
|
||||
var response m.AlertStateType = m.AlertStatePending
|
||||
@@ -283,5 +283,5 @@ func PauseAllAlerts(c *m.ReqContext, dto dtos.PauseAllAlertsCommand) Response {
|
||||
"alertsAffected": updateCmd.ResultCount,
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
+30
-30
@@ -30,7 +30,7 @@ func GetAnnotations(c *m.ReqContext) Response {
|
||||
|
||||
items, err := repo.Find(query)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get annotations", err)
|
||||
return Error(500, "Failed to get annotations", err)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
@@ -40,7 +40,7 @@ func GetAnnotations(c *m.ReqContext) Response {
|
||||
item.Time = item.Time
|
||||
}
|
||||
|
||||
return Json(200, items)
|
||||
return JSON(200, items)
|
||||
}
|
||||
|
||||
type CreateAnnotationError struct {
|
||||
@@ -60,7 +60,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
|
||||
|
||||
if cmd.Text == "" {
|
||||
err := &CreateAnnotationError{"text field should not be empty"}
|
||||
return ApiError(500, "Failed to save annotation", err)
|
||||
return Error(500, "Failed to save annotation", err)
|
||||
}
|
||||
|
||||
item := annotations.Item{
|
||||
@@ -75,7 +75,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
|
||||
}
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return ApiError(500, "Failed to save annotation", err)
|
||||
return Error(500, "Failed to save annotation", err)
|
||||
}
|
||||
|
||||
startID := item.Id
|
||||
@@ -89,24 +89,24 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
|
||||
}
|
||||
|
||||
if err := repo.Update(&item); err != nil {
|
||||
return ApiError(500, "Failed set regionId on annotation", err)
|
||||
return Error(500, "Failed set regionId on annotation", err)
|
||||
}
|
||||
|
||||
item.Id = 0
|
||||
item.Epoch = cmd.TimeEnd
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return ApiError(500, "Failed save annotation for region end time", err)
|
||||
return Error(500, "Failed save annotation for region end time", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Annotation added",
|
||||
"id": startID,
|
||||
"endId": item.Id,
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Annotation added",
|
||||
"id": startID,
|
||||
})
|
||||
@@ -125,7 +125,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
|
||||
|
||||
if cmd.What == "" {
|
||||
err := &CreateAnnotationError{"what field should not be empty"}
|
||||
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||
return Error(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
|
||||
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
|
||||
@@ -145,12 +145,12 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
|
||||
tagsArray = append(tagsArray, tagStr)
|
||||
} else {
|
||||
err := &CreateAnnotationError{"tag should be a string"}
|
||||
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||
return Error(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
err := &CreateAnnotationError{"unsupported tags format"}
|
||||
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||
return Error(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
|
||||
item := annotations.Item{
|
||||
@@ -162,10 +162,10 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
|
||||
}
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return ApiError(500, "Failed to save Graphite annotation", err)
|
||||
return Error(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Graphite annotation added",
|
||||
"id": item.Id,
|
||||
})
|
||||
@@ -190,7 +190,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
}
|
||||
|
||||
if err := repo.Update(&item); err != nil {
|
||||
return ApiError(500, "Failed to update annotation", err)
|
||||
return Error(500, "Failed to update annotation", err)
|
||||
}
|
||||
|
||||
if cmd.IsRegion {
|
||||
@@ -203,11 +203,11 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
itemRight.Id = 0
|
||||
|
||||
if err := repo.Update(&itemRight); err != nil {
|
||||
return ApiError(500, "Failed to update annotation for region end time", err)
|
||||
return Error(500, "Failed to update annotation for region end time", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation updated")
|
||||
return Success("Annotation updated")
|
||||
}
|
||||
|
||||
func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response {
|
||||
@@ -220,29 +220,29 @@ func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete annotations", err)
|
||||
return Error(500, "Failed to delete annotations", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotations deleted")
|
||||
return Success("Annotations deleted")
|
||||
}
|
||||
|
||||
func DeleteAnnotationById(c *m.ReqContext) Response {
|
||||
func DeleteAnnotationByID(c *m.ReqContext) Response {
|
||||
repo := annotations.GetRepository()
|
||||
annotationId := c.ParamsInt64(":annotationId")
|
||||
annotationID := c.ParamsInt64(":annotationId")
|
||||
|
||||
if resp := canSave(c, repo, annotationId); resp != nil {
|
||||
if resp := canSave(c, repo, annotationID); resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
err := repo.Delete(&annotations.DeleteParams{
|
||||
Id: annotationId,
|
||||
Id: annotationID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete annotation", err)
|
||||
return Error(500, "Failed to delete annotation", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation deleted")
|
||||
return Success("Annotation deleted")
|
||||
}
|
||||
|
||||
func DeleteAnnotationRegion(c *m.ReqContext) Response {
|
||||
@@ -258,10 +258,10 @@ func DeleteAnnotationRegion(c *m.ReqContext) Response {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete annotation region", err)
|
||||
return Error(500, "Failed to delete annotation region", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Annotation region deleted")
|
||||
return Success("Annotation region deleted")
|
||||
}
|
||||
|
||||
func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
|
||||
@@ -279,11 +279,11 @@ func canSaveByDashboardID(c *m.ReqContext, dashboardID int64) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func canSave(c *m.ReqContext, repo annotations.Repository, annotationId int64) Response {
|
||||
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationId, OrgId: c.OrgId})
|
||||
func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) Response {
|
||||
items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId})
|
||||
|
||||
if err != nil || len(items) == 0 {
|
||||
return ApiError(500, "Could not find annotation to update", err)
|
||||
return Error(500, "Could not find annotation to update", err)
|
||||
}
|
||||
|
||||
dashboardID := items[0].DashboardId
|
||||
@@ -299,7 +299,7 @@ func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID in
|
||||
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId})
|
||||
|
||||
if err != nil || len(items) == 0 {
|
||||
return ApiError(500, "Could not find annotation to update", err)
|
||||
return Error(500, "Could not find annotation to update", err)
|
||||
}
|
||||
|
||||
dashboardID := items[0].DashboardId
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationById
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
@@ -68,7 +68,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationById
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
@@ -132,7 +132,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationById
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
@@ -159,7 +159,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) {
|
||||
sc.handlerFunc = DeleteAnnotationById
|
||||
sc.handlerFunc = DeleteAnnotationByID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
+6
-6
@@ -9,14 +9,14 @@ import (
|
||||
)
|
||||
|
||||
// Register adds http routes
|
||||
func (hs *HttpServer) registerRoutes() {
|
||||
func (hs *HTTPServer) registerRoutes() {
|
||||
macaronR := hs.macaron
|
||||
reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
|
||||
reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
|
||||
reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
|
||||
reqOrgAdmin := middleware.RoleAuth(m.ROLE_ADMIN)
|
||||
redirectFromLegacyDashboardUrl := middleware.RedirectFromLegacyDashboardURL()
|
||||
redirectFromLegacyDashboardSoloUrl := middleware.RedirectFromLegacyDashboardSoloUrl()
|
||||
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
|
||||
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
|
||||
quota := middleware.Quota
|
||||
bind := binding.Bind
|
||||
|
||||
@@ -67,11 +67,11 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
r.Get("/d/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/d/:uid", reqSignedIn, Index)
|
||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardUrl, Index)
|
||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index)
|
||||
r.Get("/dashboard/script/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloUrl, Index)
|
||||
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index)
|
||||
r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
@@ -341,7 +341,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
apiRoute.Group("/annotations", func(annotationsRoute RouteRegister) {
|
||||
annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
|
||||
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationById))
|
||||
annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
|
||||
annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
|
||||
annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
|
||||
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
|
||||
|
||||
+7
-7
@@ -11,7 +11,7 @@ func GetAPIKeys(c *m.ReqContext) Response {
|
||||
query := m.GetApiKeysQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to list api keys", err)
|
||||
return Error(500, "Failed to list api keys", err)
|
||||
}
|
||||
|
||||
result := make([]*m.ApiKeyDTO, len(query.Result))
|
||||
@@ -23,7 +23,7 @@ func GetAPIKeys(c *m.ReqContext) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func DeleteAPIKey(c *m.ReqContext) Response {
|
||||
@@ -33,15 +33,15 @@ func DeleteAPIKey(c *m.ReqContext) Response {
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete API key", err)
|
||||
return Error(500, "Failed to delete API key", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("API key deleted")
|
||||
return Success("API key deleted")
|
||||
}
|
||||
|
||||
func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
|
||||
if !cmd.Role.IsValid() {
|
||||
return ApiError(400, "Invalid role specified", nil)
|
||||
return Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
|
||||
cmd.OrgId = c.OrgId
|
||||
@@ -50,12 +50,12 @@ func AddAPIKey(c *m.ReqContext, cmd m.AddApiKeyCommand) Response {
|
||||
cmd.Key = newKeyInfo.HashedKey
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to add API key", err)
|
||||
return Error(500, "Failed to add API key", err)
|
||||
}
|
||||
|
||||
result := &dtos.NewApiKeyResult{
|
||||
Name: cmd.Result.Name,
|
||||
Key: newKeyInfo.ClientSecret}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
+13
-9
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
var (
|
||||
NotFound = func() Response {
|
||||
return ApiError(404, "Not found", nil)
|
||||
return Error(404, "Not found", nil)
|
||||
}
|
||||
ServerError = func(err error) Response {
|
||||
return ApiError(500, "Server error", err)
|
||||
return Error(500, "Server error", err)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,22 +67,25 @@ func (r *NormalResponse) Header(key, value string) *NormalResponse {
|
||||
return r
|
||||
}
|
||||
|
||||
// functions to create responses
|
||||
// Empty create an empty response
|
||||
func Empty(status int) *NormalResponse {
|
||||
return Respond(status, nil)
|
||||
}
|
||||
|
||||
func Json(status int, body interface{}) *NormalResponse {
|
||||
// JSON create a JSON response
|
||||
func JSON(status int, body interface{}) *NormalResponse {
|
||||
return Respond(status, body).Header("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
func ApiSuccess(message string) *NormalResponse {
|
||||
// Success create a successful response
|
||||
func Success(message string) *NormalResponse {
|
||||
resp := make(map[string]interface{})
|
||||
resp["message"] = message
|
||||
return Json(200, resp)
|
||||
return JSON(200, resp)
|
||||
}
|
||||
|
||||
func ApiError(status int, message string, err error) *NormalResponse {
|
||||
// Error create a erroneous response
|
||||
func Error(status int, message string, err error) *NormalResponse {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
switch status {
|
||||
@@ -102,7 +105,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
|
||||
}
|
||||
}
|
||||
|
||||
resp := Json(status, data)
|
||||
resp := JSON(status, data)
|
||||
|
||||
if err != nil {
|
||||
resp.errMessage = message
|
||||
@@ -112,6 +115,7 @@ func ApiError(status int, message string, err error) *NormalResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
// Respond create a response
|
||||
func Respond(status int, body interface{}) *NormalResponse {
|
||||
var b []byte
|
||||
var err error
|
||||
@@ -122,7 +126,7 @@ func Respond(status int, body interface{}) *NormalResponse {
|
||||
b = []byte(t)
|
||||
default:
|
||||
if b, err = json.Marshal(body); err != nil {
|
||||
return ApiError(500, "body json marshal", err)
|
||||
return Error(500, "body json marshal", err)
|
||||
}
|
||||
}
|
||||
return &NormalResponse{
|
||||
|
||||
+36
-36
@@ -37,10 +37,10 @@ func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
|
||||
|
||||
func dashboardGuardianResponse(err error) Response {
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
return Error(500, "Error while checking dashboard permissions", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Access denied to this dashboard", nil)
|
||||
return Error(403, "Access denied to this dashboard", nil)
|
||||
}
|
||||
|
||||
func GetDashboard(c *m.ReqContext) Response {
|
||||
@@ -60,7 +60,7 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
|
||||
isStarred, err := isDashboardStarredByUser(c, dash.Id)
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking if dashboard was starred by user", err)
|
||||
return Error(500, "Error while checking if dashboard was starred by user", err)
|
||||
}
|
||||
|
||||
// Finding creator and last updater of the dashboard
|
||||
@@ -96,7 +96,7 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
if dash.FolderId > 0 {
|
||||
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Dashboard folder could not be read", err)
|
||||
return Error(500, "Dashboard folder could not be read", err)
|
||||
}
|
||||
meta.FolderTitle = query.Result.Title
|
||||
meta.FolderUrl = query.Result.GetUrl()
|
||||
@@ -111,7 +111,7 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Get)
|
||||
return Json(200, dto)
|
||||
return JSON(200, dto)
|
||||
}
|
||||
|
||||
func getUserLogin(userID int64) string {
|
||||
@@ -133,7 +133,7 @@ func getDashboardHelper(orgID int64, slug string, id int64, uid string) (*m.Dash
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, ApiError(404, "Dashboard not found", err)
|
||||
return nil, Error(404, "Dashboard not found", err)
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
@@ -143,11 +143,11 @@ func DeleteDashboard(c *m.ReqContext) Response {
|
||||
query := m.GetDashboardsBySlugQuery{OrgId: c.OrgId, Slug: c.Params(":slug")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to retrieve dashboards by slug", err)
|
||||
return Error(500, "Failed to retrieve dashboards by slug", err)
|
||||
}
|
||||
|
||||
if len(query.Result) > 1 {
|
||||
return Json(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
|
||||
return JSON(412, util.DynMap{"status": "multiple-slugs-exists", "message": m.ErrDashboardsWithSameSlugExists.Error()})
|
||||
}
|
||||
|
||||
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, "")
|
||||
@@ -162,10 +162,10 @@ func DeleteDashboard(c *m.ReqContext) Response {
|
||||
|
||||
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete dashboard", err)
|
||||
return Error(500, "Failed to delete dashboard", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"title": dash.Title,
|
||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||
})
|
||||
@@ -184,10 +184,10 @@ func DeleteDashboardByUID(c *m.ReqContext) Response {
|
||||
|
||||
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete dashboard", err)
|
||||
return Error(500, "Failed to delete dashboard", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"title": dash.Title,
|
||||
"message": fmt.Sprintf("Dashboard %s deleted", dash.Title),
|
||||
})
|
||||
@@ -202,10 +202,10 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
if dash.Id == 0 && dash.Uid == "" {
|
||||
limitReached, err := quota.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
return ApiError(500, "failed to get quota", err)
|
||||
return Error(500, "failed to get quota", err)
|
||||
}
|
||||
if limitReached {
|
||||
return ApiError(403, "Quota reached", nil)
|
||||
return Error(403, "Quota reached", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,23 +229,23 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
err == m.ErrFolderNotFound ||
|
||||
err == m.ErrDashboardFolderCannotHaveParent ||
|
||||
err == m.ErrDashboardFolderNameExists {
|
||||
return ApiError(400, err.Error(), nil)
|
||||
return Error(400, err.Error(), nil)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardUpdateAccessDenied {
|
||||
return ApiError(403, err.Error(), err)
|
||||
return Error(403, err.Error(), err)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardContainsInvalidAlertData {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
return Error(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardWithSameNameInFolderExists {
|
||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
return JSON(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||
}
|
||||
if err == m.ErrDashboardVersionMismatch {
|
||||
return Json(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
||||
return JSON(412, util.DynMap{"status": "version-mismatch", "message": err.Error()})
|
||||
}
|
||||
if pluginErr, ok := err.(m.UpdatePluginDashboardError); ok {
|
||||
message := "The dashboard belongs to plugin " + pluginErr.PluginId + "."
|
||||
@@ -253,20 +253,20 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
if pluginDef, exist := plugins.Plugins[pluginErr.PluginId]; exist {
|
||||
message = "The dashboard belongs to plugin " + pluginDef.Name + "."
|
||||
}
|
||||
return Json(412, util.DynMap{"status": "plugin-dashboard", "message": message})
|
||||
return JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
|
||||
}
|
||||
if err == m.ErrDashboardNotFound {
|
||||
return Json(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
||||
return JSON(404, util.DynMap{"status": "not-found", "message": err.Error()})
|
||||
}
|
||||
return ApiError(500, "Failed to save dashboard", err)
|
||||
return Error(500, "Failed to save dashboard", err)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardFailedToUpdateAlertData {
|
||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
return Error(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
}
|
||||
|
||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"status": "success",
|
||||
"slug": dashboard.Slug,
|
||||
"version": dashboard.Version,
|
||||
@@ -279,7 +279,7 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
func GetHomeDashboard(c *m.ReqContext) Response {
|
||||
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
|
||||
if err := bus.Dispatch(&prefsQuery); err != nil {
|
||||
return ApiError(500, "Failed to get preferences", err)
|
||||
return Error(500, "Failed to get preferences", err)
|
||||
}
|
||||
|
||||
if prefsQuery.Result.HomeDashboardId != 0 {
|
||||
@@ -288,7 +288,7 @@ func GetHomeDashboard(c *m.ReqContext) Response {
|
||||
if err == nil {
|
||||
url := m.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
|
||||
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
|
||||
return Json(200, &dashRedirect)
|
||||
return JSON(200, &dashRedirect)
|
||||
}
|
||||
log.Warn("Failed to get slug from database, %s", err.Error())
|
||||
}
|
||||
@@ -296,7 +296,7 @@ func GetHomeDashboard(c *m.ReqContext) Response {
|
||||
filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to load home dashboard", err)
|
||||
return Error(500, "Failed to load home dashboard", err)
|
||||
}
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
@@ -306,14 +306,14 @@ func GetHomeDashboard(c *m.ReqContext) Response {
|
||||
|
||||
jsonParser := json.NewDecoder(file)
|
||||
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
||||
return ApiError(500, "Failed to load home dashboard", err)
|
||||
return Error(500, "Failed to load home dashboard", err)
|
||||
}
|
||||
|
||||
if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) {
|
||||
addGettingStartedPanelToHomeDashboard(dash.Dashboard)
|
||||
}
|
||||
|
||||
return Json(200, &dash)
|
||||
return JSON(200, &dash)
|
||||
}
|
||||
|
||||
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||
@@ -351,7 +351,7 @@ func GetDashboardVersions(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
|
||||
return Error(404, fmt.Sprintf("No versions found for dashboardId %d", dashID), err)
|
||||
}
|
||||
|
||||
for _, version := range query.Result {
|
||||
@@ -370,7 +370,7 @@ func GetDashboardVersions(c *m.ReqContext) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// GetDashboardVersion returns the dashboard version with the given ID.
|
||||
@@ -389,7 +389,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
||||
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
||||
}
|
||||
|
||||
creator := "Anonymous"
|
||||
@@ -402,7 +402,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
|
||||
CreatedBy: creator,
|
||||
}
|
||||
|
||||
return Json(200, dashVersionMeta)
|
||||
return JSON(200, dashVersionMeta)
|
||||
}
|
||||
|
||||
// POST /api/dashboards/calculate-diff performs diffs on two dashboards
|
||||
@@ -438,9 +438,9 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
|
||||
result, err := dashdiffs.CalculateDiff(&options)
|
||||
if err != nil {
|
||||
if err == m.ErrDashboardVersionNotFound {
|
||||
return ApiError(404, "Dashboard version not found", err)
|
||||
return Error(404, "Dashboard version not found", err)
|
||||
}
|
||||
return ApiError(500, "Unable to compute diff", err)
|
||||
return Error(500, "Unable to compute diff", err)
|
||||
}
|
||||
|
||||
if options.DiffType == dashdiffs.DiffDelta {
|
||||
@@ -464,7 +464,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
|
||||
|
||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&versionQuery); err != nil {
|
||||
return ApiError(404, "Dashboard version not found", nil)
|
||||
return Error(404, "Dashboard version not found", nil)
|
||||
}
|
||||
|
||||
version := versionQuery.Result
|
||||
|
||||
@@ -25,7 +25,7 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
|
||||
|
||||
acl, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get dashboard permissions", err)
|
||||
return Error(500, "Failed to get dashboard permissions", err)
|
||||
}
|
||||
|
||||
for _, perm := range acl {
|
||||
@@ -34,7 +34,7 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, acl)
|
||||
return JSON(200, acl)
|
||||
}
|
||||
|
||||
func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||
@@ -70,21 +70,21 @@ func UpdateDashboardPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclC
|
||||
if err != nil {
|
||||
if err == guardian.ErrGuardianPermissionExists ||
|
||||
err == guardian.ErrGuardianOverride {
|
||||
return ApiError(400, err.Error(), err)
|
||||
return Error(400, err.Error(), err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||
return Error(500, "Error while checking dashboard permissions", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
|
||||
return Error(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
|
||||
return ApiError(409, err.Error(), err)
|
||||
return Error(409, err.Error(), err)
|
||||
}
|
||||
return ApiError(500, "Failed to create permission", err)
|
||||
return Error(500, "Failed to create permission", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Dashboard permissions updated")
|
||||
return Success("Dashboard permissions updated")
|
||||
}
|
||||
|
||||
@@ -99,11 +99,11 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response {
|
||||
|
||||
err := bus.Dispatch(query)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get dashboard snapshot", err)
|
||||
return Error(500, "Failed to get dashboard snapshot", err)
|
||||
}
|
||||
|
||||
if query.Result == nil {
|
||||
return ApiError(404, "Failed to get dashboard snapshot", nil)
|
||||
return Error(404, "Failed to get dashboard snapshot", nil)
|
||||
}
|
||||
dashboard := query.Result.Dashboard
|
||||
dashboardID := dashboard.Get("id").MustInt64()
|
||||
@@ -111,20 +111,20 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response {
|
||||
guardian := guardian.New(dashboardID, c.OrgId, c.SignedInUser)
|
||||
canEdit, err := guardian.CanEdit()
|
||||
if err != nil {
|
||||
return ApiError(500, "Error while checking permissions for snapshot", err)
|
||||
return Error(500, "Error while checking permissions for snapshot", err)
|
||||
}
|
||||
|
||||
if !canEdit && query.Result.UserId != c.SignedInUser.UserId {
|
||||
return ApiError(403, "Access denied to this snapshot", nil)
|
||||
return Error(403, "Access denied to this snapshot", nil)
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: key}
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete dashboard snapshot", err)
|
||||
return Error(500, "Failed to delete dashboard snapshot", err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
|
||||
return JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
|
||||
}
|
||||
|
||||
// GET /api/dashboard/snapshots
|
||||
@@ -145,7 +145,7 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
if err != nil {
|
||||
return ApiError(500, "Search failed", err)
|
||||
return Error(500, "Search failed", err)
|
||||
}
|
||||
|
||||
dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
|
||||
@@ -165,5 +165,5 @@ func SearchDashboardSnapshots(c *m.ReqContext) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, dtos)
|
||||
return JSON(200, dtos)
|
||||
}
|
||||
|
||||
@@ -638,7 +638,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
|
||||
Convey("Should result in 412 Precondition failed", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 412)
|
||||
result := sc.ToJson()
|
||||
result := sc.ToJSON()
|
||||
So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists")
|
||||
So(result.Get("message").MustString(), ShouldEqual, m.ErrDashboardsWithSameSlugExists.Error())
|
||||
})
|
||||
@@ -686,7 +686,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("It should return correct response data", func() {
|
||||
result := sc.ToJson()
|
||||
result := sc.ToJSON()
|
||||
So(result.Get("status").MustString(), ShouldEqual, "success")
|
||||
So(result.Get("id").MustInt64(), ShouldEqual, 2)
|
||||
So(result.Get("uid").MustString(), ShouldEqual, "uid")
|
||||
@@ -903,7 +903,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
|
||||
})
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) ToJson() *simplejson.Json {
|
||||
func (sc *scenarioContext) ToJSON() *simplejson.Json {
|
||||
var result *simplejson.Json
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&result)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
||||
|
||||
func (hs *HttpServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) {
|
||||
func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) {
|
||||
cacheKey := fmt.Sprintf("ds-%d", id)
|
||||
|
||||
if !nocache {
|
||||
@@ -34,7 +34,7 @@ func (hs *HttpServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func (hs *HttpServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||
|
||||
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
||||
|
||||
+28
-28
@@ -14,7 +14,7 @@ func GetDataSources(c *m.ReqContext) Response {
|
||||
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
return Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
@@ -46,7 +46,7 @@ func GetDataSources(c *m.ReqContext) Response {
|
||||
|
||||
sort.Sort(result)
|
||||
|
||||
return Json(200, &result)
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
func GetDataSourceByID(c *m.ReqContext) Response {
|
||||
@@ -57,66 +57,66 @@ func GetDataSourceByID(c *m.ReqContext) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrDataSourceNotFound {
|
||||
return ApiError(404, "Data source not found", nil)
|
||||
return Error(404, "Data source not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
return Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
ds := query.Result
|
||||
dtos := convertModelToDtos(ds)
|
||||
|
||||
return Json(200, &dtos)
|
||||
return JSON(200, &dtos)
|
||||
}
|
||||
|
||||
func DeleteDataSourceByID(c *m.ReqContext) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
if id <= 0 {
|
||||
return ApiError(400, "Missing valid datasource id", nil)
|
||||
return Error(400, "Missing valid datasource id", nil)
|
||||
}
|
||||
|
||||
ds, err := getRawDataSourceByID(id, c.OrgId)
|
||||
if err != nil {
|
||||
return ApiError(400, "Failed to delete datasource", nil)
|
||||
return Error(400, "Failed to delete datasource", nil)
|
||||
}
|
||||
|
||||
if ds.ReadOnly {
|
||||
return ApiError(403, "Cannot delete read-only data source", nil)
|
||||
return Error(403, "Cannot delete read-only data source", nil)
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByIdCommand{Id: id, OrgId: c.OrgId}
|
||||
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
return Error(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Data source deleted")
|
||||
return Success("Data source deleted")
|
||||
}
|
||||
|
||||
func DeleteDataSourceByName(c *m.ReqContext) Response {
|
||||
name := c.Params(":name")
|
||||
|
||||
if name == "" {
|
||||
return ApiError(400, "Missing valid datasource name", nil)
|
||||
return Error(400, "Missing valid datasource name", nil)
|
||||
}
|
||||
|
||||
getCmd := &m.GetDataSourceByNameQuery{Name: name, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(getCmd); err != nil {
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
return Error(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
if getCmd.Result.ReadOnly {
|
||||
return ApiError(403, "Cannot delete read-only data source", nil)
|
||||
return Error(403, "Cannot delete read-only data source", nil)
|
||||
}
|
||||
|
||||
cmd := &m.DeleteDataSourceByNameCommand{Name: name, OrgId: c.OrgId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to delete datasource", err)
|
||||
return Error(500, "Failed to delete datasource", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Data source deleted")
|
||||
return Success("Data source deleted")
|
||||
}
|
||||
|
||||
func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
|
||||
@@ -124,14 +124,14 @@ func AddDataSource(c *m.ReqContext, cmd m.AddDataSourceCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrDataSourceNameExists {
|
||||
return ApiError(409, err.Error(), err)
|
||||
return Error(409, err.Error(), err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to add datasource", err)
|
||||
return Error(500, "Failed to add datasource", err)
|
||||
}
|
||||
|
||||
ds := convertModelToDtos(cmd.Result)
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Datasource added",
|
||||
"id": cmd.Result.Id,
|
||||
"name": cmd.Result.Name,
|
||||
@@ -145,18 +145,18 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response {
|
||||
|
||||
err := fillWithSecureJSONData(&cmd)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
return Error(500, "Failed to update datasource", err)
|
||||
}
|
||||
|
||||
err = bus.Dispatch(&cmd)
|
||||
if err != nil {
|
||||
if err == m.ErrDataSourceUpdatingOldVersion {
|
||||
return ApiError(500, "Failed to update datasource. Reload new version and try again", err)
|
||||
return Error(500, "Failed to update datasource. Reload new version and try again", err)
|
||||
}
|
||||
return ApiError(500, "Failed to update datasource", err)
|
||||
return Error(500, "Failed to update datasource", err)
|
||||
}
|
||||
ds := convertModelToDtos(cmd.Result)
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"message": "Datasource updated",
|
||||
"id": cmd.Id,
|
||||
"name": cmd.Name,
|
||||
@@ -208,14 +208,14 @@ func GetDataSourceByName(c *m.ReqContext) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrDataSourceNotFound {
|
||||
return ApiError(404, "Data source not found", nil)
|
||||
return Error(404, "Data source not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
return Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
dtos := convertModelToDtos(query.Result)
|
||||
dtos.ReadOnly = true
|
||||
return Json(200, &dtos)
|
||||
return JSON(200, &dtos)
|
||||
}
|
||||
|
||||
// Get /api/datasources/id/:name
|
||||
@@ -224,9 +224,9 @@ func GetDataSourceIDByName(c *m.ReqContext) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrDataSourceNotFound {
|
||||
return ApiError(404, "Data source not found", nil)
|
||||
return Error(404, "Data source not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to query datasources", err)
|
||||
return Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
ds := query.Result
|
||||
@@ -234,7 +234,7 @@ func GetDataSourceIDByName(c *m.ReqContext) Response {
|
||||
Id: ds.Id,
|
||||
}
|
||||
|
||||
return Json(200, &dtos)
|
||||
return JSON(200, &dtos)
|
||||
}
|
||||
|
||||
func convertModelToDtos(ds *m.DataSource) dtos.DataSource {
|
||||
|
||||
@@ -2,12 +2,12 @@ package dtos
|
||||
|
||||
type Prefs struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
type UpdatePrefsCmd struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
+13
-13
@@ -28,30 +28,30 @@ func GetFolders(c *m.ReqContext) Response {
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func GetFolderByUID(c *m.ReqContext) Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||
folder, err := s.GetFolderByUID(c.Params(":uid"))
|
||||
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||
return Json(200, toFolderDto(g, folder))
|
||||
return JSON(200, toFolderDto(g, folder))
|
||||
}
|
||||
|
||||
func GetFolderByID(c *m.ReqContext) Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||
folder, err := s.GetFolderById(c.ParamsInt64(":id"))
|
||||
folder, err := s.GetFolderByID(c.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
g := guardian.New(folder.Id, c.OrgId, c.SignedInUser)
|
||||
return Json(200, toFolderDto(g, folder))
|
||||
return JSON(200, toFolderDto(g, folder))
|
||||
}
|
||||
|
||||
func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
|
||||
@@ -62,7 +62,7 @@ func CreateFolder(c *m.ReqContext, cmd m.CreateFolderCommand) Response {
|
||||
}
|
||||
|
||||
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||
return Json(200, toFolderDto(g, cmd.Result))
|
||||
return JSON(200, toFolderDto(g, cmd.Result))
|
||||
}
|
||||
|
||||
func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
|
||||
@@ -73,7 +73,7 @@ func UpdateFolder(c *m.ReqContext, cmd m.UpdateFolderCommand) Response {
|
||||
}
|
||||
|
||||
g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser)
|
||||
return Json(200, toFolderDto(g, cmd.Result))
|
||||
return JSON(200, toFolderDto(g, cmd.Result))
|
||||
}
|
||||
|
||||
func DeleteFolder(c *m.ReqContext) Response {
|
||||
@@ -83,7 +83,7 @@ func DeleteFolder(c *m.ReqContext) Response {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"title": f.Title,
|
||||
"message": fmt.Sprintf("Folder %s deleted", f.Title),
|
||||
})
|
||||
@@ -127,20 +127,20 @@ func toFolderError(err error) Response {
|
||||
err == m.ErrDashboardTypeMismatch ||
|
||||
err == m.ErrDashboardInvalidUid ||
|
||||
err == m.ErrDashboardUidToLong {
|
||||
return ApiError(400, err.Error(), nil)
|
||||
return Error(400, err.Error(), nil)
|
||||
}
|
||||
|
||||
if err == m.ErrFolderAccessDenied {
|
||||
return ApiError(403, "Access denied", err)
|
||||
return Error(403, "Access denied", err)
|
||||
}
|
||||
|
||||
if err == m.ErrFolderNotFound {
|
||||
return Json(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
|
||||
return JSON(404, util.DynMap{"status": "not-found", "message": m.ErrFolderNotFound.Error()})
|
||||
}
|
||||
|
||||
if err == m.ErrFolderVersionMismatch {
|
||||
return Json(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
|
||||
return JSON(412, util.DynMap{"status": "version-mismatch", "message": m.ErrFolderVersionMismatch.Error()})
|
||||
}
|
||||
|
||||
return ApiError(500, "Folder API error", err)
|
||||
return Error(500, "Folder API error", err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func GetFolderPermissionList(c *m.ReqContext) Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||
folder, err := s.GetFolderByUID(c.Params(":uid"))
|
||||
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
@@ -26,7 +26,7 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
|
||||
|
||||
acl, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get folder permissions", err)
|
||||
return Error(500, "Failed to get folder permissions", err)
|
||||
}
|
||||
|
||||
for _, perm := range acl {
|
||||
@@ -38,12 +38,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(200, acl)
|
||||
return JSON(200, acl)
|
||||
}
|
||||
|
||||
func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||
s := dashboards.NewFolderService(c.OrgId, c.SignedInUser)
|
||||
folder, err := s.GetFolderByUid(c.Params(":uid"))
|
||||
folder, err := s.GetFolderByUID(c.Params(":uid"))
|
||||
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
@@ -79,13 +79,13 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
|
||||
if err != nil {
|
||||
if err == guardian.ErrGuardianPermissionExists ||
|
||||
err == guardian.ErrGuardianOverride {
|
||||
return ApiError(400, err.Error(), err)
|
||||
return Error(400, err.Error(), err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Error while checking folder permissions", err)
|
||||
return Error(500, "Error while checking folder permissions", err)
|
||||
}
|
||||
|
||||
return ApiError(403, "Cannot remove own admin permission for a folder", nil)
|
||||
return Error(403, "Cannot remove own admin permission for a folder", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
@@ -97,11 +97,11 @@ func UpdateFolderPermissions(c *m.ReqContext, apiCmd dtos.UpdateDashboardAclComm
|
||||
}
|
||||
|
||||
if err == m.ErrFolderAclInfoMissing || err == m.ErrFolderPermissionFolderEmpty {
|
||||
return ApiError(409, err.Error(), err)
|
||||
return Error(409, err.Error(), err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to create permission", err)
|
||||
return Error(500, "Failed to create permission", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Folder permissions updated")
|
||||
return Success("Folder permissions updated")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||
Convey("Folder permissions test", t, func() {
|
||||
Convey("Given folder not exists", func() {
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUidError: m.ErrFolderNotFound,
|
||||
GetFolderByUIDError: m.ErrFolderNotFound,
|
||||
}
|
||||
|
||||
origNewFolderService := dashboards.NewFolderService
|
||||
@@ -49,7 +49,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUidResult: &m.Folder{
|
||||
GetFolderByUIDResult: &m.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
@@ -96,7 +96,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUidResult: &m.Folder{
|
||||
GetFolderByUIDResult: &m.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
@@ -142,7 +142,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||
})
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUidResult: &m.Folder{
|
||||
GetFolderByUIDResult: &m.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
@@ -178,7 +178,7 @@ func TestFolderPermissionApiEndpoint(t *testing.T) {
|
||||
)
|
||||
|
||||
mock := &fakeFolderService{
|
||||
GetFolderByUidResult: &m.Folder{
|
||||
GetFolderByUIDResult: &m.Folder{
|
||||
Id: 1,
|
||||
Uid: "uid",
|
||||
Title: "Folder",
|
||||
|
||||
@@ -204,10 +204,10 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
|
||||
type fakeFolderService struct {
|
||||
GetFoldersResult []*m.Folder
|
||||
GetFoldersError error
|
||||
GetFolderByUidResult *m.Folder
|
||||
GetFolderByUidError error
|
||||
GetFolderByIdResult *m.Folder
|
||||
GetFolderByIdError error
|
||||
GetFolderByUIDResult *m.Folder
|
||||
GetFolderByUIDError error
|
||||
GetFolderByIDResult *m.Folder
|
||||
GetFolderByIDError error
|
||||
CreateFolderResult *m.Folder
|
||||
CreateFolderError error
|
||||
UpdateFolderResult *m.Folder
|
||||
@@ -221,12 +221,12 @@ func (s *fakeFolderService) GetFolders(limit int) ([]*m.Folder, error) {
|
||||
return s.GetFoldersResult, s.GetFoldersError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) GetFolderById(id int64) (*m.Folder, error) {
|
||||
return s.GetFolderByIdResult, s.GetFolderByIdError
|
||||
func (s *fakeFolderService) GetFolderByID(id int64) (*m.Folder, error) {
|
||||
return s.GetFolderByIDResult, s.GetFolderByIDError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) GetFolderByUid(uid string) (*m.Folder, error) {
|
||||
return s.GetFolderByUidResult, s.GetFolderByUidError
|
||||
func (s *fakeFolderService) GetFolderByUID(uid string) (*m.Folder, error) {
|
||||
return s.GetFolderByUIDResult, s.GetFolderByUIDError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
|
||||
@@ -234,7 +234,7 @@ func (s *fakeFolderService) CreateFolder(cmd *m.CreateFolderCommand) error {
|
||||
return s.CreateFolderError
|
||||
}
|
||||
|
||||
func (s *fakeFolderService) UpdateFolder(existingUid string, cmd *m.UpdateFolderCommand) error {
|
||||
func (s *fakeFolderService) UpdateFolder(existingUID string, cmd *m.UpdateFolderCommand) error {
|
||||
cmd.Result = s.UpdateFolderResult
|
||||
return s.UpdateFolderError
|
||||
}
|
||||
|
||||
+14
-11
@@ -29,7 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type HttpServer struct {
|
||||
type HTTPServer struct {
|
||||
log log.Logger
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
@@ -39,14 +39,14 @@ type HttpServer struct {
|
||||
httpSrv *http.Server
|
||||
}
|
||||
|
||||
func NewHTTPServer() *HttpServer {
|
||||
return &HttpServer{
|
||||
func NewHTTPServer() *HTTPServer {
|
||||
return &HTTPServer{
|
||||
log: log.New("http.server"),
|
||||
cache: gocache.New(5*time.Minute, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
hs.context = ctx
|
||||
@@ -74,12 +74,15 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
case setting.SOCKET:
|
||||
ln, err := net.Listen("unix", setting.SocketPath)
|
||||
ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: setting.SocketPath, Net: "unix"})
|
||||
if err != nil {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make socket writable by group
|
||||
os.Chmod(setting.SocketPath, 0660)
|
||||
|
||||
err = hs.httpSrv.Serve(ln)
|
||||
if err != nil {
|
||||
hs.log.Debug("server was shutdown gracefully")
|
||||
@@ -93,13 +96,13 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
||||
func (hs *HTTPServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("Stopped HTTP server")
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
if certfile == "" {
|
||||
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
||||
}
|
||||
@@ -141,7 +144,7 @@ func (hs *HttpServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
func (hs *HTTPServer) newMacaron() *macaron.Macaron {
|
||||
macaron.Env = setting.Env
|
||||
m := macaron.New()
|
||||
|
||||
@@ -188,7 +191,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
return m
|
||||
}
|
||||
|
||||
func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" {
|
||||
return
|
||||
}
|
||||
@@ -197,7 +200,7 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
|
||||
ServeHTTP(ctx.Resp, ctx.Req.Request)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
|
||||
func (hs *HTTPServer) healthHandler(ctx *macaron.Context) {
|
||||
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
|
||||
if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
|
||||
return
|
||||
@@ -221,7 +224,7 @@ func (hs *HttpServer) healthHandler(ctx *macaron.Context) {
|
||||
ctx.Resp.Write(dataBytes)
|
||||
}
|
||||
|
||||
func (hs *HttpServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
|
||||
func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) {
|
||||
headers := func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
|
||||
+4
-4
@@ -98,7 +98,7 @@ func LoginAPIPing(c *m.ReqContext) {
|
||||
|
||||
func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
||||
if setting.DisableLoginForm {
|
||||
return ApiError(401, "Login is disabled", nil)
|
||||
return Error(401, "Login is disabled", nil)
|
||||
}
|
||||
|
||||
authQuery := login.LoginUserQuery{
|
||||
@@ -109,10 +109,10 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&authQuery); err != nil {
|
||||
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
|
||||
return ApiError(401, "Invalid username or password", err)
|
||||
return Error(401, "Invalid username or password", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Error while trying to authenticate user", err)
|
||||
return Error(500, "Error while trying to authenticate user", err)
|
||||
}
|
||||
|
||||
user := authQuery.User
|
||||
@@ -130,7 +130,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
||||
|
||||
metrics.M_Api_Login_Post.Inc()
|
||||
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func loginUserWithUser(user *m.User, c *m.ReqContext) {
|
||||
|
||||
+11
-11
@@ -17,17 +17,17 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
|
||||
timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To)
|
||||
|
||||
if len(reqDto.Queries) == 0 {
|
||||
return ApiError(400, "No queries found in query", nil)
|
||||
return Error(400, "No queries found in query", nil)
|
||||
}
|
||||
|
||||
dsID, err := reqDto.Queries[0].Get("datasourceId").Int64()
|
||||
if err != nil {
|
||||
return ApiError(400, "Query missing datasourceId", nil)
|
||||
return Error(400, "Query missing datasourceId", nil)
|
||||
}
|
||||
|
||||
dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&dsQuery); err != nil {
|
||||
return ApiError(500, "failed to fetch data source", err)
|
||||
return Error(500, "failed to fetch data source", err)
|
||||
}
|
||||
|
||||
request := &tsdb.TsdbQuery{TimeRange: timeRange}
|
||||
@@ -44,7 +44,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
|
||||
|
||||
resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
return Error(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
statusCode := 200
|
||||
@@ -56,7 +56,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response {
|
||||
}
|
||||
}
|
||||
|
||||
return Json(statusCode, &resp)
|
||||
return JSON(statusCode, &resp)
|
||||
}
|
||||
|
||||
// GET /api/tsdb/testdata/scenarios
|
||||
@@ -72,22 +72,22 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
|
||||
})
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
// Genereates a index out of range error
|
||||
func GenerateError(c *m.ReqContext) Response {
|
||||
var array []string
|
||||
return Json(200, array[20])
|
||||
return JSON(200, array[20])
|
||||
}
|
||||
|
||||
// GET /api/tsdb/testdata/gensql
|
||||
func GenerateSQLTestData(c *m.ReqContext) Response {
|
||||
if err := bus.Dispatch(&m.InsertSqlTestDataCommand{}); err != nil {
|
||||
return ApiError(500, "Failed to insert test data", err)
|
||||
return Error(500, "Failed to insert test data", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{"message": "OK"})
|
||||
return JSON(200, &util.DynMap{"message": "OK"})
|
||||
}
|
||||
|
||||
// GET /api/tsdb/testdata/random-walk
|
||||
@@ -111,8 +111,8 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response {
|
||||
|
||||
resp, err := tsdb.HandleRequest(context.Background(), dsInfo, request)
|
||||
if err != nil {
|
||||
return ApiError(500, "Metric request error", err)
|
||||
return Error(500, "Metric request error", err)
|
||||
}
|
||||
|
||||
return Json(200, &resp)
|
||||
return JSON(200, &resp)
|
||||
}
|
||||
|
||||
+22
-22
@@ -24,10 +24,10 @@ func GetOrgByName(c *m.ReqContext) Response {
|
||||
query := m.GetOrgByNameQuery{Name: c.Params(":name")}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrOrgNotFound {
|
||||
return ApiError(404, "Organization not found", err)
|
||||
return Error(404, "Organization not found", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get organization", err)
|
||||
return Error(500, "Failed to get organization", err)
|
||||
}
|
||||
org := query.Result
|
||||
result := m.OrgDetailsDTO{
|
||||
@@ -43,18 +43,18 @@ func GetOrgByName(c *m.ReqContext) Response {
|
||||
},
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
func getOrgHelper(orgId int64) Response {
|
||||
query := m.GetOrgByIdQuery{Id: orgId}
|
||||
func getOrgHelper(orgID int64) Response {
|
||||
query := m.GetOrgByIdQuery{Id: orgID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrOrgNotFound {
|
||||
return ApiError(404, "Organization not found", err)
|
||||
return Error(404, "Organization not found", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get organization", err)
|
||||
return Error(500, "Failed to get organization", err)
|
||||
}
|
||||
|
||||
org := query.Result
|
||||
@@ -71,26 +71,26 @@ func getOrgHelper(orgId int64) Response {
|
||||
},
|
||||
}
|
||||
|
||||
return Json(200, &result)
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
// POST /api/orgs
|
||||
func CreateOrg(c *m.ReqContext, cmd m.CreateOrgCommand) Response {
|
||||
if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
|
||||
return ApiError(403, "Access denied", nil)
|
||||
return Error(403, "Access denied", nil)
|
||||
}
|
||||
|
||||
cmd.UserId = c.UserId
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrOrgNameTaken {
|
||||
return ApiError(409, "Organization name taken", err)
|
||||
return Error(409, "Organization name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to create organization", err)
|
||||
return Error(500, "Failed to create organization", err)
|
||||
}
|
||||
|
||||
metrics.M_Api_Org_Create.Inc()
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
return JSON(200, &util.DynMap{
|
||||
"orgId": cmd.Result.Id,
|
||||
"message": "Organization created",
|
||||
})
|
||||
@@ -110,12 +110,12 @@ func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) Response {
|
||||
cmd := m.UpdateOrgCommand{Name: form.Name, OrgId: orgID}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrOrgNameTaken {
|
||||
return ApiError(400, "Organization name taken", err)
|
||||
return Error(400, "Organization name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to update organization", err)
|
||||
return Error(500, "Failed to update organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Organization updated")
|
||||
return Success("Organization updated")
|
||||
}
|
||||
|
||||
// PUT /api/org/address
|
||||
@@ -142,21 +142,21 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) Respons
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update org address", err)
|
||||
return Error(500, "Failed to update org address", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Address updated")
|
||||
return Success("Address updated")
|
||||
}
|
||||
|
||||
// GET /api/orgs/:orgId
|
||||
func DeleteOrgByID(c *m.ReqContext) 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 Error(404, "Failed to delete organization. ID not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to update organization", err)
|
||||
return Error(500, "Failed to update organization", err)
|
||||
}
|
||||
return ApiSuccess("Organization deleted")
|
||||
return Success("Organization deleted")
|
||||
}
|
||||
|
||||
func SearchOrgs(c *m.ReqContext) Response {
|
||||
@@ -168,8 +168,8 @@ func SearchOrgs(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to search orgs", err)
|
||||
return Error(500, "Failed to search orgs", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
+26
-26
@@ -16,30 +16,30 @@ func GetPendingOrgInvites(c *m.ReqContext) Response {
|
||||
query := m.GetTempUsersQuery{OrgId: c.OrgId, Status: m.TmpUserInvitePending}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get invites from db", err)
|
||||
return Error(500, "Failed to get invites from db", err)
|
||||
}
|
||||
|
||||
for _, invite := range query.Result {
|
||||
invite.Url = setting.ToAbsUrl("invite/" + invite.Code)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
|
||||
if !inviteDto.Role.IsValid() {
|
||||
return ApiError(400, "Invalid role specified", nil)
|
||||
return Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
|
||||
// first try get existing user
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: inviteDto.LoginOrEmail}
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
if err != m.ErrUserNotFound {
|
||||
return ApiError(500, "Failed to query db for existing user check", err)
|
||||
return Error(500, "Failed to query db for existing user check", err)
|
||||
}
|
||||
|
||||
if setting.DisableLoginForm {
|
||||
return ApiError(401, "User could not be found", nil)
|
||||
return Error(401, "User could not be found", nil)
|
||||
}
|
||||
} else {
|
||||
return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
|
||||
@@ -56,7 +56,7 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
|
||||
cmd.RemoteAddr = c.Req.RemoteAddr
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to save invite to database", err)
|
||||
return Error(500, "Failed to save invite to database", err)
|
||||
}
|
||||
|
||||
// send invite email
|
||||
@@ -74,18 +74,18 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&emailCmd); err != nil {
|
||||
return ApiError(500, "Failed to send email invite", err)
|
||||
return Error(500, "Failed to send email invite", err)
|
||||
}
|
||||
|
||||
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
|
||||
if err := bus.Dispatch(&emailSentCmd); err != nil {
|
||||
return ApiError(500, "Failed to update invite with email sent info", err)
|
||||
return Error(500, "Failed to update invite with email sent info", err)
|
||||
}
|
||||
|
||||
return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
|
||||
return Success(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
|
||||
}
|
||||
|
||||
return ApiSuccess(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
|
||||
return Success(fmt.Sprintf("Created invite for %s", inviteDto.LoginOrEmail))
|
||||
}
|
||||
|
||||
func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddInviteForm) Response {
|
||||
@@ -93,9 +93,9 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI
|
||||
createOrgUserCmd := m.AddOrgUserCommand{OrgId: c.OrgId, UserId: user.Id, Role: inviteDto.Role}
|
||||
if err := bus.Dispatch(&createOrgUserCmd); err != nil {
|
||||
if err == m.ErrOrgUserAlreadyAdded {
|
||||
return ApiError(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
|
||||
return Error(412, fmt.Sprintf("User %s is already added to organization", inviteDto.LoginOrEmail), err)
|
||||
}
|
||||
return ApiError(500, "Error while trying to create org user", err)
|
||||
return Error(500, "Error while trying to create org user", err)
|
||||
}
|
||||
|
||||
if inviteDto.SendEmail && util.IsEmail(user.Email) {
|
||||
@@ -110,11 +110,11 @@ func inviteExistingUserToOrg(c *m.ReqContext, user *m.User, inviteDto *dtos.AddI
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&emailCmd); err != nil {
|
||||
return ApiError(500, "Failed to send email invited_to_org", err)
|
||||
return Error(500, "Failed to send email invited_to_org", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ApiSuccess(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
|
||||
return Success(fmt.Sprintf("Existing Grafana user %s added to org %s", user.NameOrFallback(), c.OrgName))
|
||||
}
|
||||
|
||||
func RevokeInvite(c *m.ReqContext) Response {
|
||||
@@ -122,7 +122,7 @@ func RevokeInvite(c *m.ReqContext) Response {
|
||||
return rsp
|
||||
}
|
||||
|
||||
return ApiSuccess("Invite revoked")
|
||||
return Success("Invite revoked")
|
||||
}
|
||||
|
||||
func GetInviteInfoByCode(c *m.ReqContext) Response {
|
||||
@@ -130,14 +130,14 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTempUserNotFound {
|
||||
return ApiError(404, "Invite not found", nil)
|
||||
return Error(404, "Invite not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get invite", err)
|
||||
return Error(500, "Failed to get invite", err)
|
||||
}
|
||||
|
||||
invite := query.Result
|
||||
|
||||
return Json(200, dtos.InviteInfo{
|
||||
return JSON(200, dtos.InviteInfo{
|
||||
Email: invite.Email,
|
||||
Name: invite.Name,
|
||||
Username: invite.Email,
|
||||
@@ -150,14 +150,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTempUserNotFound {
|
||||
return ApiError(404, "Invite not found", nil)
|
||||
return Error(404, "Invite not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get invite", err)
|
||||
return Error(500, "Failed to get invite", err)
|
||||
}
|
||||
|
||||
invite := query.Result
|
||||
if invite.Status != m.TmpUserInvitePending {
|
||||
return ApiError(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
|
||||
return Error(412, fmt.Sprintf("Invite cannot be used in status %s", invite.Status), nil)
|
||||
}
|
||||
|
||||
cmd := m.CreateUserCommand{
|
||||
@@ -169,7 +169,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "failed to create user", err)
|
||||
return Error(500, "failed to create user", err)
|
||||
}
|
||||
|
||||
user := &cmd.Result
|
||||
@@ -188,14 +188,14 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
|
||||
metrics.M_Api_User_SignUpCompleted.Inc()
|
||||
metrics.M_Api_User_SignUpInvite.Inc()
|
||||
|
||||
return ApiSuccess("User created and logged in")
|
||||
return Success("User created and logged in")
|
||||
}
|
||||
|
||||
func updateTempUserStatus(code string, status m.TempUserStatus) (bool, Response) {
|
||||
// update temp user status
|
||||
updateTmpUserCmd := m.UpdateTempUserStatusCommand{Code: code, Status: status}
|
||||
if err := bus.Dispatch(&updateTmpUserCmd); err != nil {
|
||||
return false, ApiError(500, "Failed to update invite status", err)
|
||||
return false, Error(500, "Failed to update invite status", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -206,7 +206,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
|
||||
addOrgUserCmd := m.AddOrgUserCommand{OrgId: invite.OrgId, UserId: user.Id, Role: invite.Role}
|
||||
if err := bus.Dispatch(&addOrgUserCmd); err != nil {
|
||||
if err != m.ErrOrgUserAlreadyAdded {
|
||||
return false, ApiError(500, "Error while trying to create org user", err)
|
||||
return false, Error(500, "Error while trying to create org user", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ func applyUserInvite(user *m.User, invite *m.TempUserDTO, setActive bool) (bool,
|
||||
if setActive {
|
||||
// set org to active
|
||||
if err := bus.Dispatch(&m.SetUsingOrgCommand{OrgId: invite.OrgId, UserId: user.Id}); err != nil {
|
||||
return false, ApiError(500, "Failed to set org as active", err)
|
||||
return false, Error(500, "Failed to set org as active", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -20,13 +20,13 @@ func AddOrgUser(c *m.ReqContext, cmd m.AddOrgUserCommand) Response {
|
||||
|
||||
func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
||||
if !cmd.Role.IsValid() {
|
||||
return ApiError(400, "Invalid role specified", nil)
|
||||
return Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail}
|
||||
err := bus.Dispatch(&userQuery)
|
||||
if err != nil {
|
||||
return ApiError(404, "User not found", nil)
|
||||
return Error(404, "User not found", nil)
|
||||
}
|
||||
|
||||
userToAdd := userQuery.Result
|
||||
@@ -35,12 +35,12 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrOrgUserAlreadyAdded {
|
||||
return ApiError(409, "User is already member of this organization", nil)
|
||||
return Error(409, "User is already member of this organization", nil)
|
||||
}
|
||||
return ApiError(500, "Could not add user to organization", err)
|
||||
return Error(500, "Could not add user to organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User added to organization")
|
||||
return Success("User added to organization")
|
||||
}
|
||||
|
||||
// GET /api/org/users
|
||||
@@ -61,14 +61,14 @@ func getOrgUsersHelper(orgID int64, query string, limit int) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&q); err != nil {
|
||||
return ApiError(500, "Failed to get account user", err)
|
||||
return Error(500, "Failed to get account user", err)
|
||||
}
|
||||
|
||||
for _, user := range q.Result {
|
||||
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||
}
|
||||
|
||||
return Json(200, q.Result)
|
||||
return JSON(200, q.Result)
|
||||
}
|
||||
|
||||
// PATCH /api/org/users/:userId
|
||||
@@ -87,17 +87,17 @@ func UpdateOrgUser(c *m.ReqContext, cmd m.UpdateOrgUserCommand) Response {
|
||||
|
||||
func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
|
||||
if !cmd.Role.IsValid() {
|
||||
return ApiError(400, "Invalid role specified", nil)
|
||||
return Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrLastOrgAdmin {
|
||||
return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
|
||||
return Error(400, "Cannot change role so that there is no organization admin left", nil)
|
||||
}
|
||||
return ApiError(500, "Failed update org user", err)
|
||||
return Error(500, "Failed update org user", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Organization user updated")
|
||||
return Success("Organization user updated")
|
||||
}
|
||||
|
||||
// DELETE /api/org/users/:userId
|
||||
@@ -118,10 +118,10 @@ func removeOrgUserHelper(orgID int64, userID int64) Response {
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrLastOrgAdmin {
|
||||
return ApiError(400, "Cannot remove last organization admin", nil)
|
||||
return Error(400, "Cannot remove last organization admin", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to remove user from organization", err)
|
||||
return Error(500, "Failed to remove user from organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User removed from organization")
|
||||
return Success("User removed from organization")
|
||||
}
|
||||
|
||||
+8
-8
@@ -12,15 +12,15 @@ func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailFor
|
||||
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
c.Logger.Info("Requested password reset for user that was not found", "user", userQuery.LoginOrEmail)
|
||||
return ApiError(200, "Email sent", err)
|
||||
return Error(200, "Email sent", err)
|
||||
}
|
||||
|
||||
emailCmd := m.SendResetPasswordEmailCommand{User: userQuery.Result}
|
||||
if err := bus.Dispatch(&emailCmd); err != nil {
|
||||
return ApiError(500, "Failed to send email", err)
|
||||
return Error(500, "Failed to send email", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Email sent")
|
||||
return Success("Email sent")
|
||||
}
|
||||
|
||||
func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
|
||||
@@ -28,13 +28,13 @@ func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrInvalidEmailCode {
|
||||
return ApiError(400, "Invalid or expired reset password code", nil)
|
||||
return Error(400, "Invalid or expired reset password code", nil)
|
||||
}
|
||||
return ApiError(500, "Unknown error validating email code", err)
|
||||
return Error(500, "Unknown error validating email code", err)
|
||||
}
|
||||
|
||||
if form.NewPassword != form.ConfirmPassword {
|
||||
return ApiError(400, "Passwords do not match", nil)
|
||||
return Error(400, "Passwords do not match", nil)
|
||||
}
|
||||
|
||||
cmd := m.ChangeUserPasswordCommand{}
|
||||
@@ -42,8 +42,8 @@ func ResetPassword(c *m.ReqContext, form dtos.ResetUserPasswordForm) Response {
|
||||
cmd.NewPassword = util.EncodePassword(form.NewPassword, query.Result.Salt)
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to change user password", err)
|
||||
return Error(500, "Failed to change user password", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User password changed")
|
||||
return Success("User password changed")
|
||||
}
|
||||
|
||||
+15
-15
@@ -55,10 +55,10 @@ func SearchPlaylists(c *m.ReqContext) Response {
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
if err != nil {
|
||||
return ApiError(500, "Search failed", err)
|
||||
return Error(500, "Search failed", err)
|
||||
}
|
||||
|
||||
return Json(200, searchQuery.Result)
|
||||
return JSON(200, searchQuery.Result)
|
||||
}
|
||||
|
||||
func GetPlaylist(c *m.ReqContext) Response {
|
||||
@@ -66,7 +66,7 @@ func GetPlaylist(c *m.ReqContext) Response {
|
||||
cmd := m.GetPlaylistByIdQuery{Id: id}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Playlist not found", err)
|
||||
return Error(500, "Playlist not found", err)
|
||||
}
|
||||
|
||||
playlistDTOs, _ := LoadPlaylistItemDTOs(id)
|
||||
@@ -79,7 +79,7 @@ func GetPlaylist(c *m.ReqContext) Response {
|
||||
Items: playlistDTOs,
|
||||
}
|
||||
|
||||
return Json(200, dto)
|
||||
return JSON(200, dto)
|
||||
}
|
||||
|
||||
func LoadPlaylistItemDTOs(id int64) ([]m.PlaylistItemDTO, error) {
|
||||
@@ -120,10 +120,10 @@ func GetPlaylistItems(c *m.ReqContext) Response {
|
||||
playlistDTOs, err := LoadPlaylistItemDTOs(id)
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Could not load playlist items", err)
|
||||
return Error(500, "Could not load playlist items", err)
|
||||
}
|
||||
|
||||
return Json(200, playlistDTOs)
|
||||
return JSON(200, playlistDTOs)
|
||||
}
|
||||
|
||||
func GetPlaylistDashboards(c *m.ReqContext) Response {
|
||||
@@ -131,10 +131,10 @@ func GetPlaylistDashboards(c *m.ReqContext) Response {
|
||||
|
||||
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistID)
|
||||
if err != nil {
|
||||
return ApiError(500, "Could not load dashboards", err)
|
||||
return Error(500, "Could not load dashboards", err)
|
||||
}
|
||||
|
||||
return Json(200, playlists)
|
||||
return JSON(200, playlists)
|
||||
}
|
||||
|
||||
func DeletePlaylist(c *m.ReqContext) Response {
|
||||
@@ -142,34 +142,34 @@ func DeletePlaylist(c *m.ReqContext) Response {
|
||||
|
||||
cmd := m.DeletePlaylistCommand{Id: id, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to delete playlist", err)
|
||||
return Error(500, "Failed to delete playlist", err)
|
||||
}
|
||||
|
||||
return Json(200, "")
|
||||
return JSON(200, "")
|
||||
}
|
||||
|
||||
func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to create playlist", err)
|
||||
return Error(500, "Failed to create playlist", err)
|
||||
}
|
||||
|
||||
return Json(200, cmd.Result)
|
||||
return JSON(200, cmd.Result)
|
||||
}
|
||||
|
||||
func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to save playlist", err)
|
||||
return Error(500, "Failed to save playlist", err)
|
||||
}
|
||||
|
||||
playlistDTOs, err := LoadPlaylistItemDTOs(cmd.Id)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to save playlist", err)
|
||||
return Error(500, "Failed to save playlist", err)
|
||||
}
|
||||
|
||||
cmd.Result.Items = playlistDTOs
|
||||
return Json(200, cmd.Result)
|
||||
return JSON(200, cmd.Result)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]int) (dtos.PlaylistDashboardsSlice, error) {
|
||||
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}
|
||||
if len(dashboardByIDs) > 0 {
|
||||
dashboardQuery := m.GetDashboardsQuery{DashboardIds: dashboardByIDs}
|
||||
if err := bus.Dispatch(&dashboardQuery); err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -26,7 +26,7 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
|
||||
Slug: item.Slug,
|
||||
Title: item.Title,
|
||||
Uri: "db/" + item.Slug,
|
||||
Order: dashboardIdOrder[item.Id],
|
||||
Order: dashboardIDOrder[item.Id],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func LoadPlaylistDashboards(orgID int64, signedInUser *m.SignedInUser, playlistI
|
||||
|
||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||
|
||||
var k, _ = populateDashboardsById(dashboardByIDs, dashboardIDOrder)
|
||||
var k, _ = populateDashboardsByID(dashboardByIDs, dashboardIDOrder)
|
||||
result = append(result, k...)
|
||||
result = append(result, populateDashboardsByTag(orgID, signedInUser, dashboardByTag, dashboardTagOrder)...)
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("data-proxy-log")
|
||||
client *http.Client = &http.Client{
|
||||
logger = log.New("data-proxy-log")
|
||||
client = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||
}
|
||||
@@ -49,14 +49,14 @@ type DataSourceProxy struct {
|
||||
}
|
||||
|
||||
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
targetURL, _ := url.Parse(ds.Url)
|
||||
|
||||
return &DataSourceProxy{
|
||||
ds: ds,
|
||||
plugin: plugin,
|
||||
ctx: ctx,
|
||||
proxyPath: proxyPath,
|
||||
targetUrl: targetUrl,
|
||||
targetUrl: targetURL,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ func (proxy *DataSourceProxy) HandleRequest() {
|
||||
span.SetTag("user_id", proxy.ctx.SignedInUser.UserId)
|
||||
span.SetTag("org_id", proxy.ctx.SignedInUser.OrgId)
|
||||
|
||||
proxy.addTraceFromHeaderValue(span, "X-Panel-Id", "panel_id")
|
||||
proxy.addTraceFromHeaderValue(span, "X-Dashboard-Id", "dashboard_id")
|
||||
|
||||
opentracing.GlobalTracer().Inject(
|
||||
span.Context(),
|
||||
opentracing.HTTPHeaders,
|
||||
@@ -98,6 +101,14 @@ func (proxy *DataSourceProxy) HandleRequest() {
|
||||
proxy.ctx.Resp.Header().Del("Set-Cookie")
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, headerName string, tagName string) {
|
||||
panelId := proxy.ctx.Req.Header.Get(headerName)
|
||||
dashId, err := strconv.Atoi(panelId)
|
||||
if err == nil {
|
||||
span.SetTag(tagName, dashId)
|
||||
}
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = proxy.targetUrl.Scheme
|
||||
@@ -279,16 +290,16 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
|
||||
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
routeUrl, err := url.Parse(proxy.route.Url)
|
||||
routeURL, err := url.Parse(proxy.route.Url)
|
||||
if err != nil {
|
||||
logger.Error("Error parsing plugin route url")
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Scheme = routeUrl.Scheme
|
||||
req.URL.Host = routeUrl.Host
|
||||
req.Host = routeUrl.Host
|
||||
req.URL.Path = util.JoinUrlFragments(routeUrl.Path, proxy.proxyPath)
|
||||
req.URL.Scheme = routeURL.Scheme
|
||||
req.URL.Host = routeURL.Host
|
||||
req.Host = routeURL.Host
|
||||
req.URL.Path = util.JoinUrlFragments(routeURL.Path, proxy.proxyPath)
|
||||
|
||||
if err := addHeaders(&req.Header, proxy.route, data); err != nil {
|
||||
logger.Error("Failed to render plugin headers", "error", err)
|
||||
@@ -320,11 +331,11 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range proxy.route.TokenAuth.Params {
|
||||
if interpolatedParam, err := interpolateString(value, data); err != nil {
|
||||
interpolatedParam, err := interpolateString(value, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
params.Add(key, interpolatedParam)
|
||||
}
|
||||
params.Add(key, interpolatedParam)
|
||||
}
|
||||
|
||||
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
|
||||
@@ -354,13 +365,13 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", errors.New(fmt.Sprintf("Could not parse template %s.", text))
|
||||
return "", fmt.Errorf("could not parse template %s", text)
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", errors.New(fmt.Sprintf("Failed to execute template %s.", text))
|
||||
return "", fmt.Errorf("failed to execute template %s", text)
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
|
||||
@@ -107,8 +107,8 @@ func TestDSRouteRule(t *testing.T) {
|
||||
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
requestURL, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestURL}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
@@ -132,8 +132,8 @@ func TestDSRouteRule(t *testing.T) {
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
requestURL, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestURL}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
@@ -162,8 +162,8 @@ func TestDSRouteRule(t *testing.T) {
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
requestURL, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestURL, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
@@ -188,8 +188,8 @@ func TestDSRouteRule(t *testing.T) {
|
||||
ctx := &m.ReqContext{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||
requestURL, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestURL, Header: make(http.Header)}
|
||||
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||
req.Header.Set("Cookie", cookies)
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ type templateData struct {
|
||||
SecureJsonData map[string]string
|
||||
}
|
||||
|
||||
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.Header, error) {
|
||||
func getHeaders(route *plugins.AppPluginRoute, orgId int64, appID string) (http.Header, error) {
|
||||
result := http.Header{}
|
||||
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appId}
|
||||
query := m.GetPluginSettingByIdQuery{OrgId: orgId, PluginId: appID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
@@ -37,16 +37,16 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
|
||||
return result, err
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
|
||||
targetUrl, _ := url.Parse(route.Url)
|
||||
func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPluginRoute, appID string) *httputil.ReverseProxy {
|
||||
targetURL, _ := url.Parse(route.Url)
|
||||
|
||||
director := func(req *http.Request) {
|
||||
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
req.URL.Host = targetUrl.Host
|
||||
req.Host = targetUrl.Host
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
req.URL.Host = targetURL.Host
|
||||
req.Host = targetURL.Host
|
||||
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
req.URL.Path = util.JoinUrlFragments(targetURL.Path, proxyPath)
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
@@ -80,7 +80,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
|
||||
req.Header.Add("X-Grafana-Context", string(ctxJson))
|
||||
|
||||
if len(route.Headers) > 0 {
|
||||
headers, err := getHeaders(route, ctx.OrgId, appId)
|
||||
headers, err := getHeaders(route, ctx.OrgId, appID)
|
||||
if err != nil {
|
||||
ctx.JsonApiErr(500, "Could not generate plugin route header", err)
|
||||
return
|
||||
|
||||
+15
-15
@@ -19,7 +19,7 @@ func GetPluginList(c *m.ReqContext) Response {
|
||||
pluginSettingsMap, err := plugins.GetPluginSettings(c.OrgId)
|
||||
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to get list of plugins", err)
|
||||
return Error(500, "Failed to get list of plugins", err)
|
||||
}
|
||||
|
||||
result := make(dtos.PluginList, 0)
|
||||
@@ -75,7 +75,7 @@ func GetPluginList(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
sort.Sort(result)
|
||||
return Json(200, result)
|
||||
return JSON(200, result)
|
||||
}
|
||||
|
||||
func GetPluginSettingByID(c *m.ReqContext) Response {
|
||||
@@ -83,7 +83,7 @@ func GetPluginSettingByID(c *m.ReqContext) Response {
|
||||
|
||||
def, exists := plugins.Plugins[pluginID]
|
||||
if !exists {
|
||||
return ApiError(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
return Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
}
|
||||
|
||||
dto := &dtos.PluginSetting{
|
||||
@@ -104,7 +104,7 @@ func GetPluginSettingByID(c *m.ReqContext) Response {
|
||||
query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err != m.ErrPluginSettingNotFound {
|
||||
return ApiError(500, "Failed to get login settings", nil)
|
||||
return Error(500, "Failed to get login settings", nil)
|
||||
}
|
||||
} else {
|
||||
dto.Enabled = query.Result.Enabled
|
||||
@@ -112,7 +112,7 @@ func GetPluginSettingByID(c *m.ReqContext) Response {
|
||||
dto.JsonData = query.Result.JsonData
|
||||
}
|
||||
|
||||
return Json(200, dto)
|
||||
return JSON(200, dto)
|
||||
}
|
||||
|
||||
func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
|
||||
@@ -122,14 +122,14 @@ func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response
|
||||
cmd.PluginId = pluginID
|
||||
|
||||
if _, ok := plugins.Apps[cmd.PluginId]; !ok {
|
||||
return ApiError(404, "Plugin not installed.", nil)
|
||||
return Error(404, "Plugin not installed.", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update plugin setting", err)
|
||||
return Error(500, "Failed to update plugin setting", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Plugin settings updated")
|
||||
return Success("Plugin settings updated")
|
||||
}
|
||||
|
||||
func GetPluginDashboards(c *m.ReqContext) Response {
|
||||
@@ -138,13 +138,13 @@ func GetPluginDashboards(c *m.ReqContext) Response {
|
||||
list, err := plugins.GetPluginDashboards(c.OrgId, pluginID)
|
||||
if err != nil {
|
||||
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
|
||||
return ApiError(404, notfound.Error(), nil)
|
||||
return Error(404, notfound.Error(), nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get plugin dashboards", err)
|
||||
return Error(500, "Failed to get plugin dashboards", err)
|
||||
}
|
||||
|
||||
return Json(200, list)
|
||||
return JSON(200, list)
|
||||
}
|
||||
|
||||
func GetPluginMarkdown(c *m.ReqContext) Response {
|
||||
@@ -154,10 +154,10 @@ func GetPluginMarkdown(c *m.ReqContext) Response {
|
||||
content, err := plugins.GetPluginMarkdown(pluginID, name)
|
||||
if err != nil {
|
||||
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
|
||||
return ApiError(404, notfound.Error(), nil)
|
||||
return Error(404, notfound.Error(), nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Could not get markdown file", err)
|
||||
return Error(500, "Could not get markdown file", err)
|
||||
}
|
||||
|
||||
resp := Respond(200, content)
|
||||
@@ -178,8 +178,8 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to import dashboard", err)
|
||||
return Error(500, "Failed to import dashboard", err)
|
||||
}
|
||||
|
||||
return Json(200, cmd.Result)
|
||||
return JSON(200, cmd.Result)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to set home dashboard", err)
|
||||
return Error(500, "Failed to set home dashboard", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Home dashboard set")
|
||||
return Success("Home dashboard set")
|
||||
}
|
||||
|
||||
// GET /api/user/preferences
|
||||
@@ -28,16 +28,16 @@ func getPreferencesFor(orgID int64, userID int64) Response {
|
||||
prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID}
|
||||
|
||||
if err := bus.Dispatch(&prefsQuery); err != nil {
|
||||
return ApiError(500, "Failed to get preferences", err)
|
||||
return Error(500, "Failed to get preferences", err)
|
||||
}
|
||||
|
||||
dto := dtos.Prefs{
|
||||
Theme: prefsQuery.Result.Theme,
|
||||
HomeDashboardId: prefsQuery.Result.HomeDashboardId,
|
||||
HomeDashboardID: prefsQuery.Result.HomeDashboardId,
|
||||
Timezone: prefsQuery.Result.Timezone,
|
||||
}
|
||||
|
||||
return Json(200, &dto)
|
||||
return JSON(200, &dto)
|
||||
}
|
||||
|
||||
// PUT /api/user/preferences
|
||||
@@ -51,14 +51,14 @@ func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd
|
||||
OrgId: orgID,
|
||||
Theme: dtoCmd.Theme,
|
||||
Timezone: dtoCmd.Timezone,
|
||||
HomeDashboardId: dtoCmd.HomeDashboardId,
|
||||
HomeDashboardId: dtoCmd.HomeDashboardID,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||
return ApiError(500, "Failed to save preferences", err)
|
||||
return Error(500, "Failed to save preferences", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Preferences updated")
|
||||
return Success("Preferences updated")
|
||||
}
|
||||
|
||||
// GET /api/org/preferences
|
||||
|
||||
+14
-14
@@ -8,60 +8,60 @@ import (
|
||||
|
||||
func GetOrgQuotas(c *m.ReqContext) Response {
|
||||
if !setting.Quota.Enabled {
|
||||
return ApiError(404, "Quotas not enabled", nil)
|
||||
return Error(404, "Quotas not enabled", nil)
|
||||
}
|
||||
query := m.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get org quotas", err)
|
||||
return Error(500, "Failed to get org quotas", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func UpdateOrgQuota(c *m.ReqContext, cmd m.UpdateOrgQuotaCmd) Response {
|
||||
if !setting.Quota.Enabled {
|
||||
return ApiError(404, "Quotas not enabled", nil)
|
||||
return Error(404, "Quotas not enabled", nil)
|
||||
}
|
||||
cmd.OrgId = c.ParamsInt64(":orgId")
|
||||
cmd.Target = c.Params(":target")
|
||||
|
||||
if _, ok := setting.Quota.Org.ToMap()[cmd.Target]; !ok {
|
||||
return ApiError(404, "Invalid quota target", nil)
|
||||
return Error(404, "Invalid quota target", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update org quotas", err)
|
||||
return Error(500, "Failed to update org quotas", err)
|
||||
}
|
||||
return ApiSuccess("Organization quota updated")
|
||||
return Success("Organization quota updated")
|
||||
}
|
||||
|
||||
func GetUserQuotas(c *m.ReqContext) Response {
|
||||
if !setting.Quota.Enabled {
|
||||
return ApiError(404, "Quotas not enabled", nil)
|
||||
return Error(404, "Quotas not enabled", nil)
|
||||
}
|
||||
query := m.GetUserQuotasQuery{UserId: c.ParamsInt64(":id")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get org quotas", err)
|
||||
return Error(500, "Failed to get org quotas", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func UpdateUserQuota(c *m.ReqContext, cmd m.UpdateUserQuotaCmd) Response {
|
||||
if !setting.Quota.Enabled {
|
||||
return ApiError(404, "Quotas not enabled", nil)
|
||||
return Error(404, "Quotas not enabled", nil)
|
||||
}
|
||||
cmd.UserId = c.ParamsInt64(":id")
|
||||
cmd.Target = c.Params(":target")
|
||||
|
||||
if _, ok := setting.Quota.User.ToMap()[cmd.Target]; !ok {
|
||||
return ApiError(404, "Invalid quota target", nil)
|
||||
return Error(404, "Invalid quota target", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update org quotas", err)
|
||||
return Error(500, "Failed to update org quotas", err)
|
||||
}
|
||||
return ApiSuccess("Organization quota updated")
|
||||
return Success("Organization quota updated")
|
||||
}
|
||||
|
||||
+13
-13
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// GET /api/user/signup/options
|
||||
func GetSignUpOptions(c *m.ReqContext) Response {
|
||||
return Json(200, util.DynMap{
|
||||
return JSON(200, util.DynMap{
|
||||
"verifyEmailEnabled": setting.VerifyEmailEnabled,
|
||||
"autoAssignOrg": setting.AutoAssignOrg,
|
||||
})
|
||||
@@ -21,12 +21,12 @@ func GetSignUpOptions(c *m.ReqContext) Response {
|
||||
// POST /api/user/signup
|
||||
func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
|
||||
if !setting.AllowUserSignUp {
|
||||
return ApiError(401, "User signup is disabled", nil)
|
||||
return Error(401, "User signup is disabled", nil)
|
||||
}
|
||||
|
||||
existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
|
||||
if err := bus.Dispatch(&existing); err == nil {
|
||||
return ApiError(422, "User with same email address already exists", nil)
|
||||
return Error(422, "User with same email address already exists", nil)
|
||||
}
|
||||
|
||||
cmd := m.CreateTempUserCommand{}
|
||||
@@ -38,7 +38,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
|
||||
cmd.RemoteAddr = c.Req.RemoteAddr
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to create signup", err)
|
||||
return Error(500, "Failed to create signup", err)
|
||||
}
|
||||
|
||||
bus.Publish(&events.SignUpStarted{
|
||||
@@ -48,12 +48,12 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
|
||||
|
||||
metrics.M_Api_User_SignUpStarted.Inc()
|
||||
|
||||
return Json(200, util.DynMap{"status": "SignUpCreated"})
|
||||
return JSON(200, util.DynMap{"status": "SignUpCreated"})
|
||||
}
|
||||
|
||||
func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
|
||||
if !setting.AllowUserSignUp {
|
||||
return ApiError(401, "User signup is disabled", nil)
|
||||
return Error(401, "User signup is disabled", nil)
|
||||
}
|
||||
|
||||
createUserCmd := m.CreateUserCommand{
|
||||
@@ -75,12 +75,12 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
|
||||
// check if user exists
|
||||
existing := m.GetUserByLoginQuery{LoginOrEmail: form.Email}
|
||||
if err := bus.Dispatch(&existing); err == nil {
|
||||
return ApiError(401, "User with same email address already exists", nil)
|
||||
return Error(401, "User with same email address already exists", nil)
|
||||
}
|
||||
|
||||
// dispatch create command
|
||||
if err := bus.Dispatch(&createUserCmd); err != nil {
|
||||
return ApiError(500, "Failed to create user", err)
|
||||
return Error(500, "Failed to create user", err)
|
||||
}
|
||||
|
||||
// publish signup event
|
||||
@@ -98,7 +98,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
|
||||
// check for pending invites
|
||||
invitesQuery := m.GetTempUsersQuery{Email: form.Email, Status: m.TmpUserInvitePending}
|
||||
if err := bus.Dispatch(&invitesQuery); err != nil {
|
||||
return ApiError(500, "Failed to query database for invites", err)
|
||||
return Error(500, "Failed to query database for invites", err)
|
||||
}
|
||||
|
||||
apiResponse := util.DynMap{"message": "User sign up completed successfully", "code": "redirect-to-landing-page"}
|
||||
@@ -112,7 +112,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
|
||||
loginUserWithUser(user, c)
|
||||
metrics.M_Api_User_SignUpCompleted.Inc()
|
||||
|
||||
return Json(200, apiResponse)
|
||||
return JSON(200, apiResponse)
|
||||
}
|
||||
|
||||
func verifyUserSignUpEmail(email string, code string) (bool, Response) {
|
||||
@@ -120,14 +120,14 @@ func verifyUserSignUpEmail(email string, code string) (bool, Response) {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTempUserNotFound {
|
||||
return false, ApiError(404, "Invalid email verification code", nil)
|
||||
return false, Error(404, "Invalid email verification code", nil)
|
||||
}
|
||||
return false, ApiError(500, "Failed to read temp user", err)
|
||||
return false, Error(500, "Failed to read temp user", err)
|
||||
}
|
||||
|
||||
tempUser := query.Result
|
||||
if tempUser.Email != email {
|
||||
return false, ApiError(404, "Email verification code does not match email", nil)
|
||||
return false, Error(404, "Email verification code does not match email", nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
+7
-7
@@ -7,20 +7,20 @@ import (
|
||||
|
||||
func StarDashboard(c *m.ReqContext) Response {
|
||||
if !c.IsSignedIn {
|
||||
return ApiError(412, "You need to sign in to star dashboards", nil)
|
||||
return Error(412, "You need to sign in to star dashboards", nil)
|
||||
}
|
||||
|
||||
cmd := m.StarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
|
||||
|
||||
if cmd.DashboardId <= 0 {
|
||||
return ApiError(400, "Missing dashboard id", nil)
|
||||
return Error(400, "Missing dashboard id", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to star dashboard", err)
|
||||
return Error(500, "Failed to star dashboard", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Dashboard starred!")
|
||||
return Success("Dashboard starred!")
|
||||
}
|
||||
|
||||
func UnstarDashboard(c *m.ReqContext) Response {
|
||||
@@ -28,12 +28,12 @@ func UnstarDashboard(c *m.ReqContext) Response {
|
||||
cmd := m.UnstarDashboardCommand{UserId: c.UserId, DashboardId: c.ParamsInt64(":id")}
|
||||
|
||||
if cmd.DashboardId <= 0 {
|
||||
return ApiError(400, "Missing dashboard id", nil)
|
||||
return Error(400, "Missing dashboard id", nil)
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to unstar dashboard", err)
|
||||
return Error(500, "Failed to unstar dashboard", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Dashboard unstarred")
|
||||
return Success("Dashboard unstarred")
|
||||
}
|
||||
|
||||
+14
-14
@@ -12,12 +12,12 @@ func CreateTeam(c *m.ReqContext, cmd m.CreateTeamCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
return ApiError(409, "Team name taken", err)
|
||||
return Error(409, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to create Team", err)
|
||||
return Error(500, "Failed to create Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
return JSON(200, &util.DynMap{
|
||||
"teamId": cmd.Result.Id,
|
||||
"message": "Team created",
|
||||
})
|
||||
@@ -29,23 +29,23 @@ func UpdateTeam(c *m.ReqContext, cmd m.UpdateTeamCommand) Response {
|
||||
cmd.Id = c.ParamsInt64(":teamId")
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNameTaken {
|
||||
return ApiError(400, "Team name taken", err)
|
||||
return Error(400, "Team name taken", err)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
return Error(500, "Failed to update Team", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Team updated")
|
||||
return Success("Team updated")
|
||||
}
|
||||
|
||||
// DELETE /api/teams/:teamId
|
||||
func DeleteTeamByID(c *m.ReqContext) Response {
|
||||
if err := bus.Dispatch(&m.DeleteTeamCommand{OrgId: c.OrgId, Id: c.ParamsInt64(":teamId")}); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Failed to delete Team. ID not found", nil)
|
||||
return Error(404, "Failed to delete Team. ID not found", nil)
|
||||
}
|
||||
return ApiError(500, "Failed to update Team", err)
|
||||
return Error(500, "Failed to update Team", err)
|
||||
}
|
||||
return ApiSuccess("Team deleted")
|
||||
return Success("Team deleted")
|
||||
}
|
||||
|
||||
// GET /api/teams/search
|
||||
@@ -68,7 +68,7 @@ func SearchTeams(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to search Teams", err)
|
||||
return Error(500, "Failed to search Teams", err)
|
||||
}
|
||||
|
||||
for _, team := range query.Result.Teams {
|
||||
@@ -78,7 +78,7 @@ func SearchTeams(c *m.ReqContext) Response {
|
||||
query.Result.Page = page
|
||||
query.Result.PerPage = perPage
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
@@ -87,11 +87,11 @@ func GetTeamByID(c *m.ReqContext) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Team not found", err)
|
||||
return Error(404, "Team not found", err)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to get Team", err)
|
||||
return Error(500, "Failed to get Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &query.Result)
|
||||
return JSON(200, &query.Result)
|
||||
}
|
||||
|
||||
+10
-10
@@ -12,14 +12,14 @@ func GetTeamMembers(c *m.ReqContext) Response {
|
||||
query := m.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get Team Members", err)
|
||||
return Error(500, "Failed to get Team Members", err)
|
||||
}
|
||||
|
||||
for _, member := range query.Result {
|
||||
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// POST /api/teams/:teamId/members
|
||||
@@ -29,17 +29,17 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Team not found", nil)
|
||||
return Error(404, "Team not found", nil)
|
||||
}
|
||||
|
||||
if err == m.ErrTeamMemberAlreadyAdded {
|
||||
return ApiError(400, "User is already added to this team", nil)
|
||||
return Error(400, "User is already added to this team", nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to add Member to Team", err)
|
||||
return Error(500, "Failed to add Member to Team", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{
|
||||
return JSON(200, &util.DynMap{
|
||||
"message": "Member added to Team",
|
||||
})
|
||||
}
|
||||
@@ -48,14 +48,14 @@ func AddTeamMember(c *m.ReqContext, cmd m.AddTeamMemberCommand) Response {
|
||||
func RemoveTeamMember(c *m.ReqContext) Response {
|
||||
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
||||
if err == m.ErrTeamNotFound {
|
||||
return ApiError(404, "Team not found", nil)
|
||||
return Error(404, "Team not found", nil)
|
||||
}
|
||||
|
||||
if err == m.ErrTeamMemberNotFound {
|
||||
return ApiError(404, "Team member not found", nil)
|
||||
return Error(404, "Team member not found", nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Failed to remove Member from Team", err)
|
||||
return Error(500, "Failed to remove Member from Team", err)
|
||||
}
|
||||
return ApiSuccess("Team Member removed")
|
||||
return Success("Team Member removed")
|
||||
}
|
||||
|
||||
+33
-33
@@ -23,12 +23,12 @@ func getUserUserProfile(userID int64) Response {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrUserNotFound {
|
||||
return ApiError(404, m.ErrUserNotFound.Error(), nil)
|
||||
return Error(404, m.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get user", err)
|
||||
return Error(500, "Failed to get user", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// GET /api/users/lookup
|
||||
@@ -36,9 +36,9 @@ func GetUserByLoginOrEmail(c *m.ReqContext) Response {
|
||||
query := m.GetUserByLoginQuery{LoginOrEmail: c.Query("loginOrEmail")}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
if err == m.ErrUserNotFound {
|
||||
return ApiError(404, m.ErrUserNotFound.Error(), nil)
|
||||
return Error(404, m.ErrUserNotFound.Error(), nil)
|
||||
}
|
||||
return ApiError(500, "Failed to get user", err)
|
||||
return Error(500, "Failed to get user", err)
|
||||
}
|
||||
user := query.Result
|
||||
result := m.UserProfileDTO{
|
||||
@@ -50,17 +50,17 @@ func GetUserByLoginOrEmail(c *m.ReqContext) Response {
|
||||
IsGrafanaAdmin: user.IsAdmin,
|
||||
OrgId: user.OrgId,
|
||||
}
|
||||
return Json(200, &result)
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
// POST /api/user
|
||||
func UpdateSignedInUser(c *m.ReqContext, cmd m.UpdateUserCommand) Response {
|
||||
if setting.AuthProxyEnabled {
|
||||
if setting.AuthProxyHeaderProperty == "email" && cmd.Email != c.Email {
|
||||
return ApiError(400, "Not allowed to change email when auth proxy is using email property", nil)
|
||||
return Error(400, "Not allowed to change email when auth proxy is using email property", nil)
|
||||
}
|
||||
if setting.AuthProxyHeaderProperty == "username" && cmd.Login != c.Login {
|
||||
return ApiError(400, "Not allowed to change username when auth proxy is using username property", nil)
|
||||
return Error(400, "Not allowed to change username when auth proxy is using username property", nil)
|
||||
}
|
||||
}
|
||||
cmd.UserId = c.UserId
|
||||
@@ -79,31 +79,31 @@ func UpdateUserActiveOrg(c *m.ReqContext) Response {
|
||||
orgID := c.ParamsInt64(":orgId")
|
||||
|
||||
if !validateUsingOrg(userID, orgID) {
|
||||
return ApiError(401, "Not a valid organization", nil)
|
||||
return Error(401, "Not a valid organization", nil)
|
||||
}
|
||||
|
||||
cmd := m.SetUsingOrgCommand{UserId: userID, OrgId: orgID}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to change active organization", err)
|
||||
return Error(500, "Failed to change active organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Active organization changed")
|
||||
return Success("Active organization changed")
|
||||
}
|
||||
|
||||
func handleUpdateUser(cmd m.UpdateUserCommand) Response {
|
||||
if len(cmd.Login) == 0 {
|
||||
cmd.Login = cmd.Email
|
||||
if len(cmd.Login) == 0 {
|
||||
return ApiError(400, "Validation error, need to specify either username or email", nil)
|
||||
return Error(400, "Validation error, need to specify either username or email", nil)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update user", err)
|
||||
return Error(500, "Failed to update user", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User updated")
|
||||
return Success("User updated")
|
||||
}
|
||||
|
||||
// GET /api/user/orgs
|
||||
@@ -120,10 +120,10 @@ func getUserOrgList(userID int64) Response {
|
||||
query := m.GetUserOrgListQuery{UserId: userID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return ApiError(500, "Failed to get user organizations", err)
|
||||
return Error(500, "Failed to get user organizations", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func validateUsingOrg(userID int64, orgID int64) bool {
|
||||
@@ -149,16 +149,16 @@ func UserSetUsingOrg(c *m.ReqContext) Response {
|
||||
orgID := c.ParamsInt64(":id")
|
||||
|
||||
if !validateUsingOrg(c.UserId, orgID) {
|
||||
return ApiError(401, "Not a valid organization", nil)
|
||||
return Error(401, "Not a valid organization", nil)
|
||||
}
|
||||
|
||||
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to change active organization", err)
|
||||
return Error(500, "Failed to change active organization", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("Active organization changed")
|
||||
return Success("Active organization changed")
|
||||
}
|
||||
|
||||
// GET /profile/switch-org/:id
|
||||
@@ -180,53 +180,53 @@ func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
|
||||
|
||||
func ChangeUserPassword(c *m.ReqContext, cmd m.ChangeUserPasswordCommand) Response {
|
||||
if setting.LdapEnabled || setting.AuthProxyEnabled {
|
||||
return ApiError(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
|
||||
return Error(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil)
|
||||
}
|
||||
|
||||
userQuery := m.GetUserByIdQuery{Id: c.UserId}
|
||||
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
return ApiError(500, "Could not read user from database", err)
|
||||
return Error(500, "Could not read user from database", err)
|
||||
}
|
||||
|
||||
passwordHashed := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt)
|
||||
if passwordHashed != userQuery.Result.Password {
|
||||
return ApiError(401, "Invalid old password", nil)
|
||||
return Error(401, "Invalid old password", nil)
|
||||
}
|
||||
|
||||
password := m.Password(cmd.NewPassword)
|
||||
if password.IsWeak() {
|
||||
return ApiError(400, "New password is too short", nil)
|
||||
return Error(400, "New password is too short", nil)
|
||||
}
|
||||
|
||||
cmd.UserId = c.UserId
|
||||
cmd.NewPassword = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt)
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to change user password", err)
|
||||
return Error(500, "Failed to change user password", err)
|
||||
}
|
||||
|
||||
return ApiSuccess("User password changed")
|
||||
return Success("User password changed")
|
||||
}
|
||||
|
||||
// GET /api/users
|
||||
func SearchUsers(c *m.ReqContext) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
return Error(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result.Users)
|
||||
return JSON(200, query.Result.Users)
|
||||
}
|
||||
|
||||
// GET /api/users/search
|
||||
func SearchUsersWithPaging(c *m.ReqContext) Response {
|
||||
query, err := searchUser(c)
|
||||
if err != nil {
|
||||
return ApiError(500, "Failed to fetch users", err)
|
||||
return Error(500, "Failed to fetch users", err)
|
||||
}
|
||||
|
||||
return Json(200, query.Result)
|
||||
return JSON(200, query.Result)
|
||||
}
|
||||
|
||||
func searchUser(c *m.ReqContext) (*m.SearchUsersQuery, error) {
|
||||
@@ -269,10 +269,10 @@ func SetHelpFlag(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update help flag", err)
|
||||
return Error(500, "Failed to update help flag", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
|
||||
return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
|
||||
}
|
||||
|
||||
func ClearHelpFlags(c *m.ReqContext) Response {
|
||||
@@ -282,8 +282,8 @@ func ClearHelpFlags(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return ApiError(500, "Failed to update help flag", err)
|
||||
return Error(500, "Failed to update help flag", err)
|
||||
}
|
||||
|
||||
return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
|
||||
return JSON(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1})
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ type GrafanaServerImpl struct {
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
|
||||
httpServer *api.HttpServer
|
||||
httpServer *api.HTTPServer
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() error {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
|
||||
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
if !setting.AuthProxyEnabled {
|
||||
return false
|
||||
}
|
||||
@@ -31,31 +31,31 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
|
||||
}
|
||||
|
||||
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
|
||||
query.OrgId = orgId
|
||||
query.OrgId = orgID
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
if err != m.ErrUserNotFound {
|
||||
ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
|
||||
if setting.AuthProxyAutoSignUp {
|
||||
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
|
||||
if setting.LdapEnabled {
|
||||
cmd.SkipOrgSetup = true
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgId}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
ctx.Handle(500, "Failed find user after creation", err)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if !setting.AuthProxyAutoSignUp {
|
||||
return false
|
||||
}
|
||||
|
||||
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
|
||||
if setting.LdapEnabled {
|
||||
cmd.SkipOrgSetup = true
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
ctx.Handle(500, "Failed find user after creation", err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// initialize session
|
||||
@@ -96,50 +96,53 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgId int64) bool {
|
||||
}
|
||||
|
||||
var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
|
||||
if setting.LdapEnabled {
|
||||
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
||||
if !setting.LdapEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lastLdapSync int64
|
||||
if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
|
||||
lastLdapSync = lastLdapSyncInSession.(int64)
|
||||
}
|
||||
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
||||
|
||||
if lastLdapSync < expireEpoch {
|
||||
ldapCfg := login.LdapCfg
|
||||
var lastLdapSync int64
|
||||
if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
|
||||
lastLdapSync = lastLdapSyncInSession.(int64)
|
||||
}
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
author := login.NewLdapAuthenticator(server)
|
||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
if lastLdapSync < expireEpoch {
|
||||
ldapCfg := login.LdapCfg
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
author := login.NewLdapAuthenticator(server)
|
||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
|
||||
}
|
||||
|
||||
ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
|
||||
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) > 0 {
|
||||
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
||||
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
|
||||
sourceIP := remoteAddrSplit[0]
|
||||
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
|
||||
return nil
|
||||
}
|
||||
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
||||
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
|
||||
sourceIP := remoteAddrSplit[0]
|
||||
|
||||
found := false
|
||||
for _, proxyIP := range proxies {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
found := false
|
||||
for _, proxyIP := range proxies {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
|
||||
err := errors.New(msg)
|
||||
return err
|
||||
}
|
||||
if !found {
|
||||
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
|
||||
err := errors.New(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"gopkg.in/macaron.v1"
|
||||
)
|
||||
|
||||
func getDashboardUrlBySlug(orgId int64, slug string) (string, error) {
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: orgId}
|
||||
func getDashboardURLBySlug(orgID int64, slug string) (string, error) {
|
||||
query := m.GetDashboardQuery{Slug: slug, OrgId: orgID}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return "", m.ErrDashboardNotFound
|
||||
@@ -25,7 +25,7 @@ func RedirectFromLegacyDashboardURL() macaron.Handler {
|
||||
slug := c.Params("slug")
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil {
|
||||
url = fmt.Sprintf("%s?%s", url, c.Req.URL.RawQuery)
|
||||
c.Redirect(url, 301)
|
||||
return
|
||||
@@ -34,13 +34,13 @@ func RedirectFromLegacyDashboardURL() macaron.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func RedirectFromLegacyDashboardSoloUrl() macaron.Handler {
|
||||
func RedirectFromLegacyDashboardSoloURL() macaron.Handler {
|
||||
return func(c *m.ReqContext) {
|
||||
slug := c.Params("slug")
|
||||
renderRequest := c.QueryBool("render")
|
||||
|
||||
if slug != "" {
|
||||
if url, err := getDashboardUrlBySlug(c.OrgId, slug); err == nil {
|
||||
if url, err := getDashboardURLBySlug(c.OrgId, slug); err == nil {
|
||||
if renderRequest && strings.Contains(url, setting.AppSubUrl) {
|
||||
url = strings.Replace(url, setting.AppSubUrl, "", 1)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
|
||||
Convey("Given the dashboard redirect middleware", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
redirectFromLegacyDashboardUrl := RedirectFromLegacyDashboardURL()
|
||||
redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloUrl()
|
||||
redirectFromLegacyDashboardSoloUrl := RedirectFromLegacyDashboardSoloURL()
|
||||
|
||||
fakeDash := m.NewDashboard("Child dash")
|
||||
fakeDash.Id = 1
|
||||
|
||||
@@ -19,16 +19,16 @@ func initContextWithRenderAuth(ctx *m.ReqContext) bool {
|
||||
renderKeysLock.Lock()
|
||||
defer renderKeysLock.Unlock()
|
||||
|
||||
if renderUser, exists := renderKeys[key]; !exists {
|
||||
renderUser, exists := renderKeys[key]
|
||||
if !exists {
|
||||
ctx.JsonApiErr(401, "Invalid Render Key", nil)
|
||||
return true
|
||||
} else {
|
||||
|
||||
ctx.IsSignedIn = true
|
||||
ctx.SignedInUser = renderUser
|
||||
ctx.IsRenderCall = true
|
||||
return true
|
||||
}
|
||||
|
||||
ctx.IsSignedIn = true
|
||||
ctx.SignedInUser = renderUser
|
||||
ctx.IsRenderCall = true
|
||||
return true
|
||||
}
|
||||
|
||||
type renderContextFunc func(key string) (string, error)
|
||||
|
||||
@@ -86,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
case <-grafanaCtx.Done():
|
||||
return dispatcherGroup.Wait()
|
||||
case job := <-e.execQueue:
|
||||
dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
|
||||
dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
unfinishedWorkTimeout time.Duration = time.Second * 5
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
alertMaxAttempts int = 3
|
||||
)
|
||||
|
||||
func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
}
|
||||
}()
|
||||
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
attemptChan := make(chan int, 1)
|
||||
|
||||
// Initialize with first attemptID=1
|
||||
attemptChan <- 1
|
||||
job.Running = true
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-grafanaCtx.Done():
|
||||
// In case grafana server context is cancel, let a chance to job processing
|
||||
// to finish gracefully - by waiting a timeout duration - before forcing its end.
|
||||
unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout)
|
||||
select {
|
||||
case <-unfinishedWorkTimer.C:
|
||||
return e.endJob(grafanaCtx.Err(), cancelChan, job)
|
||||
case <-attemptChan:
|
||||
return e.endJob(nil, cancelChan, job)
|
||||
}
|
||||
case attemptID, more := <-attemptChan:
|
||||
if !more {
|
||||
return e.endJob(nil, cancelChan, job)
|
||||
}
|
||||
go e.processJob(attemptID, attemptChan, cancelChan, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
job.Running = false
|
||||
close(cancelChan)
|
||||
for cancelFn := range cancelChan {
|
||||
cancelFn()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
@@ -104,14 +150,13 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
}()
|
||||
|
||||
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
|
||||
cancelChan <- cancelFn
|
||||
span := opentracing.StartSpan("alert execution")
|
||||
alertCtx = opentracing.ContextWithSpan(alertCtx, span)
|
||||
|
||||
job.Running = true
|
||||
evalContext := NewEvalContext(alertCtx, job.Rule)
|
||||
evalContext.Ctx = alertCtx
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@@ -122,43 +167,36 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
|
||||
tlog.String("message", "failed to execute alert rule. panic was recovered."),
|
||||
)
|
||||
span.Finish()
|
||||
close(done)
|
||||
close(attemptChan)
|
||||
}
|
||||
}()
|
||||
|
||||
e.evalHandler.Eval(evalContext)
|
||||
e.resultHandler.Handle(evalContext)
|
||||
|
||||
span.SetTag("alertId", evalContext.Rule.Id)
|
||||
span.SetTag("dashboardId", evalContext.Rule.DashboardId)
|
||||
span.SetTag("firing", evalContext.Firing)
|
||||
span.SetTag("nodatapoints", evalContext.NoDataFound)
|
||||
span.SetTag("attemptID", attemptID)
|
||||
|
||||
if evalContext.Error != nil {
|
||||
ext.Error.Set(span, true)
|
||||
span.LogFields(
|
||||
tlog.Error(evalContext.Error),
|
||||
tlog.String("message", "alerting execution failed"),
|
||||
tlog.String("message", "alerting execution attempt failed"),
|
||||
)
|
||||
if attemptID < alertMaxAttempts {
|
||||
span.Finish()
|
||||
e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
|
||||
attemptChan <- (attemptID + 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
evalContext.Rule.State = evalContext.GetNewState()
|
||||
e.resultHandler.Handle(evalContext)
|
||||
span.Finish()
|
||||
close(done)
|
||||
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
|
||||
close(attemptChan)
|
||||
}()
|
||||
|
||||
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,118 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type FakeEvalHandler struct {
|
||||
SuccessCallID int // 0 means never sucess
|
||||
CallNb int
|
||||
}
|
||||
|
||||
func NewFakeEvalHandler(successCallID int) *FakeEvalHandler {
|
||||
return &FakeEvalHandler{
|
||||
SuccessCallID: successCallID,
|
||||
CallNb: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) {
|
||||
handler.CallNb++
|
||||
if handler.CallNb != handler.SuccessCallID {
|
||||
evalContext.Error = errors.New("Fake evaluation failure")
|
||||
}
|
||||
}
|
||||
|
||||
type FakeResultHandler struct{}
|
||||
|
||||
func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestEngineProcessJob(t *testing.T) {
|
||||
Convey("Alerting engine job processing", t, func() {
|
||||
engine := NewEngine()
|
||||
engine.resultHandler = &FakeResultHandler{}
|
||||
job := &Job{Running: true, Rule: &Rule{}}
|
||||
|
||||
Convey("Should trigger retry if needed", func() {
|
||||
|
||||
Convey("error + not last attempt -> retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
|
||||
for i := 1; i < alertMaxAttempts; i++ {
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(i, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, i+1)
|
||||
So(more, ShouldEqual, true)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("error + last attempt -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(0)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("no error -> no retry", func() {
|
||||
engine.evalHandler = NewFakeEvalHandler(1)
|
||||
attemptChan := make(chan int, 1)
|
||||
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
|
||||
|
||||
engine.processJob(1, attemptChan, cancelChan, job)
|
||||
nextAttemptID, more := <-attemptChan
|
||||
|
||||
So(nextAttemptID, ShouldEqual, 0)
|
||||
So(more, ShouldEqual, false)
|
||||
So(<-cancelChan, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
|
||||
Convey("never sucess -> max retries number", func() {
|
||||
expectedAttempts := alertMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("always sucess -> never retry", func() {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("some errors before sucess -> some retries", func() {
|
||||
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
engine.processJobWithRetry(context.TODO(), job)
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -112,3 +112,34 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
if c.Error != nil {
|
||||
c.log.Error("Alert Rule Result Error",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
"error", c.Error,
|
||||
"changing state to", c.Rule.ExecutionErrorState.ToAlertState())
|
||||
|
||||
if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.ExecutionErrorState.ToAlertState()
|
||||
|
||||
} else if c.Firing {
|
||||
return m.AlertStateAlerting
|
||||
|
||||
} else if c.NoDataFound {
|
||||
c.log.Info("Alert Rule returned no data",
|
||||
"ruleId", c.Rule.Id,
|
||||
"name", c.Rule.Name,
|
||||
"changing state to", c.Rule.NoDataState.ToAlertState())
|
||||
|
||||
if c.Rule.NoDataState == m.NoDataKeepState {
|
||||
return c.PrevAlertState
|
||||
}
|
||||
return c.Rule.NoDataState.ToAlertState()
|
||||
}
|
||||
|
||||
return m.AlertStateOK
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@@ -12,7 +13,7 @@ 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("Should update alert state when needed", func() {
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
@@ -28,5 +29,71 @@ func TestAlertingEvalContext(t *testing.T) {
|
||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should compute and replace properly new rule state", func() {
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = ctx.GetNewState()
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type DefaultEvalHandler struct {
|
||||
@@ -66,40 +65,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
context.Firing = firing
|
||||
context.NoDataFound = noDataFound
|
||||
context.EndTime = time.Now()
|
||||
context.Rule.State = e.getNewState(context)
|
||||
|
||||
elapsedTime := context.EndTime.Sub(context.StartTime).Nanoseconds() / int64(time.Millisecond)
|
||||
metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime))
|
||||
}
|
||||
|
||||
// This should be move into evalContext once its been refactored. (Carl Bergquist)
|
||||
func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
|
||||
if evalContext.Error != nil {
|
||||
handler.log.Error("Alert Rule Result Error",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"error", evalContext.Error,
|
||||
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.ExecutionErrorState.ToAlertState()
|
||||
}
|
||||
} else if evalContext.Firing {
|
||||
return models.AlertStateAlerting
|
||||
} else if evalContext.NoDataFound {
|
||||
handler.log.Info("Alert Rule returned no data",
|
||||
"ruleId", evalContext.Rule.Id,
|
||||
"name", evalContext.Rule.Name,
|
||||
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
|
||||
|
||||
if evalContext.Rule.NoDataState == models.NoDataKeepState {
|
||||
return evalContext.PrevAlertState
|
||||
} else {
|
||||
return evalContext.Rule.NoDataState.ToAlertState()
|
||||
}
|
||||
}
|
||||
|
||||
return models.AlertStateOK
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package alerting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@@ -203,73 +201,5 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
||||
handler.Eval(context)
|
||||
So(context.NoDataFound, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("EvalHandler can replace alert state based for errors and no_data", func() {
|
||||
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
|
||||
dummieError := fmt.Errorf("dummie error")
|
||||
Convey("Should update alert state", func() {
|
||||
|
||||
Convey("ok -> alerting", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Firing = true
|
||||
|
||||
So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> error(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Error = dummieError
|
||||
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(alerting)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataSetAlerting
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
|
||||
})
|
||||
|
||||
Convey("ok -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStateOK
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
|
||||
})
|
||||
|
||||
Convey("pending -> no_data(keep_last)", func() {
|
||||
ctx.PrevAlertState = models.AlertStatePending
|
||||
ctx.Rule.NoDataState = models.NoDataKeepState
|
||||
ctx.NoDataFound = true
|
||||
|
||||
ctx.Rule.State = handler.getNewState(ctx)
|
||||
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext {
|
||||
context.IsTestRun = true
|
||||
|
||||
handler.Eval(context)
|
||||
context.Rule.State = context.GetNewState()
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
// FolderService service for operating on folders
|
||||
type FolderService interface {
|
||||
GetFolders(limit int) ([]*models.Folder, error)
|
||||
GetFolderById(id int64) (*models.Folder, error)
|
||||
GetFolderByUid(uid string) (*models.Folder, error)
|
||||
GetFolderByID(id int64) (*models.Folder, error)
|
||||
GetFolderByUID(uid string) (*models.Folder, error)
|
||||
CreateFolder(cmd *models.CreateFolderCommand) error
|
||||
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
|
||||
DeleteFolder(uid string) (*models.Folder, error)
|
||||
@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error)
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error) {
|
||||
func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
|
||||
dashFolder, err := getFolder(query)
|
||||
|
||||
@@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) GetFolderById(id int64) (*models.Folder, error)
|
||||
return dashToFolder(dashFolder), nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetFolderByUid(uid string) (*models.Folder, error) {
|
||||
func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, error) {
|
||||
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
|
||||
dashFolder, err := getFolder(query)
|
||||
|
||||
|
||||
@@ -36,13 +36,13 @@ func TestFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When get folder by id should return access denied error", func() {
|
||||
_, err := service.GetFolderById(1)
|
||||
_, err := service.GetFolderByID(1)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
|
||||
Convey("When get folder by uid should return access denied error", func() {
|
||||
_, err := service.GetFolderByUid("uid")
|
||||
_, err := service.GetFolderByUID("uid")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrFolderAccessDenied)
|
||||
})
|
||||
@@ -147,14 +147,14 @@ func TestFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When get folder by id should return folder", func() {
|
||||
f, _ := service.GetFolderById(1)
|
||||
f, _ := service.GetFolderByID(1)
|
||||
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||
})
|
||||
|
||||
Convey("When get folder by uid should return folder", func() {
|
||||
f, _ := service.GetFolderByUid("uid")
|
||||
f, _ := service.GetFolderByUID("uid")
|
||||
So(f.Id, ShouldEqual, dashFolder.Id)
|
||||
So(f.Uid, ShouldEqual, dashFolder.Uid)
|
||||
So(f.Title, ShouldEqual, dashFolder.Title)
|
||||
|
||||
@@ -28,12 +28,9 @@ func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint,
|
||||
return &GraphiteExecutor{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
glog log.Logger
|
||||
)
|
||||
var glog = log.New("tsdb.graphite")
|
||||
|
||||
func init() {
|
||||
glog = log.New("tsdb.graphite")
|
||||
tsdb.RegisterTsdbQueryEndpoint("graphite", NewGraphiteExecutor)
|
||||
}
|
||||
|
||||
@@ -52,6 +49,7 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
|
||||
}
|
||||
|
||||
for _, query := range tsdbQuery.Queries {
|
||||
glog.Info("graphite", "query", query.Model)
|
||||
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
|
||||
target = fixIntervalFormat(fullTarget)
|
||||
} else {
|
||||
@@ -79,6 +77,9 @@ func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource,
|
||||
span.SetTag("target", target)
|
||||
span.SetTag("from", from)
|
||||
span.SetTag("until", until)
|
||||
span.SetTag("datasource_id", dsInfo.Id)
|
||||
span.SetTag("org_id", dsInfo.OrgId)
|
||||
|
||||
defer span.Finish()
|
||||
|
||||
opentracing.GlobalTracer().Inject(
|
||||
|
||||
@@ -73,25 +73,20 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("%s AS time", args[0]), nil
|
||||
case "__utcTime":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) AS time", args[0]), nil
|
||||
case "__timeEpoch":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s) ) AS time", args[0]), nil
|
||||
return fmt.Sprintf("DATEDIFF(second, '1970-01-01', %s) AS time", args[0]), nil
|
||||
case "__timeFilter":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
return fmt.Sprintf("%s >= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND %s <= DATEADD(s, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
|
||||
return fmt.Sprintf("%s >= DATEADD(s, %d, '1970-01-01') AND %s <= DATEADD(s, %d, '1970-01-01')", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
|
||||
case "__timeFrom":
|
||||
return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
|
||||
return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
|
||||
case "__timeTo":
|
||||
return fmt.Sprintf("DATEADD(second, %d+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
|
||||
return fmt.Sprintf("DATEADD(second, %d, '1970-01-01')", uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
|
||||
case "__timeGroup":
|
||||
if len(args) < 2 {
|
||||
return "", fmt.Errorf("macro %v needs time column and interval", name)
|
||||
@@ -113,7 +108,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
|
||||
m.Query.Model.Set("fillValue", floatVal)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), %s))/%.0f as int)*%.0f as int)", args[0], interval.Seconds(), interval.Seconds()), nil
|
||||
return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
|
||||
case "__unixEpochFilter":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
|
||||
@@ -25,46 +25,39 @@ func TestMacroEngine(t *testing.T) {
|
||||
So(sql, ShouldEqual, "select time_column AS time")
|
||||
})
|
||||
|
||||
Convey("interpolate __utcTime function", func() {
|
||||
sql, err := engine.Interpolate(query, nil, "select $__utcTime(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) AS time")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeEpoch function", func() {
|
||||
sql, err := engine.Interpolate(query, nil, "select $__timeEpoch(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time")
|
||||
So(sql, ShouldEqual, "select DATEDIFF(second, '1970-01-01', time_column) AS time")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeEpoch function wrapped in aggregation", func() {
|
||||
sql, err := engine.Interpolate(query, nil, "select min($__timeEpoch(time_column))")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select min(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column) ) AS time)")
|
||||
So(sql, ShouldEqual, "select min(DATEDIFF(second, '1970-01-01', time_column) AS time)")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
|
||||
So(sql, ShouldEqual, "WHERE time_column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND time_column <= DATEADD(s, 18446744066914187038, '1970-01-01')")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)")
|
||||
So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with spaces around arguments", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "GROUP BY cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time_column))/300 as int)*300 as int)")
|
||||
So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeGroup function with fill (value = NULL)", func() {
|
||||
@@ -97,21 +90,21 @@ func TestMacroEngine(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
|
||||
So(sql, ShouldEqual, "select DATEADD(second, 18446744066914186738, '1970-01-01')")
|
||||
})
|
||||
|
||||
Convey("interpolate __timeTo function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038+DATEDIFF(second,GETUTCDATE(),GETDATE()), '1970-01-01')")
|
||||
So(sql, ShouldEqual, "select DATEADD(second, 18446744066914187038, '1970-01-01')")
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(18446744066914186738)")
|
||||
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, "select 18446744066914186738 >= 18446744066914186738 AND 18446744066914186738 <= 18446744066914187038")
|
||||
So(sql, ShouldEqual, "select time_column >= 18446744066914186738 AND time_column <= 18446744066914187038")
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFrom function", func() {
|
||||
|
||||
@@ -119,15 +119,10 @@ func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
|
||||
return err
|
||||
}
|
||||
|
||||
// convert column named time to unix timestamp to make
|
||||
// native datetime mssql types work in annotation queries
|
||||
if timeIndex != -1 {
|
||||
switch value := values[timeIndex].(type) {
|
||||
case time.Time:
|
||||
values[timeIndex] = float64(value.Unix())
|
||||
}
|
||||
}
|
||||
|
||||
// converts column named time to unix timestamp in milliseconds
|
||||
// to make native mssql datetime types and epoch dates work in
|
||||
// annotation and table queries.
|
||||
tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
|
||||
table.Rows = append(table.Rows, values)
|
||||
}
|
||||
|
||||
|
||||
+379
-189
@@ -19,6 +19,8 @@ import (
|
||||
// and set up a MSSQL db named grafanatest and a user/password grafana/Password!
|
||||
// Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a
|
||||
// preconfigured MSSQL server suitable for running these tests.
|
||||
// Thers's also a dashboard.json in same directory that you can import to Grafana
|
||||
// once you've created a datasource for the test server/database.
|
||||
// If needed, change the variable below to the IP address of the database.
|
||||
var serverIP string = "localhost"
|
||||
|
||||
@@ -37,44 +39,44 @@ func TestMSSQL(t *testing.T) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC)
|
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
|
||||
|
||||
Convey("Given a table with different native data types", func() {
|
||||
sql := `
|
||||
IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.[mssql_types]
|
||||
IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.[mssql_types]
|
||||
|
||||
CREATE TABLE [mssql_types] (
|
||||
c_bit bit,
|
||||
CREATE TABLE [mssql_types] (
|
||||
c_bit bit,
|
||||
|
||||
c_tinyint tinyint,
|
||||
c_smallint smallint,
|
||||
c_int int,
|
||||
c_bigint bigint,
|
||||
c_tinyint tinyint,
|
||||
c_smallint smallint,
|
||||
c_int int,
|
||||
c_bigint bigint,
|
||||
|
||||
c_money money,
|
||||
c_smallmoney smallmoney,
|
||||
c_numeric numeric(10,5),
|
||||
c_real real,
|
||||
c_decimal decimal(10,2),
|
||||
c_float float,
|
||||
c_money money,
|
||||
c_smallmoney smallmoney,
|
||||
c_numeric numeric(10,5),
|
||||
c_real real,
|
||||
c_decimal decimal(10,2),
|
||||
c_float float,
|
||||
|
||||
c_char char(10),
|
||||
c_varchar varchar(10),
|
||||
c_text text,
|
||||
c_char char(10),
|
||||
c_varchar varchar(10),
|
||||
c_text text,
|
||||
|
||||
c_nchar nchar(12),
|
||||
c_nvarchar nvarchar(12),
|
||||
c_ntext ntext,
|
||||
c_nchar nchar(12),
|
||||
c_nvarchar nvarchar(12),
|
||||
c_ntext ntext,
|
||||
|
||||
c_datetime datetime,
|
||||
c_datetime2 datetime2,
|
||||
c_smalldatetime smalldatetime,
|
||||
c_date date,
|
||||
c_time time,
|
||||
c_datetimeoffset datetimeoffset
|
||||
)
|
||||
`
|
||||
c_datetime datetime,
|
||||
c_datetime2 datetime2,
|
||||
c_smalldatetime smalldatetime,
|
||||
c_date date,
|
||||
c_time time,
|
||||
c_datetimeoffset datetimeoffset
|
||||
)
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -87,14 +89,14 @@ func TestMSSQL(t *testing.T) {
|
||||
d2 := dt2.Format(dt2Format)
|
||||
|
||||
sql = fmt.Sprintf(`
|
||||
INSERT INTO [mssql_types]
|
||||
SELECT
|
||||
1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12,
|
||||
1.11, 2.22, 3.33,
|
||||
'char10', 'varchar10', 'text',
|
||||
N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺',
|
||||
CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00')
|
||||
`, d, d2, d, d, d, d2)
|
||||
INSERT INTO [mssql_types]
|
||||
SELECT
|
||||
1, 5, 20020, 980300, 1420070400, '$20000.15', '£2.15', 12345.12,
|
||||
1.11, 2.22, 3.33,
|
||||
'char10', 'varchar10', 'text',
|
||||
N'☺nchar12☺', N'☺nvarchar12☺', N'☺text☺',
|
||||
CAST('%s' AS DATETIME), CAST('%s' AS DATETIME2), CAST('%s' AS SMALLDATETIME), CAST('%s' AS DATE), CAST('%s' AS TIME), SWITCHOFFSET(CAST('%s' AS DATETIMEOFFSET), '-07:00')
|
||||
`, d, d2, d, d, d, d2)
|
||||
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -151,14 +153,14 @@ func TestMSSQL(t *testing.T) {
|
||||
|
||||
Convey("Given a table with metrics that lacks data for some series ", func() {
|
||||
sql := `
|
||||
IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.[metric]
|
||||
IF OBJECT_ID('dbo.[metric]', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.[metric]
|
||||
|
||||
CREATE TABLE [metric] (
|
||||
time datetime,
|
||||
value int
|
||||
)
|
||||
`
|
||||
CREATE TABLE [metric] (
|
||||
time datetime,
|
||||
value int
|
||||
)
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -186,14 +188,8 @@ func TestMSSQL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
dtFormat := "2006-01-02 15:04:05.999999999"
|
||||
for _, s := range series {
|
||||
sql = fmt.Sprintf(`
|
||||
INSERT INTO metric (time, value)
|
||||
VALUES(CAST('%s' AS DATETIME), %d)
|
||||
`, s.Time.Format(dtFormat), s.Value)
|
||||
|
||||
_, err = sess.Exec(sql)
|
||||
_, err = sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
@@ -211,22 +207,32 @@ func TestMSSQL(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(len(points), ShouldEqual, 6)
|
||||
|
||||
So(len(points), ShouldEqual, 4)
|
||||
actualValueFirst := points[0][0].Float64
|
||||
actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0)
|
||||
So(actualValueFirst, ShouldEqual, 15)
|
||||
So(actualTimeFirst, ShouldEqual, fromStart)
|
||||
dt := fromStart
|
||||
|
||||
actualValueLast := points[3][0].Float64
|
||||
actualTimeLast := time.Unix(int64(points[3][1].Float64)/1000, 0)
|
||||
So(actualValueLast, ShouldEqual, 20)
|
||||
So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute))
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 3; i < 6; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
|
||||
@@ -247,33 +253,34 @@ func TestMSSQL(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
|
||||
So(len(points), ShouldEqual, 7)
|
||||
actualValueFirst := points[0][0].Float64
|
||||
actualTimeFirst := time.Unix(int64(points[0][1].Float64)/1000, 0)
|
||||
So(actualValueFirst, ShouldEqual, 15)
|
||||
So(actualTimeFirst, ShouldEqual, fromStart)
|
||||
|
||||
actualNullPoint := points[3][0]
|
||||
actualNullTime := time.Unix(int64(points[3][1].Float64)/1000, 0)
|
||||
So(actualNullPoint.Valid, ShouldBeFalse)
|
||||
So(actualNullTime, ShouldEqual, fromStart.Add(15*time.Minute))
|
||||
dt := fromStart
|
||||
|
||||
actualValueLast := points[5][0].Float64
|
||||
actualTimeLast := time.Unix(int64(points[5][1].Float64)/1000, 0)
|
||||
So(actualValueLast, ShouldEqual, 20)
|
||||
So(actualTimeLast, ShouldEqual, fromStart.Add(25*time.Minute))
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
actualLastNullPoint := points[6][0]
|
||||
actualLastNullTime := time.Unix(int64(points[6][1].Float64)/1000, 0)
|
||||
So(actualLastNullPoint.Valid, ShouldBeFalse)
|
||||
So(actualLastNullTime, ShouldEqual, fromStart.Add(30*time.Minute))
|
||||
So(points[3][0].Valid, ShouldBeFalse)
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 4; i < 7; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
|
||||
@@ -294,53 +301,44 @@ func TestMSSQL(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
|
||||
So(points[6][0].Float64, ShouldEqual, 1.5)
|
||||
So(points[3][0].Float64, ShouldEqual, 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with metrics having multiple values and measurements", func() {
|
||||
sql := `
|
||||
IF OBJECT_ID('dbo.[metric_values]', 'U') IS NOT NULL
|
||||
DROP TABLE dbo.[metric_values]
|
||||
|
||||
CREATE TABLE [metric_values] (
|
||||
time datetime,
|
||||
measurement nvarchar(100),
|
||||
valueOne int,
|
||||
valueTwo int,
|
||||
)
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
type metricValues struct {
|
||||
type metric_values struct {
|
||||
Time time.Time
|
||||
Measurement string
|
||||
ValueOne int64
|
||||
ValueTwo int64
|
||||
ValueOne int64 `xorm:"integer 'valueOne'"`
|
||||
ValueTwo int64 `xorm:"integer 'valueTwo'"`
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(metric_values{})
|
||||
}
|
||||
err := sess.CreateTable(metric_values{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
rnd := func(min, max int64) int64 {
|
||||
return rand.Int63n(max-min) + min
|
||||
}
|
||||
|
||||
series := []*metricValues{}
|
||||
series := []*metric_values{}
|
||||
for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
|
||||
series = append(series, &metricValues{
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric A",
|
||||
ValueOne: rnd(0, 100),
|
||||
ValueTwo: rnd(0, 100),
|
||||
})
|
||||
series = append(series, &metricValues{
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric B",
|
||||
ValueOne: rnd(0, 100),
|
||||
@@ -348,14 +346,8 @@ func TestMSSQL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
dtFormat := "2006-01-02 15:04:05"
|
||||
for _, s := range series {
|
||||
sql = fmt.Sprintf(`
|
||||
INSERT metric_values (time, measurement, valueOne, valueTwo)
|
||||
VALUES(CAST('%s' AS DATETIME), '%s', %d, %d)
|
||||
`, s.Time.Format(dtFormat), s.Measurement, s.ValueOne, s.ValueTwo)
|
||||
|
||||
_, err = sess.Exec(sql)
|
||||
_, err = sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
@@ -373,8 +365,8 @@ func TestMSSQL(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
@@ -396,8 +388,8 @@ func TestMSSQL(t *testing.T) {
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
@@ -407,45 +399,55 @@ func TestMSSQL(t *testing.T) {
|
||||
|
||||
Convey("Given a stored procedure that takes @from and @to in epoch time", func() {
|
||||
sql := `
|
||||
IF object_id('sp_test_epoch') IS NOT NULL
|
||||
DROP PROCEDURE sp_test_epoch
|
||||
`
|
||||
IF object_id('sp_test_epoch') IS NOT NULL
|
||||
DROP PROCEDURE sp_test_epoch
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sql = `
|
||||
CREATE PROCEDURE sp_test_epoch(
|
||||
@from int,
|
||||
@to int
|
||||
) AS
|
||||
BEGIN
|
||||
SELECT
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time,
|
||||
measurement + ' - value one' as metric,
|
||||
avg(valueOne) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01')
|
||||
GROUP BY
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int),
|
||||
measurement
|
||||
UNION ALL
|
||||
SELECT
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int) as time,
|
||||
measurement + ' - value two' as metric,
|
||||
avg(valueTwo) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time >= DATEADD(s, @from, '1970-01-01') AND time <= DATEADD(s, @to, '1970-01-01')
|
||||
GROUP BY
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, DATEADD(second, DATEDIFF(second,GETDATE(),GETUTCDATE()), time))/600 as int)*600 as int),
|
||||
measurement
|
||||
ORDER BY 1
|
||||
END
|
||||
`
|
||||
CREATE PROCEDURE sp_test_epoch(
|
||||
@from int,
|
||||
@to int,
|
||||
@interval nvarchar(50) = '5m',
|
||||
@metric nvarchar(200) = 'ALL'
|
||||
) AS
|
||||
BEGIN
|
||||
DECLARE @dInterval int
|
||||
SELECT @dInterval = 300
|
||||
|
||||
IF @interval = '10m'
|
||||
SELECT @dInterval = 600
|
||||
|
||||
SELECT
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
|
||||
measurement + ' - value one' as metric,
|
||||
avg(valueOne) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
|
||||
(@metric = 'ALL' OR measurement = @metric)
|
||||
GROUP BY
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
|
||||
measurement
|
||||
UNION ALL
|
||||
SELECT
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
|
||||
measurement + ' - value two' as metric,
|
||||
avg(valueTwo) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND
|
||||
(@metric = 'ALL' OR measurement = @metric)
|
||||
GROUP BY
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
|
||||
measurement
|
||||
ORDER BY 1
|
||||
END
|
||||
`
|
||||
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -456,10 +458,10 @@ func TestMSSQL(t *testing.T) {
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `DECLARE
|
||||
@from int = $__unixEpochFrom(),
|
||||
@to int = $__unixEpochTo()
|
||||
@from int = $__unixEpochFrom(),
|
||||
@to int = $__unixEpochTo()
|
||||
|
||||
EXEC dbo.sp_test_epoch @from, @to`,
|
||||
EXEC dbo.sp_test_epoch @from, @to`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
@@ -474,6 +476,7 @@ func TestMSSQL(t *testing.T) {
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
So(err, ShouldBeNil)
|
||||
fmt.Println("query", "sql", queryResult.Meta)
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 4)
|
||||
@@ -486,45 +489,55 @@ func TestMSSQL(t *testing.T) {
|
||||
|
||||
Convey("Given a stored procedure that takes @from and @to in datetime", func() {
|
||||
sql := `
|
||||
IF object_id('sp_test_datetime') IS NOT NULL
|
||||
DROP PROCEDURE sp_test_datetime
|
||||
`
|
||||
IF object_id('sp_test_datetime') IS NOT NULL
|
||||
DROP PROCEDURE sp_test_datetime
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sql = `
|
||||
CREATE PROCEDURE sp_test_datetime(
|
||||
@from datetime,
|
||||
@to datetime
|
||||
) AS
|
||||
BEGIN
|
||||
SELECT
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time,
|
||||
measurement + ' - value one' as metric,
|
||||
avg(valueOne) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time >= @from AND time <= @to
|
||||
GROUP BY
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int),
|
||||
measurement
|
||||
UNION ALL
|
||||
SELECT
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int) as time,
|
||||
measurement + ' - value two' as metric,
|
||||
avg(valueTwo) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time >= @from AND time <= @to
|
||||
GROUP BY
|
||||
cast(cast(DATEDIFF(second, {d '1970-01-01'}, time)/600 as int)*600 as int),
|
||||
measurement
|
||||
ORDER BY 1
|
||||
END
|
||||
`
|
||||
CREATE PROCEDURE sp_test_datetime(
|
||||
@from datetime,
|
||||
@to datetime,
|
||||
@interval nvarchar(50) = '5m',
|
||||
@metric nvarchar(200) = 'ALL'
|
||||
) AS
|
||||
BEGIN
|
||||
DECLARE @dInterval int
|
||||
SELECT @dInterval = 300
|
||||
|
||||
IF @interval = '10m'
|
||||
SELECT @dInterval = 600
|
||||
|
||||
SELECT
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
|
||||
measurement + ' - value one' as metric,
|
||||
avg(valueOne) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time BETWEEN @from AND @to AND
|
||||
(@metric = 'ALL' OR measurement = @metric)
|
||||
GROUP BY
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
|
||||
measurement
|
||||
UNION ALL
|
||||
SELECT
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time,
|
||||
measurement + ' - value two' as metric,
|
||||
avg(valueTwo) as value
|
||||
FROM
|
||||
metric_values
|
||||
WHERE
|
||||
time BETWEEN @from AND @to AND
|
||||
(@metric = 'ALL' OR measurement = @metric)
|
||||
GROUP BY
|
||||
CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval,
|
||||
measurement
|
||||
ORDER BY 1
|
||||
END
|
||||
`
|
||||
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
@@ -535,10 +548,10 @@ func TestMSSQL(t *testing.T) {
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `DECLARE
|
||||
@from int = $__unixEpochFrom(),
|
||||
@to int = $__unixEpochTo()
|
||||
@from int = $__unixEpochFrom(),
|
||||
@to int = $__unixEpochTo()
|
||||
|
||||
EXEC dbo.sp_test_epoch @from, @to`,
|
||||
EXEC dbo.sp_test_epoch @from, @to`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
@@ -570,7 +583,7 @@ func TestMSSQL(t *testing.T) {
|
||||
DROP TABLE dbo.[event]
|
||||
|
||||
CREATE TABLE [event] (
|
||||
time_sec bigint,
|
||||
time_sec int,
|
||||
description nvarchar(100),
|
||||
tags nvarchar(100),
|
||||
)
|
||||
@@ -654,12 +667,191 @@ func TestMSSQL(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in datetime format", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
dtFormat := "2006-01-02 15:04:05.999999999"
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
CAST('%s' AS DATETIME) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Format(dtFormat)),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
cast(%d as int) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()*1000),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as bigint) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a datetime null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as datetime) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
|
||||
x, err := xorm.NewEngine(sqlutil.TestDB_Mssql.DriverName, strings.Replace(sqlutil.TestDB_Mssql.ConnStr, "localhost", serverIP, 1))
|
||||
x.DatabaseTZ = time.UTC
|
||||
x.TZLocation = time.UTC
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
@@ -667,8 +859,6 @@ func InitMSSQLTestDB(t *testing.T) *xorm.Engine {
|
||||
t.Fatalf("Failed to init mssql db %v", err)
|
||||
}
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str
|
||||
|
||||
func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) {
|
||||
switch name {
|
||||
case "__time":
|
||||
case "__timeEpoch", "__time":
|
||||
if len(args) == 0 {
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
|
||||
+104
-101
@@ -81,7 +81,7 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
|
||||
// check if there is a column named time
|
||||
for i, col := range columnNames {
|
||||
switch col {
|
||||
case "time_sec":
|
||||
case "time", "time_sec":
|
||||
timeIndex = i
|
||||
}
|
||||
}
|
||||
@@ -96,13 +96,10 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
|
||||
return err
|
||||
}
|
||||
|
||||
// for annotations, convert to epoch
|
||||
if timeIndex != -1 {
|
||||
switch value := values[timeIndex].(type) {
|
||||
case time.Time:
|
||||
values[timeIndex] = float64(value.UnixNano() / 1e9)
|
||||
}
|
||||
}
|
||||
// converts column named time to unix timestamp in milliseconds to make
|
||||
// native mysql datetime types and epoch dates work in
|
||||
// annotation and table queries.
|
||||
tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
|
||||
|
||||
table.Rows = append(table.Rows, values)
|
||||
}
|
||||
@@ -185,9 +182,37 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
||||
return err
|
||||
}
|
||||
|
||||
rowData := NewStringStringScan(columnNames)
|
||||
columnTypes, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowLimit := 1000000
|
||||
rowCount := 0
|
||||
timeIndex := -1
|
||||
metricIndex := -1
|
||||
|
||||
// check columns of resultset: a column named time is mandatory
|
||||
// the first text column is treated as metric name unless a column named metric is present
|
||||
for i, col := range columnNames {
|
||||
switch col {
|
||||
case "time", "time_sec":
|
||||
timeIndex = i
|
||||
case "metric":
|
||||
metricIndex = i
|
||||
default:
|
||||
if metricIndex == -1 {
|
||||
switch columnTypes[i].DatabaseTypeName() {
|
||||
case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
|
||||
metricIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if timeIndex == -1 {
|
||||
return fmt.Errorf("Found no column named time or time_sec")
|
||||
}
|
||||
|
||||
fillMissing := query.Model.Get("fill").MustBool(false)
|
||||
var fillInterval float64
|
||||
@@ -198,53 +223,90 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
||||
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
|
||||
fillValue.Valid = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ; rows.Next(); rowCount++ {
|
||||
for rows.Next() {
|
||||
var timestamp float64
|
||||
var value null.Float
|
||||
var metric string
|
||||
|
||||
if rowCount > rowLimit {
|
||||
return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
|
||||
return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit)
|
||||
}
|
||||
|
||||
err := rowData.Update(rows.Rows)
|
||||
values, err := e.getTypedRowData(rows)
|
||||
if err != nil {
|
||||
e.log.Error("MySQL response parsing", "error", err)
|
||||
return fmt.Errorf("MySQL response parsing error %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if rowData.metric == "" {
|
||||
rowData.metric = "Unknown"
|
||||
switch columnValue := values[timeIndex].(type) {
|
||||
case int64:
|
||||
timestamp = float64(columnValue * 1000)
|
||||
case float64:
|
||||
timestamp = columnValue * 1000
|
||||
case time.Time:
|
||||
timestamp = float64(columnValue.UnixNano() / 1e6)
|
||||
default:
|
||||
return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue)
|
||||
}
|
||||
|
||||
if !rowData.time.Valid {
|
||||
return fmt.Errorf("Found row with no time value")
|
||||
}
|
||||
|
||||
series, exist := pointsBySeries[rowData.metric]
|
||||
if exist == false {
|
||||
series = &tsdb.TimeSeries{Name: rowData.metric}
|
||||
pointsBySeries[rowData.metric] = series
|
||||
seriesByQueryOrder.PushBack(rowData.metric)
|
||||
}
|
||||
|
||||
if fillMissing {
|
||||
var intervalStart float64
|
||||
if exist == false {
|
||||
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
||||
if metricIndex >= 0 {
|
||||
if columnValue, ok := values[metricIndex].(string); ok == true {
|
||||
metric = columnValue
|
||||
} else {
|
||||
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
||||
}
|
||||
|
||||
// align interval start
|
||||
intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
|
||||
|
||||
for i := intervalStart; i < rowData.time.Float64; i += fillInterval {
|
||||
series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
|
||||
rowCount++
|
||||
return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex])
|
||||
}
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
|
||||
for i, col := range columnNames {
|
||||
if i == timeIndex || i == metricIndex {
|
||||
continue
|
||||
}
|
||||
|
||||
switch columnValue := values[i].(type) {
|
||||
case int64:
|
||||
value = null.FloatFrom(float64(columnValue))
|
||||
case float64:
|
||||
value = null.FloatFrom(columnValue)
|
||||
case nil:
|
||||
value.Valid = false
|
||||
default:
|
||||
return fmt.Errorf("Value column must have numeric datatype, column: %s type: %T value: %v", col, columnValue, columnValue)
|
||||
}
|
||||
if metricIndex == -1 {
|
||||
metric = col
|
||||
}
|
||||
|
||||
series, exist := pointsBySeries[metric]
|
||||
if exist == false {
|
||||
series = &tsdb.TimeSeries{Name: metric}
|
||||
pointsBySeries[metric] = series
|
||||
seriesByQueryOrder.PushBack(metric)
|
||||
}
|
||||
|
||||
if fillMissing {
|
||||
var intervalStart float64
|
||||
if exist == false {
|
||||
intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6)
|
||||
} else {
|
||||
intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval
|
||||
}
|
||||
|
||||
// align interval start
|
||||
intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval
|
||||
|
||||
for i := intervalStart; i < timestamp; i += fillInterval {
|
||||
series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)})
|
||||
rowCount++
|
||||
}
|
||||
}
|
||||
|
||||
series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)})
|
||||
|
||||
e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value)
|
||||
rowCount++
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
|
||||
@@ -269,62 +331,3 @@ func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.
|
||||
result.Meta.Set("rowCount", rowCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringStringScan struct {
|
||||
rowPtrs []interface{}
|
||||
rowValues []string
|
||||
columnNames []string
|
||||
columnCount int
|
||||
|
||||
time null.Float
|
||||
value null.Float
|
||||
metric string
|
||||
}
|
||||
|
||||
func NewStringStringScan(columnNames []string) *stringStringScan {
|
||||
s := &stringStringScan{
|
||||
columnCount: len(columnNames),
|
||||
columnNames: columnNames,
|
||||
rowPtrs: make([]interface{}, len(columnNames)),
|
||||
rowValues: make([]string, len(columnNames)),
|
||||
}
|
||||
|
||||
for i := 0; i < s.columnCount; i++ {
|
||||
s.rowPtrs[i] = new(sql.RawBytes)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *stringStringScan) Update(rows *sql.Rows) error {
|
||||
if err := rows.Scan(s.rowPtrs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.time = null.FloatFromPtr(nil)
|
||||
s.value = null.FloatFromPtr(nil)
|
||||
|
||||
for i := 0; i < s.columnCount; i++ {
|
||||
if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok {
|
||||
s.rowValues[i] = string(*rb)
|
||||
|
||||
switch s.columnNames[i] {
|
||||
case "time_sec":
|
||||
if sec, err := strconv.ParseInt(s.rowValues[i], 10, 64); err == nil {
|
||||
s.time = null.FloatFrom(float64(sec * 1000))
|
||||
}
|
||||
case "value":
|
||||
if value, err := strconv.ParseFloat(s.rowValues[i], 64); err == nil {
|
||||
s.value = null.FloatFrom(value)
|
||||
}
|
||||
case "metric":
|
||||
s.metric = s.rowValues[i]
|
||||
}
|
||||
|
||||
*rb = nil // reset pointer to discard current value to avoid a bug
|
||||
} else {
|
||||
return fmt.Errorf("Cannot convert index %d column %s to type *sql.RawBytes", i, s.columnNames[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+622
-94
@@ -1,6 +1,8 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,10 @@ import (
|
||||
|
||||
// To run this test, remove the Skip from SkipConvey
|
||||
// and set up a MySQL db named grafana_tests and a user/password grafana/password
|
||||
// Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a
|
||||
// preconfigured MySQL server suitable for running these tests.
|
||||
// Thers's also a dashboard.json in same directory that you can import to Grafana
|
||||
// once you've created a datasource for the test server/database.
|
||||
func TestMySQL(t *testing.T) {
|
||||
SkipConvey("MySQL", t, func() {
|
||||
x := InitMySQLTestDB(t)
|
||||
@@ -29,110 +35,621 @@ func TestMySQL(t *testing.T) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
sql := "CREATE TABLE `mysql_types` ("
|
||||
sql += "`atinyint` tinyint(1) NOT NULL,"
|
||||
sql += "`avarchar` varchar(3) NOT NULL,"
|
||||
sql += "`achar` char(3),"
|
||||
sql += "`amediumint` mediumint NOT NULL,"
|
||||
sql += "`asmallint` smallint NOT NULL,"
|
||||
sql += "`abigint` bigint NOT NULL,"
|
||||
sql += "`aint` int(11) NOT NULL,"
|
||||
sql += "`adouble` double(10,2),"
|
||||
sql += "`anewdecimal` decimal(10,2),"
|
||||
sql += "`afloat` float(10,2) NOT NULL,"
|
||||
sql += "`atimestamp` timestamp NOT NULL,"
|
||||
sql += "`adatetime` datetime NOT NULL,"
|
||||
sql += "`atime` time NOT NULL,"
|
||||
// sql += "`ayear` year," // Crashes xorm when running cleandb
|
||||
sql += "`abit` bit(1),"
|
||||
sql += "`atinytext` tinytext,"
|
||||
sql += "`atinyblob` tinyblob,"
|
||||
sql += "`atext` text,"
|
||||
sql += "`ablob` blob,"
|
||||
sql += "`amediumtext` mediumtext,"
|
||||
sql += "`amediumblob` mediumblob,"
|
||||
sql += "`alongtext` longtext,"
|
||||
sql += "`alongblob` longblob,"
|
||||
sql += "`aenum` enum('val1', 'val2'),"
|
||||
sql += "`aset` set('a', 'b', 'c', 'd'),"
|
||||
sql += "`adate` date,"
|
||||
sql += "`time_sec` datetime(6),"
|
||||
sql += "`aintnull` int(11),"
|
||||
sql += "`afloatnull` float(10,2),"
|
||||
sql += "`avarcharnull` varchar(3),"
|
||||
sql += "`adecimalnull` decimal(10,2)"
|
||||
sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.Local)
|
||||
|
||||
sql = "INSERT INTO `mysql_types` "
|
||||
sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
|
||||
sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, "
|
||||
sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
|
||||
sql += "`aenum`, `aset`, `adate`, `time_sec`) "
|
||||
sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
|
||||
sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', "
|
||||
sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
|
||||
sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');"
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Query with Table format should map MySQL column types to Go types", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT * FROM mysql_types",
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
Convey("Given a table with different native data types", func() {
|
||||
if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable("mysql_types")
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
sql := "CREATE TABLE `mysql_types` ("
|
||||
sql += "`atinyint` tinyint(1) NOT NULL,"
|
||||
sql += "`avarchar` varchar(3) NOT NULL,"
|
||||
sql += "`achar` char(3),"
|
||||
sql += "`amediumint` mediumint NOT NULL,"
|
||||
sql += "`asmallint` smallint NOT NULL,"
|
||||
sql += "`abigint` bigint NOT NULL,"
|
||||
sql += "`aint` int(11) NOT NULL,"
|
||||
sql += "`adouble` double(10,2),"
|
||||
sql += "`anewdecimal` decimal(10,2),"
|
||||
sql += "`afloat` float(10,2) NOT NULL,"
|
||||
sql += "`atimestamp` timestamp NOT NULL,"
|
||||
sql += "`adatetime` datetime NOT NULL,"
|
||||
sql += "`atime` time NOT NULL,"
|
||||
sql += "`ayear` year," // Crashes xorm when running cleandb
|
||||
sql += "`abit` bit(1),"
|
||||
sql += "`atinytext` tinytext,"
|
||||
sql += "`atinyblob` tinyblob,"
|
||||
sql += "`atext` text,"
|
||||
sql += "`ablob` blob,"
|
||||
sql += "`amediumtext` mediumtext,"
|
||||
sql += "`amediumblob` mediumblob,"
|
||||
sql += "`alongtext` longtext,"
|
||||
sql += "`alongblob` longblob,"
|
||||
sql += "`aenum` enum('val1', 'val2'),"
|
||||
sql += "`aset` set('a', 'b', 'c', 'd'),"
|
||||
sql += "`adate` date,"
|
||||
sql += "`time_sec` datetime(6),"
|
||||
sql += "`aintnull` int(11),"
|
||||
sql += "`afloatnull` float(10,2),"
|
||||
sql += "`avarcharnull` varchar(3),"
|
||||
sql += "`adecimalnull` decimal(10,2)"
|
||||
sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
column := queryResult.Tables[0].Rows[0]
|
||||
sql = "INSERT INTO `mysql_types` "
|
||||
sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
|
||||
sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `ayear`, `abit`, `atinytext`, "
|
||||
sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
|
||||
sql += "`aenum`, `aset`, `adate`, `time_sec`) "
|
||||
sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
|
||||
sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', '2018', 1, 'tinytext', "
|
||||
sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
|
||||
sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');"
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(*column[0].(*int8), ShouldEqual, 1)
|
||||
So(column[1].(string), ShouldEqual, "abc")
|
||||
So(column[2].(string), ShouldEqual, "def")
|
||||
So(*column[3].(*int32), ShouldEqual, 1)
|
||||
So(*column[4].(*int16), ShouldEqual, 10)
|
||||
So(*column[5].(*int64), ShouldEqual, 100)
|
||||
So(*column[6].(*int32), ShouldEqual, 1420070400)
|
||||
So(column[7].(float64), ShouldEqual, 1.11)
|
||||
So(column[8].(float64), ShouldEqual, 2.22)
|
||||
So(*column[9].(*float32), ShouldEqual, 3.33)
|
||||
_, offset := time.Now().Zone()
|
||||
So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
|
||||
So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
|
||||
So(column[12].(string), ShouldEqual, "11:11:11")
|
||||
So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
|
||||
So(column[14].(string), ShouldEqual, "tinytext")
|
||||
So(column[15].(string), ShouldEqual, "tinyblob")
|
||||
So(column[16].(string), ShouldEqual, "text")
|
||||
So(column[17].(string), ShouldEqual, "blob")
|
||||
So(column[18].(string), ShouldEqual, "mediumtext")
|
||||
So(column[19].(string), ShouldEqual, "mediumblob")
|
||||
So(column[20].(string), ShouldEqual, "longtext")
|
||||
So(column[21].(string), ShouldEqual, "longblob")
|
||||
So(column[22].(string), ShouldEqual, "val2")
|
||||
So(column[23].(string), ShouldEqual, "a,b")
|
||||
So(column[24].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
|
||||
So(column[25].(float64), ShouldEqual, 1514764861)
|
||||
So(column[26], ShouldEqual, nil)
|
||||
So(column[27], ShouldEqual, nil)
|
||||
So(column[28], ShouldEqual, "")
|
||||
So(column[29], ShouldEqual, nil)
|
||||
Convey("Query with Table format should map MySQL column types to Go types", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT * FROM mysql_types",
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
column := queryResult.Tables[0].Rows[0]
|
||||
|
||||
So(*column[0].(*int8), ShouldEqual, 1)
|
||||
So(column[1].(string), ShouldEqual, "abc")
|
||||
So(column[2].(string), ShouldEqual, "def")
|
||||
So(*column[3].(*int32), ShouldEqual, 1)
|
||||
So(*column[4].(*int16), ShouldEqual, 10)
|
||||
So(*column[5].(*int64), ShouldEqual, 100)
|
||||
So(*column[6].(*int32), ShouldEqual, 1420070400)
|
||||
So(column[7].(float64), ShouldEqual, 1.11)
|
||||
So(column[8].(float64), ShouldEqual, 2.22)
|
||||
So(*column[9].(*float32), ShouldEqual, 3.33)
|
||||
_, offset := time.Now().Zone()
|
||||
So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
|
||||
So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
|
||||
So(column[12].(string), ShouldEqual, "11:11:11")
|
||||
So(column[13].(int64), ShouldEqual, 2018)
|
||||
So(*column[14].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
|
||||
So(column[15].(string), ShouldEqual, "tinytext")
|
||||
So(column[16].(string), ShouldEqual, "tinyblob")
|
||||
So(column[17].(string), ShouldEqual, "text")
|
||||
So(column[18].(string), ShouldEqual, "blob")
|
||||
So(column[19].(string), ShouldEqual, "mediumtext")
|
||||
So(column[20].(string), ShouldEqual, "mediumblob")
|
||||
So(column[21].(string), ShouldEqual, "longtext")
|
||||
So(column[22].(string), ShouldEqual, "longblob")
|
||||
So(column[23].(string), ShouldEqual, "val2")
|
||||
So(column[24].(string), ShouldEqual, "a,b")
|
||||
So(column[25].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
|
||||
So(column[26].(float64), ShouldEqual, float64(1514764861000))
|
||||
So(column[27], ShouldEqual, nil)
|
||||
So(column[28], ShouldEqual, nil)
|
||||
So(column[29], ShouldEqual, "")
|
||||
So(column[30], ShouldEqual, nil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with metrics that lacks data for some series ", func() {
|
||||
type metric struct {
|
||||
Time time.Time
|
||||
Value int64
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(metric{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(metric{})
|
||||
}
|
||||
err := sess.CreateTable(metric{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
series := []*metric{}
|
||||
firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second)
|
||||
secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second)
|
||||
|
||||
for _, t := range firstRange {
|
||||
series = append(series, &metric{
|
||||
Time: t,
|
||||
Value: 15,
|
||||
})
|
||||
}
|
||||
|
||||
for _, t := range secondRange {
|
||||
series = append(series, &metric{
|
||||
Time: t,
|
||||
Value: 20,
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
_, err = sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing a metric query using timeGroup", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m') as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(len(points), ShouldEqual, 6)
|
||||
|
||||
dt := fromStart
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 3; i < 6; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m', NULL) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(len(points), ShouldEqual, 7)
|
||||
|
||||
dt := fromStart
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
So(points[3][0].Valid, ShouldBeFalse)
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 4; i < 7; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m', 1.5) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(points[3][0].Float64, ShouldEqual, 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with metrics having multiple values and measurements", func() {
|
||||
type metric_values struct {
|
||||
Time time.Time
|
||||
Measurement string
|
||||
ValueOne int64 `xorm:"integer 'valueOne'"`
|
||||
ValueTwo int64 `xorm:"integer 'valueTwo'"`
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(metric_values{})
|
||||
}
|
||||
err := sess.CreateTable(metric_values{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
rnd := func(min, max int64) int64 {
|
||||
return rand.Int63n(max-min) + min
|
||||
}
|
||||
|
||||
series := []*metric_values{}
|
||||
for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric A",
|
||||
ValueOne: rnd(0, 100),
|
||||
ValueTwo: rnd(0, 100),
|
||||
})
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric B",
|
||||
ValueOne: rnd(0, 100),
|
||||
ValueTwo: rnd(0, 100),
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
_, err := sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values ORDER BY 1`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
So(queryResult.Series[0].Name, ShouldEqual, "Metric B - value one")
|
||||
So(queryResult.Series[1].Name, ShouldEqual, "Metric A - value one")
|
||||
})
|
||||
|
||||
Convey("When doing a metric query grouping by time should return correct series", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
So(queryResult.Series[0].Name, ShouldEqual, "valueOne")
|
||||
So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with event data", func() {
|
||||
type event struct {
|
||||
TimeSec int64
|
||||
Description string
|
||||
Tags string
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(event{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(event{})
|
||||
}
|
||||
err := sess.CreateTable(event{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
events := []*event{}
|
||||
for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) {
|
||||
events = append(events, &event{
|
||||
TimeSec: t.Unix(),
|
||||
Description: "Someone deployed something",
|
||||
Tags: "deploy",
|
||||
})
|
||||
events = append(events, &event{
|
||||
TimeSec: t.Add(5 * time.Minute).Unix(),
|
||||
Description: "New support ticket registered",
|
||||
Tags: "ticket",
|
||||
})
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
_, err = sess.Insert(e)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing an annotation query of deploy events should return expected result", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "Deploys",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["Deploys"]
|
||||
So(err, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query of ticket events should return expected result", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT time_sec, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "Tickets",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["Tickets"]
|
||||
So(err, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in datetime format", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.UTC)
|
||||
dtFormat := "2006-01-02 15:04:05.999999999"
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
CAST('%s' as datetime) as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Format(dtFormat)),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format (signed integer) should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 0, time.Local)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
CAST('%d' as signed integer) as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()*1000),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a unsigned integer null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as unsigned integer) as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a DATETIME null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as DATETIME) as time_sec,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func InitMySQLTestDB(t *testing.T) *xorm.Engine {
|
||||
x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr+"&parseTime=true")
|
||||
x.DatabaseTZ = time.Local
|
||||
x.TZLocation = time.Local
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
@@ -140,7 +657,18 @@ func InitMySQLTestDB(t *testing.T) *xorm.Engine {
|
||||
t.Fatalf("Failed to init mysql db %v", err)
|
||||
}
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time {
|
||||
durationSec := int64(duration.Seconds())
|
||||
intervalSec := int64(interval.Seconds())
|
||||
timeRange := []time.Time{}
|
||||
|
||||
for i := int64(0); i < durationSec; i += intervalSec {
|
||||
timeRange = append(timeRange, from)
|
||||
from = from.Add(time.Duration(int64(time.Second) * intervalSec))
|
||||
}
|
||||
|
||||
return timeRange
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSo
|
||||
}
|
||||
|
||||
func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error {
|
||||
|
||||
columnNames, err := rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -100,14 +99,10 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
|
||||
return err
|
||||
}
|
||||
|
||||
// convert column named time to unix timestamp to make
|
||||
// native datetime postgres types work in annotation queries
|
||||
if timeIndex != -1 {
|
||||
switch value := values[timeIndex].(type) {
|
||||
case time.Time:
|
||||
values[timeIndex] = float64(value.UnixNano() / 1e9)
|
||||
}
|
||||
}
|
||||
// converts column named time to unix timestamp in milliseconds to make
|
||||
// native postgres datetime types and epoch dates work in
|
||||
// annotation and table queries.
|
||||
tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex)
|
||||
|
||||
table.Rows = append(table.Rows, values)
|
||||
}
|
||||
@@ -118,7 +113,6 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro
|
||||
}
|
||||
|
||||
func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
|
||||
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -209,7 +203,6 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co
|
||||
fillValue.Float64 = query.Model.Get("fillValue").MustFloat64()
|
||||
fillValue.Valid = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +16,11 @@ import (
|
||||
)
|
||||
|
||||
// To run this test, remove the Skip from SkipConvey
|
||||
// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest
|
||||
// and set up a PostgreSQL db named grafanatest and a user/password grafanatest/grafanatest!
|
||||
// Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a
|
||||
// preconfigured Postgres server suitable for running these tests.
|
||||
// Thers's also a dashboard.json in same directory that you can import to Grafana
|
||||
// once you've created a datasource for the test server/database.
|
||||
func TestPostgres(t *testing.T) {
|
||||
SkipConvey("PostgreSQL", t, func() {
|
||||
x := InitPostgresTestDB(t)
|
||||
@@ -30,88 +36,599 @@ func TestPostgres(t *testing.T) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
sql := `
|
||||
CREATE TABLE postgres_types(
|
||||
c00_smallint smallint,
|
||||
c01_integer integer,
|
||||
c02_bigint bigint,
|
||||
fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local)
|
||||
|
||||
c03_real real,
|
||||
c04_double double precision,
|
||||
c05_decimal decimal(10,2),
|
||||
c06_numeric numeric(10,2),
|
||||
Convey("Given a table with different native data types", func() {
|
||||
sql := `
|
||||
DROP TABLE IF EXISTS postgres_types;
|
||||
CREATE TABLE postgres_types(
|
||||
c00_smallint smallint,
|
||||
c01_integer integer,
|
||||
c02_bigint bigint,
|
||||
|
||||
c07_char char(10),
|
||||
c08_varchar varchar(10),
|
||||
c09_text text,
|
||||
c03_real real,
|
||||
c04_double double precision,
|
||||
c05_decimal decimal(10,2),
|
||||
c06_numeric numeric(10,2),
|
||||
|
||||
c10_timestamp timestamp without time zone,
|
||||
c11_timestamptz timestamp with time zone,
|
||||
c12_date date,
|
||||
c13_time time without time zone,
|
||||
c14_timetz time with time zone,
|
||||
c15_interval interval
|
||||
);
|
||||
`
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
c07_char char(10),
|
||||
c08_varchar varchar(10),
|
||||
c09_text text,
|
||||
|
||||
sql = `
|
||||
INSERT INTO postgres_types VALUES(
|
||||
1,2,3,
|
||||
4.5,6.7,1.1,1.2,
|
||||
'char10','varchar10','text',
|
||||
c10_timestamp timestamp without time zone,
|
||||
c11_timestamptz timestamp with time zone,
|
||||
c12_date date,
|
||||
c13_time time without time zone,
|
||||
c14_timetz time with time zone,
|
||||
|
||||
now(),now(),now(),now(),now(),'15m'::interval
|
||||
);
|
||||
`
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Query with Table format should map PostgreSQL column types to Go types", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT * FROM postgres_types",
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["A"]
|
||||
c15_interval interval
|
||||
);
|
||||
`
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
column := queryResult.Tables[0].Rows[0]
|
||||
So(column[0].(int64), ShouldEqual, 1)
|
||||
So(column[1].(int64), ShouldEqual, 2)
|
||||
So(column[2].(int64), ShouldEqual, 3)
|
||||
So(column[3].(float64), ShouldEqual, 4.5)
|
||||
So(column[4].(float64), ShouldEqual, 6.7)
|
||||
// libpq doesnt properly convert decimal, numeric and char to go types but returns []uint8 instead
|
||||
// So(column[5].(float64), ShouldEqual, 1.1)
|
||||
// So(column[6].(float64), ShouldEqual, 1.2)
|
||||
// So(column[7].(string), ShouldEqual, "char")
|
||||
So(column[8].(string), ShouldEqual, "varchar10")
|
||||
So(column[9].(string), ShouldEqual, "text")
|
||||
sql = `
|
||||
INSERT INTO postgres_types VALUES(
|
||||
1,2,3,
|
||||
4.5,6.7,1.1,1.2,
|
||||
'char10','varchar10','text',
|
||||
|
||||
So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
now(),now(),now(),now(),now(),'15m'::interval
|
||||
);
|
||||
`
|
||||
_, err = sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// libpq doesnt properly convert interval to go types but returns []uint8 instead
|
||||
// So(column[15].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
Convey("When doing a table query should map Postgres column types to Go types", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT * FROM postgres_types",
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
column := queryResult.Tables[0].Rows[0]
|
||||
So(column[0].(int64), ShouldEqual, 1)
|
||||
So(column[1].(int64), ShouldEqual, 2)
|
||||
So(column[2].(int64), ShouldEqual, 3)
|
||||
|
||||
So(column[3].(float64), ShouldEqual, 4.5)
|
||||
So(column[4].(float64), ShouldEqual, 6.7)
|
||||
So(column[5].(float64), ShouldEqual, 1.1)
|
||||
So(column[6].(float64), ShouldEqual, 1.2)
|
||||
|
||||
So(column[7].(string), ShouldEqual, "char10 ")
|
||||
So(column[8].(string), ShouldEqual, "varchar10")
|
||||
So(column[9].(string), ShouldEqual, "text")
|
||||
|
||||
So(column[10].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[11].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[12].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[13].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
So(column[14].(time.Time), ShouldHaveSameTypeAs, time.Now())
|
||||
|
||||
So(column[15].(string), ShouldEqual, "00:15:00")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with metrics that lacks data for some series ", func() {
|
||||
sql := `
|
||||
DROP TABLE IF EXISTS metric;
|
||||
CREATE TABLE metric (
|
||||
time timestamp,
|
||||
value integer
|
||||
)
|
||||
`
|
||||
|
||||
_, err := sess.Exec(sql)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
type metric struct {
|
||||
Time time.Time
|
||||
Value int64
|
||||
}
|
||||
|
||||
series := []*metric{}
|
||||
firstRange := genTimeRangeByInterval(fromStart, 10*time.Minute, 10*time.Second)
|
||||
secondRange := genTimeRangeByInterval(fromStart.Add(20*time.Minute), 10*time.Minute, 10*time.Second)
|
||||
|
||||
for _, t := range firstRange {
|
||||
series = append(series, &metric{
|
||||
Time: t,
|
||||
Value: 15,
|
||||
})
|
||||
}
|
||||
|
||||
for _, t := range secondRange {
|
||||
series = append(series, &metric{
|
||||
Time: t,
|
||||
Value: 20,
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
_, err = sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing a metric query using timeGroup", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(len(points), ShouldEqual, 6)
|
||||
|
||||
dt := fromStart
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 3; i < 6; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with NULL fill enabled", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(len(points), ShouldEqual, 7)
|
||||
|
||||
dt := fromStart
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 15)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
So(points[3][0].Valid, ShouldBeFalse)
|
||||
|
||||
// adjust for 5 minute gap
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
for i := 4; i < 7; i++ {
|
||||
aValue := points[i][0].Float64
|
||||
aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
|
||||
So(aValue, ShouldEqual, 20)
|
||||
So(aTime, ShouldEqual, dt)
|
||||
dt = dt.Add(5 * time.Minute)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("When doing a metric query using timeGroup with float fill enabled", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1",
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
points := queryResult.Series[0].Points
|
||||
So(points[3][0].Float64, ShouldEqual, 1.5)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with metrics having multiple values and measurements", func() {
|
||||
type metric_values struct {
|
||||
Time time.Time
|
||||
Measurement string
|
||||
ValueOne int64 `xorm:"integer 'valueOne'"`
|
||||
ValueTwo int64 `xorm:"integer 'valueTwo'"`
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(metric_values{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(metric_values{})
|
||||
}
|
||||
err := sess.CreateTable(metric_values{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
rnd := func(min, max int64) int64 {
|
||||
return rand.Int63n(max-min) + min
|
||||
}
|
||||
|
||||
series := []*metric_values{}
|
||||
for _, t := range genTimeRangeByInterval(fromStart.Add(-30*time.Minute), 90*time.Minute, 5*time.Minute) {
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric A",
|
||||
ValueOne: rnd(0, 100),
|
||||
ValueTwo: rnd(0, 100),
|
||||
})
|
||||
series = append(series, &metric_values{
|
||||
Time: t,
|
||||
Measurement: "Metric B",
|
||||
ValueOne: rnd(0, 100),
|
||||
ValueTwo: rnd(0, 100),
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range series {
|
||||
_, err := sess.Insert(s)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing a metric query grouping by time and select metric column should return correct series", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT $__timeEpoch(time), measurement || ' - value one' as metric, "valueOne" FROM metric_values ORDER BY 1`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one")
|
||||
So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one")
|
||||
})
|
||||
|
||||
Convey("When doing a metric query grouping by time should return correct series", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT $__timeEpoch(time), "valueOne", "valueTwo" FROM metric_values ORDER BY 1`,
|
||||
"format": "time_series",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
||||
So(len(queryResult.Series), ShouldEqual, 2)
|
||||
So(queryResult.Series[0].Name, ShouldEqual, "valueOne")
|
||||
So(queryResult.Series[1].Name, ShouldEqual, "valueTwo")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a table with event data", func() {
|
||||
type event struct {
|
||||
TimeSec int64
|
||||
Description string
|
||||
Tags string
|
||||
}
|
||||
|
||||
if exist, err := sess.IsTableExist(event{}); err != nil || exist {
|
||||
So(err, ShouldBeNil)
|
||||
sess.DropTable(event{})
|
||||
}
|
||||
err := sess.CreateTable(event{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
events := []*event{}
|
||||
for _, t := range genTimeRangeByInterval(fromStart.Add(-20*time.Minute), 60*time.Minute, 25*time.Minute) {
|
||||
events = append(events, &event{
|
||||
TimeSec: t.Unix(),
|
||||
Description: "Someone deployed something",
|
||||
Tags: "deploy",
|
||||
})
|
||||
events = append(events, &event{
|
||||
TimeSec: t.Add(5 * time.Minute).Unix(),
|
||||
Description: "New support ticket registered",
|
||||
Tags: "ticket",
|
||||
})
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
_, err = sess.Insert(e)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
Convey("When doing an annotation query of deploy events should return expected result", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='deploy' ORDER BY 1 ASC`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "Deploys",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["Deploys"]
|
||||
So(err, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query of ticket events should return expected result", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT "time_sec" as time, description as text, tags FROM event WHERE $__unixEpochFilter(time_sec) AND tags='ticket' ORDER BY 1 ASC`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "Tickets",
|
||||
},
|
||||
},
|
||||
TimeRange: &tsdb.TimeRange{
|
||||
From: fmt.Sprintf("%v", fromStart.Add(-20*time.Minute).Unix()*1000),
|
||||
To: fmt.Sprintf("%v", fromStart.Add(40*time.Minute).Unix()*1000),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
queryResult := resp.Results["Tickets"]
|
||||
So(err, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in datetime format", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
dtFormat := "2006-01-02 15:04:05.999999999"
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
CAST('%s' AS TIMESTAMP) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Format(dtFormat)),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(float64), ShouldEqual, float64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch second format (int) should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
cast(%d as bigint) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, int64(dt.Unix()*1000))
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column in epoch millisecond format should return ms", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": fmt.Sprintf(`SELECT
|
||||
%d as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`, dt.Unix()*1000),
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0].(int64), ShouldEqual, dt.Unix()*1000)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a bigint null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as bigint) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("When doing an annotation query with a time column holding a timestamp null value should return nil", func() {
|
||||
query := &tsdb.TsdbQuery{
|
||||
Queries: []*tsdb.Query{
|
||||
{
|
||||
Model: simplejson.NewFromAny(map[string]interface{}{
|
||||
"rawSql": `SELECT
|
||||
cast(null as timestamp) as time,
|
||||
'message' as text,
|
||||
'tag1,tag2' as tags
|
||||
`,
|
||||
"format": "table",
|
||||
}),
|
||||
RefId: "A",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
So(len(queryResult.Tables[0].Rows), ShouldEqual, 1)
|
||||
columns := queryResult.Tables[0].Rows[0]
|
||||
|
||||
//Should be in milliseconds
|
||||
So(columns[0], ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func InitPostgresTestDB(t *testing.T) *xorm.Engine {
|
||||
x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
x.DatabaseTZ = time.UTC
|
||||
x.TZLocation = time.UTC
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
@@ -119,7 +636,18 @@ func InitPostgresTestDB(t *testing.T) *xorm.Engine {
|
||||
t.Fatalf("Failed to init postgres db %v", err)
|
||||
}
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func genTimeRangeByInterval(from time.Time, duration time.Duration, interval time.Duration) []time.Time {
|
||||
durationSec := int64(duration.Seconds())
|
||||
intervalSec := int64(interval.Seconds())
|
||||
timeRange := []time.Time{}
|
||||
|
||||
for i := int64(0); i < durationSec; i += intervalSec {
|
||||
timeRange = append(timeRange, from)
|
||||
from = from.Add(time.Duration(int64(time.Second) * intervalSec))
|
||||
}
|
||||
|
||||
return timeRange
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package tsdb
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/core"
|
||||
"github.com/go-xorm/xorm"
|
||||
@@ -133,3 +134,30 @@ func (e *DefaultSqlEngine) Query(
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ConvertTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds
|
||||
// to make native datetime types and epoch dates work in annotation and table queries.
|
||||
func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) {
|
||||
if timeIndex >= 0 {
|
||||
switch value := values[timeIndex].(type) {
|
||||
case time.Time:
|
||||
values[timeIndex] = EpochPrecisionToMs(float64(value.Unix()))
|
||||
case *time.Time:
|
||||
if value != nil {
|
||||
values[timeIndex] = EpochPrecisionToMs(float64((*value).Unix()))
|
||||
}
|
||||
case int64:
|
||||
values[timeIndex] = int64(EpochPrecisionToMs(float64(value)))
|
||||
case *int64:
|
||||
if value != nil {
|
||||
values[timeIndex] = int64(EpochPrecisionToMs(float64(*value)))
|
||||
}
|
||||
case float64:
|
||||
values[timeIndex] = EpochPrecisionToMs(value)
|
||||
case *float64:
|
||||
if value != nil {
|
||||
values[timeIndex] = EpochPrecisionToMs(*value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package tsdb
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSqlEngine(t *testing.T) {
|
||||
Convey("SqlEngine", t, func() {
|
||||
Convey("Given row values with time columns when converting them", func() {
|
||||
dt := time.Date(2018, 3, 14, 21, 20, 6, 527e6, time.UTC)
|
||||
fixtures := make([]interface{}, 8)
|
||||
fixtures[0] = dt
|
||||
fixtures[1] = dt.Unix() * 1000
|
||||
fixtures[2] = dt.Unix()
|
||||
fixtures[3] = float64(dt.Unix() * 1000)
|
||||
fixtures[4] = float64(dt.Unix())
|
||||
|
||||
var nilDt *time.Time
|
||||
var nilInt64 *int64
|
||||
var nilFloat64 *float64
|
||||
fixtures[5] = nilDt
|
||||
fixtures[6] = nilInt64
|
||||
fixtures[7] = nilFloat64
|
||||
|
||||
for i := range fixtures {
|
||||
ConvertSqlTimeColumnToEpochMs(fixtures, i)
|
||||
}
|
||||
|
||||
Convey("Should convert sql time columns to epoch time in ms ", func() {
|
||||
expected := float64(dt.Unix() * 1000)
|
||||
So(fixtures[0].(float64), ShouldEqual, expected)
|
||||
So(fixtures[1].(int64), ShouldEqual, expected)
|
||||
So(fixtures[2].(int64), ShouldEqual, expected)
|
||||
So(fixtures[3].(float64), ShouldEqual, expected)
|
||||
So(fixtures[4].(float64), ShouldEqual, expected)
|
||||
|
||||
So(fixtures[5], ShouldBeNil)
|
||||
So(fixtures[6], ShouldBeNil)
|
||||
So(fixtures[7], ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -88,3 +88,13 @@ func (tr *TimeRange) ParseTo() (time.Time, error) {
|
||||
|
||||
return time.Time{}, fmt.Errorf("cannot parse to value %s", tr.To)
|
||||
}
|
||||
|
||||
// EpochPrecisionToMs converts epoch precision to millisecond, if needed.
|
||||
// Only seconds to milliseconds supported right now
|
||||
func EpochPrecisionToMs(value float64) float64 {
|
||||
if int64(value)/1e10 == 0 {
|
||||
return float64(value * 1e3)
|
||||
}
|
||||
|
||||
return float64(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user