Merge branch 'master' into add_google_hangouts_chat_notifier

* master: (322 commits)
  graphInterval needs to update after query execution, fixes #14364
  Explore: Parse initial dates
  Aligned styling of stats popover/box with rest of grafana & minor css refactoring
  Prometheus: Make result transformer more robust for empty responses
  Rebase fixes
  Explore: Logging line parsing and field stats
  fixed unit tests
  made unknown color theme aware and sync with graph color, some minor cleanup
  Explore: improve error handling
  use render props instead of cloneElement
  sort of a hacky way to figure if the small variation should be used for the label
  add basic button group component, using the the same label style as is
  explore logs styling
  wip: alternative level styling & hover effect
  wip: explore logs styling
  more detailed error message for loki
  If user login equals user email, only show the email once #14341
  UserPicker and TeamPicker should use min-width instead of fixed widths to avoid overflowing form buttons.  #14341
  wip: explore logs styling
  restoring monospace & making sure width are correct when hiding columns
  ...
This commit is contained in:
bergquist
2018-12-07 14:31:13 +01:00
271 changed files with 7482 additions and 2018 deletions

View File

@@ -76,6 +76,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
c.JsonOK("User password updated")
}
// PUT /api/admin/users/:id/permissions
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
userID := c.ParamsInt64(":id")
@@ -85,6 +86,11 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis
}
if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastGrafanaAdmin {
c.JsonApiErr(400, m.ErrLastGrafanaAdmin.Error(), nil)
return
}
c.JsonApiErr(500, "Failed to update user permissions", err)
return
}

View File

@@ -0,0 +1,50 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestAdminApiEndpoint(t *testing.T) {
role := m.ROLE_ADMIN
Convey("Given a server admin attempts to remove themself as an admin", t, func() {
updateCmd := dtos.AdminUpdateUserPermissionsForm{
IsGrafanaAdmin: false,
}
bus.AddHandler("test", func(cmd *m.UpdateUserPermissionsCommand) error {
return m.ErrLastGrafanaAdmin
})
putAdminScenario("When calling PUT on", "/api/admin/users/1/permissions", "/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 400)
})
})
}
func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *m.ReqContext) {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = role
AdminUpdateUserPermissions(c, cmd)
})
sc.m.Put(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -295,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
return Error(500, "", err)
}
var response m.AlertStateType = m.AlertStatePending
var response m.AlertStateType = m.AlertStateUnknown
pausedState := "un-paused"
if cmd.Paused {
response = m.AlertStatePaused

View File

@@ -140,6 +140,7 @@ func (hs *HTTPServer) registerRoutes() {
usersRoute.Get("/", Wrap(SearchUsers))
usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
usersRoute.Get("/:id", Wrap(GetUserByID))
usersRoute.Get("/:id/teams", Wrap(GetUserTeams))
usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))

19
pkg/api/basic_auth.go Normal file
View File

@@ -0,0 +1,19 @@
package api
import (
"crypto/subtle"
macaron "gopkg.in/macaron.v1"
)
// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials
// and returns true if the provided credentials match the expected username and password.
// Returns false if the request is unauthenticated.
// Uses constant-time comparison in order to mitigate timing attacks.
func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool {
user, pass, ok := req.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
return false
}
return true
}

View File

@@ -0,0 +1,45 @@
package api
import (
"encoding/base64"
"fmt"
"net/http"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/macaron.v1"
)
func TestBasicAuthenticatedRequest(t *testing.T) {
expectedUser := "prometheus"
expectedPass := "password"
Convey("Given a valid set of basic auth credentials", t, func() {
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
So(err, ShouldBeNil)
req := macaron.Request{
Request: httpReq,
}
encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
So(authenticated, ShouldBeTrue)
})
Convey("Given an invalid set of basic auth credentials", t, func() {
httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
So(err, ShouldBeNil)
req := macaron.Request{
Request: httpReq,
}
encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass")
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
So(authenticated, ShouldBeFalse)
})
}
func encodeBasicAuthCredentials(user, pass string) string {
creds := fmt.Sprintf("%s:%s", user, pass)
return base64.StdEncoding.EncodeToString([]byte(creds))
}

View File

@@ -277,10 +277,6 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
return Error(500, "Failed to save dashboard", err)
}
if err == m.ErrDashboardFailedToUpdateAlertData {
return Error(500, "Invalid alert data. Cannot save dashboard", err)
}
c.TimeRequest(metrics.M_Api_Dashboard_Save)
return JSON(200, util.DynMap{
"status": "success",

View File

@@ -727,7 +727,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},

View File

@@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
return
}
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
@@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
},
))
}
func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
}

View File

