storj/satellite/nodeevents/customerio.go
Cameron 38275bd710 satellite/nodeevents: deduplicate node IDs
Remove dupulicate node IDs when sending notifications.

Change-Id: I76c642e342081f8fdf8248443de8383b9e88eed2
2023-02-08 23:14:18 +00:00

123 lines
3.0 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package nodeevents
import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/zeebo/errs"
"go.uber.org/zap"
)
// CustomerioConfig handles customer.io credentials info.
type CustomerioConfig struct {
URL string `help:"the url for the customer.io endpoint to send node event data to" default:"https://track.customer.io/api/v1"`
SiteID string `help:"the account id for the customer.io api" default:""`
APIKey string `help:"api key for the customer.io api" default:""`
RequestTimeout time.Duration `help:"timeout for the http request to customer.io endpoint" default:"30s"`
}
// CustomerioNotifier notifies customer.io about node events.
type CustomerioNotifier struct {
log *zap.Logger
config CustomerioConfig
client *http.Client
}
// CustomerioBatch contains info regarding a batch of node events
// for a particular node operator email address.
type CustomerioBatch struct {
Name string `json:"name"`
Data CustomerioData `json:"data"`
}
// CustomerioData contains the satellite name and the node IDs that had an occurrence of the event.
type CustomerioData struct {
Satellite string `json:"satellite"`
NodeIDs string `json:"nodeIDs"`
}
// NewCustomerioNotifier is a constructor for CustomerioNotifier.
func NewCustomerioNotifier(log *zap.Logger, config CustomerioConfig) *CustomerioNotifier {
return &CustomerioNotifier{
log: log,
config: config,
client: &http.Client{
Timeout: config.RequestTimeout,
},
}
}
// Notify sends node event data to customer.io.
func (c *CustomerioNotifier) Notify(ctx context.Context, satellite string, events []NodeEvent) (err error) {
defer mon.Task()(&ctx)(&err)
if len(events) == 0 {
return nil
}
email := events[0].Email
eventName, err := events[0].Event.Name()
if err != nil {
return err
}
var nodeIDs string
idsMap := make(map[string]struct{})
for _, e := range events {
idStr := e.NodeID.String()
if _, ok := idsMap[idStr]; !ok {
idsMap[idStr] = struct{}{}
nodeIDs = nodeIDs + idStr + ","
}
}
nodeIDs = strings.TrimSuffix(nodeIDs, ",")
batch := CustomerioBatch{
Name: eventName,
Data: CustomerioData{
Satellite: satellite,
NodeIDs: nodeIDs,
},
}
data, err := json.Marshal(batch)
if err != nil {
return err
}
url := c.config.URL + "/customers/" + email + "/events"
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
bytes.NewReader(data),
)
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")
req.SetBasicAuth(c.config.SiteID, c.config.APIKey)
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer func() { err = errs.Combine(err, resp.Body.Close()) }()
c.log.Info("batch sent to customer.io", zap.String("email", email), zap.String("event", eventName), zap.String("node IDs", nodeIDs))
if resp.StatusCode != http.StatusOK {
return errs.New("unexpected status code: %d", resp.StatusCode)
}
return err
}