mirror of https://github.com/grafana/grafana.git
				
				
				
			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>
This commit is contained in:
		
							parent
							
								
									5d52e50f6f
								
							
						
					
					
						commit
						9ada4b6052
					
				|  | @ -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 = browser | ||||
| 
 | ||||
| [expressions] | ||||
| # Disable expressions & UI features | ||||
| enabled = true | ||||
|  |  | |||
|  | @ -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 = 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 | |||
| ### 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`. | ||||
| 
 | ||||
| ## [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 { | |||
|     sampleRate: 1, | ||||
|   }; | ||||
|   marketplaceUrl?: string; | ||||
|   expressionsEnabled = false; | ||||
| 
 | ||||
|   constructor(options: GrafanaBootConfig) { | ||||
|     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 | |||
| 			"licenseUrl":      hs.License.LicenseURL(c.SignedInUser), | ||||
| 			"edition":         hs.License.Edition(), | ||||
| 		}, | ||||
| 		"featureToggles":    hs.Cfg.FeatureToggles, | ||||
| 		"rendererAvailable": hs.RenderService.IsAvailable(), | ||||
| 		"http2Enabled":      hs.Cfg.Protocol == setting.HTTP2Scheme, | ||||
| 		"sentry":            hs.Cfg.Sentry, | ||||
| 		"marketplaceUrl":    hs.Cfg.MarketplaceURL, | ||||
| 		"featureToggles":     hs.Cfg.FeatureToggles, | ||||
| 		"rendererAvailable":  hs.RenderService.IsAvailable(), | ||||
| 		"http2Enabled":       hs.Cfg.Protocol == setting.HTTP2Scheme, | ||||
| 		"sentry":             hs.Cfg.Sentry, | ||||
| 		"marketplaceUrl":     hs.Cfg.MarketplaceURL, | ||||
| 		"expressionsEnabled": hs.Cfg.ExpressionsEnabled, | ||||
| 	} | ||||
| 
 | ||||