@@ -0,0 +1,30 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestHTTPServer(t *testing.T) {
Convey("Given a HTTPServer", t, func() {
ts := &HTTPServer{
Cfg: setting.NewCfg(),
}
Convey("Given that basic auth on the metrics endpoint is enabled", func() {
ts.Cfg.MetricsEndpointBasicAuthUsername = "foo"
ts.Cfg.MetricsEndpointBasicAuthPassword = "bar"
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue)
})
Convey("Given that basic auth on the metrics endpoint is disabled", func() {
ts.Cfg.MetricsEndpointBasicAuthUsername = ""
ts.Cfg.MetricsEndpointBasicAuthPassword = ""
So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse)
})
})
}

View File

@@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
viewData.Settings["loginError"] = loginError
}
if tryOAuthAutoLogin(c) {
return
}
if !tryLoginUsingRememberCookie(c) {
c.HTML(200, ViewIndex, viewData)
return
@@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
c.Redirect(setting.AppSubUrl + "/")
}
func tryOAuthAutoLogin(c *m.ReqContext) bool {
if !setting.OAuthAutoLogin {
return false
}
oauthInfos := setting.OAuthService.OAuthInfos
if len(oauthInfos) != 1 {
log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.")
return false
}
for key := range setting.OAuthService.OAuthInfos {
redirectUrl := setting.AppSubUrl + "/login/" + key
log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
c.Redirect(redirectUrl, 307)
return true
}
return false
}
func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
// Check auto-login.
uname := c.GetCookie(setting.CookieUserName)

View File

@@ -4,10 +4,18 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response {
if setting.LdapEnabled || setting.AuthProxyEnabled {
return Error(401, "Not allowed to reset password when LDAP or Auth Proxy is enabled", nil)
}
if setting.DisableLoginForm {
return Error(401, "Not allowed to reset password when login form is disabled", nil)
}
userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
if err := bus.Dispatch(&userQuery); err != nil {

View File

@@ -51,7 +51,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
if token, err := tokenProvider.getAccessToken(data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
@@ -60,7 +60,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
logger.Error("Failed to get access token", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}
}
@@ -73,7 +73,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
if err != nil {
logger.Error("Failed to get default access token from meta data server", "error", err)
} else {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
}
}
}

View File

@@ -87,7 +87,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
}
for key, value := range headers {
log.Trace("setting key %v value %v", key, value[0])
log.Trace("setting key %v value <redacted>", key)
req.Header.Set(key, value[0])
}
}

View File

