Skip to content

Commit

Permalink
feat: add anonymous usage reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
theSuess committed Oct 24, 2024
1 parent b2df115 commit 0510544
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 9 deletions.
19 changes: 19 additions & 0 deletions cmd/grr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Opts struct {
JsonnetPaths []string
Targets []string
OutputFormat string
DisableStats bool
IsDir bool // used internally to denote that the resource path argument pointed at a directory

// Used for supporting resources without envelopes
Expand Down Expand Up @@ -197,6 +198,24 @@ func createContextCmd() *cli.Command {
return initialiseLogging(cmd, &opts)
}

func hashCmd() *cli.Command {
cmd := &cli.Command{
Use: "hash",
Short: "Generate a hash over all configuration values",
}
var opts LoggingOpts

cmd.Run = func(cmd *cli.Command, args []string) error {
hash, err := config.Hash()
if err != nil {
return err
}
fmt.Printf("Config Hash: %s\n", hash)
return nil
}
return initialiseLogging(cmd, &opts)
}

func checkCmd(registry grizzly.Registry) *cli.Command {
cmd := &cli.Command{
Use: "check",
Expand Down
18 changes: 14 additions & 4 deletions cmd/grr/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ func pullCmd(registry grizzly.Registry) *cli.Command {

cmd.Flags().BoolVarP(&continueOnError, "continue-on-error", "e", false, "don't stop pulling on error")

eventsRecorder := grizzly.NewWriterRecorder(os.Stdout, getEventFormatter())

cmd.Run = func(cmd *cli.Command, args []string) error {
eventsRecorder := getEventsRecorder(opts)
format, onlySpec, err := getOutputFormat(opts)
if err != nil {
return err
Expand Down Expand Up @@ -219,9 +218,8 @@ func applyCmd(registry grizzly.Registry) *cli.Command {

cmd.Flags().BoolVarP(&continueOnError, "continue-on-error", "e", false, "don't stop apply on first error")

eventsRecorder := grizzly.NewWriterRecorder(os.Stdout, getEventFormatter())

cmd.Run = func(cmd *cli.Command, args []string) error {
eventsRecorder := getEventsRecorder(opts)
resourceKind, folderUID, err := getOnlySpec(opts)
if err != nil {
return err
Expand Down Expand Up @@ -509,6 +507,7 @@ func configCmd(registry grizzly.Registry) *cli.Command {
cmd.AddCommand(unsetCmd())
cmd.AddCommand(createContextCmd())
cmd.AddCommand(checkCmd(registry))
cmd.AddCommand(hashCmd())
return cmd
}

Expand All @@ -523,6 +522,8 @@ func initialiseCmd(cmd *cli.Command, opts *Opts) *cli.Command {
cmd.Flags().StringSliceVarP(&opts.JsonnetPaths, "jpath", "J", getDefaultJsonnetFolders(), "Specify an additional library search dir (right-most wins)")
cmd.Flags().StringVarP(&opts.OutputFormat, "output", "o", "", "Output format")

cmd.Flags().BoolVar(&opts.DisableStats, "disable-reporting", false, "disable sending of anonymous usage stats to Grafana Labs")

return initialiseLogging(cmd, &opts.LoggingOpts)
}

Expand Down Expand Up @@ -558,6 +559,15 @@ func initialiseLogging(cmd *cli.Command, loggingOpts *LoggingOpts) *cli.Command
func getDefaultJsonnetFolders() []string {
return []string{"vendor", "lib", "."}
}

func getEventsRecorder(opts Opts) grizzly.EventsRecorder {
wr := grizzly.NewWriterRecorder(os.Stdout, getEventFormatter())
if opts.DisableStats {
return wr
}
return grizzly.NewUsageRecorder(wr)
}

func getOutputFormat(opts Opts) (string, bool, error) {
var onlySpec bool
context, err := config.CurrentContext()
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"os"
Expand Down Expand Up @@ -187,6 +188,16 @@ var acceptableKeys = map[string]string{
"only-spec": "bool",
}

func Hash() (string, error) {
cfg := viper.AllSettings()
out := sha256.New()
err := json.NewEncoder(out).Encode(cfg)
if err != nil {
return "", fmt.Errorf("writing to hash")
}
return fmt.Sprintf("%X", out.Sum(nil)), nil
}

func Get(path, outputFormat string) (string, error) {
ctx := viper.GetString(CurrentContextSetting)

Expand Down
82 changes: 82 additions & 0 deletions pkg/grizzly/events.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package grizzly

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"time"

"github.com/fatih/color"
"github.com/grafana/grizzly/pkg/config"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
)

type EventSeverity uint8
Expand Down Expand Up @@ -114,3 +123,76 @@ func (recorder *WriterRecorder) Record(event Event) {
func (recorder *WriterRecorder) Summary() Summary {
return *recorder.summary
}

var _ EventsRecorder = (*WriterRecorder)(nil)

type UsageRecorder struct {
wr *WriterRecorder
endpoint string
}

// Record implements EventsRecorder.
func (u *UsageRecorder) Record(event Event) {
u.wr.Record(event)
}

// Summary implements EventsRecorder.
func (u *UsageRecorder) Summary() Summary {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
group, ctx := errgroup.WithContext(ctx)
for op, n := range u.wr.summary.EventCounts {
group.Go(func() error {
return u.reportUsage(ctx, op.ID, n)
})
}
err := group.Wait()
if err != nil {
log.Debugf("failed to send usage stats: %v", err)
}

return u.wr.Summary()
}

func (u *UsageRecorder) reportUsage(ctx context.Context, op string, n int) error {
var buff bytes.Buffer
configHash, err := config.Hash()
if err != nil {
configHash = "failed-to-hash-config"
}
err = json.NewEncoder(&buff).Encode(map[string]interface{}{
"uuid": configHash,
"arch": runtime.GOARCH,
"os": runtime.GOOS,
"resources": n,
"operation": op,
"createdAt": time.Now(),
"version": config.Version,
})
if err != nil {
return fmt.Errorf("encoding usage report")
}
req, err := http.NewRequest(http.MethodPost, u.endpoint, &buff)
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Add("content-type", "application/json")
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("sending post request: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("expected OK, got %s", resp.Status)
}
return nil
}

var _ EventsRecorder = (*UsageRecorder)(nil)

func NewUsageRecorder(wr *WriterRecorder) *UsageRecorder {
return &UsageRecorder{
wr: wr,
endpoint: "https://stats.grafana.org/grizzly-usage-report",
}
}
11 changes: 6 additions & 5 deletions pkg/grizzly/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func listWide(listedResources []listedResource) ([]byte, error) {
// Pull pulls remote resources and stores them in the local file system.
// The given resourcePath must be a directory, where all resources will be stored.
// If opts.JSONSpec is true, which is only applicable for dashboards, saves the spec as a JSON file.
func Pull(registry Registry, resourcePath string, onlySpec bool, outputFormat string, targets []string, continueOnError bool, eventsRecorder eventsRecorder) error {
func Pull(registry Registry, resourcePath string, onlySpec bool, outputFormat string, targets []string, continueOnError bool, eventsRecorder EventsRecorder) error {
resourcePathIsFile, err := isFile(resourcePath)
if err != nil {
return err
Expand Down Expand Up @@ -377,12 +377,13 @@ func Diff(registry Registry, resources Resources, onlySpec bool, outputFormat st
return nil
}

type eventsRecorder interface {
type EventsRecorder interface {
Record(event Event)
Summary() Summary
}

// Apply pushes resources to endpoints
func Apply(registry Registry, resources Resources, continueOnError bool, eventsRecorder eventsRecorder) error {
func Apply(registry Registry, resources Resources, continueOnError bool, eventsRecorder EventsRecorder) error {
var finalErr error

for _, resource := range resources.AsList() {
Expand All @@ -405,7 +406,7 @@ func Apply(registry Registry, resources Resources, continueOnError bool, eventsR
return finalErr
}

func applyResource(registry Registry, resource Resource, trailRecorder eventsRecorder) error {
func applyResource(registry Registry, resource Resource, trailRecorder EventsRecorder) error {
resourceRef := resource.Ref().String()

handler, err := registry.GetHandler(resource.Kind())
Expand Down Expand Up @@ -489,7 +490,7 @@ func Snapshot(registry Registry, resources Resources, expiresSeconds int) error

// Watch watches a directory for changes then pushes Jsonnet resource to endpoints
// when changes are noticed.
func Watch(registry Registry, watchDir string, resourcePath string, parser Parser, parserOpts ParserOptions, trailRecorder eventsRecorder) error {
func Watch(registry Registry, watchDir string, resourcePath string, parser Parser, parserOpts ParserOptions, trailRecorder EventsRecorder) error {
updateWatchedResource := func(path string) error {
log.Infof("Changes detected in %q. Applying %q", path, resourcePath)
resources, err := parser.Parse(resourcePath, parserOpts)
Expand Down

0 comments on commit 0510544

Please sign in to comment.