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

Add usage reporter to track feature flags #1661

Merged
merged 33 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9b2f7d0
Add usage reporter to track feature flags
marctc Apr 22, 2022
c8787ce
Merge branch 'main' into features_flag
marctc Apr 29, 2022
80ede21
Add comments to exported structs
marctc Apr 29, 2022
1597ee0
Fix lint issues
marctc Apr 29, 2022
0969737
Update CHANGELOG.md
marctc May 2, 2022
a5eb479
Update docs/user/configuration/flags.md
marctc May 2, 2022
311f31b
Update docs/user/configuration/flags.md
marctc May 2, 2022
062f056
Update CHANGELOG.md
marctc May 2, 2022
967e681
Remove unneeded param
marctc May 2, 2022
d290d79
Remove unneeded param
marctc May 2, 2022
77fd3da
Refactor
marctc May 2, 2022
939f31d
Merge branch 'main' into features_flag
marctc May 2, 2022
fe68013
Fix linter
marctc May 2, 2022
56d7d8b
Update doc
marctc May 2, 2022
09ec60c
Fix tests
marctc May 2, 2022
93a28c8
Renamings
marctc May 2, 2022
c0ee78f
Fix
marctc May 2, 2022
33a3dbd
Use StatusOK
marctc May 2, 2022
3cc5c03
Fix CHANGELOG
marctc May 2, 2022
64435f6
Fix CHANGELOG
marctc May 2, 2022
d663744
Use standard json lib
marctc May 3, 2022
e02ab98
Seed file OS compatible
marctc May 4, 2022
8fa757c
Update docs/user/configuration/flags.md
marctc May 4, 2022
fbd216b
Change report time to 4h
marctc May 4, 2022
02dcc14
Opt-out by default
marctc May 4, 2022
3825cf0
Update pkg/config/config.go
marctc May 5, 2022
7147d26
Change to disable-reporting
marctc May 5, 2022
d12888c
Update docs/user/configuration/flags.md
marctc May 18, 2022
aacbe7a
Update docs/user/configuration/flags.md
marctc May 18, 2022
c81b244
Update docs/user/configuration/flags.md
marctc May 18, 2022
b17d9c0
Merge branch 'main' into features_flag
marctc May 18, 2022
2d57d7a
refactor func
marctc May 19, 2022
fcaa74e
remove link
marctc May 20, 2022
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Main (unreleased)

- Introduce SNMP exporter integration. (@v-zhuravlev)

- Configure the agent to report the use of feature flags to grafana.com. (@marctc)


### Enhancements

- integrations-next: Integrations using autoscrape will now autoscrape metrics
marctc marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
19 changes: 19 additions & 0 deletions cmd/agent/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/grafana/agent/pkg/metrics/instance"
"github.com/grafana/agent/pkg/server"
"github.com/grafana/agent/pkg/traces"
"github.com/grafana/agent/pkg/usagestats"
"github.com/oklog/run"
"google.golang.org/grpc"
"gopkg.in/yaml.v2"
Expand All @@ -42,6 +43,7 @@ type Entrypoint struct {
lokiLogs *logs.Logs
tempoTraces *traces.Traces
integrations config.Integrations
reporter *usagestats.Reporter

reloadListener net.Listener
reloadServer *http.Server
Expand Down Expand Up @@ -84,11 +86,17 @@ func NewEntrypoint(logger *server.Logger, cfg *config.Config, reloader Reloader)
if err != nil {
return nil, err
}

ep.integrations, err = config.NewIntegrations(logger, &cfg.Integrations, integrationGlobals)
if err != nil {
return nil, err
}

ep.reporter, err = usagestats.NewReporter(logger, cfg)
if err != nil {
return nil, err
}

ep.wire(ep.srv.HTTP, ep.srv.GRPC)

// Mostly everything should be up to date except for the server, which hasn't
Expand Down Expand Up @@ -311,6 +319,17 @@ func (ep *Entrypoint) Start() error {
srvCancel()
})

ep.mut.Lock()
cfg := ep.cfg
ep.mut.Unlock()
if cfg.EnableUsageReport {
g.Add(func() error {
return ep.reporter.Start(srvContext)
}, func(e error) {
srvCancel()
})
}