@@ -113,7 +113,16 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
// GET /api/user/teams
func GetSignedInUserTeamList(c *m.ReqContext) Response {
query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
return getUserTeamList(c.OrgId, c.UserId)
}
// GET /api/users/:id/teams
func GetUserTeams(c *m.ReqContext) Response {
return getUserTeamList(c.OrgId, c.ParamsInt64(":id"))
}
func getUserTeamList(userID int64, orgID int64) Response {
query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID}
if err := bus.Dispatch(&query); err != nil {
return Error(500, "Failed to get user teams", err)
@@ -122,11 +131,10 @@ func GetSignedInUserTeamList(c *m.ReqContext) Response {
for _, team := range query.Result {
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
}
return JSON(200, query.Result)
}
// GET /api/user/:id/orgs
// GET /api/users/:id/orgs
func GetUserOrgList(c *m.ReqContext) Response {
return getUserOrgList(c.ParamsInt64(":id"))
}

View File

@@ -13,7 +13,7 @@ import (
"syscall"
"time"
extensions "github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
@@ -39,6 +39,7 @@ var buildstamp string
var configFile = flag.String("config", "", "path to config file")
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
var pidFile = flag.String("pidfile", "", "path to pid file")
var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed")
func main() {
v := flag.Bool("v", false, "prints current version and exits")
@@ -53,7 +54,10 @@ func main() {
if *profile {
runtime.SetBlockProfileRate(1)
go func() {
http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
if err != nil {
panic(err)
}
}()
f, err := os.Create("trace.out")
@@ -79,6 +83,7 @@ func main() {
setting.BuildStamp = buildstampInt64
setting.BuildBranch = buildBranch
setting.IsEnterprise = extensions.IsEnterprise
setting.Packaging = validPackaging(*packaging)
metrics.SetBuildInformation(version, commit, buildBranch)
@@ -95,6 +100,16 @@ func main() {
os.Exit(code)
}
func validPackaging(packaging string) string {
validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"}
for _, vt := range validTypes {
if packaging == vt {
return packaging
}
}
return "unknown"
}
func listenToSystemSignals(server *GrafanaServerImpl) {
signalChan := make(chan os.Signal, 1)
sighupChan := make(chan os.Signal, 1)

View File

@@ -67,6 +67,7 @@ type GrafanaServerImpl struct {
}
func (g *GrafanaServerImpl) Run() error {
var err error
g.loadConfiguration()
g.writePIDFile()
@@ -74,20 +75,38 @@ func (g *GrafanaServerImpl) Run() error {
social.NewOAuthService()
serviceGraph := inject.Graph{}
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
serviceGraph.Provide(&inject.Object{Value: g.cfg})
serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
err = serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
err = serviceGraph.Provide(&inject.Object{Value: g.cfg})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
err = serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
err = serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
// self registered services
services := registry.GetServices()
// Add all services to dependency graph
for _, service := range services {
serviceGraph.Provide(&inject.Object{Value: service.Instance})
err = serviceGraph.Provide(&inject.Object{Value: service.Instance})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
}
serviceGraph.Provide(&inject.Object{Value: g})
err = serviceGraph.Provide(&inject.Object{Value: g})
if err != nil {
return fmt.Errorf("Failed to provide object to the graph: %v", err)
}
// Inject dependencies to services
if err := serviceGraph.Populate(); err != nil {
@@ -144,6 +163,7 @@ func (g *GrafanaServerImpl) Run() error {
}
sendSystemdNotification("READY=1")
return g.childRoutines.Wait()
}

View File

@@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence
// MIT License
package dynmap

View File

@@ -1,5 +1,5 @@
// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
// MIT Licence
// MIT License
package dynmap

View File

@@ -313,7 +313,7 @@ func init() {
// SetBuildInformation sets the build information for this binary
func SetBuildInformation(version, revision, branch string) {
// We export this info twice for backwards compability.
// We export this info twice for backwards compatibility.
// Once this have been released for some time we should be able to remote `M_Grafana_Version`
// The reason we added a new one is that its common practice in the prometheus community
// to name this metric `*_build_info` so its easy to do aggregation on all programs.
@@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) {
metrics := map[string]interface{}{}
report := map[string]interface{}{
"version": version,
"metrics": metrics,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"edition": getEdition(),
"version": version,
"metrics": metrics,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"edition": getEdition(),
"packaging": setting.Packaging,
}
statsQuery := models.GetSystemStatsQuery{}
@@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) {
}
metrics["stats.ds.other.count"] = dsOtherCount
metrics["stats.packaging."+setting.Packaging+".count"] = 1
dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := bus.Dispatch(&dsAccessStats); err != nil {
metricsLogger.Error("Failed to get datasource access stats", "error", err)

View File

@@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) {
setting.BasicAuthEnabled = true
setting.LdapEnabled = true
setting.AuthProxyEnabled = true
setting.Packaging = "deb"
wg.Add(1)
sendUsageStats(oauthProviders)
@@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) {
So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
})
})

View File

@@ -115,6 +115,7 @@ func Recovery() macaron.Handler {
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
c.Data["Theme"] = setting.DefaultTheme
if setting.Env == setting.DEV {
if theErr, ok := err.(error); ok {

View File

@@ -19,6 +19,7 @@ const (
AlertStateAlerting AlertStateType = "alerting"
AlertStateOK AlertStateType = "ok"
AlertStatePending AlertStateType = "pending"
AlertStateUnknown AlertStateType = "unknown"
)
const (
@@ -39,7 +40,12 @@ var (
)
func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
return s == AlertStateOK ||
s == AlertStateNoData ||
s == AlertStatePaused ||
s == AlertStatePending ||
s == AlertStateAlerting ||
s == AlertStateUnknown
}
func (s NoDataOption) IsValid() bool {
@@ -66,12 +72,13 @@ type Alert struct {
PanelId int64
Name string
Message string
Severity string
Severity string //Unused
State AlertStateType
Handler int64
Handler int64 //Unused
Silenced bool
ExecutionError string
Frequency int64
For time.Duration
EvalData *simplejson.Json
NewStateDate time.Time

View File

@@ -21,7 +21,6 @@ var (
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")

View File

@@ -7,7 +7,8 @@ import (
// Typed errors
var (
ErrUserNotFound = errors.New("User not found")
ErrUserNotFound = errors.New("User not found")
ErrLastGrafanaAdmin = errors.New("Cannot remove last grafana admin")
)
type Password string

View File

@@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription {
Color: "#D63232",
Text: "Alerting",
}
case m.AlertStateUnknown:
return &StateDescription{
Color: "#888888",
Text: "Unknown",
}
default:
panic("Unknown rule state " + c.Rule.State)
panic("Unknown rule state for alert " + c.Rule.State)
}
}
@@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
}
// GetNewState returns the new state from the alert rule evaluation
func (c *EvalContext) GetNewState() m.AlertStateType {
ns := getNewStateInternal(c)
if ns != m.AlertStateAlerting || c.Rule.For == 0 {
return ns
}
since := time.Since(c.Rule.LastStateChange)
if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For {
return m.AlertStateAlerting
}
if c.PrevAlertState == m.AlertStateAlerting {
return m.AlertStateAlerting
}
return m.AlertStatePending
}
func getNewStateInternal(c *EvalContext) m.AlertStateType {
if c.Error != nil {
c.log.Error("Alert Rule Result Error",
"ruleId", c.Rule.Id,
@@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType {
return c.PrevAlertState
}
return c.Rule.ExecutionErrorState.ToAlertState()
}
} else if c.Firing {
if c.Firing {
return m.AlertStateAlerting
}
} else if c.NoDataFound {
if c.NoDataFound {
c.log.Info("Alert Rule returned no data",
"ruleId", c.Rule.Id,
"name", c.Rule.Name,

View File

@@ -2,11 +2,11 @@ package alerting
import (
"context"
"fmt"
"errors"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestStateIsUpdatedWhenNeeded(t *testing.T) {
@@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) {
})
}
func TestAlertingEvalContext(t *testing.T) {
Convey("Should compute and replace properly new rule state", t, func() {
func TestGetStateFromEvalContext(t *testing.T) {
tcs := []struct {
name string
expected models.AlertStateType
applyFn func(ec *EvalContext)
}{
{
name: "ok -> alerting",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.Firing = true
ec.PrevAlertState = models.AlertStateOK
},
},
{
name: "ok -> error(alerting)",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
},
},
{
name: "ok -> pending. since its been firing for less than FOR",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
ec.Rule.For = time.Minute * 5
},
},
{
name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
ec.Rule.For = time.Minute * 2
},
},
{
name: "pending -> alerting. since its been firing for more than FOR and prev state is pending",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
ec.Rule.For = time.Minute * 2
},
},
{
name: "alerting -> alerting. should not update regardless of FOR",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateAlerting
ec.Firing = true
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
ec.Rule.For = time.Minute * 2
},
},
{
name: "ok -> ok. should not update regardless of FOR",
expected: models.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
ec.Rule.For = time.Minute * 2
},
},
{
name: "ok -> error(keep_last)",
expected: models.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
},
},
{
name: "pending -> error(keep_last)",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Error = errors.New("test error")
ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
},
},
{
name: "ok -> no_data(alerting)",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Rule.NoDataState = models.NoDataSetAlerting
ec.NoDataFound = true
},
},
{
name: "ok -> no_data(keep_last)",
expected: models.AlertStateOK,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStateOK
ec.Rule.NoDataState = models.NoDataKeepState
ec.NoDataFound = true
},
},
{
name: "pending -> no_data(keep_last)",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Rule.NoDataState = models.NoDataKeepState
ec.NoDataFound = true
},
},
{
name: "pending -> no_data(alerting) with for duration have not passed",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Rule.NoDataState = models.NoDataSetAlerting
ec.NoDataFound = true
ec.Rule.For = time.Minute * 5
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
},
},
{
name: "pending -> no_data(alerting) should set alerting since time passed FOR",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Rule.NoDataState = models.NoDataSetAlerting
ec.NoDataFound = true
ec.Rule.For = time.Minute * 2
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
},
},
{
name: "pending -> error(alerting) with for duration have not passed ",
expected: models.AlertStatePending,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ec.Error = errors.New("test error")
ec.Rule.For = time.Minute * 5
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
},
},
{
name: "pending -> error(alerting) should set alerting since time passed FOR",
expected: models.AlertStateAlerting,
applyFn: func(ec *EvalContext) {
ec.PrevAlertState = models.AlertStatePending
ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ec.Error = errors.New("test error")
ec.Rule.For = time.Minute * 2
ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
},
},
}
for _, tc := range tcs {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
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)
})
})
tc.applyFn(ctx)
have := ctx.GetNewState()
if have != tc.expected {
t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have))
}
}
}

