The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/featuremgmt/manager.go

255 lines
6.4 KiB

package featuremgmt
import (
"context"
"fmt"
"reflect"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ FeatureToggles = (*FeatureManager)(nil)
)
type FeatureManager struct {
isDevMod bool
restartRequired bool
Settings setting.FeatureMgmtSettings
flags map[string]*FeatureFlag
enabled map[string]bool // only the "on" values
startup map[string]bool // the explicit values registered at startup
warnings map[string]string // potential warnings about the flag
log log.Logger
}
// This will merge the flags with the current configuration
func (fm *FeatureManager) registerFlags(flags ...FeatureFlag) {
for _, add := range flags {
if add.Name == "" {
continue // skip it with warning?
}
flag, ok := fm.flags[add.Name]
if !ok {
f := add // make a copy
fm.flags[add.Name] = &f
continue
}
// Selectively update properties
if add.Description != "" {
flag.Description = add.Description
}
if add.Expression != "" {
flag.Expression = add.Expression
}
// The most recently defined state
if add.Stage != FeatureStageUnknown {
flag.Stage = add.Stage
}
// Only gets more restrictive
if add.RequiresDevMode {
flag.RequiresDevMode = true
}
if add.RequiresRestart {
flag.RequiresRestart = true
}
}
// This will evaluate all flags
fm.update()
}
// meetsRequirements checks if grafana is able to run the given feature due to dev mode or licensing requirements
func (fm *FeatureManager) meetsRequirements(ff *FeatureFlag) (bool, string) {
if ff.RequiresDevMode && !fm.isDevMod {
return false, "requires dev mode"
}
return true, ""
}
// Update
func (fm *FeatureManager) update() {
enabled := make(map[string]bool)
for _, flag := range fm.flags {
// if grafana cannot run the feature, omit metrics around it
ok, reason := fm.meetsRequirements(flag)
if !ok {
fm.warnings[flag.Name] = reason
continue
}
// Update the registry
track := 0.0
startup, ok := fm.startup[flag.Name]
if startup || (!ok && flag.Expression == "true") {
track = 1
enabled[flag.Name] = true
}
// Register value with prometheus metric
featureToggleInfo.WithLabelValues(flag.Name).Set(track)
}
fm.enabled = enabled
}
// IsEnabled checks if a feature is enabled
func (fm *FeatureManager) IsEnabled(ctx context.Context, flag string) bool {
return fm.enabled[flag]
}
// IsEnabledGlobally checks if a feature is for all tenants
func (fm *FeatureManager) IsEnabledGlobally(flag string) bool {
return fm.enabled[flag]
}
// GetEnabled returns a map containing only the features that are enabled
func (fm *FeatureManager) GetEnabled(ctx context.Context) map[string]bool {
enabled := make(map[string]bool, len(fm.enabled))
for key, val := range fm.enabled {
if val {
enabled[key] = true
}
}
return enabled
}
// GetFlags returns all flag definitions
func (fm *FeatureManager) GetFlags() []FeatureFlag {
v := make([]FeatureFlag, 0, len(fm.flags))
for _, value := range fm.flags {
v = append(v, *value)
}
return v
}
// isFeatureEditingAllowed checks if the backend is properly configured to allow feature toggle changes from the UI
func (fm *FeatureManager) IsFeatureEditingAllowed() bool {
return fm.Settings.AllowEditing && fm.Settings.UpdateWebhook != ""
}
// indicate if a change has been made (not that accurate, but better than nothing)
func (fm *FeatureManager) IsRestartRequired() bool {
return fm.restartRequired
}
// Flags that can be edited
func (fm *FeatureManager) IsEditableFromAdminPage(key string) bool {
flag, ok := fm.flags[key]
if !ok ||
!fm.IsFeatureEditingAllowed() ||
!flag.AllowSelfServe ||
flag.Name == FlagFeatureToggleAdminPage {
return false
}
return flag.Stage == FeatureStageGeneralAvailability ||
flag.Stage == FeatureStagePublicPreview ||
flag.Stage == FeatureStageDeprecated
}
// Flags that should not be shown in the UI (regardless of their state)
func (fm *FeatureManager) IsHiddenFromAdminPage(key string, lenient bool) bool {
_, hide := fm.Settings.HiddenToggles[key]
flag, ok := fm.flags[key]
if !ok || flag.HideFromAdminPage || hide {
return true // unknown flag (should we show it as a warning!)
}
// Explicitly hidden from configs
_, found := fm.Settings.HiddenToggles[key]
if found {
return true
}
if lenient {
return false
}
return flag.Stage == FeatureStageUnknown ||
flag.Stage == FeatureStageExperimental ||
flag.Stage == FeatureStagePrivatePreview
}
// Get the flags that were explicitly set on startup
func (fm *FeatureManager) GetStartupFlags() map[string]bool {
return fm.startup
}
// Perhaps expose the flag warnings
func (fm *FeatureManager) GetWarning() map[string]string {
return fm.warnings
}
func (fm *FeatureManager) SetRestartRequired() {
fm.restartRequired = true
}
// ############# Test Functions #############
func WithFeatures(spec ...any) FeatureToggles {
return WithManager(spec...)
}
// WithFeatures is used to define feature toggles for testing.
// The arguments are a list of strings that are optionally followed by a boolean value for example:
// WithFeatures([]any{"my_feature", "other_feature"}) or WithFeatures([]any{"my_feature", true})
func WithManager(spec ...any) *FeatureManager {
count := len(spec)
features := make(map[string]*FeatureFlag, count)
enabled := make(map[string]bool, count)
idx := 0
for idx < count {
key := fmt.Sprintf("%v", spec[idx])
val := true
idx++
if idx < count && reflect.TypeOf(spec[idx]).Kind() == reflect.Bool {
val = spec[idx].(bool)
idx++
}
features[key] = &FeatureFlag{Name: key}
if val {
enabled[key] = true
}
}
return &FeatureManager{enabled: enabled, flags: features, startup: enabled, warnings: map[string]string{}}
}
// WithFeatureManager is used to define feature toggle manager for testing.
// It should be used when your test feature toggles require metadata beyond `Name` and `Enabled`.
// You should provide a feature toggle Name at a minimum.
func WithFeatureManager(cfg setting.FeatureMgmtSettings, flags []*FeatureFlag, disabled ...string) *FeatureManager {
count := len(flags)
features := make(map[string]*FeatureFlag, count)
enabled := make(map[string]bool, count)
dis := make(map[string]bool)
for _, v := range disabled {
dis[v] = true
}
for _, f := range flags {
if f.Name == "" {
continue
}
features[f.Name] = f
enabled[f.Name] = !dis[f.Name]
}
return &FeatureManager{
Settings: cfg,
enabled: enabled,
flags: features,
startup: enabled,
warnings: map[string]string{},
}
}