| 	return jsonObj, nil | ||||
|  |  | |||
|  | @ -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 { | ||||
| 		return response.Error(500, "expression request error", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import ( | |||
| 	"context" | ||||
| 
 | ||||
| 	"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
 | ||||
|  | @ -20,6 +21,14 @@ const DatasourceUID = "-100" | |||
| 
 | ||||
| // Service is service representation for expression handling.
 | ||||
| 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.
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ import ( | |||
| 	"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{ | ||||
| 		PluginContext: backend.PluginContext{ | ||||
| 			OrgID: query.User.OrgId, | ||||
|  | @ -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 { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | @ -69,17 +69,20 @@ func WrapTransformData(ctx context.Context, query *tsdb.TsdbQuery) (*tsdb.Respon | |||
| 
 | ||||
| // TransformData takes Queries which are either expressions nodes
 | ||||
| // or are datasource requests.
 | ||||
| func TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { | ||||
| 	svc := Service{} | ||||
| func (s *Service) TransformData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { | ||||
| 	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)
 | ||||
| 	// and parsing graph nodes from the queries.
 | ||||
| 	pipeline, err := svc.BuildPipeline(req) | ||||
| 	pipeline, err := s.BuildPipeline(req) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Error(codes.InvalidArgument, err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	// Execute the pipeline
 | ||||
| 	responses, err := svc.ExecutePipeline(ctx, pipeline) | ||||
| 	responses, err := s.ExecutePipeline(ctx, pipeline) | ||||
| 	if err != nil { | ||||
| 		return nil, status.Error(codes.Unknown, err.Error()) | ||||
| 	} | ||||
|  |  | |||
|  | @ -39,7 +39,8 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond | |||
| 		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 { | ||||
| 		return response.Error(400, "Failed to evaluate conditions", err) | ||||
| 	} | ||||
|  | @ -69,7 +70,8 @@ func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Re | |||
| 		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 { | ||||
| 		return response.Error(400, "Failed to evaluate alert", err) | ||||
| 	} | ||||
|  |  | |||
|  | @ -7,6 +7,8 @@ import ( | |||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| 
 | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/backend" | ||||
| 	"github.com/grafana/grafana-plugin-sdk-go/data" | ||||
| 	"github.com/grafana/grafana/pkg/expr" | ||||
|  | @ -14,6 +16,10 @@ import ( | |||
| 
 | ||||
| const alertingEvaluationTimeout = 30 * time.Second | ||||
| 
 | ||||
| type Evaluator struct { | ||||
| 	Cfg *setting.Cfg | ||||
| } | ||||
| 
 | ||||
| // invalidEvalResultFormatError is an error for invalid format of the alert definition evaluation results.
 | ||||
| type invalidEvalResultFormatError struct { | ||||
| 	refID  string | ||||
|  | @ -87,7 +93,8 @@ func (c Condition) IsValid() bool { | |||
| 
 | ||||
| // AlertExecCtx is the context provided for executing an alert condition.
 | ||||
| type AlertExecCtx struct { | ||||
| 	OrgID int64 | ||||
| 	OrgID              int64 | ||||
| 	ExpressionsEnabled bool | ||||
| 
 | ||||
| 	Ctx context.Context | ||||
| } | ||||
|  | @ -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 { | ||||
| 		return &result, err | ||||
| 	} | ||||
|  | @ -210,11 +218,11 @@ func (evalResults Results) AsDataFrame() data.Frame { | |||
| } | ||||
| 
 | ||||
| // 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) | ||||
| 	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) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -47,7 +47,13 @@ func (ng *AlertNG) Init() error { | |||
| 	ng.log = log.New("ngalert") | ||||
| 
 | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ func (ng *AlertNG) definitionRoutine(grafanaCtx context.Context, key alertDefini | |||
| 					OrgID:                 alertDefinition.OrgID, | ||||
| 					QueriesAndExpressions: alertDefinition.Data, | ||||
| 				} | ||||
| 				results, err := eval.ConditionEval(&condition, ctx.now) | ||||
| 				results, err := ng.schedule.evaluator.ConditionEval(&condition, ctx.now) | ||||
| 				end = timeNow() | ||||
| 				if err != nil { | ||||
| 					// consider saving alert instance on error
 | ||||
|  | @ -118,19 +118,30 @@ type schedule struct { | |||
| 	stopApplied func(alertDefinitionKey) | ||||
| 
 | ||||
| 	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.
 | ||||
| func newScheduler(c clock.Clock, baseInterval time.Duration, logger log.Logger, evalApplied func(alertDefinitionKey, time.Time)) *schedule { | ||||
| 	ticker := alerting.NewTicker(c.Now(), time.Second*0, c, int64(baseInterval.Seconds())) | ||||
| func newScheduler(cfg schedulerCfg) *schedule { | ||||
| 	ticker := alerting.NewTicker(cfg.c.Now(), time.Second*0, cfg.c, int64(cfg.baseInterval.Seconds())) | ||||
| 	sch := schedule{ | ||||
| 		registry:     alertDefinitionRegistry{alertDefinitionInfo: make(map[alertDefinitionKey]alertDefinitionInfo)}, | ||||
| 		maxAttempts:  maxAttempts, | ||||
| 		clock:        c, | ||||
| 		baseInterval: baseInterval, | ||||
| 		log:          logger, | ||||
| 		clock:        cfg.c, | ||||
| 		baseInterval: cfg.baseInterval, | ||||
| 		log:          cfg.logger, | ||||
| 		heartbeat:    ticker, | ||||
| 		evalApplied:  evalApplied, | ||||
| 		evalApplied:  cfg.evalApplied, | ||||
| 		evaluator:    cfg.evaluator, | ||||
| 	} | ||||
| 	return &sch | ||||
| } | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/infra/log" | ||||
| 	"github.com/grafana/grafana/pkg/registry" | ||||
| 	"github.com/grafana/grafana/pkg/services/ngalert/eval" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
|  | @ -26,7 +27,13 @@ func TestAlertingTicker(t *testing.T) { | |||
| 	t.Cleanup(registry.ClearOverrides) | ||||
| 
 | ||||
| 	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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -339,6 +339,9 @@ type Cfg struct { | |||
| 	AutoAssignOrg     bool | ||||
| 	AutoAssignOrgId   int | ||||
| 	AutoAssignOrgRole string | ||||
| 
 | ||||
| 	// ExpressionsEnabled specifies whether expressions are enabled.
 | ||||
| 	ExpressionsEnabled bool | ||||
| } | ||||
| 
 | ||||
| // IsLiveEnabled returns if grafana live should be enabled
 | ||||
|  | @ -482,6 +485,11 @@ func (cfg *Cfg) readAnnotationSettings() { | |||
| 	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 { | ||||
| 	MaxAge   time.Duration | ||||
| 	MaxCount int64 | ||||
|  | @ -850,6 +858,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { | |||
| 	cfg.readSmtpSettings() | ||||
| 	cfg.readQuotaSettings() | ||||
| 	cfg.readAnnotationSettings() | ||||
| 	cfg.readExpressionsSettings() | ||||
| 	if err := cfg.readGrafanaEnvironmentMetrics(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
|  | @ -315,7 +315,7 @@ export class QueryGroup extends PureComponent<Props, State> { | |||
|           </Button> | ||||
|         )} | ||||
|         {isAddingMixed && this.renderMixedPicker()} | ||||
|         {this.isExpressionsSupported(dsSettings) && ( | ||||
|         {config.expressionsEnabled && this.isExpressionsSupported(dsSettings) && ( | ||||
|           <Tooltip content="Experimental feature: queries could stop working in next version" placement="right"> | ||||
|             <Button | ||||
|               icon="plus" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue