Commit 9ada4b60 by Sofia Papagiannaki Committed by GitHub

Expressions: Add option to disable feature (#30541)

* Expressions: Add option to disable feature

* Apply suggestions from code review

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
parent 5d52e50f
...@@ -893,3 +893,7 @@ use_browser_locale = false ...@@ -893,3 +893,7 @@ use_browser_locale = false
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. # Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
default_timezone = browser default_timezone = browser
[expressions]
# Disable expressions & UI features
enabled = true
...@@ -883,3 +883,7 @@ ...@@ -883,3 +883,7 @@
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc. # Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
;default_timezone = browser ;default_timezone = browser
[expressions]
# Disable expressions & UI features
;enabled = true
...@@ -1505,3 +1505,8 @@ Set this to `true` to have date formats automatically derived from your browser ...@@ -1505,3 +1505,8 @@ Set this to `true` to have date formats automatically derived from your browser
### default_timezone ### default_timezone
Used as the default time zone for user preferences. Can be either `browser` for the browser local time zone or a time zone name from the IANA Time Zone database, such as `UTC` or `Europe/Amsterdam`. Used as the default time zone for user preferences. Can be either `browser` for the browser local time zone or a time zone name from the IANA Time Zone database, such as `UTC` or `Europe/Amsterdam`.
## [expressions]
>Note: This is available in Grafana v7.4 and later versions.
### enabled
Set this to `false` to disable expressions and hide them in the Grafana UI. Default is `true`.
...@@ -68,6 +68,7 @@ export class GrafanaBootConfig implements GrafanaConfig { ...@@ -68,6 +68,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
sampleRate: 1, sampleRate: 1,
}; };
marketplaceUrl?: string; marketplaceUrl?: string;
expressionsEnabled = false;
constructor(options: GrafanaBootConfig) { constructor(options: GrafanaBootConfig) {
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark); this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
......
...@@ -237,11 +237,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i ...@@ -237,11 +237,12 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
"licenseUrl": hs.License.LicenseURL(c.SignedInUser), "licenseUrl": hs.License.LicenseURL(c.SignedInUser),
"edition": hs.License.Edition(), "edition": hs.License.Edition(),
}, },
"featureToggles": hs.Cfg.FeatureToggles, "featureToggles": hs.Cfg.FeatureToggles,
"rendererAvailable": hs.RenderService.IsAvailable(), "rendererAvailable": hs.RenderService.IsAvailable(),
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme, "http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
"sentry": hs.Cfg.Sentry, "sentry": hs.Cfg.Sentry,
"marketplaceUrl": hs.Cfg.MarketplaceURL, "marketplaceUrl": hs.Cfg.MarketplaceURL,
"expressionsEnabled": hs.Cfg.ExpressionsEnabled,
} }
return jsonObj, nil return jsonObj, nil
......
...@@ -121,7 +121,8 @@ func (hs *HTTPServer) handleExpressions(c *models.ReqContext, reqDTO dtos.Metric ...@@ -121,7 +121,8 @@ func (hs *HTTPServer) handleExpressions(c *models.ReqContext, reqDTO dtos.Metric
}) })
} }
resp, err := expr.WrapTransformData(c.Req.Context(), request) exprService := expr.Service{Cfg: hs.Cfg}
resp, err := exprService.WrapTransformData(c.Req.Context(), request)
if err != nil { if err != nil {
return response.Error(500, "expression request error", err) return response.Error(500, "expression request error", err)
} }
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/setting"
) )
// DatasourceName is the string constant used as the datasource name in requests // DatasourceName is the string constant used as the datasource name in requests
...@@ -20,6 +21,14 @@ const DatasourceUID = "-100" ...@@ -20,6 +21,14 @@ const DatasourceUID = "-100"
// Service is service representation for expression handling. // Service is service representation for expression handling.
type Service struct { type Service struct {
Cfg *setting.Cfg
}
func (s *Service) isDisabled() bool {
if s.Cfg == nil {
return true
}
return !s.Cfg.ExpressionsEnabled
} }
// BuildPipeline builds a pipeline from a request. // BuildPipeline builds a pipeline from a request.
......
...@@ -16,7 +16,7 @@ import ( ...@@ -16,7 +16,7 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) { func (s *Service) WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
sdkReq := &backend.QueryDataRequest{ sdkReq := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{ PluginContext: backend.PluginContext{
OrgID: query.User.OrgId, OrgID: query.User.OrgId,
...@@ -41,7 +41,7 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon ...@@ -41,7 +41,7 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon
}, },
}) })
} }
pbRes, err := TransformData(ctx, sdkReq) pbRes, err := s.TransformData(ctx, sdkReq)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -69,17 +69,20 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon ...@@ -69,17 +69,20 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon
// TransformData takes Queries which are either expressions nodes // TransformData takes Queries which are either expressions nodes
// or are datasource requests. // or are datasource requests.
func TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { func (s *Service) TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
svc := Service{} if s.isDisabled() {
return nil, status.Error(codes.PermissionDenied, "Expressions are disabled")
}
// Build the pipeline from the request, checking for ordering issues (e.g. loops) // Build the pipeline from the request, checking for ordering issues (e.g. loops)
// and parsing graph nodes from the queries. // and parsing graph nodes from the queries.
pipeline, err := svc.BuildPipeline(req) pipeline, err := s.BuildPipeline(req)
if err != nil { if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
// Execute the pipeline // Execute the pipeline
responses, err := svc.ExecutePipeline(ctx, pipeline) responses, err := s.ExecutePipeline(ctx, pipeline)
if err != nil { if err != nil {
return nil, status.Error(codes.Unknown, err.Error()) return nil, status.Error(codes.Unknown, err.Error())
} }
......
...@@ -39,7 +39,8 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond ...@@ -39,7 +39,8 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond
return response.Error(400, "invalid condition", err) return response.Error(400, "invalid condition", err)
} }
evalResults, err := eval.ConditionEval(&dto.Condition, timeNow()) evaluator := eval.Evaluator{Cfg: ng.Cfg}
evalResults, err := evaluator.ConditionEval(&dto.Condition, timeNow())
if err != nil { if err != nil {
return response.Error(400, "Failed to evaluate conditions", err) return response.Error(400, "Failed to evaluate conditions", err)
} }
...@@ -69,7 +70,8 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Re ...@@ -69,7 +70,8 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Re
return response.Error(400, "invalid condition", err) return response.Error(400, "invalid condition", err)
} }
evalResults, err := eval.ConditionEval(condition, timeNow()) evaluator := eval.Evaluator{Cfg: ng.Cfg}
evalResults, err := evaluator.ConditionEval(condition, timeNow())
if err != nil { if err != nil {
return response.Error(400, "Failed to evaluate alert", err) return response.Error(400, "Failed to evaluate alert", err)
} }
......
...@@ -7,6 +7,8 @@ import ( ...@@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/expr"
...@@ -14,6 +16,10 @@ import ( ...@@ -14,6 +16,10 @@ import (
const alertingEvaluationTimeout = 30 * time.Second const alertingEvaluationTimeout = 30 * time.Second
type Evaluator struct {
Cfg *setting.Cfg
}
// invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results. // invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
type invalidEvalResultFormatError struct { type invalidEvalResultFormatError struct {
refID string refID string
...@@ -87,7 +93,8 @@ func (c Condition) IsValid() bool { ...@@ -87,7 +93,8 @@ func (c Condition) IsValid() bool {
// AlertExecCtx is the context provided for executing an alert condition. // AlertExecCtx is the context provided for executing an alert condition.
type AlertExecCtx struct { type AlertExecCtx struct {
OrgID int64 OrgID int64
ExpressionsEnabled bool
Ctx context.Context Ctx context.Context
} }
...@@ -133,7 +140,8 @@ func (c *Condition) execute(ctx AlertExecCtx, now time.Time) (*ExecutionResults, ...@@ -133,7 +140,8 @@ func (c *Condition) execute(ctx AlertExecCtx, now time.Time) (*ExecutionResults,
}) })
} }
pbRes, err := expr.TransformData(ctx.Ctx, queryDataReq) exprService := expr.Service{Cfg: &setting.Cfg{ExpressionsEnabled: ctx.ExpressionsEnabled}}
pbRes, err := exprService.TransformData(ctx.Ctx, queryDataReq)
if err != nil { if err != nil {
return &result, err return &result, err
} }
...@@ -210,11 +218,11 @@ func (evalResults Results) AsDataFrame() data.Frame { ...@@ -210,11 +218,11 @@ func (evalResults Results) AsDataFrame() data.Frame {
} }
// ConditionEval executes conditions and evaluates the result. // ConditionEval executes conditions and evaluates the result.
func ConditionEval(condition *Condition, now time.Time) (Results, error) { func (e *Evaluator) ConditionEval(condition *Condition, now time.Time) (Results, error) {
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertingEvaluationTimeout) alertCtx, cancelFn := context.WithTimeout(context.Background(), alertingEvaluationTimeout)
defer cancelFn() defer cancelFn()
alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx} alertExecCtx := AlertExecCtx{OrgID: condition.OrgID, Ctx: alertCtx, ExpressionsEnabled: e.Cfg.ExpressionsEnabled}
execResult, err := condition.execute(alertExecCtx, now) execResult, err := condition.execute(alertExecCtx, now)
if err != nil { if err != nil {
......
...@@ -47,7 +47,13 @@ func (ng *AlertNG) Init() error { ...@@ -47,7 +47,13 @@ func (ng *AlertNG) Init() error {
ng.log = log.New("ngalert") ng.log = log.New("ngalert")
ng.registerAPIEndpoints() ng.registerAPIEndpoints()
ng.schedule = newScheduler(clock.New(), baseIntervalSeconds*time.Second, ng.log, nil) schedCfg := schedulerCfg{
c: clock.New(),
baseInterval: baseIntervalSeconds * time.Second,
logger: ng.log,
evaluator: eval.Evaluator{Cfg: ng.Cfg},
}
ng.schedule = newScheduler(schedCfg)
return nil return nil
} }
......
...@@ -47,7 +47,7 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, key alertDefini ...@@ -47,7 +47,7 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, key alertDefini
OrgID: alertDefinition.OrgID, OrgID: alertDefinition.OrgID,
QueriesAndExpressions: alertDefinition.Data, QueriesAndExpressions: alertDefinition.Data,
} }
results, err := eval.ConditionEval(&condition, ctx.now) results, err := ng.schedule.evaluator.ConditionEval(&condition, ctx.now)
end = timeNow() end = timeNow()
if err != nil { if err != nil {
// consider saving alert instance on error // consider saving alert instance on error
...@@ -118,19 +118,30 @@ type schedule struct { ...@@ -118,19 +118,30 @@ type schedule struct {
stopApplied func(alertDefinitionKey) stopApplied func(alertDefinitionKey)
log log.Logger log log.Logger
evaluator eval.Evaluator
}
type schedulerCfg struct {
c clock.Clock
baseInterval time.Duration
logger log.Logger
evalApplied func(alertDefinitionKey, time.Time)
evaluator eval.Evaluator
} }
// newScheduler returns a new schedule. // newScheduler returns a new schedule.
func newScheduler(c clock.Clock, baseInterval time.Duration, logger log.Logger, evalApplied func(alertDefinitionKey, time.Time)) *schedule { func newScheduler(cfg schedulerCfg) *schedule {
ticker := alerting.NewTicker(c.Now(), time.Second*0, c, int64(baseInterval.Seconds())) ticker := alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds()))
sch := schedule{ sch := schedule{
registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)}, registry: alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)},
maxAttempts: maxAttempts, maxAttempts: maxAttempts,
clock: c, clock: cfg.c,
baseInterval: baseInterval, baseInterval: cfg.baseInterval,
log: logger, log: cfg.logger,
heartbeat: ticker, heartbeat: ticker,
evalApplied: evalApplied, evalApplied: cfg.evalApplied,
evaluator: cfg.evaluator,
} }
return &sch return &sch
} }
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
...@@ -26,7 +27,13 @@ func TestAlertingTicker(t *testing.T) { ...@@ -26,7 +27,13 @@ func TestAlertingTicker(t *testing.T) {
t.Cleanup(registry.ClearOverrides) t.Cleanup(registry.ClearOverrides)
mockedClock := clock.NewMock() mockedClock := clock.NewMock()
ng.schedule = newScheduler(mockedClock, time.Second, log.New("ngalert.schedule.test"), nil) schefCfg := schedulerCfg{
c: mockedClock,
baseInterval: time.Second,
logger: log.New("ngalert.schedule.test"),
evaluator: eval.Evaluator{Cfg: ng.Cfg},
}
ng.schedule = newScheduler(schefCfg)
alerts := make([]*AlertDefinition, 0) alerts := make([]*AlertDefinition, 0)
......
...@@ -339,6 +339,9 @@ type Cfg struct { ...@@ -339,6 +339,9 @@ type Cfg struct {
AutoAssignOrg bool AutoAssignOrg bool
AutoAssignOrgId int AutoAssignOrgId int
AutoAssignOrgRole string AutoAssignOrgRole string
// ExpressionsEnabled specifies whether expressions are enabled.
ExpressionsEnabled bool
} }
// IsLiveEnabled returns if grafana live should be enabled // IsLiveEnabled returns if grafana live should be enabled
...@@ -482,6 +485,11 @@ func (cfg *Cfg) readAnnotationSettings() { ...@@ -482,6 +485,11 @@ func (cfg *Cfg) readAnnotationSettings() {
cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age") cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age")
} }
func (cfg *Cfg) readExpressionsSettings() {
expressions := cfg.Raw.Section("expressions")
cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true)
}
type AnnotationCleanupSettings struct { type AnnotationCleanupSettings struct {
MaxAge time.Duration MaxAge time.Duration
MaxCount int64 MaxCount int64
...@@ -850,6 +858,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { ...@@ -850,6 +858,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
cfg.readSmtpSettings() cfg.readSmtpSettings()
cfg.readQuotaSettings() cfg.readQuotaSettings()
cfg.readAnnotationSettings() cfg.readAnnotationSettings()
cfg.readExpressionsSettings()
if err := cfg.readGrafanaEnvironmentMetrics(); err != nil { if err := cfg.readGrafanaEnvironmentMetrics(); err != nil {
return err return err
} }
......
...@@ -315,7 +315,7 @@ export class QueryGroup extends PureComponent<Props, State> { ...@@ -315,7 +315,7 @@ export class QueryGroup extends PureComponent<Props, State> {
</Button> </Button>
)} )}
{isAddingMixed && this.renderMixedPicker()} {isAddingMixed && this.renderMixedPicker()}
{this.isExpressionsSupported(dsSettings) && ( {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && (
<Tooltip content="Experimental feature: queries could stop working in next version" placement="right"> <Tooltip content="Experimental feature: queries could stop working in next version" placement="right">
<Button <Button
icon="plus" icon="plus"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment