Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature gate implementation and documentation #4108

Merged
merged 17 commits into from
Oct 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions service/featuregate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Collector Feature Gates

This package provides a mechanism that allows operators to enable and disable
experimental or transitional features at deployment time. These flags should
be able to govern the behavior of the application starting as early as possible
and should be available to every component such that decisions may be made
based on flags at the component level.

## Usage

Feature gates must be defined and registered with the global registry in
an `init()` function. This makes the `Gate` available to be configured and
queried with a default value of its `Enabled` property.

```go
const myFeatureGateID = "namespaced.uniqueIdentifier"

func init() {
Aneurysm9 marked this conversation as resolved.
Show resolved Hide resolved
featuregate.Register(featuregate.Gate{
ID: fancyNewFeatureGate,
Description: "A brief description of what the gate controls",
Enabled: false,
})
}
```

The status of the gate may later be checked by interrogating the global
feature gate registry:

```go
if featuregate.IsEnabled(myFeatureGateID) {
setupNewFeature()
}
```

Note that querying the registry takes a read lock and accesses a map, so it
should be done once and the result cached for local use if repeated checks
are required. Avoid querying the registry in a loop.
Aneurysm9 marked this conversation as resolved.
Show resolved Hide resolved

## Controlling Gates

```
N.B.: This feature has not yet been implemented and is subject to change.
```

Feature gates can be enabled or disabled via the CLI, with the
`--feature-gates` flag. When using the CLI flag, gate
identifiers must be presented as a comma-delimited list. Gate identifiers
prefixed with `-` will disable the gate and prefixing with `+` or with no
prefix will enable the gate.

```shell
otelcol --config=config.yaml --feature-gates=gate1,-gate2,+gate3
```

This will enable `gate1` and `gate3` and disable `gate2`.

## Feature Lifecycle

Features controlled by a `Gate` should follow a three-stage lifecycle,
modeled after the [system used by Kubernetes](https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/#feature-stages):

1. An `alpha` stage where the feature is disabled by default and must be enabled
through a `Gate`.
2. A `beta` stage where the feature has been well tested and is enabled by
default but can be disabled through a `Gate`.
3. A generally available stage where the feature is permanently enabled and
the `Gate` is no longer operative.

Features that prove unworkable in the `alpha` stage may be discontinued
without proceeding to the `beta` stage. Features that make it to the `beta`
stage will not be dropped and will eventually reach general availability
where the `Gate` that allowed them to be disabled during the `beta` stage
will be removed.
101 changes: 101 additions & 0 deletions service/featuregate/gates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package featuregate

import (
"fmt"
"sync"
)

// Gate represents an individual feature that may be enabled or disabled based
// on the lifecycle state of the feature and CLI flags specified by the user.
type Gate struct {
ID string
Description string
Enabled bool
}

var reg = &registry{gates: make(map[string]Gate)}

// IsEnabled returns true if a registered feature gate is enabled and false otherwise.
func IsEnabled(id string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aneurysm9 maybe as a prototype after this PR is merged, we can give a try to have the "Registry" public, with the equivalent funcs on it, and have only a "GetRegistry" func that gets the global one. This way for testing users can pass a "non-global" registry in their code, so limit the interaction with the global state.

return reg.isEnabled(id)
}

// List returns a slice of copies of all registered Gates.
func List() []Gate {
return reg.list()
}

// Register a Gate. May only be called in an init() function.
// Will panic() if a Gate with the same ID is already registered.
func Register(g Gate) {
if err := reg.add(g); err != nil {
panic(err)
}
}

// Apply a configuration in the form of a map of Gate identifiers to boolean values.
Aneurysm9 marked this conversation as resolved.
Show resolved Hide resolved
// Sets only those values provided in the map, other gate values are not changed.
func Apply(cfg map[string]bool) {
reg.apply(cfg)
}

type registry struct {
sync.RWMutex
gates map[string]Gate
}

func (r *registry) apply(cfg map[string]bool) {
r.Lock()
defer r.Unlock()
for id, val := range cfg {
if g, ok := r.gates[id]; ok {
g.Enabled = val
r.gates[g.ID] = g
}
}
}

func (r *registry) add(g Gate) error {
r.Lock()
defer r.Unlock()
if _, ok := r.gates[g.ID]; ok {
return fmt.Errorf("attempted to add pre-existing gate %q", g.ID)
}

r.gates[g.ID] = g
return nil
}

func (r *registry) isEnabled(id string) bool {
r.RLock()
defer r.RUnlock()
g, ok := r.gates[id]
return ok && g.Enabled
}

func (r *registry) list() []Gate {
r.RLock()
defer r.RUnlock()
ret := make([]Gate, len(r.gates))
i := 0
for _, gate := range r.gates {
ret[i] = gate
i++
}

return ret
}
66 changes: 66 additions & 0 deletions service/featuregate/gates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package featuregate

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRegistry(t *testing.T) {
r := registry{gates: map[string]Gate{}}

gate := Gate{
ID: "foo",
Description: "Test Gate",
Enabled: true,
}

assert.Empty(t, r.list())
assert.False(t, r.isEnabled(gate.ID))

assert.NoError(t, r.add(gate))
assert.Len(t, r.list(), 1)
assert.True(t, r.isEnabled(gate.ID))

r.apply(map[string]bool{gate.ID: false})
assert.False(t, r.isEnabled(gate.ID))

assert.Error(t, r.add(gate))
}

func TestGlobalRegistry(t *testing.T) {
gate := Gate{
ID: "feature_gate_test.foo",
Description: "Test Gate",
Enabled: true,
}

assert.NotContains(t, List(), gate)
assert.False(t, IsEnabled(gate.ID))

assert.NotPanics(t, func() { Register(gate) })
assert.Contains(t, List(), gate)
assert.True(t, IsEnabled(gate.ID))

Apply(map[string]bool{gate.ID: false})
assert.False(t, IsEnabled(gate.ID))

assert.Panics(t, func() { Register(gate) })
reg.Lock()
delete(reg.gates, gate.ID)
reg.Unlock()
}