-
Notifications
You must be signed in to change notification settings - Fork 478
/
reporter.go
166 lines (148 loc) · 4.31 KB
/
reporter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package usagestats
import (
"context"
"encoding/json"
"errors"
"math"
"os"
"path/filepath"
"runtime"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"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
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) (*Reporter, error) {
r := &Reporter{
logger: logger,
}
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 := os.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 os.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, metricsFunc func() map[string]interface{}) 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 agent stats", "date", time.Now())
if err := rep.reportUsage(ctx, next, metricsFunc()); err != nil {
level.Info(rep.logger).Log("msg", "failed to report usage", "err", err)
continue
}
rep.lastReport = next
next = next.Add(reportInterval)
case <-ctx.Done():
return ctx.Err()
}
}
}
// reportUsage reports the usage to grafana.com.
func (rep *Reporter) reportUsage(ctx context.Context, interval time.Time, metrics map[string]interface{}) 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, metrics); 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()
}
// 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 {
duration := math.Ceil(float64(now.Sub(createdAt)) / float64(interval))
return createdAt.Add(time.Duration(duration) * interval)
}