View File

@@ -2,8 +2,8 @@ package alerting
import (
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -115,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
return nil, ValidationError{Reason: "Could not parse frequency"}
}
rawFor := jsonAlert.Get("for").MustString()
var forValue time.Duration
if rawFor != "" {
forValue, err = time.ParseDuration(rawFor)
if err != nil {
return nil, ValidationError{Reason: "Could not parse for"}
}
}
alert := &m.Alert{
DashboardId: e.Dash.Id,
OrgId: e.OrgID,
@@ -124,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
Handler: jsonAlert.Get("handler").MustInt64(),
Message: jsonAlert.Get("message").MustString(),
Frequency: frequency,
For: forValue,
}
for _, condition := range jsonAlert.Get("conditions").MustArray() {

View File

@@ -3,6 +3,7 @@ package alerting
import (
"io/ioutil"
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) {
return nil
})
json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
So(err, ShouldBeNil)
Convey("Extractor should not modify the original json", func() {
@@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) {
So(alerts[1].PanelId, ShouldEqual, 4)
})
Convey("should extract for param", func() {
So(alerts[0].For, ShouldEqual, time.Minute*2)
So(alerts[1].For, ShouldEqual, time.Duration(0))
})
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Message, ShouldEqual, "desc1")
@@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Panels missing id should return error", func() {
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(panelWithoutId)
@@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Panel with id set to zero should return error", func() {
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(panelWithIdZero)
@@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse alerts from dashboard without rows", func() {
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse and validate dashboard containing influxdb alert", func() {
json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Should be able to extract collapsed panels", func() {
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
@@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
})
Convey("Parse and validate dashboard without id and containing an alert", func() {
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
So(err, ShouldBeNil)
dashJSON, err := simplejson.NewJson(json)

View File

@@ -1,13 +1,60 @@
package notifiers
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestWhenAlertManagerShouldNotify(t *testing.T) {
tcs := []struct {
prevState m.AlertStateType
newState m.AlertStateType
expect bool
}{
{
prevState: m.AlertStatePending,
newState: m.AlertStateOK,
expect: false,
},
{
prevState: m.AlertStateAlerting,
newState: m.AlertStateOK,
expect: true,
},
{
prevState: m.AlertStateOK,
newState: m.AlertStatePending,
expect: false,
},
{
prevState: m.AlertStateUnknown,
newState: m.AlertStatePending,
expect: false,
},
}
for _, tc := range tcs {
am := &AlertmanagerNotifier{log: log.New("test.logger")}
evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
State: tc.prevState,
})
evalContext.Rule.State = tc.newState
res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{})
if res != tc.expect {
t.Errorf("got %v expected %v", res, tc.expect)
}
}
}
func TestAlertmanagerNotifier(t *testing.T) {
Convey("Alertmanager notifier tests", t, func() {

View File

@@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
}
// Do not notify when we become OK for the first time.
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK {
return false
}
// Do not notify when we become OK for the first time.
if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending {
return false
}
// Do not notify when we become OK from pending
if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
return false
}

View File

@@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStatePending,
sendReminder: false,
state: &m.AlertNotificationState{},
expect: false,
},
@@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateAlerting,
prevState: m.AlertStateOK,
sendReminder: false,
state: &m.AlertNotificationState{},
expect: true,
},
@@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStatePending,
prevState: m.AlertStateOK,
sendReminder: false,
state: &m.AlertNotificationState{},
expect: false,
},
@@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateOK,
sendReminder: false,
state: &m.AlertNotificationState{},
expect: false,
},
@@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateOK,
sendReminder: true,
state: &m.AlertNotificationState{},
expect: false,
},
@@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
newState: m.AlertStateOK,
prevState: m.AlertStateAlerting,
sendReminder: false,
state: &m.AlertNotificationState{},
expect: true,
},
@@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
prevState: m.AlertStateAlerting,
frequency: time.Minute * 10,
sendReminder: true,
state: &m.AlertNotificationState{},
expect: true,
},
@@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) {
prevState: m.AlertStateOK,
state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
expect: true,
},
{
name: "unknown -> ok",
prevState: m.AlertStateUnknown,
newState: m.AlertStateOK,
expect: false,
},
{
name: "unknown -> pending",
prevState: m.AlertStateUnknown,
newState: m.AlertStatePending,
expect: false,
},
{
name: "unknown -> alerting",
prevState: m.AlertStateUnknown,
newState: m.AlertStateAlerting,
expect: true,
},
}
@@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) {
State: tc.prevState,
})
if tc.state == nil {
tc.state = &m.AlertNotificationState{}
}
evalContext.Rule.State = tc.newState
nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}

View File

@@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
// when two servers are raising. This makes sure that the server
// with the last state change always sends a notification.
evalContext.Rule.StateChanges = cmd.Result.StateChanges
// Update the last state change of the alert rule in memory
evalContext.Rule.LastStateChange = time.Now()
}
// save annotation

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -18,6 +19,8 @@ type Rule struct {
Frequency int64
Name string
Message string
LastStateChange time.Time
For time.Duration
NoDataState m.NoDataOption
ExecutionErrorState m.ExecutionErrorOption
State m.AlertStateType
@@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
model.Message = ruleDef.Message
model.Frequency = ruleDef.Frequency
model.State = ruleDef.State
model.LastStateChange = ruleDef.NewStateDate
model.For = ruleDef.For
model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
model.StateChanges = ruleDef.StateChanges

View File

@@ -23,6 +23,7 @@
"message": "desc1",
"handler": 1,
"frequency": "60s",
"for": "2m",
"conditions": [
{
"type": "query",

View File

@@ -165,7 +165,7 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return models.ErrDashboardFailedToUpdateAlertData
return err
}
return nil

View File

@@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
if alertToUpdate.ContainsUpdates(alert) {
alert.Updated = timeNow()
alert.State = alertToUpdate.State
sess.MustCols("message")
sess.MustCols("message", "for")
_, err := sess.ID(alert.Id).Update(alert)
if err != nil {
return err
@@ -204,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
} else {
alert.Updated = timeNow()
alert.Created = timeNow()
alert.State = m.AlertStatePending
alert.State = m.AlertStateUnknown
alert.NewStateDate = timeNow()
_, err := sess.Insert(alert)
@@ -299,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
params = append(params, string(m.AlertStatePaused))
params = append(params, timeNow())
} else {
params = append(params, string(m.AlertStatePending))
params = append(params, string(m.AlertStateUnknown))
params = append(params, timeNow())
}
@@ -323,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
if cmd.Paused {
newState = string(m.AlertStatePaused)
} else {
newState = string(m.AlertStatePending)
newState = string(m.AlertStateUnknown)
}
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())

View File

@@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(alert.DashboardId, ShouldEqual, testDash.Id)
So(alert.PanelId, ShouldEqual, 1)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.State, ShouldEqual, "pending")
So(alert.State, ShouldEqual, m.AlertStateUnknown)
So(alert.NewStateDate, ShouldNotBeNil)
So(alert.EvalData, ShouldNotBeNil)
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
@@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(query.Result[0].Name, ShouldEqual, "Name")
Convey("Alert state should not be updated", func() {
So(query.Result[0].State, ShouldEqual, "pending")
So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown)
})
})