go func() {
for range notifier {
ep.TriggerReload()
Expand Down
12 changes: 12 additions & 0 deletions docs/user/configuration/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ Valid feature names are:
* `dynamic-config`: Enable support for [dynamic configuration]({{< relref "./dynamic-config" >}})
* `extra-scrape-metrics`: When enabled, additional time series are exposed for each metrics instance scrape. See [Extra scrape metrics](https://prometheus.io/docs/prometheus/latest/feature_flags/#extra-scrape-metrics).

### Report use of feature flags

By default, Grafana Agent sends anonymous, but uniquely-identifiable information
of the enabled feature flags from your running Grafana Agent instance to Grafana Labs.
These statistics are sent to https://stats.grafana.org/.

Statistics help us better understand how Grafana Agent is used.
This helps us prioritize features and documentation.

If you would like to disable the reporting, Grafana Agent provides the flag `-disable-reporting`
to stop the reporting.

## Configuration file

* `-config.file`: Path to the configuration file to load. May be an HTTP(s) URL when the `remote-configs` feature is enabled
marctc marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
25 changes: 19 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var DefaultConfig = Config{
Metrics: metrics.DefaultConfig,
Integrations: DefaultVersionedIntegrations,
EnableConfigEndpoints: false,
EnableUsageReport: true,
}

// Config contains underlying configurations for the agent
Expand All @@ -74,6 +75,10 @@ type Config struct {

// Toggle for config endpoint(s)
EnableConfigEndpoints bool `yaml:"-"`

// Report enabled features options
EnableUsageReport bool `yaml:"-"`
EnabledFeatures []string `yaml:"-"`
}

// UnmarshalYAML implements yaml.Unmarshaler.
Expand Down Expand Up @@ -334,7 +339,6 @@ func Load(fs *flag.FlagSet, args []string) (*Config, error) {
} else if expandArgs {
return fmt.Errorf("-config.expand-env can not be used with file type %s", fileTypeDynamic)
}

return LoadDynamicConfiguration(path, expandArgs, c)
default:
return fmt.Errorf("unknown file type %q. accepted values: %s", fileType, strings.Join(fileTypes, ", "))
Expand All @@ -350,17 +354,20 @@ func load(fs *flag.FlagSet, args []string, loader loaderFunc) (*Config, error) {
var (
cfg = DefaultConfig

printVersion bool
file string
fileType string
configExpandEnv bool
printVersion bool
file string
fileType string
configExpandEnv bool
disableReporting bool
)

fs.StringVar(&file, "config.file", "", "configuration file to load")
fs.StringVar(&fileType, "config.file.type", "yaml", fmt.Sprintf("Type of file pointed to by -config.file flag. Supported values: %s. %s requires dynamic-config and integrations-next features to be enabled.", strings.Join(fileTypes, ", "), fileTypeDynamic))
fs.BoolVar(&printVersion, "version", false, "Print this build's version information")
fs.BoolVar(&printVersion, "version", false, "Print this build's version information.")
fs.BoolVar(&configExpandEnv, "config.expand-env", false, "Expands ${var} in config according to the values of the environment variables.")
fs.BoolVar(&disableReporting, "disable-reporting", false, "Disable reporting of enabled feature flags to Grafana.")
cfg.RegisterFlags(fs)

features.Register(fs, allFeatures)

if err := fs.Parse(args); err != nil {
Expand Down Expand Up @@ -399,6 +406,12 @@ func load(fs *flag.FlagSet, args []string, loader loaderFunc) (*Config, error) {
cfg.Metrics.Global.ExtraMetrics = true
}

if disableReporting {
cfg.EnableUsageReport = false
} else {
cfg.EnabledFeatures = features.GetAllEnabled(fs)
}

// Finally, apply defaults to config that wasn't specified by file or flag
if err := cfg.Validate(fs); err != nil {
return nil, fmt.Errorf("error in config file: %w", err)
Expand Down
19 changes: 18 additions & 1 deletion pkg/config/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func normalize(f Feature) Feature {
return Feature(strings.ToLower(string(f)))
}

// Enabled retruns true if a feature is enabled. Enable will panic if fs has
// Enabled returns true if a feature is enabled. Enable will panic if fs has
// not been passed to Register or name is an unknown feature.
func Enabled(fs *flag.FlagSet, name Feature) bool {
name = normalize(name)
Expand Down Expand Up @@ -113,6 +113,23 @@ func Validate(fs *flag.FlagSet, deps []Dependency) error {
return err
}

// GetAllEnabled returns the list of all enabled features
func GetAllEnabled(fs *flag.FlagSet) []string {
f := fs.Lookup(setFlagName)
if f == nil {
panic("feature flag not registered to fs")
}
s, ok := f.Value.(*set)
if !ok {
panic("registered feature flag not appropriate type")
}
var enabled []string
for feature := range s.enabled {
enabled = append(enabled, string(feature))
}
return enabled
}

// set implements flag.Value and holds the set of enabled features.
// set should be provided to a flag.FlagSet with:
//
Expand Down
176 changes: 176 additions & 0 deletions pkg/usagestats/reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package usagestats

import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"math"
"os"
"path/filepath"
"runtime"
"time"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"github.com/grafana/agent/pkg/config"
"github.com/grafana/dskit/backoff"
"github.com/grafana/dskit/multierror"
"github.com/prometheus/common/version"
)

var (
reportCheckInterval = time.Minute
reportInterval = 4 * time.Hour
)

// Reporter holds the agent seed information and sends report of usage
type Reporter struct {
logger log.Logger
cfg *config.Config

agentSeed *AgentSeed
lastReport time.Time
}

// AgentSeed identifies a unique agent
type AgentSeed struct {
UID string `json:"UID"`
CreatedAt time.Time `json:"created_at"`
Version string `json:"version"`
}

// NewReporter creates a Reporter that will send periodically reports to grafana.com
func NewReporter(logger log.Logger, cfg *config.Config) (*Reporter, error) {
r := &Reporter{
logger: logger,
cfg: cfg,
}
return r, nil
}

func (rep *Reporter) init(ctx context.Context) error {
path := agentSeedFileName()

if fileExists(path) {
seed, err := rep.readSeedFile(path)
rep.agentSeed = seed
return err
}
rep.agentSeed = &AgentSeed{
UID: uuid.NewString(),
Version: version.Version,
CreatedAt: time.Now(),
}
return rep.writeSeedFile(*rep.agentSeed, path)
}

func fileExists(path string) bool {
_, err := os.Stat(path)
return !errors.Is(err, os.ErrNotExist)
}

// readSeedFile reads the agent seed file
func (rep *Reporter) readSeedFile(path string) (*AgentSeed, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
seed := &AgentSeed{}
err = json.Unmarshal(data, seed)
if err != nil {
return nil, err
}
return seed, nil
}

// writeSeedFile writes the agent seed file
func (rep *Reporter) writeSeedFile(seed AgentSeed, path string) error {
data, err := json.Marshal(seed)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0644)
}

func agentSeedFileName() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "agent_seed.json")
}
// linux/mac
return "/tmp/agent_seed.json"
}

// Start inits the reporter seed and start sending report for every interval
func (rep *Reporter) Start(ctx context.Context) error {
level.Info(rep.logger).Log("msg", "running usage stats reporter")
err := rep.init(ctx)
if err != nil {
level.Info(rep.logger).Log("msg", "failed to init seed", "err", err)
return err
}

// check every minute if we should report.
ticker := time.NewTicker(reportCheckInterval)
defer ticker.Stop()

// find when to send the next report.
next := nextReport(reportInterval, rep.agentSeed.CreatedAt, time.Now())
if rep.lastReport.IsZero() {
// if we never reported assumed it was the last interval.
rep.lastReport = next.Add(-reportInterval)
}
for {
select {
case <-ticker.C:
now := time.Now()
if !next.Equal(now) && now.Sub(rep.lastReport) < reportInterval {
continue
}
level.Info(rep.logger).Log("msg", "reporting cluster stats", "date", time.Now())
if err := rep.reportUsage(ctx, next); err != nil {
level.Info(rep.logger).Log("msg", "failed to report usage", "err", err)
continue
}
rep.lastReport = next
next = next.Add(reportInterval)
DanCech marked this conversation as resolved.
Show resolved Hide resolved
case <-ctx.Done():
return ctx.Err()
}
}
}

// reportUsage reports the usage to grafana.com.
func (rep *Reporter) reportUsage(ctx context.Context, interval time.Time) error {
backoff := backoff.New(ctx, backoff.Config{
MinBackoff: time.Second,
MaxBackoff: 30 * time.Second,
MaxRetries: 5,
})
var errs multierror.MultiError
for backoff.Ongoing() {
if err := sendReport(ctx, rep.agentSeed, interval, rep.getMetrics()); err != nil {
level.Info(rep.logger).Log("msg", "failed to send usage report", "retries", backoff.NumRetries(), "err", err)
errs.Add(err)
backoff.Wait()
continue
}
level.Info(rep.logger).Log("msg", "usage report sent with success")
return nil
}
return errs.Err()
}

func (rep *Reporter) getMetrics() map[string]interface{} {
return map[string]interface{}{
"enabled-features": rep.cfg.EnabledFeatures,
}
}

// nextReport compute the next report time based on the interval.
// The interval is based off the creation of the agent seed to avoid all agents reporting at the same time.
func nextReport(interval time.Duration, createdAt, now time.Time) time.Time {
// createdAt * (x * interval ) >= now
return createdAt.Add(time.Duration(math.Ceil(float64(now.Sub(createdAt))/float64(interval))) * interval)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe separate this into a few statements for readability

}
Loading