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

feat: usage stats #510

Merged
merged 5 commits into from
Oct 30, 2024
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ integration: run-test-image-locally dev

# Compilation
dev:
go build -ldflags "-X main.Version=dev-${VERSION}" ./cmd/grr
go build -ldflags "-X github.com/grafana/grizzly/pkg/config.Version=dev-${VERSION}" ./cmd/grr

LDFLAGS := '-s -w -extldflags "-static" -X main.Version=${VERSION}'
LDFLAGS := '-s -w -extldflags "-static" -X github.com/grafana/grizzly/pkg/config.Version=${VERSION}'
static:
CGO_ENABLED=0 GOOS=linux go build -ldflags=${LDFLAGS} ./cmd/grr

Expand Down
1 change: 1 addition & 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
6 changes: 1 addition & 5 deletions cmd/grr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ import (
log "github.com/sirupsen/logrus"
)

// Version is the current version of the grr command.
// To be overwritten at build time
var Version = "dev"

type silentError struct {
Err error
}
Expand All @@ -34,7 +30,7 @@ func main() {
rootCmd := &cli.Command{
Use: "grr",
Short: "Grizzly",
Version: Version,
Version: config.Version,
}

log.SetFormatter(&log.TextFormatter{
Expand Down
7 changes: 4 additions & 3 deletions cmd/grr/selfupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/go-clix/cli"
"github.com/grafana/grizzly/internal/grizzly"
"github.com/grafana/grizzly/pkg/config"
)

func selfUpdateCmd() *cli.Command {
Expand All @@ -21,12 +22,12 @@ func selfUpdateCmd() *cli.Command {
cmd.Run = func(cmd *cli.Command, args []string) error {
updater := grizzly.NewSelfUpdater(http.DefaultClient)

newVersion, err := updater.UpdateSelf(context.Background(), Version)
newVersion, err := updater.UpdateSelf(context.Background(), config.Version)
if errors.Is(err, grizzly.ErrNextVersionIsMajorBump) {
return fmt.Errorf("self-update aborted as the next version (%[1]s) is a major bump from the current one (%[2]s). Please update manually: https://github.com/grafana/grizzly/releases/tag/%[1]s", newVersion, Version)
return fmt.Errorf("self-update aborted as the next version (%[1]s) is a major bump from the current one (%[2]s). Please update manually: https://github.com/grafana/grizzly/releases/tag/%[1]s", newVersion, config.Version)
}
if errors.Is(err, grizzly.ErrCurrentVersionIsLatest) {
fmt.Printf("Current version is the latest: %s\n", Version)
fmt.Printf("Current version is the latest: %s\n", config.Version)
return nil
}
if err != nil {
Expand Down
17 changes: 13 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 @@ -523,6 +521,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")
theSuess marked this conversation as resolved.
Show resolved Hide resolved

return initialiseLogging(cmd, &opts.LoggingOpts)
}

Expand Down Expand Up @@ -558,6 +558,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 || config.UsageStatsDisabled() {
return wr
}
return grizzly.NewUsageRecorder(wr)
}

func getOutputFormat(opts Opts) (string, bool, error) {
var onlySpec bool
context, err := config.CurrentContext()
Expand Down
5 changes: 4 additions & 1 deletion docs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ menu:
- name: Hidden Elements
url: /hidden-elements/
weight: 11
- name: Usage statistics
url: /usage-statistics/
weight: 12
- name: GitHub
url: https://github.com/grafana/grizzly/
weight: 12
weight: 13

markup:
defaultMarkdownHandler: goldmark
Expand Down
25 changes: 25 additions & 0 deletions docs/content/usage-statistics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
date: "2024-10-24T00:00:00+00:00"
title: "Usage statistics"
---

By default, Grizzly sends anonymous, but uniquely identifiable usage information to Grafana Labs. These statistics are sent to stats.grafana.org.

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

The usage information includes the following details:

* A hash of your config file
* Timestamp of when the report was created
* The version of Grizzly.
* The operating system Grizzly is running on.
* The system architecture Grizzly is running on.
* The operation performed (`pull`/`apply`)
* The number of resources affected

This list may change over time.
For auditing, the code performing this reporting can be found at the end of the [events.go](https://github.com/grafana/grizzly/blob/main/pkg/grizzly/events.go) file.

## Opt-out of data collection

You can use the `--disable-reporting` command line flag to disable the reporting and opt-out of the data collection.
22 changes: 21 additions & 1 deletion 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 All @@ -16,9 +17,14 @@ import (
)

const (
CurrentContextSetting = "current-context"
CurrentContextSetting = "current-context"
DisableReportingSetting = "disable-reporting"
)

// Version is the current version of the grr command.
// To be overwritten at build time
var Version = "dev"

func Initialise() {
viper.SetConfigName("settings")
viper.SetConfigType("yaml")
Expand Down Expand Up @@ -142,6 +148,10 @@ func UseContext(context string) error {
return fmt.Errorf("context %s not found", context)
}

func UsageStatsDisabled() bool {
return viper.GetBool(DisableReportingSetting)
}

func CurrentContext() (*Context, error) {
name := viper.GetString(CurrentContextSetting)
if name == "" {
Expand Down Expand Up @@ -183,6 +193,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
Loading