View File

@@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) {
mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
Name: "for", Type: DB_BigInt, Nullable: true,
}))
}

View File

@@ -187,7 +187,7 @@ func TestAccountDataAccess(t *testing.T) {
err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId})
So(err, ShouldBeNil)
// remove frome ac2 from ac1 org
// remove ac2 user from ac1 org
remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true}
err = RemoveOrgUser(&remCmd)
So(err, ShouldBeNil)

View File

@@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
OrgId: cmd.OrgId,
Updated: time.Now(),
Target: cmd.Target,
OrgId: cmd.OrgId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()
@@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
UserId: cmd.UserId,
Updated: time.Now(),
Target: cmd.Target,
UserId: cmd.UserId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()

View File

@@ -2,6 +2,7 @@ package sqlstore
import (
"testing"
"time"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@@ -168,5 +169,69 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 1)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should org quota updating is successful even if it called multiple time", func() {
orgCmd := m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 5,
}
err := UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
orgCmd = m.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 10,
}
err = UpdateOrgQuota(&orgCmd)
So(err, ShouldBeNil)
query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should user quota updating is successful even if it called multiple time", func() {
userQuotaCmd := m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 5,
}
err := UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
userQuotaCmd = m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err = UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = GetUserQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 10)
})
})
}

View File

@@ -504,8 +504,18 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
user.IsAdmin = cmd.IsGrafanaAdmin
sess.UseBool("is_admin")
_, err := sess.ID(user.Id).Update(&user)
return err
if err != nil {
return err
}
// validate that after update there is at least one server admin
if err := validateOneAdminLeft(sess); err != nil {
return err
}
return nil
})
}
@@ -522,3 +532,17 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
return err
})
}
func validateOneAdminLeft(sess *DBSession) error {
// validate that there is an admin user left
count, err := sess.Where("is_admin=?", true).Count(&m.User{})
if err != nil {
return err
}
if count == 0 {
return m.ErrLastGrafanaAdmin
}
return nil
}

View File

@@ -155,6 +155,32 @@ func TestUserDataAccess(t *testing.T) {
})
})
})
Convey("Given one grafana admin user", func() {
var err error
createUserCmd := &m.CreateUserCommand{
Email: fmt.Sprint("admin", "@test.com"),
Name: fmt.Sprint("admin"),
Login: fmt.Sprint("admin"),
IsAdmin: true,
}
err = CreateUser(context.Background(), createUserCmd)
So(err, ShouldBeNil)
Convey("Cannot make themselves a non-admin", func() {
updateUserPermsCmd := m.UpdateUserPermissionsCommand{IsGrafanaAdmin: false, UserId: 1}
updatePermsError := UpdateUserPermissions(&updateUserPermsCmd)
So(updatePermsError, ShouldEqual, m.ErrLastGrafanaAdmin)
query := m.GetUserByIdQuery{Id: createUserCmd.Result.Id}
getUserError := GetUserById(&query)
So(getUserError, ShouldBeNil)
So(query.Result.IsAdmin, ShouldEqual, true)
})
})
})
}

View File

@@ -57,6 +57,9 @@ var (
IsEnterprise bool
ApplicationName string
// packaging
Packaging = "unknown"
// Paths
HomePath string
PluginsPath string
@@ -112,6 +115,7 @@ var (
ExternalUserMngLinkUrl string
ExternalUserMngLinkName string
ExternalUserMngInfo string
OAuthAutoLogin bool
ViewersCanEdit bool
// Http auth
@@ -215,6 +219,8 @@ type Cfg struct {
DisableBruteForceLoginProtection bool
TempDataLifetime time.Duration
MetricsEndpointEnabled bool
MetricsEndpointBasicAuthUsername string
MetricsEndpointBasicAuthPassword string
EnableAlphaPanels bool
EnterpriseLicensePath string
}
@@ -626,6 +632,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
auth := iniFile.Section("auth")
DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
// anonymous access
@@ -676,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String()
cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String()
analytics := iniFile.Section("analytics")
ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)

View File

@@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
@@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
}
return &BasicUserInfo{
Id: data.Id,
Name: data.Name,
Email: data.Email,
Login: data.Email,

View File

@@ -126,6 +126,18 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
}
eg.Go(func() error {
defer func() {
if err := recover(); err != nil {
plog.Error("Execute Query Panic", "error", err, "stack", log.Stack(1))
if theErr, ok := err.(error); ok {
resultChan <- &tsdb.QueryResult{
RefId: query.RefId,
Error: theErr,
}
}
}
}()
queryRes, err := e.executeQuery(ectx, query, queryContext)
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
return err
@@ -146,6 +158,17 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
for region, getMetricDataQuery := range getMetricDataQueries {
q := getMetricDataQuery
eg.Go(func() error {
defer func() {
if err := recover(); err != nil {
plog.Error("Execute Get Metric Data Query Panic", "error", err, "stack", log.Stack(1))
if theErr, ok := err.(error); ok {
resultChan <- &tsdb.QueryResult{
Error: theErr,
}
}
}
}()
queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
return err
@@ -188,8 +211,8 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch
return nil, err
}
if endTime.Before(startTime) {
return nil, fmt.Errorf("Invalid time range: End time can't be before start time")
if !startTime.Before(endTime) {
return nil, fmt.Errorf("Invalid time range: Start time must be before end time")
}
params := &cloudwatch.GetMetricStatisticsInput{

View File

@@ -1,9 +1,13 @@
package cloudwatch
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/components/null"
@@ -14,6 +18,24 @@ import (
func TestCloudWatch(t *testing.T) {
Convey("CloudWatch", t, func() {
Convey("executeQuery", func() {
e := &CloudWatchExecutor{
DataSource: &models.DataSource{
JsonData: simplejson.New(),
},
}
Convey("End time before start time should result in error", func() {
_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")})
So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
})
Convey("End time equals start time should result in error", func() {
_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-1h")})
So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
})
})
Convey("can parse cloudwatch json model", func() {
json := `
{

View File

@@ -46,6 +46,8 @@ func init() {
"AWS/Billing": {"EstimatedCharges"},
"AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
"AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
"AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
"AWS/CodeBuild": {"BuildDuration", "Builds", "DownloadSourceDuration", "Duration", "FailedBuilds", "FinalizingDuration", "InstallDuration", "PostBuildDuration", "PreBuildDuration", "ProvisioningDuration", "QueuedDuration", "SubmittedDuration", "SucceededBuilds", "UploadArtifactsDuration"},
"AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
"AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
"AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
@@ -121,6 +123,8 @@ func init() {
"AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"},
"AWS/CloudFront": {"DistributionId", "Region"},
"AWS/CloudSearch": {},
"AWS/CloudHSM": {"Region", "ClusterId", "HsmId"},
"AWS/CodeBuild": {"ProjectName"},
"AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
"AWS/DX": {"ConnectionId"},

View File

@@ -65,7 +65,7 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb
clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", "))
switch version {
case 2, 5, 56:
case 2, 5, 56, 60:
return &baseClientImpl{
ctx: ctx,
ds: ds,

View File

@@ -90,6 +90,19 @@ func TestClient(t *testing.T) {
So(err, ShouldBeNil)
So(c.GetVersion(), ShouldEqual, 56)
})
Convey("When version 60 should return v6.0 client", func() {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 60,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, nil)
So(err, ShouldBeNil)
So(c.GetVersion(), ShouldEqual, 60)
})
})
Convey("Given a fake http client", func() {
@@ -153,8 +166,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
fmt.Println("body", string(headerBytes))
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "count")
@@ -209,8 +220,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
fmt.Println("body", string(headerBytes))
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch")
@@ -265,8 +274,6 @@ func TestClient(t *testing.T) {
jBody, err := simplejson.NewJson(bodyBytes)
So(err, ShouldBeNil)
fmt.Println("body", string(headerBytes))
So(jHeader.Get("index").MustString(), ShouldEqual, "metrics-2018.05.15")
So(jHeader.Get("ignore_unavailable").MustBool(false), ShouldEqual, true)
So(jHeader.Get("search_type").MustString(), ShouldEqual, "query_then_fetch")

View File

@@ -240,6 +240,7 @@ type DateHistogramAgg struct {
Missing *string `json:"missing,omitempty"`
ExtendedBounds *ExtendedBounds `json:"extended_bounds"`
Format string `json:"format"`
Offset string `json:"offset,omitempty"`
}
// FiltersAggregation represents a filters aggregation

View File

@@ -541,7 +541,7 @@ func getErrorFromElasticResponse(response *es.SearchResponse) *tsdb.QueryResult
} else if reason != "" {
result.ErrorString = reason
} else {
result.ErrorString = "Unkown elasticsearch error response"
result.ErrorString = "Unknown elasticsearch error response"
}
return result

View File

@@ -134,6 +134,10 @@ func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFro
a.Interval = "$__interval"
}
if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil {
a.Offset = offset
}
if missing, err := bucketAgg.Settings.Get("missing").String(); err == nil {
a.Missing = &missing
}

View File

@@ -32,6 +32,7 @@ func init() {
renders["median"] = QueryDefinition{Renderer: functionRenderer}
renders["sum"] = QueryDefinition{Renderer: functionRenderer}
renders["mode"] = QueryDefinition{Renderer: functionRenderer}
renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer}
renders["holt_winters"] = QueryDefinition{
Renderer: functionRenderer,

View File

@@ -23,6 +23,7 @@ func TestInfluxdbQueryPart(t *testing.T) {
{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
{mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`},
}
queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}

View File

@@ -66,6 +66,10 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
}
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)

View File

@@ -52,6 +52,20 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "select '2018-04-12T18:00:00Z'")
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "select '2018-04-12T18:05:00Z'")
})
Convey("interpolate __timeGroup function", func() {
sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)

View File

@@ -61,6 +61,10 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
}
return fmt.Sprintf("%s BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil
case "__timeFrom":
return fmt.Sprintf("FROM_UNIXTIME(%d)", m.timeRange.GetFromAsSecondsEpoch()), nil
case "__timeTo":
return fmt.Sprintf("FROM_UNIXTIME(%d)", m.timeRange.GetToAsSecondsEpoch()), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)

View File

@@ -63,6 +63,20 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", from.Unix()))
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select FROM_UNIXTIME(%d)", to.Unix()))
})
Convey("interpolate __unixEpochFilter function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__unixEpochFilter(time)")
So(err, ShouldBeNil)

View File

@@ -761,7 +761,7 @@ func TestMySQL(t *testing.T) {
{
DataSource: &models.DataSource{JsonData: simplejson.New()},
Model: simplejson.NewFromAny(map[string]interface{}{
"rawSql": `SELECT time FROM metric_values WHERE time > $__timeFrom() OR time < $__timeFrom() OR 1 < $__unixEpochFrom() OR $__unixEpochTo() > 1 ORDER BY 1`,
"rawSql": `SELECT time FROM metric_values WHERE time > $__timeFrom() OR time < $__timeTo() OR 1 < $__unixEpochFrom() OR $__unixEpochTo() > 1 ORDER BY 1`,
"format": "time_series",
}),
RefId: "A",
@@ -773,7 +773,7 @@ func TestMySQL(t *testing.T) {
So(err, ShouldBeNil)
queryResult := resp.Results["A"]
So(queryResult.Error, ShouldBeNil)
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > '2018-03-15T12:55:00Z' OR time < '2018-03-15T12:55:00Z' OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT time FROM metric_values WHERE time > FROM_UNIXTIME(1521118500) OR time < FROM_UNIXTIME(1521118800) OR 1 < 1521118500 OR 1521118800 > 1 ORDER BY 1")
})

View File

@@ -84,7 +84,7 @@ func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsd
postData, err := json.Marshal(data)
if err != nil {
plog.Info("Failed marshalling data", "error", err)
plog.Info("Failed marshaling data", "error", err)
return nil, fmt.Errorf("Failed to create request. error: %v", err)
}

View File

@@ -87,6 +87,10 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
}
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeFrom":
return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil
case "__timeTo":
return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
case "__timeGroup":
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name)

