github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/usage/usage.go (about) 1 package usage 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "hash/fnv" 9 "net/http" 10 "runtime" 11 "strconv" 12 "time" 13 14 "github.com/grafana/synthetic-monitoring-agent/internal/feature" 15 sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring" 16 ) 17 18 // Reporter represents a way of communicating reports to different backend systems. 19 type Reporter interface { 20 ReportProbe(ctx context.Context, probe sm.Probe, features feature.Collection) error 21 } 22 23 // report represents a specific usage event that will be sent to https://stats.grafana.com to be processed and stored. 24 // Each attribute represents a column in a BigQuery table that can be easily searched. 25 // Adding new attributes to report will not automatically update the table, and instead needs to be handled in https://github.com/grafana/usage-stats 26 type report struct { 27 CreatedAt string `json:"createdAt"` 28 OS string `json:"os"` 29 Arch string `json:"arch"` 30 Report string `json:"report"` 31 Version string `json:"version"` 32 Public bool `json:"public"` 33 Features string `json:"features"` 34 UsageStatsId string `json:"usageStatsId"` 35 TenantID int64 `json:"tenantId"` 36 ProbeID int64 `json:"probeId"` 37 } 38 39 // HTTPReporter represents 40 type HTTPReporter struct { 41 endpoint string 42 client *http.Client 43 } 44 45 const ( 46 // UsageReportApplication aligns with the usage-stats service endpoint defined 47 // in github.com/grafana/usage-stats for synthetic monitoring agents 48 UsageStatsApplication = "synthetic-monitoring-agent-usage-report" 49 // Base Endpoint for usage stats 50 ProdStatsEndpoint = "https://stats.grafana.org" 51 ) 52 53 func NewHTTPReporter(endpoint string) *HTTPReporter { 54 return &HTTPReporter{ 55 endpoint: endpoint, 56 client: &http.Client{Timeout: 10 * time.Second}, 57 } 58 } 59 60 // submitReport is responsible for sending a report to the stats endpoint via an http POST request. The primary concern is that 61 // the http server responds with http.StatusOK. Otherwise, there are no other expected responses. 62 func (hr *HTTPReporter) submitReport(ctx context.Context, report *report) error { 63 jsonData, err := json.Marshal(&report) 64 if err != nil { 65 return fmt.Errorf("failed to marshal data: %w", err) 66 } 67 68 endpoint := fmt.Sprintf("%s/%s", hr.endpoint, UsageStatsApplication) 69 70 req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(jsonData)) 71 if err != nil { 72 return fmt.Errorf("failed to create request: %w", err) 73 } 74 75 req.Header.Set("Content-Type", "application/json") 76 77 resp, err := hr.client.Do(req) 78 if err != nil { 79 return fmt.Errorf("failed to do request: %w", err) 80 } 81 defer resp.Body.Close() 82 83 if resp.StatusCode != http.StatusOK { 84 return fmt.Errorf("unexpected status code: %d", resp.StatusCode) 85 } 86 87 return nil 88 } 89 90 // ReportProbe creates a report from the probe and then sends the report to the stats api endpoint via the report method. 91 func (hr *HTTPReporter) ReportProbe(ctx context.Context, probe sm.Probe, features feature.Collection) error { 92 hashValue, err := hashOfProbe(probe) 93 if err != nil { 94 return err 95 } 96 97 r := &report{ 98 Report: probe.String(), 99 CreatedAt: time.Now().Format(time.RFC3339), 100 OS: runtime.GOOS, 101 Arch: runtime.GOARCH, 102 Version: probe.Version, 103 UsageStatsId: hashValue, 104 Features: features.String(), 105 Public: probe.Public, 106 TenantID: probe.TenantId, 107 ProbeID: probe.Id, 108 } 109 110 return hr.submitReport(ctx, r) 111 } 112 113 type NoOPReporter struct{} 114 115 func NewNoOPReporter() *NoOPReporter { 116 return &NoOPReporter{} 117 } 118 119 func (r *NoOPReporter) ReportProbe(_ context.Context, _ sm.Probe, _ feature.Collection) error { 120 return nil 121 } 122 123 // hashOfProbe returns a string representation of the probe by concatenating identifying attributes and 124 // generating an FNV(https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) hash. 125 // The hash function is deterministic to ensure a probe always results in the same string. 126 func hashOfProbe(p sm.Probe) (string, error) { 127 s := p.Name + strconv.FormatInt(p.Id, 10) + p.Region + strconv.FormatBool(p.Public) + strconv.FormatInt(p.TenantId, 10) 128 h := fnv.New64a() 129 130 _, err := h.Write([]byte(s)) 131 if err != nil { 132 return "", err 133 } 134 135 sum := h.Sum64() 136 137 return strconv.FormatUint(sum, 10), nil 138 }