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 4 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 @@ -36,6 +36,9 @@ Main (unreleased)

- Introduce ebpf exporter v2 integration. (@tpaschalis)
tpaschalis marked this conversation as resolved.
Show resolved Hide resolved

- Add ability to configure agent to report to grafana.com the usage of feature flags (@marctc)
marctc marked this conversation as resolved.
Show resolved Hide resolved


### Enhancements

- integrations-next: Integrations using autoscrape will now autoscrape metrics
marctc marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
9 changes: 9 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 @@ -258,6 +266,7 @@ func (ep *Entrypoint) Stop() {
ep.mut.Lock()
defer ep.mut.Unlock()

ep.reporter.Service.StopAsync()
ep.integrations.Stop()
ep.lokiLogs.Stop()
ep.promMetrics.Stop()
Expand Down
5 changes: 5 additions & 0 deletions docs/user/configuration/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ 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 usage of feature flags
marctc marked this conversation as resolved.
Show resolved Hide resolved

Grafana Agent provides the flag `-usage-report` to report the usage of feature flags to grafana.com.
marctc marked this conversation as resolved.
Show resolved Hide resolved
This helps to understand which enabled features are being used of your running Agent instance (all data sent is anonymous).
marctc marked this conversation as resolved.
Show resolved Hide resolved

## 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
14 changes: 13 additions & 1 deletion 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: false,
marctc marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 @@ -354,13 +358,16 @@ func load(fs *flag.FlagSet, args []string, loader loaderFunc) (*Config, error) {
file string
fileType string
configExpandEnv bool
usageStats 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(&configExpandEnv, "config.expand-env", false, "Expands ${var} in config according to the values of the environment variables.")
fs.BoolVar(&usageStats, "usage-report", false, "When enabled sends usage info of enabled feature flags to Grafana")
tpaschalis marked this conversation as resolved.
Show resolved Hide resolved
cfg.RegisterFlags(fs)

features.Register(fs, allFeatures)

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

if usageStats {
cfg.EnableUsageReport = true
cfg.EnabledFeatures = features.GetAllEnabled(fs, allFeatures)
}

// 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, ff []Feature) []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
174 changes: 174 additions & 0 deletions pkg/usagestats/reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package usagestats

import (
"context"
"errors"
"io/ioutil"
"math"
"os"
"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/grafana/dskit/services"
"github.com/grafana/loki/pkg/util/build"
)

const (
// File name for the cluster seed file.
clusterSeedFileName = "cluster_seed.json"
mattdurham marked this conversation as resolved.
Show resolved Hide resolved
)

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

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

cluster *ClusterSeed
lastReport time.Time
}

// 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,
}

if cfg.EnableUsageReport {
r.Service = services.NewBasicService(nil, r.start, nil)
err := r.Service.StartAsync(context.Background())
if err != nil {
return nil, err
}
} else {
// builds an empty service
r.Service = services.NewBasicService(nil, nil, nil)
}
return r, nil
}

func (rep *Reporter) init(ctx context.Context) error {
if fileExists(clusterSeedFileName) {
seed, err := rep.readSeedFile()
rep.cluster = seed
return err
} else {
rep.cluster = &ClusterSeed{
UID: uuid.NewString(),
PrometheusVersion: build.GetVersion(),
CreatedAt: time.Now(),
}
return rep.writeSeedFile(*rep.cluster)
}
}

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

// readSeedFile reads the cluster seed file
func (rep *Reporter) readSeedFile() (*ClusterSeed, error) {
data, err := ioutil.ReadFile(clusterSeedFileName)
if err != nil {
return nil, err
}
seed, err := JSONCodec.Decode(data)
if err != nil {
return nil, err
}
return seed.(*ClusterSeed), nil
}

// writeSeedFile writes the cluster seed file
func (rep *Reporter) writeSeedFile(seed ClusterSeed) error {
data, err := JSONCodec.Encode(seed)
if err != nil {
return err
}
return ioutil.WriteFile(clusterSeedFileName, data, 0644)
}

// 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.cluster.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.cluster, 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 cluster seed to avoid all cluster 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