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  }