View File

@@ -44,6 +44,20 @@ func TestMacroEngine(t *testing.T) {
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
})
Convey("interpolate __timeFrom function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeFrom()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "select '2018-04-12T18:00:00Z'")
})
Convey("interpolate __timeTo function", func() {
sql, err := engine.Interpolate(query, timeRange, "select $__timeTo()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "select '2018-04-12T18:05:00Z'")
})
Convey("interpolate __timeGroup function pre 5.3 compatibility", func() {
sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value")

View File

@@ -196,8 +196,6 @@ var Interpolate = func(query *Query, timeRange *TimeRange, sql string) (string,
sql = strings.Replace(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1)
sql = strings.Replace(sql, "$__interval", interval.Text, -1)
sql = strings.Replace(sql, "$__timeFrom()", fmt.Sprintf("'%s'", timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), -1)
sql = strings.Replace(sql, "$__timeTo()", fmt.Sprintf("'%s'", timeRange.GetToAsTimeUTC().Format(time.RFC3339)), -1)
sql = strings.Replace(sql, "$__unixEpochFrom()", fmt.Sprintf("%d", timeRange.GetFromAsSecondsEpoch()), -1)
sql = strings.Replace(sql, "$__unixEpochTo()", fmt.Sprintf("%d", timeRange.GetToAsSecondsEpoch()), -1)

View File

@@ -44,20 +44,6 @@ func TestSqlEngine(t *testing.T) {
So(sql, ShouldEqual, "select 60000 ")
})
Convey("interpolate __timeFrom function", func() {
sql, err := Interpolate(query, timeRange, "select $__timeFrom()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339)))
})
Convey("interpolate __timeTo function", func() {
sql, err := Interpolate(query, timeRange, "select $__timeTo()")
So(err, ShouldBeNil)
So(sql, ShouldEqual, fmt.Sprintf("select '%s'", to.Format(time.RFC3339)))
})
Convey("interpolate __unixEpochFrom function", func() {
sql, err := Interpolate(query, timeRange, "select $__unixEpochFrom()")
So(err, ShouldBeNil)