* Expressions: Add option to disable feature
* Apply suggestions from code review
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
(cherry picked from commit 9ada4b6052)
Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com>
This commit is contained in:
co-authored by
Sofia Papagiannaki
parent
7592031b36
commit
fcfb12d0b3
@@ -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 @@
|
|||||||
|
|
||||||
# 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
|
|||||||
### 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 {
|
|||||||
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
|
|||||||
"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
|
||||||
|
|||||||
+2
-1
@@ -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 (
|
|||||||
"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"
|
|||||||
|
|
||||||
// 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 (
|
|||||||
"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
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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
|
|||||||
|
|
||||||
// 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
|
|||||||
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
|
|||||||
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 (
|
|||||||
"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 (
|
|||||||
|
|
||||||
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 {
|
|||||||
|
|
||||||
// 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,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
|||||||
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
|
|||||||
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 {
|
|||||||
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 (
|
|||||||
|
|
||||||
"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) {
|
|||||||
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 {
|
|||||||
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() {
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
</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"
|
||||||
|
|||||||
Reference in New Issue
Block a user